- Introduction
- Tools
- Investigation
- Strategy
- The Toolbox
- Setting A Toolbox Breakpoint
- Deeper into MacsBug
- A Quick Crack
- The Algorithm
- Conclusion
Introduction
In this project, we will be looking at an easy algorithm to reverse in Grateful Dead: D2S2. I will also be going over some basic setup since this is the first in my tutorial series. At the end of this project, you will be able to identify a 68k vs PPC application, set a Toolbox breakpoint, manually bypass the authentication algorithm, and then fully reverse the algorithm to generate a valid serial number. A passing familiarity with 68k assembly will help, but it’s not necessary.
Tools
We will be using MacsBug 6.6.3 and Resorcerer 1.2.5. To install MacsBug, simply drop it in your System Folder and reboot. MacsBug is a low level debugger that displays the assembly language instructions sent to the CPU. Resorcerer is used to view and edit the binary data of the application.

Investigation
MacsBug uses different commands to break on Macintosh Toolbox calls depending on the architecture (more on that later). The first thing to do is determine if the application is 68k or PPC. While there are applications designed for this purpose, I find it just as easy to open the application in Resorcerer and peek at the Data Fork <DF> resource.


As a rule of thumb, PPC applications will have the string Joy!peffpwpc at the very beginning of the Data Fork. Since the Data Fork here does not contain the string, we can safely assume it is a 68k application.
Next, we need to carefully look at the registration process. Let’s fire up the installer for D2S2 – The Digital Dead. A dialog appears and asks for a serial number. Let’s try 123456. We have to click OK to proceed.


After clicking OK, an alert appears complaining about an invalid serial number. From this simple investigation we can conclude:
- This is a 68k application.
- The serial number entry looks like a standard textbox within a standard dialog.
- The error alert looks like a standard alert.
What I mean here by standard is that the dialog and textbox looks like every other normal dialog and textbox in the OS. We can dive further and verify this by looking at the DITL and DLOG resources within Resorcerer, but I look for things like custom borders, custom fonts, or fancier controls. Compare the dialogs above with the one below. Notice the different fonts on the buttons and the different textbox size and outline. These observations are needed to figure out how to stop program execution with MacsBug at a precise point since standard dialogs and controls will usually use standard Macintosh Toolbox calls (again, more on that later).

Strategy
If we can stop program execution immediately after we click the OK button at the serial number prompt, we should land in MacsBug just before the validation routine. At a very high level, we expect the validation logic to look something like this pseudocode:
1. sn = getSerialNumberText();
2. validSN = isValidSerialNumber(sn);
3.
4. if (validSN) {
5. goodSerialNumber();
6. } else {
7. alert("Bad Serial Number!");
8. }
We want to break into the debugger right at line 1 so we can step through isValidSerialNumber(sn) and see what constitutes a valid serial number.
This strategy is in fact how all cracking works regardless of the system you are using or the age of the application. For example, cracking DOS applications would require breaking into the debugger when the Enter key is pressed. Cracking modern games involving online activation requires breaking into the debugger to inspect the memory and packets being sent to the server just prior to and after a validation check.
There are two tough parts:
- Figuring out what to break on.
- Figuring out the validation algorithm.
For the first issue, we are going to use Toolbox calls. Number two requires a bit more brain power.
The Toolbox
The Macintosh Toolbox is a set of routines that interact with the Macintosh operating system. There are hundreds of different routines. You can review the complete documentation here. Luckily, we don’t need to know more than a small subset.
From our investigation, remember it appears we are using a standard textbox within a standard dialog. Our first attempt will use the Toolbox call GetDialogItemText. This is the call used to get textbox contents from a standard dialog box. As I write more tutorials in this series, we will look at using different Toolbox calls since developers of more sophisticated copy protection schemes will rarely use something as obvious as GetDialogItemText.
Recall that what looks like a standard alert is shown when we enter an invalid serial number; we can also be looking for the Toolbox call Alert around our validation code.
Setting a Toolbox Breakpoint
With a logical guess of the Toolbox call GetDialogItemText, we can use MacsBug to stop program execution immediately before it is called. Run the installer for D2S2 – The Digital Dead again and get to the serial number prompt. Now we have to get into MacsBug. This can be done a number of ways:
- Pressing the Programmer’s Key on an older Macintosh.
- Pressing Command-Power.
- Using the CS Debugger Control Strip.
- Using my own Debugger! utility.
- The
nmi(Non-Maskable Interrupt) command from the QEMU monitor. - Ctrl-I (Interrupt) in Mini vMac.
Once we are in MacsBug, we set a breakpoint on a Toolbox call. We can do this with atb GetDialogItemText. We are using atb because from our investigation we found the application to be 68k. If we wanted a PPC breakpoint, the command would be tvb GetDialogItemText. It’s good practice to write your Toolbox breakpoints case-sensitive.

