Note: everything herein is about Pools of Darkness version 1.10 for DOS ( Mobygames )
There is a built-in cheat assigned to Alt+X that instantly wins the current battle. Which is very useful because a lot of game time is spent fighting *random* encounters. However, I couldn't find any way to enable this cheat, when Alt+X was pressed the game would just flash the message "That doesn't work"...
so I broke out my debugger :-)
The first order of business was cracking the "Paper protection" (aka code wheel) employed by the game. "Enter word X from page Y" quickly gets annoying during the rapid fire restarts used while reversing. The paper protection prompt is also somewhat pointless; pitty the fool who tries to play this game without the manual.
The game has executable code in two files: game.exe and game.ovr (overlay). The game executable is packed using EXEPACK, as per the "Packed file is corrupt" string. However, at the time, I didn't know how it was packed and I had never used IDA before. So I decided to only use DosBox Debugger and Cheat Engine to accomplish my quest.
// file offset 0x3603 thru 0x3827 in GAME.OVR // ... 37BF mov al, 0Eh 37C1 push ax 37C2 mov al, 0 37C4 push ax 37C5 mov al, 14h 37C7 push ax 37C8 call far ptr 47Fh:711h // user input prompt (wait for enter key) 37CD mov di, 7EEEh // buffer for the user's answer 37D0 push ds 37D1 push di 37D2 mov ax, 0FFh // size of buffer 37D5 push ax 37D6 call far ptr 825h:0A89h // read-in the user's anwser to buffer 37DB mov byte ptr ds:0C5BCh, 1 // integrity checkpoint 37E0 cmp byte ptr ds:7FEEh, 3 // attempts made compared to attempts max (3) 37E5 jz loc_37FB // if final attempt goto loc_37FB 37E7 mov di, 7EEEh // arg 2: user answer 37EA push ds 37EB push di 37EC mov di, 7DEEh // arg 1: correct answer 37EF push ds 37F0 push di 37F1 call far ptr 825h:0B74h // string compare 37F6 jz loc_37FB // if correct answer then goto the final attempt code 37F8 jmp loc_3625 // else load up the next question // 37FB mov byte ptr ds:0C5BDh, 1 // integrity checkpoint 3800 mov di, 7EEEh // arg 2: user answer 3803 push ds 3804 push di 3805 mov di, 7DEEh // arg 1: correct answer 3808 push ds 3809 push di 380A call far ptr 825h:0B74h // string compare 380F jnz loc_3817 3811 mov byte ptr [bp-1], 1 // if correct answer return 1 (true) 3815 jmp short loc_381B // 3817 mov byte ptr [bp-1], 0 // else wrong answer return 0 (false) // 381B mov byte ptr ds:0C5BEh, 1 // integrity checkpoint 3820 mov al, [bp-1] 3823 mov sp, bp 3825 pop bp 3826 retf
Returning from 0x3826 (game.ovr) zooms the scope out a level and lands us at offset 0x402 (game.exe)
01A2:01F8 call 0217:0025 // main menu 01A2:01FD call 01EB:0025 // code wheel 01A2:0202 cmp al,01 // check code_wheel result 01A2:0204 je 0000020E // if success jmp to integrity check 01A2:0206 mov al,00 // else 01A2:0208 push ax // 01A2:0209 call 0812:0000 // exit to DOS // integrity check: makes sure [C5BB], [C5BC], [C5BD], and [C5BE] are all non-zero 01A2:020E mov byte [7DDF],01 // iterator 01A2:0213 jmp short 00000219 01A2:0215 inc byte [7DDF] 01A2:0219 mov al,[7DDF] 01A2:021C xor ah,ah 01A2:021E mov di,ax 01A2:0220 cmp byte [di-3A46],00 // intentional? way to hide the address 01A2:0225 jne 0000022F ($+8) 01A2:0227 mov al,00 // else 01A2:0229 push ax // 01A2:022A call 0812:0000 // exit to DOS 01A2:022F cmp byte [7DDF],04 01A2:0234 jne 00000215 ($-21) // loop // we want to get here 01A2:0236 cmp byte [880C],03 01A2:023B jne 0000024F ($+12) 01A2:023D les di,[87F8] 01A2:0241 cmp byte es:[di+23],01 01A2:0246 jne 0000024F ($+7) 01A2:0248 call 02E4:0137
How would one crack the above?
NOP the call to the code wheel function and then jump over the integrity check. However, the call can't be overwritten with 0x90's because a relocation is applied to it at run-time. In a 32-bit program one could change the first byte of the call to a 0x25 or something, but my lack of 16-bit powess prevents me from doing that here. We could early return from the code wheel function but that would mean editing two files instead of one.
Is the crack clean?
Is there another integrity check right before the last battle? I'm not willing to play through this very long game to find out. So lets try to do a cleaner crack. If one were to replace the string_compare call at 0x37F1 (game.ovr) with a call to string_copy then the correct anwser would get filled in automatically, and all the normal code paths would still be hit. The game's string_copy function, as a side effect, always returns with the zero flag set.
Note: seems like a good example of why one should encrypt the user input and not decrypt the anwser...
file: GAME.OVR offset: 0x37F2 orginal data: 74 0B patch data: 6F 0A
There are two obvious ways to track down the Alt+X cheat... Hardware breakpoint on the string "That doesn't work" or breakpoint INT 15 AH=4F and step through the game's key-stroke handlers.
2588:0D4B cmp al,2D // X key scan code 2588:0D4D jne 00000D83 ($+34) // handler spans to 2588:0D83 2588:0D4F call 0243:007A // ---> to skip a battle edit this function! 2588:0D54 or al,al 2588:0D56 je 00000D6F ($+17) // 2588:0D58 push word [bp+08] 2588:0D5B push word [bp+06] 2588:0D5E mov al,03 2588:0D60 push ax 2588:0D61 mov al,00 2588:0D63 push ax 2588:0D64 call 02D7:0089 2588:0D69 mov byte [bp-02],01 2588:0D6D jmp short 00000D83 ($+14) // 2588:0D6F lea di,[bp-20] 2588:0D72 push ss 2588:0D73 push di 2588:0D74 mov di,09B2 // "That doesn't work" 2588:0D77 push cs 2588:0D78 push di 2588:0D79 call 09C7:0A6F // string_copy "That doesn't work" to stack 2588:0D7E call 028E:007F // write it to screen?
The call from 2588:0D4F to 0243:007A goes through a jump table an lands here:
26FA:3D7E push bp 26FA:3D7F mov bp,sp 26FA:3D81 sub sp,0106 26FA:3D85 mov byte [bp-01],00 26FA:3D89 lea di,[bp-0106] 26FA:3D8D push ss 26FA:3D8E push di 26FA:3D8F mov ax,0003 26FA:3D92 push ax 26FA:3D93 call 09C7:1A93 26FA:3D98 mov di,3D65 // 04h Helm 13h The Gods intervene! 26FA:3D9B push cs 26FA:3D9C push di 26FA:3D9D call 09C7:0B74 // string_compare? 26FA:3DA2 je 00003DA7 ($+3) // <--- force this jmp 26FA:3DA4 jmp 00003E51 ($+aa)
Force that jump to enable the Alt+X cheat.
file: GAME.OVR offset: 0x223E4 orginal data: 74 patch data: EB
Mission accomplished?
This is where I stopped reversing. However, as per Simeon Pilgrim, why string_compare against Helm? 'Helm' passed on the command line enables the Alt+X cheat! Unfortunately, I had spent too much time on the crack and stopped reversing upon finding a patch that worked.
p.s. Gold Box Companion & Gold Box Explorer, I'm keeping an eye on you!