Reversing and Keygenning qwertyoruiop's Crackme

I was bored this weekend and decided to take some rust out of my reversing skills before they disappear for good. I have spent the past two years or so mostly writing C code (secure C is more like an asymptote but that is why it is a fun challenge) and barely doing any serious reverse engineering and security research. So I decided to revisit some unfinished business with qwertyoruiop’s crackme. I had a look when he originally sent it but got distracted with something else at the time and never finished it. I couldn’t find any public write-up about it so I decided to write one. It is mostly targeted to newcomers to reverse engineering and macOS. You can click the pictures to see the full size version.

Today’s target is CrackMe_nr1_qwertyoruiop.app. It is a fun target written by a very young @qwertyoruiopz already showing his great talent (I think he was 12 or 14 at the time).

Let’s start by checking what our target looks like and what should be our goal.
The crackme is a very simple Cocoa app with an input field and a button.

CrackMe main window

And if we input some random data and press Activate we get the following alert:

Invalid serial

The first thing I like to do against a new target is to look at its Mach-O headers, either with otool or MachOView.

The Mach-O header tells us it is an i386 binary (this shows the crackme age - i386 is being deprecated in macOS!) with ASLR disabled (MH_PIE flag not set).

Mach-O header

More interesting is the __DATA segment, where we can see that the binary contains a few constructors. This is code that will be executed before main (technically most of the times there is a start function previous to main).

Mod Init

There are seven mod init function pointers and we will verify the code of each one. Usually in targets such as crackmes, DRM, and malware, the constructors contain interesting code that we usually want to reverse (usually to fool novice reverse engineers).

Code execution before main can also be obtained in the load or init methods of Objective-C classes. Little Snitch for example uses this as you can see in this post.
If you are reverse engineering a target always check all these places for interesting code.

Mod Init Pointers

In this case the binary appears to be well formed and with the expected segments/sections. For example if we pack the same binary with UPX we get just three segments and no linked libraries.

UPX packed target

Another point of interest are the initial memory protections of each segment. Any segment with an initial executable and writable memory protection is suspicious (if it is executable it always needs to be readable) since it is usually sign of a packer and/or cryptor. It is really not necessary for packers/cryptors to have this set in the header since they can always call memory related APIs such as mach_vm_protect to modify the current memory protection.

After looking at the headers the next step is to load the target binary in a disassembler. For most targets this is a good idea since interactive disassemblers such as IDA, Hopper, and Binary Ninja allow you to comment the code, which is extremely useful for most targets. This also gives us a good hint about the kind of code that we are against to.

I still use IDA as my main interactive disassembler. Mostly because it still deals better with abnormal code (meaning obfuscated, mangled, etc) and it has a C SDK (well C++ mostly). I don’t like Python!
Both Hopper and Binary Ninja are competent tools and provide great value if you are entering the reverse engineering field and don’t want (or can’t) spend the big bucks in IDA. If you are reverse engineering esoteric CPUs then IDA Pro is your only choice (radare2 if you are really masochist!).

Loading the code in IDA we get an almost clean target, meaning by this, a target that IDA disassembles without any major issues. When IDA fails to disassemble or recognize functions we end up with red areas at the top bar chart (or whatever color your IDA theme uses). This crackme does have a small red area:

IDA big read

There are two interesting things in this picture. First the function labelled sub_25D0 appears to be incomplete. It has three instructions and abruptly ends. Its code also doesn’t make any sense. Then it is followed by instructions and bytes that IDA failed to recognize. This is a sign of something fishy going on, usually packed/crypted/obfuscated code or something else (in this particular case it will be the latter). For example the address 0x25F0 contais the arpl instruction “Adjust RPL Field of Segment Selector”. As far as I remember I have never seen x86 code using this instruction!

The memory address range from 0x25D0 till 0x26EE is something that we will want to pay attention to.

