003: Clean Cracking ‘Where in Time is Carmen Sandiego?’

Introduction

One of the early forms of copy protection was a “doc check”. It was a prompt to enter in a certain word or page number from the game manual. Photocopier-proof sheets and even code wheels were sometimes packaged with games. Not only was this copy protection annoying, since it had to be entered on each playthrough, but preserving the needed reference materials can be a pain decades later. Using a modern emulator, QEMU, and MacsBug, we are going to completely bypass the copy protection (a “clean” crack) for Where in Time is Carmen Sandiego?.

Tools

This project uses MacsBug 6.6.3 and Resorcerer 1.2.5. For more details on their installation and usage, check out Tutorial 001. We will also use QEMU — refer to its documentation for setting up OS 9. Be sure to enable the QEMU Monitor console with -monitor stdio.

Save The State

Once we get to the end of the first level, just after catching the crook — maybe during the arrest animation, save the state in QEMU with savevm carmen1 (carmen1 can be any name you like). We will restore back to this state throughout this tutorial.

Investigation

After a few clicks, the copy protection screen should appear. In previous tutorials, we tried to figure out the Toolbox trap used in a dialog box for a serial number. Some doc checks use a standard dialog box and this would be a good strategy for those instances, but here we definitely are not going to be using GetDialogItemText. We could try StringToNum or GetKeys, but notice that after entering in five wrong answers, the game quits.

The copy protection screen. Who has an encyclopedia?

Reverse Engineering in Reverse

Since the game quits right from the copy protection screen, let’s drop into MacsBug using nmi in QEMU. Once in, atb ExitToShell. After entering a wrong guess five times, the game should try and quit and break into MacsBug. From there, ip to disassemble around the current instruction:

Disassembling from 0E25771A
'CODE 0005 00B2'
 +00CDA 0E25771A   DC.W       $F632                      ; ????       | F632
 +00CDC 0E25771C   RTS                                                | 4E75
 +00CDE 0E25771E   MOVEQ      #$01,D0                                 | 7001
 +00CE0 0E257720   MOVE.B     D0,-$09CF(A5)                           | 1B40 F631
 +00CE4 0E257724   JSR        'CODE 0005 00B2'+00CD2     ; 0E257712   | 4EBA FFEC
 +00CE8 0E257728   MOVE.W     #$FFFF,-(A7)                            | 3F3C FFFF
 +00CEC 0E25772C   CLR.W      -(A7)                                   | 4267
 +00CEE 0E25772E   JSR        'CODE 0009 00B2'+01B0E                  | 4EAD 0952
 +00CF2 0E257732  *_ExitToShell                          ; 0031B22E   | A9F4
 +00CF4 0E257734   RTS                                                | 4E75
 +00CF6 0E257736   CLR.L      -(A7)                                   | 42A7
 +00CF8 0E257738   _FrontWindow                          ; 00959EA4   | A924
 +00CFA 0E25773A   MOVEA.L    (A7)+,A0                                | 205F
 +00CFC 0E25773C   CMPA.L     -$0A8C(A5),A0                           | B1ED F574
 +00D00 0E257740   SEQ        D0                                      | 57C0
 +00D02 0E257742   NEG.B      D0                                      | 4400
 +00D04 0E257744   EXT.W      D0                                      | 4880
 +00D06 0E257746   RTS                                                | 4E75
 +00D08 0E257748   LINK       A6,#$FFF8                               | 4E56 FFF8
 +00D0C 0E25774C   CLR.L      -(A7)                                   | 42A7

The current subroutine is quite short and begins at +00CDE. We can tell this because there is an RTS right above at +00CDC. What we want to do is figure out the place in memory that called this Exit routine. We can do this by noting the memory address of the first instruction: 0E25771E. This number will likely change if the game is reopened, but because we saved the QEMU state, this memory location remains the same if we were to restore the state.

Restore the state in QEMU using loadvm carmen1. We are back just before the copy protection screen, but now drop into MacsBug and bp 0E25771E. After five wrong guesses we should now be at the entry point a few lines above ExitToShell. If this is the case, the return address should be the first address on the stack. We can verify this with dm A7 and ip @A7:

Displaying memory from A7
0E3D4EE4  0E25 41CA 4EF8 0E3D  4EF8 0E25 20C8 0004
Disassembling from @A7
'CODE 0006 00B2'
 +032A2 0E2541B2   BEQ.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 6716
 +032A4 0E2541B4   JSR        'CODE 0002 00B2'+002CC                  | 4EAD 0492
 +032A8 0E2541B8   TST.B      D0                                      | 4A00
 +032AA 0E2541BA   BNE.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 660E
 +032AC 0E2541BC   JSR        'CODE 0006 00B2'+026F2     ; 0E253602   | 4EBA F444
 +032B0 0E2541C0   CMPI.W     #$FFFF,D0                               | 0C40 FFFF
 +032B4 0E2541C4   BNE.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 6604
 +032B6 0E2541C6   JSR        'CODE 0005 00B2'+00CDE                  | 4EAD 0742
 +032BA 0E2541CA   JSR        'CODE 0006 00B2'+02B54     ; 0E253A64   | 4EBA F898
 +032BE 0E2541CE   JSR        'CODE 0006 00B2'+02AB2     ; 0E2539C2   | 4EBA F7F2
 +032C2 0E2541D2   TST.W      D0                                      | 4A40
 +032C4 0E2541D4   BEQ.S      'CODE 0006 00B2'+032CC     ; 0E2541DC   | 6706
 +032C6 0E2541D6   JSR        'CODE 0006 00B2'+0303C     ; 0E253F4C   | 4EBA FD74
 +032CA 0E2541DA   BRA.S      'CODE 0006 00B2'+032D6     ; 0E2541E6   | 600A
 +032CC 0E2541DC   JSR        'CODE 0006 00B2'+03086     ; 0E253F96   | 4EBA FDB8
 +032D0 0E2541E0   BRA.S      'CODE 0006 00B2'+032D6     ; 0E2541E6   | 6004
 +032D2 0E2541E2   JSR        'CODE 0006 00B2'+0581C     ; 0E25672C   | 4EBA 2548
 +032D6 0E2541E6   JSR        'CODE 0006 00B2'+02AB2     ; 0E2539C2   | 4EBA F7DA
 +032DA 0E2541EA   TST.W      D0                                      | 4A40
 +032DC 0E2541EC   BEQ.S      'CODE 0006 00B2'+032E6     ; 0E2541F6   | 6708