atb GetDialogItemText.Once the break is set, g will resume program execution. Again, let’s use 123456 as the serial number and click OK. We should now automatically be back in MacsBug. The Toolbox breakpoint was a success!

Deeper into MacsBug
Now that we are looking at the disassembly of our application in MacsBug, we can trudge through the code step by step. The MacsBug commands we will be using now are:
s: Step to the next instruction. This will also enter any subroutine calls.so/t: Step Over or Trace to the next instruction. This will not enter any subroutine calls.il: disassemble from current instruction (Instruction List).ip: disassemble around current instruction (Instruction list Page).dm <address>: Display Memory. Used to show the memory contents at the specified address.
The first time I break into MacsBugs, I like to ip to get my bearings. Press Enter again to disassemble a few more lines. The asterisk in the display denotes the current pc (Program Counter) or the instruction about to be executed on the CPU. You can see at +000C6 it is GetDialogItemText, which is the breakpoint we set.

ip to disassemble around the current instruction.A call to GetDialogItemText will put the textbox contents into the 32-bit (4 byte) memory address on the stack. The stack is referenced by address register A7, and we can view the contents of the stack by using dm a7. Addresses will usually change each time the application is run, but in this case the first 4 bytes are 0E1F F8C0. In other words, after we trace over GetDialogItemText, address 0E1FF8C0 will contain the textbox string.
Let’s do dm @a7, which is a shortcut to display the memory of the 32-bit address at A7. You should see the memory at location 0E1FF8C0. There is just some garbage data there, but now lets t to step over GetDialogItemText. We can then use dm ., which will display the memory at the last address returned by MacsBug.

GetDialogItemText.The ASCII contents of 0E1FF8C0 should be 123456 — our entered serial number. Notice that the first byte is 06. This is because GetDialogItemText will return a Pascal String (pstring), which has the length of the string in the first byte.
Looking ahead a few lines, there is a DisposeDialog call and then after a few more lines there is an RTS (ReTurn from Subroutine) instruction. Let’s t to trace over everything including the RTS instruction. We should now be at a PEA (Push Effective Address) instruction. The full instruction is PEA -$0100(A6) and we can translate this as “push the address of A6-0x100 onto the stack”. What is at this location? Let’s dm a6-100 to take a look… it’s our entered serial number! Note also that the memory address is the same as before: 0E1FF8C0. Let’s ip again to get our bearings.

ip after displaying A6-100.Just before the pc (Program Counter / asterisk), there is a JSR instruction at +00030. This was the subroutine that we just exited. Remember that it was getting the entered serial number via GetDialogItemText. After pc, there is another JSR instruction at +00038. A few more instructions down at +0004C we see an Alert. Thinking back to our pseudocode, it’s more than likely that the subroutine at +00038 is isValidSerialNumber(sn), where sn is passed via the stack, and the Alert is displayed if we have entered an invalid serial number.
A Quick Crack
Now that we have likely narrowed down where the validation check occurs, it’s time to do some actual reverse engineering! Let’s s to step into the subroutine referenced by the JSR at +00038. We can il a few times to disassemble from the current pc.