Usually it is a good idea to look at the strings and see if we can find the code related to them and maybe some execution flow that we can exploit towards our goals.
In this case we can start with the bad serial message “The serial you entered is not valid.”. It can be found in the method [MCAppDelegate fail] at address 0x3830. We can observe a few calls to objc_msgSend but we don’t know what are the selectors being used. IDA also fails to identify any callers references to this method, so we can’t easily identify where this method is called from (in static analysis, much easier with a debugger).

The objc_msgSend function is one of the functions from the Objective-C runtime that is responsible for delivering messages to objects. For example an Objective-C code of [object message:argument] can be seen in the disasssembled code as objc_msgSend(object_ID, selector, argument). In i386 binaries we can find the selector with a debugger by breakpointing on objc_msgSend function and displaying the second argument as a string, and in the case of x64-64 binaries we display the RSI register contents as string to display the selector. Usually disassemblers are able to extract the selector but not in this case. We will come back to this topic shortly.

There is a method with an interesting name [MCAppDelegate succeed]. It contains the message “You failed. Keep trying.” which doesn’t appear much about success. There are also no code references to this method. We can also find another potential interesting string “The serial you have entered has been validated!” but we can’t find any code/data references to it. If this is the right message we are looking for then its caller seems obfuscated or it is just a bogus clue.

Let’s get back to objc_msgSend to understand how to find the selectors and also some other data references. Most selectors aren’t directly visible in this binary because they are referenced using relative offsets and IDA is unable to compute them. This used to happen in older IDA versions but in this particular case I can’t remember if this is something that happens with i386 Objective-C code and older Xcode compilers or if there is intended “obfuscation” by qwertyoruiopz. The code that I am talking about looks like this:

objc_msgSend

This is a good example because it shows a first call where the selector can be easily identified and a second where it requires a small effort. In the first case at address 0x29BC we can see the selector argument identified by IDA at esp+4 (remember that arguments are passed from last to first into the stack and the stack grows down). IDA doesn’t display the selector string but if we look at address 0x29AC there is a string pointer being moved into EDX register than is then put in the stack. If we click on paUtf8string IDA references the string “UTF8String”. So the original Objective-C code for this is [some_NSString_string UTF8String]. Documentation says this method returns “A null-terminated UTF8 representation of the string.”.
But what is the selector at address 0x29D7? Looking at the code we know that the string pointer is coming from ESI register, and ESI gets loaded at address 0x29C7.

The instruction at address 0x29C7 is mov esi, [edx+36A4h]. It is dereferencing whatever value exists at address pointed by edx+0x36A4 memory address. The following is all the code involved in setting the value of EDX.

(...)
00002997     call    $+5
0000299C     pop     eax
(...)
000029B9     mov     [ebp+var_10], eax
(...)
000029C4     mov     edx, [ebp+var_10]
000029C7     mov     esi, [edx+36A4h]
000029CD     mov     [esp], ecx      ; id
000029D0     mov     [esp+4], esi    ; SEL
000029D4     mov     [ebp+var_14], eax
000029D7     call    _objc_msgSend

This is a thing you see often to use relative offsetting. The call and pop load the address 0x299C into EAX register and the value is stored at a local variable var_10 for future usage. So the ESI value we want to find out is 0x299C + 0x36A4 = 0x6040. At this address we can find the string “length”. The second call Objective-C code is [some_NSString_string length] retrieving the length of the string (method documentation says “The number of UTF-16 code units in the receiver.”).

NSUInteger length = [some_NSString_string length];

To find out all “hidden” selectors we just need to compute the target address and see what string is referenced at that address. This can be automated with an IDA plugin. First you find the address of the pop instruction after call $5 instruction and then track the offset and final address correspondent to each objc_msgSend call. This will make all code much easier to read.

objc_msgSend

Sorted this out let’s get back on course and reverse the constructors. IDA labels them as InitFunc_[0;6]. The first InitFunc_0 is not doing anything interesting for our purposes.

some_global_variable_at_0x5298 = [NSAutoreleasePool new];

InitFunc_1 is more interesting and will start to explain what is going on.

objc_msgSend

