Now that you've had a simple introduction to RCE on Windows CE, the next section provides a legal and hands-on tutorial of how to bypass serial protection. We describe multiple methods of circumvention of the protection scheme, which shows there's more than one "right" way to do it. We use the previous discussion as a foundation.
For our example, we use our own program, called serial.exe. This program was written in Visual C++ to provide you with a real working product on which to test and practice your newly acquired knowledge. Our program simulates a simple serial number check that imitates those of many professional programs. You will see firsthand how a cracker can reverse engineer a program to allow any serial number, regardless of length or value. To obtain this embedded crackme, please download serial.exe from http://www.securitywarrior.com.
You must first load the target file into a disassembler from the local computer, using the steps we covered earlier. In this case, we are targeting a file called serial.exe, written solely for this example (Figure 4-13).
Once the program is open, drill down to a point in the program where you can monitor what is happening. As previously discussed, there are several function calls that flag an event worth inspection. For example, using the Names window, we can locate a wcscmp call, which is probably used to validate the entered serial number with the corrected serial number. Using this functions XREF, we can easily locate the chunk of code illustrated in Figure 4-13.
Since serial.exe is a relatively simple program, all the code we need to review and play with is located within a few lines. They are as follows:
.text:00011224 MOV R4, R0 .text:00011228 ADD R0, SP, #0xC .text:0001122C BL CString::CString(void) .text:00011230 ADD R0, SP, #8 .text:00011234 BL CString::CString(void) .text:00011238 ADD R0, SP, #4 .text:0001123C BL CString::CString(void) .text:00011240 ADD R0, SP, #0x10 .text:00011244 BL CString::CString(void) .text:00011248 ADD R0, SP, #0 .text:0001124C BL CString::CString(void) .text:00011250 LDR R1, =unk_131A4 .text:00011254 ADD R0, SP, #0xC .text:00011258 BL CString::operator=(ushort) .text:0001125C LDR R1, =unk_131B0 .text:00011260 ADD R0, SP, #8 .text:00011264 BL CString::operator=(ushort) .text:00011268 LDR R1, =unk_131E0 .text:0001126C ADD R0, SP, #4 .text:00011270 BL ; CString::operator=(ushort) .text:00011274 LDR R1, =unk_1321C .text:00011278 ADD R0, SP, #0 .text:0001127C BL CString::operator=(ushort) .text:00011280 MOV R1, #1 .text:00011284 MOV R0, R4 .text:00011288 BL CWnd::UpdateData(int) .text:0001128C LDR R1, [R4,#0x7C] .text:00011290 LDR R0, [R1,#-8] .text:00011294 CMP R0, #8 .text:00011298 BLT loc_112E4 .text:0001129C BGT loc_112E4 .text:000112A0 LDR R0, [SP,#0xC] .text:000112A4 BL wcscmp .text:000112A8 MOV R2, #0 .text:000112AC MOVS R3, R0 .text:000112B0 MOV R0, #1 .text:000112B4 MOVNE R0, #0 .text:000112B8 ANDS R3, R0, #0xFF .text:000112BC LDRNE R1, [SP,#8] .text:000112C0 MOV R0, R4 .text:000112C4 MOV R3, #0 .text:000112C8 BNE loc_112F4 .text:000112CC LDR R1, [SP,#4] .text:000112D0 B loc_112F4 .text:000112E4 .text:000112E4 loc_112E4 ; CODE XREF: .text:00011298 .text:000112E4 ; .text:0001129C .text:000112E4 LDR R1, [SP] .text:000112E8 MOV R3, #0 .text:000112EC MOV R2, #0 .text:000112F0 MOV R0, R4 .text:000112F4 .text:000112F4 loc_112F4 ; CODE XREF: .text:000112C8 .text:000112F4 ; .text:000112D0 .text:000112F4 BL CWnd_ _MessageBoxW
If you have not touched anything after IDA placed you at address 0x000112A4, then that line should be highlighted blue. If you want to go back to the last address, use the back arrow at the top of the window or hit the Esc key.
Since we want to show you several tricks crackers use when extracting or bypassing protection, let's start by considering what we are viewing. At first glance at the top of our code, you can see there is a pattern. A string value appears to be loaded in from program data, and then a function is called that does something with that value. If we double-click on unk_131A4, we can see what the first value is "12345678", or our serial number. While our serial.exe example is simplified, the fact remains that any data used in a program's validation must be loaded in from the actual program data and stored in RAM. As our example illustrates, it doesn't take much to discover a plain text serial number. In addition, it should be noted that any hex editor can be used to find this value, although it may be difficult to parse out a serial number from the many other character strings that are revealed in a hex editor.
As a result of this plain text problem, many programmers build an algorithm into the program that deciphers the serial number as it is read in from memory. It's typically indicated by a BL to the memory address in the program that handles the encryption/algorithm. An example of another method of protection is to use the device owner's name or some other value to dynamically build a serial number. This completely avoids the problems, surrounding and storing it within the program file, and indirectly adds an extra layer of protection on to the program. Despite efforts to create complex and advanced serial number creation schemes, the simple switch of a 1 to a 0 can nullify many antipiracy algorithms, as you will see.
The remaining code from 0x00011250 to 0x0001127C is also used to load values from program data to the device's RAM. If you check the values at the address references, you can quickly see that three messages are loaded into memory as well. One is a "Correct serial" message, and the other two are "Incorrect serial" messages. Knowing that there are two different messages is a minor but important tidbit of information, because it tells us that failure occurs in stages or as a result of two different checks.
Moving through the code, we see that R1 is loaded with some
value out of memory, which is used to load another value into R0.
After this, in address 0x00011294, we can see that R0 is compared
to the number eight (
#8). The next two lines check the result
of the comparison, and if it is greater than or less than eight,
the program jumps to loc_112E4 and continues from there.
If we follow loc_112E4 in IDA Pro, it starts to get a bit more difficult to determine what is happening, which brings us to the second phase of the reverse engineering process: the live debugger.
As we illustrated when debugging test.exe, the MVT is a very useful tool that can help a debugger, or a cracker, work through a program's execution line by line. This type of intimate relationship allows an in-depth look at the values being processed and can also allow on-the-fly alteration of data that is stored in the registers, flags, and memory.
After the program is loaded, set a breakpoint at 0x00011280, with any changes as defined by the absolute memory block. Once the breakpoint is entered, hit the F5 key to execute the program. You should now see a Serial screen on your Pocket PC as in Figure 4-14. Enter any value in the text box and hit the Submit button.
After you click the Submit button, your PC should shift focus to the section of code we looked at earlier in IDA. Notice the little yellow arrow on the left side of the window, pointing to the address of the breakpoint. Right-click on the memory address column and note the menu that appears. You will use this menu quite frequently when debugging a program.
The MVT is slow in execution mode when it's using a USB/serial connection. If you are in the habit of jumping between programs, you will quickly become frustrated at the time required for the MVT to redraw the screen. To avoid these delays, ensure the MVT is in break mode before changing window focus.
At this point, serial.exe is
loaded on the Pocket PC and the MVT is paused at a breakpoint. The
next command the processor executes
#1. This is a simple command to
move the value 1 into register 1 (R1).
Before executing this line, look at the Registers window and note the value of R1. You should also note that all the register values are red; this is because they have all changed from the last time the program was paused. Now, hit the F11 key to execute the next line of code. After a short pause, the MVT returns to pause mode, at which time you should notice several things. The first is that most of the register values turned to black, which means they did not change values. The second is that R1 now equals 1.
The next line loads the R0 register with the value in R4. Once again, hit the F11 key to let the program execute this line of code. After a brief pause, you will see that R0 is equal to R4. Step through a few more lines of code until your yellow arrow is at address 0x00011290. At this point, let's take a look at the Registers window.
The last line of code executed was an LDR command that loaded a value (or address representing the value) from memory into a register. In this case, the value was loaded into R1, which should be equal to 0006501C. Locate the Memory window and enter the address stored by R1 into the "Address:" box. Once you hit Enter, you should see the serial number you entered.
After executing the next line, we can see that R0 is given a small integer value. Take a second and see if you can determine its significance. In R0, you should have a value equal to the number of characters in the serial you entered. In other words, if you entered "777", the value of R0 should be 3, which represents the number of characters you entered.
The next line,
is a simple comparison opcode. When this opcode is executed, it will
compare the value in R0 with the integer 8. Depending on the results
of the comparison, the status flags will be updated. These flags are
conveniently located at the bottom of the Registers window. Note
their values and hit the F11 key. If the values change to N1 Z0 C0
O0, your serial number is not 8 characters long.
At this point, serial.exe is headed for a
failure message (unless you happened to enter eight characters). The
next two lines of code use the results of the CMP to determine if
the value is greater than or equal to eight. If either is true, the
program jumps to address 0x000112E4, where a message will be
displayed on the screen. If you follow the code, you will see that
address 0x000112E4 contains the opcode
[SP]. If you follow this through
and check the memory address after this line executes, you will see
that it points to the start of the following error message at
address 0x00065014: "Incorrect serial number. Please verify it was
Now that we know the details of the first check, we want to
break the execution and restart the entire program. Perform the same
steps that you previously worked through, but set a breakpoint at
address 0x00011294 (
#8). Once the program is paused at the CMP
opcode, locate the Registers window and note the value of R0. Now,
place your cursor on the value and overwrite it with "00000008".
This very handy function of the MVT allows you to trick the program
into thinking your serial is eight characters long, thus allowing
you to bypass the check. While this works temporarily, we will need
to make a permanent change to the program to ensure any value is
acceptable at a later point.
After the change is made, use the F11 key to watch serial.exe execute through the next few lines of code. Then, continue until the pointer is at address 0x000112A4 (BL 00011754). While this command may not mean much to you in the MVT, if we jump back over to IDA Pro we can see that this is a function call to wcscmp, which is where our serial is compared to the correct serial. Knowing this, we should be able to take a look at the Registers window and determine the correct serial.
Function calls that require data to perform their operations use the values held by the registers. In other words, wcscmp will compare the values of R0 with the value of R1, which means we can easily determine what these values are. It then returns a true or false in R1.
If we look at R0 and R1, we can see that they hold the values 00064E54 and 0006501C, respectively, as illustrated by Figure 4-15 (these values may be different for your system). While these values are not the actual serial numbers, they do represent the locations in memory where the two serials are located. To verify this, place R1's value in the Memory window's "Address:" field and hit Enter. After a short pause, the Memory window should change, and you should see the serial number you entered. Next, do the same with the value held in R0. This will cause your Memory window to change to a screen similar to Figure 4-16, in which you should see the value "220.127.116.11.18.104.22.168"—in other words, the correct serial.
At this point, a cracker could stop and simply enter the newfound value to gain full access to the target program, and he could also spread the serial number around on the Internet. However, many serial validations include some form of dynamically generated serial number (based on time, name, or a matching registration key), which means any value determined by viewing it in memory will only work for that local machine. As a result, crackers often note the serial number and continue on to determine where the program can be "patched" in order to bypass the protection, regardless of the dynamic serial number.
Moving on through the program, we know the wcscmp function will compare the values held in memory, which results in an update to the condition flags and R0-R4, as follows:
If the serials are equal, R0 = 0; else R0 = 1.
If equal, address following entered serial number; else, address of failed character.
If equal, R2 = 0; else, hex value of failed character.
If equal, R3 = 0; else, hex value of correct character.
We need to once again trick the program into believing it has the right serial number. This can be done one of two ways. The first method is to actually update your serial number in memory. To do this, note the hex values of the correct serial (i.e., 31 00 32 00 33 00 34 00 35 00 36 00 37 00 38), and overwrite the entered serial number in the Memory window. When you are done, your Memory window should look like Figure 4-17.
The second method a cracker can use is to update the condition flags after the wcscmp function has updated the status flags. To do this, hit F11 until the pointer is at 0x000112A8. You should note that the Z condition flags change from 1 (equal) to 0 (not equal). However, if you don't like this condition, you can change the flags back to their original values by overwriting them. Once you do this, the program will once again think the correct serial number was entered. While this temporarily fixes the serial check, a lasting solution requires an update to the program's code.
Fortunately, we do not have to look far to find a weak point. The following explains the rest of the code that is processed until a message is provided on the Pocket PC, alerting the user to a correct (or incorrect) serial number.
This opcode clears out the R2 register so there are no remaining values that could confuse future operations:
260112A8 mov r2, #0
In the next opcode, two events occur. The first is that R0 is moved into R3. The second event updates the status flags using the new value in R3. As we previously mentioned, R0 is updated from the wcscmp function. If the entered serial number matched the correct serial number, R0 will be updated with a 0. If they didn't match, R0 will be set to 1. R3 is then updated with this value and checked to see if it is negative or zero.
260112AC movs r3, r0 Moves R0 into R3 and updates the status flags
Next, the value #1 is moved into R0. This may seem a bit odd, but by moving #1 into R0, the program is setting the stage for the next couple of lines of code.
260112B0 mov r0, #1 Move #1 into R0
Next, we see another altered MOV command. In this case, the value #0 will be moved into R0 only if the condition flags are not equal (ne), which is based on the status update performed by the previous MOV. In other words, if the serials matched, R0 would have been set to 0 and the Zero flag would have been set to 1, which means the MOVNE opcode would not be executed.
260112B4 movne r0, #0 If flags are not equal, move #0 into R0
Like the MOV opcode, the ANDS command first executes and then updates the status flags depending on the result. Looking at the last few lines, we can see that R0 should be 1 if the serials did not match. This is because R0 was set to equal #1 a few lines up and was not changed by the MOVNE opcode. Therefore, the AND opcode would result in R3 being set to the value of #1, and the condition flags would be updated to reflect the "equal" status. On the other hand, if the serials did match, R0 would be equal to 1, which would have caused the Zero flag to be set to 0, or "not equal."
260112B8 ands r3, r0, 0xFF
Next, we see another implementation of the "not equal" conditional opcode. In this case, if the ANDS opcode set the Z flag to 0—which would occur only if the string check passed—the LDRNE opcode would load R1 with the data in SP+8. Recall from our dissection of code in IDA Pro that address 0x0001125C loaded the "correct message" into this memory location. However, if the condition flags are not set at "not equal" or "not zero," this opcode will be skipped.
260112BC ldrne r1, [sp, #8]
This is an example of a straightforward move of R4 into R0:
260112C0 mov r0, r4 Move R4 into R0
This is another example of a simple move of #0 to R3:
260112C4 mov r3, #0 Move #0 into R3
Again, we see a conditional opcode. In this case, the program will branch to 0x000112F4 if the "not equal" flag is set. Since the conditional flags have not been updated since the ANDS opcode in address 0x000112B8, a correct serial number would result in the execution of this opcode.
260112C8 bne 260112F4 ; If flag not equal jump to 0x260112F4
If the wrong eight-character serial number was entered, this line would load the "incorrect" message from memory into R1:
260112CC ldr r1, [sp, #4] Load SP+4 into R1 (incorrect message)
This line tells the program to branch to address 0x260112F4:
260112D0 b 260112F4 ; Jump to 0x260112F4
The final line we will look at is the call to the MessageBoxW function. This command simply takes the value in R1, which will either be the correct message or the incorrect message, and displays it in a message box.
... 260112F4 bl 26011718 ; MessageBoxW call to display message in R1
Now that we have dissected the code, we must alter it to ensure that it will accept any serial number as the correct value. As we have illustrated, when executing the program in the MVT, we can crack the serial fairly easily by changing the register values, memory, or condition flags during program execution. However, this type of legerdemain is not going to help the average user who has no interest in reverse engineering. As a result, a cracker will have to make permanent changes to the code to ensure the serial validation will always validate the entered serial.
To do this, the cracker has to find a weak point in the code that can be changed in order to bypass security checks. Fortunately for the cracker, there is typically more than one method by which a program can be cracked. To illustrate, we demonstrate three distinct ways that serial.exe can be cracked using basic techniques.
The first method requires three separate changes to the code. The first change is at address 00011294, where R0 is compared to the value #8. If you recall, this is used to ensure that the user-provided serial number is exactly eight characters long. The comparison then updates the condition flags, which are used in the next couple of lines to determine the flow of the program.
To ensure that the flags are set at "equal," we need to
alter the compared values. The easiest way to do this is to have
the program compare two equal values (i.e.,
R0). This ensures the comparison returns as "equal,"
thus tricking the program into passing over the BLT and BGT
opcodes in the next two lines.
The next change is at address 0x000112B4, where we find a
#0 command. As we previously discussed,
this command checks the flag conditions, and if they are set at
"not equal," the opcode moves the value #0 into R0. The R0 value
is then checked when it is moved into R3, which updates the status
flags once again.
Since the MOVS command at address 00112AC will set Z = 0
(unless the correct serial is entered), the MOVNE opcode will then
execute, thus triggering a chain of events that results in a
failed validation. To correct this, we need to ensure the program
thinks R0 is always equal to #1 at line 000112B8 (
#0xFF). Since R0 would have been changed
to #1 in address 000112B0 (
#1), the ANDS opcode would result in a
"not equal" for a correct serial.
In other words, we need to change
#1 to ensure that
FF outputs 1, which is then used to
update the status flags. The program will thus be tricked into
validating the incorrect serial.
Here are the changes:
.text:00011294 CMP R0, #8 -> CMP R0, R0 .text:000112B4 MOVNE R0, #0 -> MOVNE R0,#1
Determining the necessary changes is the first step to cracking a program. The second step is to actually alter the file. To do this, a cracker uses a hex editor to make changes to the actual .exe file. However, in order to do this, the cracker must know where in the program file she needs to make changes. Fortunately, if she is using IDA Pro, a cracker only has to click on the line she wants to edit and look at the status bar at the bottom of IDA's window, as we previously discussed. As Figure 4-18 illustrates, IDA clearly displays the memory address of the currently selected line, which can then be used in a hex editor.
Once we know the addresses where we want to make our changes, we will need to determine the values with which we want to update the original hex code. (Fortunately, there are several online reference guides that can help.) We want to make the changes shown in Table 4-4 to the serial.exe file.
Table 4-4. Changes to serial.exe
CMP: R0, #8
08 00 50 E3
CMP R0, R0
00 00 50 E1
MOVNE R0, #0
00 00 A0 13
MOVNE R0, #1
01 00 A0 13
To make the changes, perform the following procedures (using UltraEdit).
Open UltraEdit and then open your local serial.exe file in UltraEdit.
Using the left-most column, locate the desired hex address.
Move to the hex code that needs to be changed, and overwrite it.
Save the file as a new file, in case you made a mistake.
The next example uses some of the same tactics as Crack 1, but it also introduces a new method of bypassing the eight-character validation, known as NOP.
The term NOP is a reference to a nonoperation, which means the code is basically null. Many crackers and hackers are familiar with the term NOP due to its prevalence in buffer overflow attacks. In buffer overflows, a NOP slide (as it is often called) is used to make a part of the program do absolutely nothing. The same NOP slide can be used when bypassing a security check in a program.
In our program, we have a CMP opcode that compares the length of the entered serial with the number 8. This results in a status change of the condition flags, which are used by the next two lines to determine if they are executed. While our previous crack bypassed this by ensuring the flags were set at "equal," we can attack the BLT and BGT opcodes by overwriting them with a NOP opcode. Once we do this, the BLT and BGT opcodes no longer exist.
Typical x86 NOPing is done using a series of 0x90s. This
will not work on an ARM processor and will
result in the following opcode:
R0. This opcode actually performs an
unsigned multiply long if the LS condition is met, and then
updates the status flags accordingly. It is not a NOP.
The trick we learned to perform a NOP on an ARM processor is
to simply replace the target code with a
R1 operation. This will move the value
R1 into R1 and will not update the status flags. The following
code illustrates the NOPing of these opcodes.
.text:00011298 BLT loc_112E4 -> MOV R1, R1 .text:0001129C BGT loc_112E4 -> MOV R1, R1
The second part of this crack was already explained in Crack 1 and requires only the alteration of the MOVNE opcode, as the following portrays:
.text:000112B4 MOVNE R0, #0 -> MOVNE R0,#1
Table 4-5 describes the changes you will have to make in your hex editor.
At this point you are probably wondering what the point of another example is when you already have two crack methods that work just fine. However, we have saved the best example for last—Crack 3 does not attack or overwrite any checks or validation opcodes, like our previous two examples. Instead, it demonstrates how to alter the registers to our benefit before any values are compared.
If you examine the opcode at 0x00001128C using the MVT, you will see that it sets R1 to the address of the serial that you entered. The length of the serial is then loaded into R0 in the next line, using R1 as the input variable. If the value pointed to by the address in R1 is eight characters long, it is then bumped up against the correct serial number in the wcscmp function. Knowing all this, we can see that the value loaded into R1 is a key piece of data. So, what if we could change the value in R1 to something more agreeable to the program, such as the correct serial?
While this is possible by using the stack pointer to guide us, the groundwork has already been done in 0x0000112A0, where the correct value is loaded into R0. Logic assumes that if it can be loaded into R0 using the provided LDR command, then we can use the same command to load the correct serial into R1. This would trick our validation algorithm into comparing the correct serial with itself, which would always result in a successful match!
The details of the required changes are as shown in Table 4-6.
Table 4-6. Changes to serial.exe for Crack 3
LDR R1, [R4, #0x7C]
7C 10 94 E5
LDR R1, [SP,#0xC]
0C 10 9D E5
Note that this crack only requires the changing of two hex characters (i.e., 7 → 0 and 4 → D). This example is by far the most elegant and foolproof of the three, which is why we saved it for last. While the other two examples are just as effective, they are each a reactive type of crack that attempts to fix a problem. This crack, on the other hand, is a preventative crack that corrects the problem before it becomes one.