Sandwich CrackMe - Tutorial
Task:
This crackme is a simple task and very well designed for the ones of us who are beginners in the world of reservse engineering. I'd give it a 2 / 10 in difficulty. Anyhow to solve it you need some basic knowledge in assembler language and how to use a disassembler / debugger like Hopper or IDA. I wrote this article about solving the crackme to show you an easy approach on how to proceed step-by-step.
Requirements:
- 1-Sandwich crackme
- XCode / LLDB
- Disassembler like Hopper or IDA free
Task to-do's:
- Launch the app or at least try to (don't worry if it won't run just read on)
- Decompile the App / Executable file
- Find the validation function
- Convert to psudo code (optional)
- Reconstruct with XCode / Objectiv-C
- Create serial generator
1. Launch the app
The first thing you always should do when attempting to reverse engineer, crack or patch an app is - Launch it and examine its functions! It's the only way to get a real feeling of the app and get to know how it works and how the workflow is. You can gather a lot of information just by clicking around the app and test it's functionalities. You also may get hints to texts, buttons, menus, labels etc. used and so you may find yourself comfortable a lot quicker in the disassebly of the app which you're going to examine later on in the reverse engineering process.
Check if it's running on your OS version - I didn't manage to get it to run on macOS BigSur - but on macOS High Sierra the app runs very well. Don't worry if it's not starting on your device - also if it has a blocked icon in the finder which says that the app is not compatible with your os version. Even if it's not running on your system, you can generate a keygen just fine by decompiling it and analyzing the machO (assembler) and / or pseudo code.
Here you can see the main (only) screen of the macOS / OSX Sandwich crackme app (1-Sandwich) from reverse.put.as/crackmes/ - with some numbers entered by me to test the serial validation function:
After clicking on the "Validate" button the app calls a function to check the entered serial. As excpected the validation of my test-serial fails and the crackme shows the following message:
So far so well ... It's time now to dig in and visit the code of this app. What we want to do now is to find the validation function for the serial check. So let's start!
2. Find the validation function
After trying to launch and examining the app is to disassemble the executable with a decompiler and try to find the serial validate function. We already have gathered some interessting informations by running the app and trying to validate a test serial. If you couldn't run the app - don't worry just carry on and continue reading!
Cranck up the disassembler of your choice (I use the recent Hopper demo version - but you can also use IDA free) and analyze the Sandwich crackme app. It's a fairly small executable and you won't have much troubles finding the validate:(void*)arg2 function. If you remember the message of the failed serial check "Error!" (Title), "The serial is not valid." (Message) and "Try again" (Button) you can also try to search the disassebly for this strings. Of course the more unique a string you search is, the more precise your results will be and you will get to the "juice" a lot quicker.
The following block is the disassembled code of the validate function. Analyze it to get an impression of what the function is doing.
00001d0e push ebp ; Objective C Implementation defined at 0x30ac (instance method) 00001d0f mov ebp, esp 00001d11 push ebx 00001d12 sub esp, 0x24 00001d15 mov ebx, dword [ebp+self] 00001d18 mov edx, dword [ebx+8] 00001d1b mov eax, dword [objc_msg_stringValue] ; @selector(stringValue) 00001d20 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001d24 mov dword [esp+0x28+var_28], edx ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001d27 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001d2c mov dword [esp+0x28+var_20], eax 00001d30 mov eax, dword [objc_msg_validateSerial_] ; @selector(validateSerial:) 00001d35 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001d39 mov dword [esp+0x28+var_28], ebx ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001d3c call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001d41 test al, al 00001d43 jne loc_1d6e <= THIS IS THE CHECK IF VALIDATION SUCCEEDED !!! 00001d45 mov dword [esp+0x28+var_18], 0x0 00001d4d mov dword [esp+0x28+var_1C], 0x0 00001d55 mov dword [esp+0x28+var_20], cfstring_Try_again ; @"Try again" <= THE BUTTON TITLE OF ALERT BOX 00001d5d mov dword [esp+0x28+var_24], cfstring_The_serial_is_not_valid_ ; @"The serial is not valid." <= ALERT BOX MESSAGE 00001d65 mov dword [esp+0x28+var_28], cfstring_Error_ ; @"Error!" <= ALERT BOX TITLE 00001d6c jmp loc_1d95 loc_1d6e: 00001d6e mov dword [esp+0x28+var_18], 0x0 ; CODE XREF=-[SandwichAppDelegate validate:]+53 00001d76 mov dword [esp+0x28+var_1C], 0x0 00001d7e mov dword [esp+0x28+var_20], cfstring_OK ; @"OK" <= THE BUTTON TITLE 00001d86 mov dword [esp+0x28+var_24], cfstring_The_serial_is_valid_ ; @"The serial is valid." <= ALERT BOX MESSAGE 00001d8e mov dword [esp+0x28+var_28], cfstring_Success_ ; @"Success!" <= ALERT BOX TITLE loc_1d95: 00001d95 call imp___symbol_stub__NSRunAlertPanel ; NSRunAlertPanel, CODE XREF=-[SandwichAppDelegate validate:]+94 00001d9a add esp, 0x24 00001d9d pop ebx 00001d9e leave 00001d9f ret ; endpAs you can see this where the app decides wether the entered serial was validated successfully or not (@ address 0x00001d43) and in return presents you with either an error or the success message.
If you use Hopper (I'd really recommend you to use Hopper even if its just the demo version!) there is a neat feature to convert the asm code into pseudo code. My conversion into pseudo code looks as follows:
/* @class SandwichAppDelegate */ -(void)validate:(void *)arg2 { if ([self validateSerial:[*(self + 0x8) stringValue]] == 0x0) { <= This is the check @ address 0x00001d43 var_20 = @"Try again"; var_24 = @"The serial is not valid."; var_28 = @"Error!"; } else { var_20 = @"OK"; var_24 = @"The serial is valid."; var_28 = @"Success!"; } NSRunAlertPanel(var_28, var_24, var_20, 0x0, 0x0); return; }
This representation of the function is obiously much shorter and quit easier to analyze. Not just that, but you also can most of the times take the pseudo code and copy it into a new XCode project (Objective-C) to rebuild the functionalities you need in a test app. So this powerfull feature should not be underestimated and missed - at least if you want to save lots of time and headscratching ;-).
As we see the validate function calls another function validateSerial:(void*)arg2 whichs return value is used to check wether the validation succeeded or not. So we continue by examining the validateSerial function. The disassembled code looks as follows:
00001b2d push ebp ; Objective C Implementation defined at 0x30a0 (instance method) 00001b2e mov ebp, esp 00001b30 sub esp, 0x28 00001b33 mov dword [ebp+var_C], ebx 00001b36 mov dword [ebp+var_8], esi 00001b39 mov dword [ebp+var_4], edi 00001b3c mov ebx, dword [ebp+arg_8] 00001b3f mov esi, dword [objc_msg_length] ; @selector(length) 00001b45 mov dword [esp+0x28+var_24], esi ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001b49 mov dword [esp+0x28+var_28], ebx ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001b4c call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001b51 cmp eax, 0x13 00001b54 jne loc_1cd2 00001b5a mov dword [esp+0x28+var_20], cfstring__ ; @"-" 00001b62 mov eax, dword [objc_msg_componentsSeparatedByString_] ; @selector(componentsSeparatedByString:) 00001b67 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001b6b mov dword [esp+0x28+var_28], ebx ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001b6e call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001b73 mov edi, eax 00001b75 mov eax, dword [objc_msg_count] ; @selector(count) 00001b7a mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001b7e mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001b81 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001b86 cmp eax, 0x4 00001b89 jne loc_1cd2 00001b8f mov dword [esp+0x28+var_20], 0x0 00001b97 mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001b9c mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001ba0 mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001ba3 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001ba8 mov dword [esp+0x28+var_24], esi ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001bac mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001baf call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001bb4 cmp eax, 0x4 00001bb7 jne loc_1cd2 00001bbd mov dword [esp+0x28+var_20], 0x1 00001bc5 mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001bca mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001bce mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001bd1 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001bd6 mov dword [esp+0x28+var_24], esi ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001bda mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001bdd call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001be2 cmp eax, 0x4 00001be5 jne loc_1cd2 00001beb mov dword [esp+0x28+var_20], 0x2 00001bf3 mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001bf8 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001bfc mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001bff call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c04 mov dword [esp+0x28+var_24], esi ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c08 mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c0b call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c10 cmp eax, 0x4 00001c13 jne loc_1cd2 00001c19 mov dword [esp+0x28+var_20], 0x3 00001c21 mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001c26 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c2a mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c2d call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c32 mov dword [esp+0x28+var_24], esi ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c36 mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c39 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c3e cmp eax, 0x4 00001c41 jne loc_1cd2 00001c47 mov dword [esp+0x28+var_20], 0x0 00001c4f mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001c54 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c58 mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c5b call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c60 mov ebx, dword [objc_msg_intValue] ; @selector(intValue) 00001c66 mov dword [esp+0x28+var_24], ebx ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c6a mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c6d call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c72 mov esi, eax 00001c74 mov dword [esp+0x28+var_20], 0x1 00001c7c mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001c81 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c85 mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c88 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c8d mov dword [esp+0x28+var_24], ebx ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001c91 mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001c94 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001c99 lea esi, dword [eax+esi] 00001c9c mov dword [esp+0x28+var_20], 0x3 00001ca4 mov eax, dword [objc_msg_objectAtIndex_] ; @selector(objectAtIndex:) 00001ca9 mov dword [esp+0x28+var_24], eax ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001cad mov dword [esp+0x28+var_28], edi ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001cb0 call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001cb5 mov dword [esp+0x28+var_24], ebx ; argument "selector" for method imp___symbol_stub__objc_msgSend 00001cb9 mov dword [esp+0x28+var_28], eax ; argument "instance" for method imp___symbol_stub__objc_msgSend 00001cbc call imp___symbol_stub__objc_msgSend ; objc_msgSend 00001cc1 sar esi, 0x2 00001cc4 mov edx, 0x19c5 00001cc9 sub edx, esi 00001ccb cmp edx, eax 00001ccd sete al 00001cd0 jmp loc_1cd4 loc_1cd2: 00001cd2 xor eax, eax ; CODE XREF=-[SandwichAppDelegate validateSerial:]+39, -[SandwichAppDelegate validateSerial:]+92, -[SandwichAppDelegate validateSerial:]+138, -[SandwichAppDelegate validateSerial:]+184, -[SandwichAppDelegate validateSerial:]+230, -[SandwichAppDelegate validateSerial:]+276 loc_1cd4: 00001cd4 mov ebx, dword [ebp+var_C] ; CODE XREF=-[SandwichAppDelegate validateSerial:]+419 00001cd7 mov esi, dword [ebp+var_8] 00001cda mov edi, dword [ebp+var_4] 00001cdd leave 00001cde ret ; endp
Again we get the help of the pseudo code feature of Hopper to simplify the code and turn it into a bit more easy to read form of code. After turning on the psudo code helper the code looks as follows:
validateSerial function pseudo code
/* @class SandwichAppDelegate */ -(bool)validateSerial:(void *)arg2 { var_C = ebx; var_8 = esi; var_4 = edi; ebx = arg2; if ([ebx length] == 0x13) { edi = [ebx componentsSeparatedByString:@"-"]; if ((([edi count] == 0x4) &&
([[edi objectAtIndex:0x0] length] == 0x4)) &&
([[edi objectAtIndex:0x1] length] == 0x4)) {
if ([[edi objectAtIndex:0x2] length] == 0x4) { if ([[edi objectAtIndex:0x3] length] == 0x4) { esi = [[edi objectAtIndex:0x0] intValue]; esi = [[edi objectAtIndex:0x1] intValue] + esi; eax = [edi objectAtIndex:0x3]; eax = [eax intValue]; eax = 0x19c5 - (SAR(esi, 0x2)) == eax ? 0x1 : 0x0; }else { eax = 0x0; } }else { eax = 0x0; } }else { eax = 0x0; } } else { eax = 0x0; } return eax; }
Now that we have the pseudo code version of the serial validation function it look lots less fightning and we can use it to create a helper app with XCode wich rebuilds the validation function. Then after we have successfully rebuilt the validation function we can turn that in to another function which is able to generate a valid serial - a key generator.
Understanding the validateSerial function
- The first interessting line is ebx = arg2 => This is where the serial to check is put into the ebx register. So from now on ebx holds the NSString value of our entered serial.
- Right after this comes a check for the length of the entered serial. It has to be exactly 0x13 chars long. 0x13 is hexadecimal and 19 in decimal land. Always remember to know in which "number-land" you are hexadecimal or decimal ( further octal binary etc.) this may lead to confusion and errors real quick an cause you lot of headaches.
We can see that if the entered serial is not exactly 19 chars long the function jumps to the end and sets the return register eax to 0x0 / 0. So if we try to debug the app and want the function to succeed we have to supply a serial with this length. - The next interessting line comes right after the length check and is the call to componentsSeparatedByString (a member function of NSString) => Go ahead and search the internet for the documentation of [NSString componentsSeparatedByString]. With the help of the documentation you will find out, that this call returns an array with substrings of the entered which are devided by the provided separator "-". The returned array is assigned to edi
- The next line is 3 checks in one if statement. First we check that we have 4 elements in the result array of componentsSeparatedByString. Then we check that the string length of element 0 and 1 is exactly 4 chars. Below that are the checks for the other 2 elements: element number 2 and 3 (why they're seperated / coded this way I don't know) => This string elements of the array also have to be 4 chars long otherways the function exits with return value 0x0 / 0.
PS: You can now double check the algorithmus by computing the total length of the necessary serial string / length. 4 x 4 chars = 16 + 3 x "-" => 19 chars total => Yes, we are on track!!! - If all this conditions are met we are going to get the int value of the first element (0x0 / 0) and store it into esi. Now we also know that the element has to be an int value - characters are thus not allowed!
- Next we are doing the same with the second element of the array (0x1 / 1) - add it to esi and reassign it to esi
- Now we get the int value of the last (0x3 / 3) element of the array and do some math with it. Let's deconstruct the statement eax = 0x19c5 - (SAR(esi, 0x2)) == eax ? 0x1 : 0x0;
- First the SAR command this is a arithmetic shift of the binary value to the right.
"The SAR command corresponds to the SAL command, only the shift direction is reversed. Zeros are added from the left, the bits falling out on the right are lost. Each individual shift corresponds to a division by the number 2, with any remainder of the division being omitted."
Example:
MOV AL, 00010000b; load value 16 to AL
MOVCL, 3; load 3 to CL
SAR AL, CL; slide AL right three times. The result is 2 (000000l0b) and corresponds to division by the number 2^3 (8)
=> So what SAR does is a division of the first param by 2^second param and then reassign the result to the first param
=> esi = esi / 2^2 - We the substract this value from 0x19c5 (6597) and check if this result is equal to eax which is the last part of the serial or the last element of the array. If it is so we assign 0x1 to eax and return from the function indicating the validation function succeeded.
- First the SAR command this is a arithmetic shift of the binary value to the right.
- AND VOILA!!! => Now we have everything to rebuild this function in our test project.
Rebuilt validateSerial function in Objective-C
- (void)validateSerial{ NSString *serial = _txtSerial.stringValue; int eax = 0; NSString *ebx = serial; if ([ebx length] == 0x13) { NSArray *edi = [ebx componentsSeparatedByString:@"-"]; if ((([edi count] == 0x4) && ([[edi objectAtIndex:0x0] length] == 0x4)) && ([[edi objectAtIndex:0x1] length] == 0x4)) { if ([[edi objectAtIndex:0x2] length] == 0x4) { if ([[edi objectAtIndex:0x3] length] == 0x4) { int esi = [[edi objectAtIndex:0x0] intValue]; esi = [[edi objectAtIndex:0x1] intValue] + esi; NSString *eax_2 = [edi objectAtIndex:0x3]; eax = [eax_2 intValue]; int tmp = (esi / pow(2, 2)); int tmp2 = (0x19c5 - tmp); eax = (tmp2 == eax ? 0x1 : 0x0); }else { eax = 0x0; } } else { eax = 0x0; } }else { eax = 0x0; } } else { eax = 0x0; } if(eax == 0x1){ [self logMsg2Output:[NSString stringWithFormat:@"Serial \"%@\" IS VALID", serial]]; }else{ [self logMsg2Output:[NSString stringWithFormat:@"Serial \"%@\" IS NOT VALID", serial]]; } }As you can see in the above Objective-C code I had to adjust the extracted pseudo code slight to get a fully functional source code that runs. The most important thing you always have to do is to Declare the variables for the register values that are moved, assigned and so on. I.e. "NSString *ebx = lic;" => This is where (in the original code) the parameter of the function gets assigned to the ebx register. For ease of use I directly take the stringValue of the NSTextBox and assign it to a new NSString variable which I also name ebx. I used the original register and variable names from the pseudo code in my rebuilt function to make it easier to compare the sources. But somtimes a register in the disassembly gets reassigned with a new value which belongs to another variable. In this case I again use the original register / variable name but append a number (i.e. eax_2) to make it easier to read and still preserve the "original syntax".
New Key-Gen function
- (IBAction)buttonGenerateLicensePressed:(id)sender {
NSString *license = @"";
int lowerBound = 0;
int upperBound = 9;
int checkSum = 0;
for (int i = 0; i < 4; i++) {
if(i < 3){
NSString *licPart = @"";
for (int j = 0; j < 4; j++) {
int rndValue = lowerBound + arc4random() % (upperBound - lowerBound);
licPart = [NSString stringWithFormat:@"%@%d", licPart, rndValue];
}
if(i == 0){
license = [NSString stringWithFormat:@"%@-", licPart];
}else{
license = [NSString stringWithFormat:@"%@%@-", license, licPart];
}
if(i < 2){
checkSum += [licPart intValue];
}
}else{
checkSum = (checkSum / pow(2, 2));
checkSum = (0x19c5 - checkSum);
license = [NSString stringWithFormat:@"%@%d", license, checkSum];
}
}
[self logMsg2Output:license];
}
The Objective-C solution (GUI)

The solution project - in Objective-C - features the rebuilt serial validate function as well as a keygen function to generate a valid key fo every name you want to register and create a valid serial for. Check it out and download the source code or visit the project on my GitHub website.
Interesting Links
- Other CrackMe Articles
- ...