il to Instruction List the subroutine.This might look intimidating, but we will go through it step by step. The first interesting block is:
+000D8 MOVEA.L $0008(A6),A3 | 266E 0008 +000DC CMPI.B #$0D,(A3) | 0C13 000D +000E0 BNE 'IBeg 0080 012E DDSS'+00194 | 6600 00B2
The instruction, MOVEA.L $0008(A6),A3 moves the address at A6+8 into A3. Let’s s over that instruction then dm a3 to see what is stored there. It’s the pstring of our serial number! The next instruction, CMPI.B #$0D,(A3) compares the byte (denoted by the .B) 0x0D and the byte at the address in A3. The brackets around A3 mean we treat the A3 register value as an address and look there, without the brackets we look at the register value itself — think of brackets like dereferencing pointers (*) in C. What’s the byte at A3? Remember that for pstrings, the length of the string is stored in the first byte, so the byte at A3 will be the length of our serial number: 6. These two instructions compare the length of our serial number to 0x0D, which is 13 in decimal. The next instruction, BNE 'IBeg 0080 01E DDSS'+00194, will Branch to offset +00194 if the comparison of the two bytes is Not Equal (technically if the subtraction of the two is non-zero, which is why BNE is sometimes denoted BNZ).
Looking further down our listing for offset +00194:
+00194 MOVEQ #$00,D0 | 7000 +00196 BRA.S 'IBeg 0080 012E DDSS'+0019A | 6002 +00198 MOVEQ #$01,D0 | 7001 +0019A MOVEA.L (A7)+,A3 | 265F +0019C UNLK A6 | 4E5E +0019E RTS | 4E75
If the length of our serial number is not 13, it will set D0 to all zeros (MOVEQ #$00,D0) then branch (BRA) to 'IBeg 0080 012E DDSS' +0019A, which cleans up and returns from the subroutine. We can s all the way through this to confirm that the branches do in fact happen. Once we step passed RTS and back to the previous subroutine, these two instructions are next:
+0003C MOVE.B D0,D6 | 1C00 +0003E TST.B D6 | 4A06
This block will move D0 into D6 (MOVE.B D0,D6) and then test if D6 is zero (TST.B D6). Remember that just before in the previous subroutine, D0 was set to all zeros (+00194) when our serial number length was not 13. Thinking back to our pseudocode, it seems reasonable to suspect that D0 is in fact the validSN variable and that if D0 is non-zero, we have a valid serial number. If we can force validSN to be 1, then the application will think we entered a valid serial number. How can we test this in MacsBug?
Before we step passed the two instructions starting at +0003C, let’s do d0=1. Notice how the D0 register is now red and set to 00000001.

Let’s g to resume program execution. We should be at the install screen… We just cracked the program!

At this point, we could use Resorcerer to modify the application and make the crack permanent. We could do this a number of different ways: changing branches, always setting D0 to 1, or by bypassing the serial number prompt altogether. I will be going into more detail about this in a future tutorial, but for now, let’s do the hard work and figure out the validation algorithm.
The Algorithm
Everything that we have done up to this point can be generalized across all software: there’s a prompt, user input, a validation check, and a result. The validation check is unique to each application and can span dozens of lines to thousands of lines.
This is a listing of the current validation subroutine:
'IBeg 0080 012E DDSS'
+000D2 *LINK A6,#$0000 | 4E56 0000
+000D6 MOVE.L A3,-(A7) | 2F0B
+000D8 MOVEA.L $0008(A6),A3 | 266E 0008
+000DC CMPI.B #$0D,(A3) | 0C13 000D
+000E0 BNE 'IBeg 0080 012E DDSS'+00194 | 6600 00B2
+000E4 ADDQ.L #$1,A3 | 528B
+000E6 MOVE.B (A3),-(A7) | 1F13
+000E8 JSR 'IBeg 0080 012E DDSS'+001A0 | 4EBA 00B6
+000EC TST.B D0 | 4A00
+000EE ADDQ.L #$2,A7 | 548F
+000F0 BEQ 'IBeg 0080 012E DDSS'+00194 | 6700 00A2
+000F4 ADDQ.L #$1,A3 | 528B
+000F6 MOVE.B (A3),-(A7) | 1F13
+000F8 JSR 'IBeg 0080 012E DDSS'+001A0 | 4EBA 00A6
+000FC TST.B D0 | 4A00
+000FE ADDQ.L #$2,A7 | 548F
+00100 BEQ 'IBeg 0080 012E DDSS'+00194 | 6700 0092
+00104 ADDQ.L #$1,A3 | 528B
+00106 MOVE.B (A3),-(A7) | 1F13
+00108 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 00C4
+0010C TST.B D0 | 4A00
+0010E ADDQ.L #$2,A7 | 548F
+00110 BEQ 'IBeg 0080 012E DDSS'+00194 | 6700 0082
+00114 ADDQ.L #$1,A3 | 528B
+00116 MOVE.B (A3),-(A7) | 1F13
+00118 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 00B4
+0011C TST.B D0 | 4A00
+0011E ADDQ.L #$2,A7 | 548F
+00120 BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6772
+00122 ADDQ.L #$1,A3 | 528B
+00124 CMPI.B #$35,(A3) ; '5' | 0C13 0035
+00128 BNE.S 'IBeg 0080 012E DDSS'+00194 | 666A
+0012A ADDQ.L #$1,A3 | 528B
+0012C MOVE.B (A3),-(A7) | 1F13
+0012E JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 009E
+00132 TST.B D0 | 4A00
+00134 ADDQ.L #$2,A7 | 548F
+00136 BEQ.S 'IBeg 0080 012E DDSS'+00194 | 675C
+00138 ADDQ.L #$1,A3 | 528B
+0013A MOVE.B (A3),-(A7) | 1F13
+0013C JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 0090
+00140 TST.B D0 | 4A00
+00142 ADDQ.L #$2,A7 | 548F
+00144 BEQ.S 'IBeg 0080 012E DDSS'+00194 | 674E
+00146 ADDQ.L #$1,A3 | 528B
+00148 MOVE.B (A3),-(A7) | 1F13
+0014A JSR 'IBeg 0080 012E DDSS'+001A0 | 4EBA 0054
+0014E TST.B D0 | 4A00
+00150 ADDQ.L #$2,A7 | 548F
+00152 BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6740
+00154 ADDQ.L #$1,A3 | 528B
+00156 MOVE.B (A3),-(A7) | 1F13
+00158 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 0074
+0015C TST.B D0 | 4A00
+0015E ADDQ.L #$2,A7 | 548F
+00160 BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6732
+00162 ADDQ.L #$1,A3 | 528B
+00164 MOVE.B (A3),-(A7) | 1F13
+00166 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 0066
+0016A TST.B D0 | 4A00
+0016C ADDQ.L #$2,A7 | 548F
+0016E BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6724
+00170 ADDQ.L #$1,A3 | 528B
+00172 MOVE.B (A3),-(A7) | 1F13
+00174 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 0058
+00178 TST.B D0 | 4A00
+0017A ADDQ.L #$2,A7 | 548F
+0017C BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6716
+0017E ADDQ.L #$1,A3 | 528B
+00180 MOVE.B (A3),-(A7) | 1F13
+00182 JSR 'IBeg 0080 012E DDSS'+001CE | 4EBA 004A
+00186 TST.B D0 | 4A00
+00188 ADDQ.L #$2,A7 | 548F
+0018A BEQ.S 'IBeg 0080 012E DDSS'+00194 | 6708
+0018C ADDQ.L #$1,A3 | 528B
+0018E CMPI.B #$4C,(A3) ; 'L' | 0C13 004C
+00192 BEQ.S 'IBeg 0080 012E DDSS'+00198 | 6704
+00194 MOVEQ #$00,D0 | 7000
+00196 BRA.S 'IBeg 0080 012E DDSS'+0019A | 6002
+00198 MOVEQ #$01,D0 | 7001
+0019A MOVEA.L (A7)+,A3 | 265F
+0019C UNLK A6 | 4E5E
+0019E RTS | 4E75
We already figured out that +000D8 to +000E0 was testing that the length of the serial number was equal to 13. Let’s carry on at +000E4. Notice there is a repeating pattern of: ADDQ, MOVE, JSR, TST, ADDQ, BEQ. What is interesting here is that there are 11 JSR instructions followed by BEQs, one comparison to ‘5’ at +00124, and one comparison to ‘L’ at +0018E. That’s a total of 13 comparisons — the same as our serial number length. Those 11 JSR instructions are only going to two subroutines: 'IBeg 0080 012E DDSS'+001A0 and 'IBeg 0080 012E DDSS'+001CE. After returning from those two subroutines, it will test D0 (TST.B D0) and then branch if equal (BEQ) to 'IBeg 0080 012E DDSS'+00194. This means that if the subroutine returns 0 in D0, it will branch to the same cleanup and return section of the current subroutine as it did when the length of the serial number was not equal to 13. So, we know that the two subroutines must always return a non-zero result to have a valid serial number.
The first routine looks like this:
'IBeg 0080 012E DDSS'
+001A0 LINK A6,#$0000 | 4E56 0000
+001A4 MOVE.L D7,-(A7) | 2F07
+001A6 MOVE.B $0008(A6),D7 | 1E2E 0008
+001AA CMPI.B #$41,D7 ; 'A' | 0C07 0041
+001AE BCS.S 'IBeg 0080 012E DDSS'+001B6 | 6506
+001B0 CMPI.B #$5A,D7 ; 'Z' | 0C07 005A
+001B4 BLS.S 'IBeg 0080 012E DDSS'+001C6 | 6310
+001B6 CMPI.B #$61,D7 ; 'a' | 0C07 0061
+001BA BCS.S 'IBeg 0080 012E DDSS'+001C2 | 6506
+001BC CMPI.B #$7A,D7 ; 'z' | 0C07 007A
+001C0 BLS.S 'IBeg 0080 012E DDSS'+001C6 | 6304
+001C2 MOVEQ #$00,D0 | 7000
+001C4 BRA.S 'IBeg 0080 012E DDSS'+001C8 | 6002
+001C6 MOVEQ #$01,D0 | 7001
+001C8 MOVE.L (A7)+,D7 | 2E1F
+001CA UNLK A6 | 4E5E
+001CC RTS | 4E75
Can you guess just by glancing at it what it is doing? The comparisons to ‘A’, ‘Z’, ‘a’, and ‘z’ should give it away. It’s simply checking to see if the byte at D7 is between ‘A’ and ‘Z’ or ‘a’ and ‘z’.
The byte at D7 happens to be the next character in the serial number. How it winds up in D7 isn’t terribly important, as it has to do with linking (LINK) the stack and local variables. The quick and dirty way to see what D7 is to just type d7. If we look at +001A6, MOVE.B $0008(A6),D7 is moving the byte at A6+8 into D7. We can dm a6+8 to see what’s there, and it’s the next byte of our serial number.
The individual BCS and BLS instructions in the routine can be translated as Branch on LOwer (BCS is equivalent to BLO) and Branch on Lower or Same. So, if the byte is not in the range of ‘A’ to ‘Z’, check if it’s in the range of ‘a’ to ‘z’. If the byte is in either range, +001C6 will set D0 to 1 (MOVEQ #$01,D0) and return, otherwise, +001C2 will set D0 to 0 (MOVEQ #$00,D0) and then return. Put simply, this first routine is checking if the input byte is a letter.
The second routine looks like this:
'IBeg 0080 012E DDSS'
+001CE LINK A6,#$0000 | 4E56 0000
+001D2 CMPI.B #$30,$0008(A6) ; '0' | 0C2E 0030 0008
+001D8 BCS.S 'IBeg 0080 012E DDSS'+001E2 | 6508
+001DA CMPI.B #$39,$0008(A6) ; '9' | 0C2E 0039 0008
+001E0 BLS.S 'IBeg 0080 012E DDSS'+001E6 | 6304
+001E2 MOVEQ #$00,D0 | 7000
+001E4 BRA.S 'IBeg 0080 012E DDSS'+001E8 | 6002
+001E6 MOVEQ #$01,D0 | 7001
+001E8 UNLK A6 | 4E5E
+001EA RTS | 4E75
In much the same way as the first routine, this routine is checking to see if the input byte is a number (if A6+8 is between ‘0’ and ‘9’). If subroutine +001A0 checks for letters and subroutine +001CE checks for numbers, we can deduce structure of a valid serial number given the validation routine at +000D2 (a = alpha; n = number):
a a n n 5 n n a n n n n L 01 02 03 04 05 06 07 08 09 10 11 12 13
Following this format, let’s restart the application and try serial number: AB12512A1234L. Success!
Conclusion
That’s a basic, but detailed introduction into what it takes to reverse engineer software. After the setup and learning the various MacsBug commands, it’s really just a matter of finding the validation routine and going through it one line at a time. The algorithm here is fairly straightforward, but even in its simplicity, it still requires careful consideration. Did you notice that the trailing ‘L’ must be uppercase (+0018E)? Future tutorials in this series will not only look at much more complicated routines, but also some of the clever ways that they are hidden.