The mach_task_self means the code is accessing its mach task port, which can be used with Mach memory and debugging APIs. That is a good hint of something interesting happening here. The 7 value being moved before the call to function sub_26EE is also interesting if you have some experience reversing certain type of macOS binaries. The function at sub_26EE is very simple and just a stub to call a system library API.

000026EE sub_26EE        proc near             ; CODE XREF: InitFunc_1+76
000026EE                                       ; InitFunc_1+C3
000026EE                 nop
000026EF                 jmp     _vm_protect
000026EF sub_26EE        endp

In this case the constructor is calling the vm_protect system API, used to modify memory permissions. The 7 means VM_PROTECT_ALL an alias for VM_PROTECT_READ | VM_PROTECT_WRITE | VM_PROTECT_EXECUTE. The prototype for vm_protect is:

kern_return_t   vm_protect
                 (vm_task_t           target_task,
                  vm_address_t            address,
                  vm_size_t                  size,
                  boolean_t           set_maximum,
                  vm_prot_t        new_protection);

The 0 corresponds to the set_maximum argument, meaning that it’s setting the current protection for the address region but not setting a maximum (if you look at the header the maximum for __TEXT segment is already set to VM_PROTECT_ALL).

The most important argument we want to extract is the address. In the first call this is easy to find out:

00002781    mov     ebx, ds:(dword_50B8 - 276Eh)[eax]

IDA takes care for us and references the address 0x50B8, which contains the value 0x25D9. Remember that the interval 0x25D0 till 0x26EE appeared to contain bogus code where we expected something to happen. That portion of code is now made writable which is a strong indicator that something is going to be modified on that area. The size argument isn’t extremely important here. The reason is that the vm_protect function will always act on multiples of 4096 bytes page size. Given that the suspicious area is just 286 bytes we don’t need to care much about the size (unless the area was crossing the boundaries of another page, which isn’t in this case since this page boundary is between 0x2000 and 0x3000). It could be relevant if we expected more code to be modified (the well formed functions that followed would also be modified for example). Anyway if we attach a debugger and insert a breakpoint we can verify that the argument size is 0x200 bytes (you can also compute it statically but it’s faster with a debugger).

The second call to vm_protect modifies protection at address 0x4f89 corresponding to __cstring section and in particular to this string “Also, you just lost the game.”. We will understand why later on.

To conclude, InitFunc_1 is making read-only areas of the binary writable so we should expect modifications in these two areas sooner or later.

Next constructor is InitFunc_2. Its commented code is below. It is simply reading the bytes that start at address 0x25d9 and XORing them with a fixed key, and then replacing them on the original address they were read from (hence explaining the need for vm_protect call). If we undefine the “code” that starts at address 0x25d9 until 0x26ED we can see a series of bytes grouped and each group terminated by a NUL byte.

00002840        push    ebp
00002841        mov     ebp, esp
00002843        push    ebx
00002844        sub     esp, 8
00002847        call    $+5
0000284C        pop     eax
0000284D        mov     ecx, ds:(dword_50B8 - 284Ch)[eax] ; < ecx = 0x50b8
                                                          ; => 0x25d9
00002853        mov     [ebp+var_8], ecx
00002856        mov     [ebp+var_C], eax ; var_c = 0x284C
00002859
00002859 loc_2859:        ; CODE XREF: InitFunc_2+52↓j
00002859        mov     eax, [ebp+var_8]
0000285C        cmp     byte ptr [eax], 0 ; check if it hit the NUL termination
0000285F        jz      loc_2897          ; finish if NUL
00002865        mov     eax, [ebp+var_8]
00002868        movsx   eax, byte ptr [eax] ; read each byte starting at 0x25d9
0000286B        mov     ecx, [ebp+var_C]
0000286E        imul    edx, [ecx+2874h], 0Ah ; ecx + 0x2874 = 0x50C0  
                                              ; *(uint32_t*)0x50c0 => 1
