skip to content
Site header image Minh Ton

crackmesone — genass3's patch protect

A super cool crackme featuring CRC patching protections, with a difficulty rating of 2. This write-up explains how I cracked it by patching the binary (and also by reconstructing or dumping the correct key from memory)!


Crackme Exploration

The crackme asks for a key. If we supply it with some random text, it’ll display a MessageBox with a beeping sound.

The name and description imply that we’re supposed to patch the crackme. In this write-up, we’ll be finding the key first (should be easy I guess) and then focus on bypassing the patching protections.

Finding The Key

Let’s decompile the crackme using Binary Ninja. We’ll skip the CRC protection and the MessageBox/Beep loading.

If we scroll down a little bit, we can see the part where the program stores our input to var_118 and likely to construct the expected key in var_128:

__builtin_strcpy(dest: &var_128, src: "GUGPUGP")
sub_140001010("%s:", &var_130)
fgets(_Buffer: &var_118, _MaxCount: 0x100, _Stream: __acrt_iob_func(_Ix: 0))
uint64_t rax_10 = strcspn(_Str: &var_118, _Control: U"\n")

if (rax_10 u>= 0x100)
    sub_140001788()
    noreturn

var_128:1.b ^= 0x33
i = -1
var_128:2.b ^= 0x33
var_128:3.b ^= 0x33
var_128:4.b ^= 0x33
var_128:5.b ^= 0x33
var_128:6.b ^= 0x33
var_118[rax_10] = 0
var_128.b = 0x74

A bit further down, we can see the crackme compares the length of our input with the expected password…

do
    i += 1
while (*(&var_128 + i) != 0)

int64_t rax_11 = -1

do
    rax_11 += 1
while (var_118[rax_11] != 0)

if (i == rax_11)
    break

… and does a comparison of the two strings:

do
    if (*(&var_128 + rax_12) != var_118[rax_12])
        goto label_140001613

    r8_3 += 1
    rax_12 += 1
while (sx.q(r8_3) u< i)

rbx_3(0, "Welcome", "Great job", 0)

Based on the above key-constructing logic, we’ll create a Python script to XOR each byte of the string GUGPUGP with 0x33, set the first byte to 0x74, and decode it into ASCII:

src = bytearray(b"GUGPUGP")
for i in range(1, 7):
    src[i] ^= 0x33
src[0] = 0x74
print(src.decode("ascii"))

Running the script reveals the key tftcftc.

Patching The Crackme

These are some approaches that we can use to solve the crackme without finding the correct key. A debugger can help us with this. Let’s head to the Debugger pane and press Launch.

Now navigate to main() using the Symbols pane.

NOP-ing The Protections

I think this is probably the most straightforward way to do this. We’ll need to select all of the instructions between these two instructions at 140001310 and 1400013d2

lea     rcx, [rel data_140003410]  {"[+] Starting application with PatchCRC protection.\n"}
...
; Select all instructions in here
...
lea     rcx, [rel data_140003470]  {"[+] CRC Monitoring Thread Started\n"}

…, right click on them and convert all of them to NOP.

Then we can continue stepping through the disassembly. We also need to feed the crackme a random string to get past the fgets call. I’ll just type 123 and press return.

After going through the key-construction logic and the two loops which calculate the length of our input and the expected key, we arrive at this cmp rdx, rax instruction for comparing them.

We definitely need to bypass the jne instruction, so let’s set a breakpoint after cmp.

>>> bp 00007ff709b41569
>>> g

A quick glance at the register values using r shows that rdx and rax are indeed the length of the correct key and our input, respectively. We know this since rax has the value of 3, which is the length of the earlier 123 input key. We’ll set the value of rax to be the same as that of rdx.

>>> r rax=7

We eventually reach the loop where the crackme compares the content of the two keys.

At the jne instruction underneath the cmp byte [rsp+rax+0x50], cl, do a right click, select Patch and choose Never Branch to bypass the jump when the loop reaches an incorrect character.

Let the code continue and we finally got the Welcome message box!

Skipping A Bad Jump

The above approach of NOP-ing is a little bit brutal I guess. Let’s analyze the patch protection part. Here’s what happens when we let the crackme run past the protection while debugging:

With some digging, it seems that sub_7ff709b411e0 is the patch monitoring function. The crackme spawns a background thread that repeatedly checks the host module for tampering with the sub_7ff709b411e0 function. This function is then passed into the CreateThread call as an argument. We’ll be patching the executable after the background thread has been created.

When we inspect sub_7ff709b411e0, there’s a jnp instruction that leads to the CRC Mismatch part. We need to bypass this jump.

Once again, right click on the instruction, then choose Patch → Never Branch.

Then we can continue executing the crackme normally!

Dumping From Memory

There’s also another way to get the correct key, using a debugger.

When we reach the character comparison instruction cmp byte [rsp+rax+0x50], cl, it’s possible that rsp+rax+0x50 is the location where the correct key is stored.

We can quickly spin up the Stack View from Debugger Menu → Create Stack View. Now we need to find the address of the key at [rsp+rax+50]:

>>> ? rsp+rax+0x50
Evaluate expression: 843666421824 = 00000c4`6e6ff840

In the Stack View, navigate to 00000c46e6ff840. We found tftcftc!

Thoughts

This is a nice crackme tbh, with the key can be found statically pretty easily though. The patching wasn’t difficult since the strings before/after the protections launch really helped me to get an idea where to bypass these checks.

I’ve learned so much from solving this crackme. I’ve only started using a debugger since my previous post crackmesone — git's simple crackme medium-hard, but now I’m starting to feel comfortable using it! This is my first time using the Patch feature and the Stack View of Binary Ninja as well, and these are super powerful tools.

Also, there’s the v2 version and the lite version of this crackme. I’ll definitely checking them out in the future!