Pools of Darkness: The Gods intervene!

Pools of Darkness: The Gods intervene!

Note: everything herein is about Pools of Darkness version 1.10 for DOS ( Mobygames )


tl:dr

Simeon Pilgrim discovered the command line options to enable the Alt+X cheat and skip the paper protection prompt. This info was staring me in the face and I missed it... so here follows a pointless tail of woe.


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!


bitpatch.com