00002878        add     edx, [ecx+2880h] ; ecx + 0x2880 = 0x50CC => 9
0000287E        xor     edx, eax ; byte read XOR ((1 * 0xA) + 9)
00002880        mov     bl, dl
00002882        mov     eax, [ebp+var_8] 
00002885        mov     [eax], bl ; replace byte read with the XOR result
00002887        mov     eax, [ebp+var_8]
0000288A        add     eax, 1 ; move to next byte
0000288F        mov     [ebp+var_8], eax
00002892        jmp     loc_2859
00002897 ; ---------------------------------------------------------------
00002897
00002897 loc_2897:        ; CODE XREF: InitFunc_2+1F↑j
00002897        add     esp, 8
0000289A        pop     ebx
0000289B        pop     ebp
0000289C        retn

This could be translated into the following C code (bytes copied from 0x25d9 address until the NUL byte is found):

#include <stdio.h>

int main(void)
{
 char obfuscated[] = { 0x5A, 0x5C, 0x43, 0x7F, 0x72, 0x67, 0x75, 0x7C,  
 	               0x61, 0x7E, 0x56, 0x6B, 0x63, 0x76, 0x61, 0x67,  
 	               0x57, 0x76, 0x65, 0x7A, 0x70, 0x76, 0x00 };
 
 for (int i = 0; i < sizeof(obfuscated); i++)
 {
   unsigned char key = 0x1 * 0xA + 0x9;
   unsigned char xor_result = obfuscated[i] ^ key;
   obfuscated[i] = xor_result;
 }

 printf("Deobfuscated string: %s\n", obfuscated);

 return 0;
}

The resulting string is “IOPlatformExpertDevice”. InitFunc_3 also deobfuscates a short string “fail” that starts at 0x25F0. InitFunc_4 deobfuscates the third string, “IOPlatformSerialNumber”. It also installs a signal handler for SIGTRAP signal:

0000290D       mov     ecx, 5
00002912       lea     edx, (sub_28F0 - 290Ch)[eax]
00002918       mov     dword ptr [esp], 5 ; SIGTRAP
0000291F       mov     [esp+4], edx    ; void (*)(int)
00002923       mov     [ebp+var_C], eax
00002926       mov     [ebp+var_10], ecx
00002929       call    _signal

We can observe that the signal callback is bogus:

000028F0 sub_28F0        
000028F0       push    ebp
000028F1       mov     ebp, esp
000028F3       mov     eax, 13371337h
000028F8       mov     dword ptr [eax], 0CC07C9h
000028FE       pop     ebp
000028FF       retn

At address 0x28F8 we will have an invalid memory dereference so the callback will just create another crash. Crash inception!

InitFunc_5 contains an Objective-C call to stringWithUTF8String: method. This method creates an NSString out of a C string. The source C string is “IOPlatformSerialNumber”. It stores the resulting object at address 0x52A0.

InitFunc_5

Finally the last constructor InitFunc_6 does something interesting using the deobfuscated strings. The platform serial number of the machine where the code is being executed is retrieved.

get platform serial number

A C version of the code (without checks):

CFMutableDictionaryRef matching;
matching = IOServiceMatching("IOPlatformExpertDevice");

io_service_t service;
service = IOServiceGetMatchingService(kIOMasterPortDefault, matching);

CFStringRef serialNumber;
serialNumber = IORegistryEntryCreateCFProperty(service, CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);

IORegistryEntryCreateCFProperty second argument is a CFStringRef, a reference to a CFString object. This is the object created in InitFunc_5 (certain objects like NSString and CFString are bridgeable between Foundation and CoreFoundation).

After this two SHA1 hashes are generated. One for the serial number, and one for the string “So you’re trying to reverse me! Hah. Have fun =P”. This is the function that does the hashing:

hashing

It simply converts the object string into a C string pointer and calls CC_SHA1 to generate the SHA1 hash.

The two resulting hashes are used to generate what appears to be a key. We can translate it to the following C code:

/* now we build the key out of mixing these two SHA1 hashes */
unsigned char key[20] = { 0 };
int counter = CC_SHA1_DIGEST_LENGTH - 1;
while (counter >= 0)
{
	unsigned char src_serial = serial_SHA1[counter];
	unsigned char src_sentence = sentence_SHA1[counter];
	key[counter] = src_serial ^ src_sentence;
	counter--;
}

