Aside from the (good) comments made by others:
- Before you can reliably modify any program, you absolutely must get to understand how and why it works. This is always challenging for a sizable program, but when its complexity has not been skillfully managed and broken up into digestible units, it's much worse. However, you can overcome that obstacle with time and effort.
If the people who wrote the code are accessible and willing to answer questions, that is "gold" and you should not waste it. Even a 15-minute conversation can make an enormous difference. Even if they have moved on to another company or department, you might be able to track them down and see if they are willing to talk.
If the source control history is available, looking up when a difficult piece of code was written and what else was changed at the same time can provide precious clues. If the authors left decent commit log messages, that is even better, but from your description of the codebase, I guess you can't expect that.
Don't get stuck for a long time trying to understand one small bit of code (unless it is the critical part for your immediate task at hand). To build understanding, it is more effective to "dip your toes into" a section, read through and try to understand what you can without getting stuck for too long, then move on to a different section. After a while you will start making connections between the parts of the program. Each part that you grasp will help you to understand other parts.
Stepping through the code in a debugger, watching where control flow goes and how data flows from one part to another can be very helpful. Another possibility is to add debug print statements in key places and watch the messages printed as you interact with the program.
If you are able to build and run the program, you can also do "experiments" to verify your understanding. For example, if you are reading code and think to yourself: "I think feature XYZ should still work even if these lines were removed", then rather than staring at the code for an hour trying to determine whether your hunch is right, you might make faster progress by deleting the lines, then running the program and seeing what happens. Another type of experiment is where you deliberately change something just to see what compiler errors will be produced.
- Try to identify parts which "can't affect anything else" (i.e. whose effects are very local), and clean those up first.
- Every part which you are able to clean up will make what remains easier to understand. Keep expanding the borders of what you understand and shrinking the borders of what you can't. Understanding is the key thing; the actual refactoring is relatively easy after that.
- If a certain function or section of code just seems really crazy or nonsensical, maybe that's because it is. Sometimes, rather than trying to delve into its craziness, it is better to figure out what that part was intended to do, delete it, then rewrite it to do what the original author had intended.
When you do this, you may find that a bunch of strange bugs disappear.