sábado, 23 de julio de 2022

Reversing a DOS Game (Part 2/3)

This is a series of 3 posts explaining the experience of reverse engineering a DOS game. The method used is far from sophisticated. Basically disassembling and inspecting the instructions. While live debuggers or live memory inspection through some emulator would bring you faster to this same conclusions, in this exercise we are only using ndiasmgrep and a hex editor to make modifications.

So if you read the previous entry, This x86 DOS instruction:

FE0EF003          dec byte [0x3f0]

Closed one of my childhood traumas, years later, from a more critic point of view we can make some considerations:

  • How great when everything just fits in 64KBs
  • So LIFE was stored in a single byte at 0x3f0... What other operations are done with that address location:
$ cat Indy.asm | grep -i \\[0x3f0
0000598F  C606F00305        mov byte [0x3f0],0x5
00005C3B  FE0EF003          dec byte [0x3f0]
00005C42  803EF00300        cmp byte [0x3f0],0x0
00005CB4  C606F00305        mov byte [0x3f0],0x5
00005D1C  FE0EF003          dec byte [0x3f0]
00008649  FE06F003          inc byte [0x3f0]
0000A669  A0F003            mov al,[0x3f0]
0000A679  A0F003            mov al,[0x3f0]

I just always saw a LIFE bar(never actually cared about the count of that bar), but in here I actually see that there were 5 lives. As we can see how this address location is initialized with 0x5 in two locations.

C606F00305        mov byte [0x3f0],0x5

You would be tempted to replace that 0x5 with a higher number, but notice that black rectangle in the UI. This is most likely too intrusive. I would suspect the code is trying to dump as many sprites in the screen depending on the integer represented by 0x3f0. So most likely we are reading invalid memory.


Here we see a comparison with 0, so this is likely the only place in the game where LIFE is checked to decide if Game is Over or not.

803EF00300        cmp byte [0x3f0],0x0

Here, as far as I remember, each time you completed a chapter, you get a new life. So this instruction must be it:

FE06F003          inc byte [0x3f0]
  • What other memory locations are decreasing or getting subtracted around 0x3f0?

$ cat Indy.asm | grep -i dec.*\\[0x3
00005C3B  FE0EF003          dec byte [0x3f0]
00005D1C  FE0EF003          dec byte [0x3f0]
000081D8  FE0EED03          dec byte [0x3ed]
00008A86  FE0EAA03          dec byte [0x3aa]
00008AAB  FE0EAA03          dec byte [0x3aa]
00008B14  FE0EAA03          dec byte [0x3aa]
00008B30  FE0EAA03          dec byte [0x3aa]
$ cat Indy.asm | grep -i sub.*\\[0x3
00007A71  2A06AA03          sub al,[0x3aa]
00007A9F  2A06AA03          sub al,[0x3aa]
00007AD1  2A06AA03          sub al,[0x3aa]
00007F47  2B069403          sub ax,[0x394]
00007F80  2B069403          sub ax,[0x394]
0000813C  802EEF030A        sub byte [0x3ef],0xa
00008659  2806EF03          sub [0x3ef],al
00008854  832E9D0308        sub word [0x39d],byte +0x8
00008859  832E9F0328        sub word [0x39f],byte +0x28
00008979  2B06FF03          sub ax,[0x3ff]
00008983  2B1EFF03          sub bx,[0x3ff]
00008C98  812E9403D000      sub word [0x394],0xd0
00009D83  2A06E203          sub al,[0x3e2]

So applying the same method to these instructions, We can see that:
  • 0x3AA is storing the time. Each chapter has a limited amount of time which can be increased by collecting torches ... or you can replace FE0EAA03 by 90909090 and don't rush anymore... 
  • 0x3ED is the number of lashes. Whenever Indy catches the whiplash artifact, he can use it 5 times. If we check operations on this address:
$ cat Indy.asm | grep -i \\[0x3ed
0000597A  A2ED03            mov [0x3ed],al
00005A2E  C606ED0300        mov byte [0x3ed],0x0
000081D1  803EED0300        cmp byte [0x3ed],0x0
000081D8  FE0EED03          dec byte [0x3ed]
0000877C  C606ED0305        mov byte [0x3ed],0x5
0000B80E  8A16ED03          mov dl,[0x3ed]
0000B857  8A16ED03          mov dl,[0x3ed]
0000B887  8A16ED03          mov dl,[0x3ed]

We can make the same assumptions as with LIFE. We can see how it gets initialized to 0, or to 5 (presumably when the artifact is caught). Likewise, we see in the same way comparison to 0 (to detect that Indy cannot use the whiplash anymore). And finally, the decreasing instruction.
So replacing FE0EED03 by 90909090 will give you infinite whiplash hits... But I am not sure how this can affect the gameplay in other chapters.

You could still change the 0x5 of:

C606ED0305        mov byte [0x3ed],0x5

To a number bigger than 0x5...like 0xFF(255) But the game might not be able as well to render that number on the dash, so this could be conflicting (not tested).

The actions described here might be seeing as hack & cheat a video game. But the message pretended is that by checking the operations performed over the memory locations, we are identifying what are doing whole portions of ASM code. So we can see this as a reverse engineering process.

  • 0x3EF is maybe the most interesting one: The ENERGY bar. Although Indy dies immediately in contact with other opponents or falling in uncontrolled areas of the level. Handling this memory portion properly will make Indy immortal by not receiving any damage during the gameplay(not only not decreasing lives as my initial intention...). Someone can beat the game even faster with this one... But this is not as easy as replacing by NOPs...

    ... TO BE CONTINUED...

miércoles, 6 de julio de 2022

Reversing a DOS Game (Part 1/3)

I would say, Indiana Jones and the Last crusade(arcade), Madshow, and Loom were very influential games in my life. The first, was the very first one I actually own. It cost my parents 500pts(equivalent today to 3€).

The game is divided in 4 chapters. The first represents when young Indiana, as in the film, is trying to escape with the Cross of Coronado. In the DOS version whenever your lives expired, you were done. Second chapter represent the catacombs of Rome together with the climb of the German castle where Indy father is retained. The third belongs to the Zeppelin scene. These two chapters, you could continue gaming and infinite number of times, there was plenty of oportunities to train in these levels. The fourth and last chapter represented the final scene of for the film, where you need to get of Holy Grail, to save Indy's father in the famous Jordanian temple. This as the first, you could not continue playing if your lifes expired, no training possiblities in this last level level.

1993, me being 11 years, my only owned game, and impossible to beat. Always dead at the fourth chapter, with no relevant progress. I eventually surrendered, but promised: "one day, my future self, will hack you, and then I will finally beat you".

During the following years there was unconsciously a plan in my head (or maybe just a fantasy): "which kind of hack, would help me beat the game without being too much intrusive at the same time..."

Almost 20 years later, 2012, and with enough ASM debugging experience behind:

The idea was to make the game to not decrease the LIFE bar. Locate the instruction performing the action and bypass it. The initial assumptions were the following:

  • There would be a x86 DEC instruction performing that action
  • Likely that DEC instruction is decreasing a memory position and not a register, not a stack variable.
  • That would be inside a middle to low size function similar to:
HandleDeadIndy() {
    ShowDeadIndy();
    FadeOut();
    DecreaseLife();
    RestartGame();
}
Some visual dissasembler like IDA made easier identify these kinds of candidate functions.

The number of DEC instructions:
# ndisasm -b 16 INDY.COM > Indy.asm
# cat Indy.asm | grep -i dec | wc -l
rounded 450.  Not impossible one by one, but I needed to apply the first two assumptions to filter out. Dissasembling the binary you can find that DEC instructions could take 1 2 3, and 4 bytes. Almost in the same the order in number of appearances, and those accessing memory are likely the 3 and 4 bytes ones, some examples:

00008B30  FE0EAA03          dec byte [0x3aa]
00008F80  FEC8              dec al
00009161  FE4C06            dec byte [si+0x6]
00009230  FEC8              dec al
000099A7  FEC9              dec cl
00009A02  4A                dec dx
00009A10  4A                dec dx

The number of candidates reduces to ~25. The strategy is to replace those bytes by the NOP instruction 0x90 (1 byte):
  • Create a copy of the binary (INDY.COM)
  • Replace that potential DEC instruction by the required number of NOPs with your favorite Hexadecimal editor
  • Play the game for testing
The expected result is an immediate crash of the game... but the mix of the assumptions taken and very good luck made the hit at the second attempt! LIFE bar was not decreasing after each dead!

That was it, the pattern FE0EF003 needs to be replaced by 90909090.

Next was remembering how to play again as I did in 1993. Where to jump, when to punch. The hacking exercise took the evening of that day, but finally beating that 4th chapter took the rest of the night... not easy. I was relieved that my younger self was wise enough to surrender and stop wasting time, because I could never beat it without that hack... 

If you read this and beat it in the same way, please post a screenshot in the comments...

That happened in 2012, still 10 years ago, but social networking allows you to find people with similar experiences and struggles, and decided to document a bit that experience.

However, now in 2022, 10 years older, I realize I did not pay any attention to the actual hacked instruction. LIFE bar not decreasing!! Time to BEAT!, just started playing after the achievement.

The method used was even inelegant, brute force maybe, and never stopped taking a careful look around that instruction. What was that instruction revealing? Could I do more?

FE0EF003          dec byte [0x3f0]

TO BE CONTINUED ...