The original code is a bit longer than the C code because there are some extra operations. But we only care about the operations until 0x2DD4 address. The other operations generate some values but they aren’t relevant for the keygen (maybe just to annoy the reverser and waste time).

hashing

The result is that the original string “Also, you just lost the game.” now has 20 bytes modified by mixing the SHA1 hashes of the platform serial number and a sentence. This gives us a strong hint that the keygen needs to be based on the machine serial number, meaning unique serials for each machine (to simplify let’s ignore SHA1 collisions are possible).

We can conclude that the constructors are responsible for generating what appears to be a unique key based on the machine executing the crackme. In case you didn’t notice there are some bytes from the first memory area that weren’t used in the constructors (only three strings were deobfuscated from that area).

Now it is time to move to main code. Both start and main do not feature any special code. Since this is a Cocoa application NSApplicationMain will be executed. This means that if we were tracing inside main we would lose track of the code execution after NSApplicationMain is called (well we could find it but it would take a while deep enough in the Objective-C runtime). NSApplicationMain doesn’t return.

What we can do is to take a look at a few methods (notifications to be precise) where code execution can follow after the call to NSApplicationMain.

The following is a list of notifications on the application delegate:

  • applicationWillFinishLaunching:
  • applicationDidFinishLaunching:
  • applicationWillHide:
  • applicationDidHide:
  • applicationWillUnhide:
  • applicationDidUnhide:
  • applicationWillBecomeActive:
  • applicationDidBecomeActive:
  • applicationWillResignActive:
  • applicationDidResignActive:
  • applicationWillUpdate:
  • applicationDidUpdate:
  • applicationWillTerminate:
  • applicationDidChangeScreenParameters:
  • applicationDidChangeOcclusionState:

In this crackme we can find [MCAppDelegate applicationDidFinishLaunching:]. Usually this is only executed once after the application has finished launching. But we will see later that in this crackme it is called a few times.

The easiest way to understand what this method code is doing is to launch the crackme under a debugger, insert a breakpoint at address 0x40C0 (the start address of this method) and trace its execution. To avoid getting lost in calls to other functions, the easiest way is to use the stepo command available in lldbinit or gdbinit if you still use gdb. This command lets us step through the code without entering calls to other functions (they are executed but we don’t step into them). Tracing the first execution we reach the following code:

initial log

This code uses NSLog to log a message to the console and calls some unknown selector. IDA is able to find the reference to the log message at 0x50B4.
The message is ”== TIP! == || To keygen, you need to know a secret hash. 1CED36375BA86C4DE17C940BF578ED68 is what you’re looking for :)”.

The unknown selector is [MCAppDelegate mk]. Later on we will understand what the secret hash is used for. For now let’s continue and take a look at the mk method.

mk start

This is the prologue of the mk method. If we trace this code we will see that the conditional jump at address 0x3B1B is executed, redirecting the execution flow to the following code:

mk end

What happens here is that the mk method is constantly being executed via a scheduled timer, essentially creating a run loop that is waiting for some magic input/flag to execute the remainder of mk code. We know that applicationDidFinishLaunching: will need to return to resume application launching, so this is a simple way to keep mk running in the background. If we insert a breakpoint at the beginning of the mk method we will get constant hits and without any user input the method will just loop.

To understand the serial number verification algorithm we need to find the place where our serial is used. The workflow could be something like insert serial number -> press activate -> verify serial number. Usually the serial is verified after we press the activate button, but it also can be verified while it is being inserted in the input text field. Let’s assume it happens when we press the activate button. In Cocoa apps pressing a button will send a message to some method. This is configured in the nib file that we can find in the Resources folder (in this case Resources/en.lproj/MainMenu.nib).

The nib is compiled but there is an util called NibUnlocker that is able to convert it to a xib. We can open the resulting xib file in Xcode and take a look at the button properties.
If we do this we find out that the activate button sends its action to [MCAppDelegate applicationDidFinishLaunching:]. As I told you this method is used in a different way than usual.