The address that called the routine should be exactly four bytes less than the return address. Sure enough, the calling line is: +032B6 0E2541C6 JSR 'CODE 0005 00B2'+00CDE. Note that 'CODE 0005 00B2'+00CDE matches the entry point of the Exit routine. Around the line, there are a few branches to the return address, 0E2541CA. Looking at the closest branch at +032B4, if D0 is not equal to FFFF (CMPI.W #$FFFF,D0), branch to the return address, bypassing the Exit routine.

Let’s note the address of the comparison for the branch, 0E2541C0, restore the state with loadvm carmen1, and set a breakpoint on it with bp 0E2541C0. After entering five guesses, MacsBug should open and D0 should contain FFFF. This value will cause the branch to fall through, running the Exit routine. Set D0=1 and g to resume execution.

It works! The game does not quit and you can proceed to the next level. This would be a working crack, but not a clean one. Not only does the doc check screen come up, but it also requires you to enter five wrong guesses. We can do better.

A Clean Crack

Recall our disassembly around the call to the Exit routine at 0E2541CA in the previous listing. The branch above our breakpoint at 0E2541C0 was at +032A8. There, if D0 is non-zero (TST.B D0 / BNE.S), bypass the Exit routine. What about above that at +032A2? Let’s ip 0E2541B2:

Disassembling from 0E254194
'CODE 0006 00B2'
 +03284 0E254194   ADDQ.L     #$2,A7                                  | 548F
 +03286 0E254196   CMPI.W     #$0004,$0008(A6)                        | 0C6E 0004 0008
 +0328C 0E25419C   BNE.S      'CODE 0006 00B2'+032D2     ; 0E2541E2   | 6644
 +0328E 0E25419E   JSR        'CODE 0002 00B2'+0030C                  | 4EAD 04E2
 +03292 0E2541A2   TST.B      D0                                      | 4A00
 +03294 0E2541A4   BNE.S      'CODE 0006 00B2'+032D6     ; 0E2541E6   | 6640
 +03296 0E2541A6   JSR        'CODE 0006 00B2'+02AC2     ; 0E2539D2   | 4EBA F82A
 +0329A 0E2541AA   MOVEA.L    -$3C92(A5),A0                           | 206D C36E
 +0329E 0E2541AE   TST.B      $000A(A0)                               | 4A28 000A
 +032A2 0E2541B2   BEQ.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 6716
 +032A4 0E2541B4   JSR        'CODE 0002 00B2'+002CC                  | 4EAD 0492
 +032A8 0E2541B8   TST.B      D0                                      | 4A00
 +032AA 0E2541BA   BNE.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 660E
 +032AC 0E2541BC   JSR        'CODE 0006 00B2'+026F2     ; 0E253602   | 4EBA F444
 +032B0 0E2541C0   CMPI.W     #$FFFF,D0                               | 0C40 FFFF
 +032B4 0E2541C4   BNE.S      'CODE 0006 00B2'+032BA     ; 0E2541CA   | 6604
 +032B6 0E2541C6   JSR        'CODE 0005 00B2'+00CDE                  | 4EAD 0742
 +032BA 0E2541CA   JSR        'CODE 0006 00B2'+02B54     ; 0E253A64   | 4EBA F898
 +032BE 0E2541CE   JSR        'CODE 0006 00B2'+02AB2     ; 0E2539C2   | 4EBA F7F2
 +032C2 0E2541D2   TST.W      D0                                      | 4A40

At +0329E there is a check if $000A(A0) is zero (TST.B $000A(A0)) and a branch if it is (BEQ.S). Since there are no other branches to the return address 0E2541CA nearby, this seems like another good place to set a breakpoint with bp 0E2541AE. Upon restoring the state with loadvm carmen1 and proceeding to the copy protection screen, it drops into MacsBug instead. Use t to step to the branch, but simulate taking the branch with pc=0E2541CA. This sets the program counter, the active memory location, as if we had taken the branch. After g to resume, the game moves on to the next level with no copy protection in sight!

Creating a Cracked Application

Now the clean crack is clear, we have to set a branch always (BRA) at CODE 6 +032A2. To do this, open the Carmen Time application in Resorcerer. Open the CODE 6 resource then Code > Goto/Find/Replace…. Type in the offset 32A2:

Going to an offset.

Now highlight the 67 in 6716 and change it to 60. The branch instruction should change to BRA accordingly:

We have to BRA instead of BNE.

Save your work and test it out by playing through the first level.

Conclusion

When reverse engineering, working backwards can be faster and easier than working forwards. Instead of catching ExitToShell, sometimes you’ll want to catch an alert with Alert, NoteAlert, CautionAlert, or StopAlert and work back from there. These traps are useful when creating NoCDs since there is likely a prompt to insert a CD. QEMU save states are another tool we can use to crack applications because memory addresses are preserved. With fixed memory addresses, setting breakpoints at key places is trivial, and are able to quickly give clarity to complex code. A clean crack makes gameplay seamless and that’s always the ultimate goal when cracking.