xib

Since we know that mk is looping in the background and applicationDidFinishLaunching: returned before we can try the activate button we can breakpoint applicationDidFinishLaunching:, insert some input, and press the activate button. This time if we trace applicationDidFinishLaunching: we land in a different code path:

button pressed

What happens here is that some variables are set (variables that control the code flow of this method and others), the serial number is read from the NSTextField as a string and the string object stored at address 0x53AC. The same applicationdidfinishlaunching: selector is scheduled the same way mk was. Because certain variables were set, the code path on next applicationdidfinishlaunching: execution is different. This time the method [MCAppDelegate d] is executed.

This is the code path inside applicationdidfinishlaunching: that will schedule the method d for execution.

calling d

The d method will call the function sub_3190. This function is responsible for verifying the length of the serial and making a copy of it. If length is odd then the serial is rejected, otherwise it proceeds to make a copy. There is no clue about the valid length of the serial number. We will need to wait a bit for that.

d method

The copy that is made is a conversion from a string to a hash. A SHA1 hash is composed by 160 bits and we usually see its string representation. But the string representation is nothing but the string output of each hash byte. In this function the reverse conversion is made, a string is converted to be represented as a byte array. So for example if we insert a serial of “AABBCCDD”, this will be converted by this function to the number 0xAABBCCDD. This gives us a strong hint that the input serial needs to be an hash. The function responsible for the length verification and string conversion is sub_2EA0.

If we still have a breakpoint on applicationdidfinishlaunching: and used an odd serial number then at address 0x41C7 we can find a call to the fail method that displays the bad serial number message. This is a fast fail path, because if we insert an even number this code path is not triggered, so the serial number check is made somewhere else. With this we found out all the code paths available in applicationdidfinishlaunching:. This method is not responsible for serial number verification.

Remember mk still running in the background?

If we insert a breakpoint on the next instruction at address 0x3B21 (inside mk method) after the conditional jump we will see that the breakpoint is never triggered until we insert an even serial number.

00003B0E     cmp     ds:(dword_52A4 - 3B01h)[eax], 0
00003B18     mov     [ebp+var_24], eax
00003B1B     jz      loc_4052
00003B21     call    sub_3320

The conditional jump will always execute until the pointer at address 0x52A4 is not NULL. When is that pointer not NULL? After function sub_3190 verifies the input serial and sets that pointer to the converted string we saw above.

000031AC     call    verify_serial_length_make_copy_sub_2EA0
000031B1     mov     ecx, [ebp+var_C]
000031B4     movsd   xmm0, qword ptr [ecx+1E0Bh]
000031BC     mov     edx, 0
000031C1     mov     [ecx+2107h], eax ; store converted serial at 0x52A4

The mk method contains three code paths:

  1. Loop until there is valid input to verify
  2. If serial is invalid display the bad serial message
  3. Something else we can’t verify if but we expect to be the valid serial message

The most interesting code path (3) is right at the beginning. Lets see the beginning of mk:

mk good

There is a call to function sub_3320. This is the serial verification function we are looking for and I will show you why in a bit. Then we have a memcmp between the “NSAlert” string and a buffer that starts at address 0x260C (referenced at address 0x3B47).

What is the address 0x260C? Is the the initial block of memory that contains the strings deobfuscated in the constructors. But this specific address is the part that no constructor or all the code we have seen until now touched. Given the memcmp usage it is a fair assumption that these bytes are obfuscated or encrypted, since the memcmp call is trying to assure that they start with “NSAlert” after the call to sub_3320 function. If the comparison fails then the fail method is called, another sign that those bytes at 0x260C are being transformed at sub_3320.

The sub_3320 function is essentially a slightly modified RC4 encryption algorithm (thanks to Dcoder for quickly pointing it).

Right at the beginning we can observe RC4’s KSA (key scheduling algorithm):

RC4 ksa

If we keep tracing the code we will see that the converted input serial will be used together with the key that was generated in ÌnitFunc_6 constructor. This will generate a new key that will be used to decrypt the contents of address 0x260C. If the key is wrong the decryption fails and the memcmp comparison will fail. We now understand all the program flow necessary to a valid serial.

Remember the secret hash that was mentioned in the NSLog message? It was 1CED36375BA86C4DE17C940BF578ED68. If we try to decrypt with this key everything will be decrypted correctly. This is the output from Dcoder’s decrypter:

NSAlert;alloc;init;autorelease;addButtonWithTitle:;setMessageText:;setInformativeText:;setAlertStyle:;beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:;OK;You won!;The serial has been successfully validated!

But this is not the serial that we need to input, since we now know that the machine serial number is being used to generate some kind of key. Because we verified that 1CED36375BA86C4DE17C940BF578ED68 is the right key that we need then the valid serial number needs to be something that will be able to generate this key. It is possible because this RC4 is slightly modified with an extra XOR so that the valid serial number is something that is able to generate this known decryption key. If we XOR each byte of the key generated in InitFunc_6 with each byte of the known secret hash we get the valid serial number.

This is the C code to generate that.

    /* now we build the key out of mixing these two SHA1 hashes */
    unsigned char key[20] = { 0 };
    int counter = CC_SHA1_DIGEST_LENGTH - 1;
    while (counter >= 0)
    {
        unsigned char src_serial = serial_SHA1[counter];
        unsigned char src_sentence = sentence_SHA1[counter];
        key[counter] = src_serial ^ src_sentence;
        counter--;
    }
    
    /* the key to decrypt the strings is the secret hash */
    /* == TIP! == || To keygen, you need to know a secret hash. 1CED36375BA86C4DE17C940BF578ED68 is what you're looking for :) */
    const unsigned char secret_hash[16] = {
        0x1C, 0xED, 0x36, 0x37, 0x5B, 0xA8, 0x6C, 0x4D,
        0xE1, 0x7C, 0x94, 0x0B, 0xF5, 0x78, 0xED, 0x68
    };
    
    /* so the valid serial number will be something that XORed with the mixed key
     * gives us the secret hash
     */
    printf("\nYour valid key is: ");
    for (int x = 0; x < 16; x++)
    {
        printf("%02x", key[x] ^ secret_hash[x]);
    }

The full keygen source code is available at Github.

$ ./keygen_CrackMe_nr1_qwertyoruiop 
                                    _
  /\ /\___ _   _  __ _  ___ _ __   / \
 / //_/ _ \ | | |/ _` |/ _ \ '_ \ /  /
/ __ \  __/ |_| | (_| |  __/ | | /\_/
\/  \/\___|\__, |\__, |\___|_| |_\/
           |___/ |___/
    for CrackMe_nr1_qwertyoruiop

(c) fG! - 2018 - https://reverse.put.as

Your IOPlatformSerialNumber is: XXXXXXXXXX
Your IOPlatformSerialNumber SHA1 hash is: 7a635df4bfdf8e31b2d8ad4dedcf5d33484bfcba
The hardcoded sentence SHA1 is: 6e6238043879e73875ace630afa8542b88d518b3

Your valid key is: 08ec53c7dc0e05442608df76b71fe470

Using the keygen key we finally get the message we were after:

You won!

We saw that the decrypt text is essentially composed by selectors and the valid serial message. The rest of the mk method good path is essentially calling these selectors to build and show the valid message alert. Nothing much to explain there.

There are a few methods that we didn’t look at. They aren’t used and can be just a decoy. There is also a function sub_2A00 that has a call to arc4random. This function is unused and could be just a tip towards identifying RC4, since arc4random random number generator was originally based in RC4.

And that’s it. The crackme isn’t super complicated but it is a good one to show and learn some tricks that can be useful to reverse other crackmes and malware samples. This writeup ended being a bit long because I wanted to explain every detail as much as possible.

Thanks to @qwertyoruiopz for writing this crackme.

Have fun,
fG!