Crackme Exploration
Upon running the crackme, it asks for a key. We need to find the correct key to solve this challenge!
The “Disassembly” Way
First Impressions
At the start of the main()
function, there are several base64-encoded string, followed by sub_7FF7C4F412A0
which is a function for decoding them. Poking around a little bit further showed that these strings are only used for printing out to the console, so they’re not related to the key.
There are a lot of debugger and timing checks as well.
By decompiling the function into pseudo-code, we can definitely see the part which happen to be constructing the key, starting from a -character base64-encoded string CcN6
through a lot of XOR operations.
The key-construction decompilation part
strcpy((char*)v83, "CcN6");
v19 = 0;
v20 = -8;
v21 = (unsigned __int8*)v83;
LOBYTE(v3) = 67;
v22 = 0x100002600LL;
while ((_BYTE)v3 != 61) {
v23 =
strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
(unsigned __int8)v3);
if (v23) {
v19 = (_DWORD)v23
+ (v19 << 6)
- (unsigned int)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
v20 += 6;
if (v20 >= 0) {
v79[0] = v19 >> v20;
if (Block[1] == v81) sub_7FF7C4F42560(Block, Block[1], v79);
else *(_BYTE*)Block[1]++ = v19 >> v20;
v20 -= 8;
}
} else if ((unsigned __int8)v3 > 0x20u || !_bittest64(&v22, v3)) {
goto LABEL_22;
}
if (++v21 == (unsigned __int8*)((char*)v83 + 4)) break;
v3 = *v21;
}
if (Block[0] == Block[1] || (void*)((char*)Block[1] - (char*)Block[0]) < (void*)3) {
LABEL_22:
v24 = v95;
if (v96.m128i_i64[1] > 0xFuLL) v24 = (void**)v95[0];
sub_7FF7C4F421C0(std::cerr, v24, v96.m128i_i64[0]);
v25 = 2;
goto LABEL_25;
}
v27 = *(_BYTE*)Block[0] ^ 0x1Bu;
v28 = *((_BYTE*)Block[0] + 1) ^ 0xF7u;
v29 = *((_BYTE*)Block[0] + 2) ^ 0x2Cu;
*(_OWORD*)Buf2 = 0;
v86 = 0;
v87 = 15;
LOBYTE(Buf2[0]) = 0;
sub_7FF7C4F42870(Buf2, 0x13u, v18, 0x13u);
v30 = Buf2;
if (v87 > 0xF) v30 = (void**)Buf2[0];
*(_BYTE*)v30 = v27 ^ 0x46;
v31 = Buf2;
if (v87 > 0xF) v31 = (void**)Buf2[0];
*((_BYTE*)v31 + 1) = v28 ^ 0x5B;
v32 = Buf2;
if (v87 > 0xF) v32 = (void**)Buf2[0];
*((_BYTE*)v32 + 2) = v29 ^ 0x22;
v33 = Buf2;
if (v87 > 0xF) v33 = (void**)Buf2[0];
*((_BYTE*)v33 + 3) = v27 ^ 0x73;
v34 = Buf2;
if (v87 > 0xF) v34 = (void**)Buf2[0];
*((_BYTE*)v34 + 4) = v28 ^ 0x58;
v35 = Buf2;
if (v87 > 0xF) v35 = (void**)Buf2[0];
*((_BYTE*)v35 + 5) = v29 ^ 0x3A;
v36 = Buf2;
if (v87 > 0xF) v36 = (void**)Buf2[0];
*((_BYTE*)v36 + 6) = v27 ^ 0x6B;
v37 = Buf2;
if (v87 > 0xF) v37 = (void**)Buf2[0];
*((_BYTE*)v37 + 7) = v28 ^ 0x67;
v38 = Buf2;
if (v87 > 0xF) v38 = (void**)Buf2[0];
*((_BYTE*)v38 + 8) = v29 ^ 0x33;
v39 = Buf2;
if (v87 > 0xF) v39 = (void**)Buf2[0];
*((_BYTE*)v39 + 9) = v27 ^ 0x71;
v40 = Buf2;
if (v87 > 0xF) v40 = (void**)Buf2[0];
*((_BYTE*)v40 + 10) = v28 ^ 0x41;
v41 = Buf2;
if (v87 > 0xF) v41 = (void**)Buf2[0];
*((_BYTE*)v41 + 11) = v29 ^ 0x24;
v42 = Buf2;
if (v87 > 0xF) v42 = (void**)Buf2[0];
*((_BYTE*)v42 + 12) = v27 ^ 0x77;
v43 = Buf2;
if (v87 > 0xF) v43 = (void**)Buf2[0];
*((_BYTE*)v43 + 13) = v28 ^ 0x7F;
v44 = Buf2;
if (v87 > 0xF) v44 = (void**)Buf2[0];
*((_BYTE*)v44 + 14) = v29 ^ 0x33;
v45 = Buf2;
if (v87 > 0xF) v45 = (void**)Buf2[0];
*((_BYTE*)v45 + 15) = v27 ^ 0x6B;
v46 = Buf2;
if (v87 > 0xF) v46 = (void**)Buf2[0];
*((_BYTE*)v46 + 16) = v28 ^ 5;
v47 = Buf2;
if (v87 > 0xF) v47 = (void**)Buf2[0];
*((_BYTE*)v47 + 17) = v29 ^ 0x64;
v48 = Buf2;
if (v87 > 0xF) v48 = (void**)Buf2[0];
*((_BYTE*)v48 + 18) = v27 ^ 0x21;
How The Key Is Built
The program first initializes a Block
buffer and copies CcN6
into v83
.
*(_OWORD*)Block = 0;
v81 = 0;
v83[0] = 0;
v83[1] = _mm_load_si128((const __m128i*)&xmmword_7FF7C4F446F0);
strcpy((char*)v83, "CcN6");
It then decodes CcN6
as base64 into 3 bytes and store these bytes in Block
.
v19 = 0;
v20 = -8;
v21 = (unsigned __int8*)v83;
LOBYTE(v3) = 67;
v22 = 0x100002600LL;
while ((_BYTE)v3 != 61) {
v23 =
strchr("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
(unsigned __int8)v3);
if (v23) {
v19 = (_DWORD)v23
+ (v19 << 6)
- (unsigned int)"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
v20 += 6;
if (v20 >= 0) {
v79[0] = v19 >> v20;
if (Block[1] == v81)
sub_7FF7C4F42560(Block, Block[1], v79);
else
*(_BYTE*)Block[1]++ = v19 >> v20;
v20 -= 8;
}
} else if ((unsigned __int8)v3 > 0x20u || !_bittest64(&v22, v3)) {
goto LABEL_22;
}
if (++v21 == (unsigned __int8*)((char*)v83 + 4))
break;
v3 = *v21;
}
The program first XOR each byte in Block
with 0x1B
, 0xF7
, and 0x2C
, respectively, then it continue doing a lot of XOR-ing to construct a -character key in Buf2
.
v27 = *(_BYTE*)Block[0] ^ 0x1Bu;
v28 = *((_BYTE*)Block[0] + 1) ^ 0xF7u;
v29 = *((_BYTE*)Block[0] + 2) ^ 0x2Cu;
*(_OWORD*)Buf2 = 0;
v86 = 0;
v87 = 15;
LOBYTE(Buf2[0]) = 0;
sub_7FF7C4F42870(Buf2, 0x13u, v18, 0x13u);
v30 = Buf2;
if (v87 > 0xF) v30 = (void**)Buf2[0];
*(_BYTE*)v30 = v27 ^ 0x46;
v31 = Buf2;
if (v87 > 0xF) v31 = (void**)Buf2[0];
*((_BYTE*)v31 + 1) = v28 ^ 0x5B;
v32 = Buf2;
if (v87 > 0xF) v32 = (void**)Buf2[0];
*((_BYTE*)v32 + 2) = v29 ^ 0x22;
v33 = Buf2;
if (v87 > 0xF) v33 = (void**)Buf2[0];
*((_BYTE*)v33 + 3) = v27 ^ 0x73;
v34 = Buf2;
if (v87 > 0xF) v34 = (void**)Buf2[0];
*((_BYTE*)v34 + 4) = v28 ^ 0x58;
v35 = Buf2;
if (v87 > 0xF) v35 = (void**)Buf2[0];
*((_BYTE*)v35 + 5) = v29 ^ 0x3A;
v36 = Buf2;
if (v87 > 0xF) v36 = (void**)Buf2[0];
*((_BYTE*)v36 + 6) = v27 ^ 0x6B;
v37 = Buf2;
if (v87 > 0xF) v37 = (void**)Buf2[0];
*((_BYTE*)v37 + 7) = v28 ^ 0x67;
v38 = Buf2;
if (v87 > 0xF) v38 = (void**)Buf2[0];
*((_BYTE*)v38 + 8) = v29 ^ 0x33;
v39 = Buf2;
if (v87 > 0xF) v39 = (void**)Buf2[0];
*((_BYTE*)v39 + 9) = v27 ^ 0x71;
v40 = Buf2;
if (v87 > 0xF) v40 = (void**)Buf2[0];
*((_BYTE*)v40 + 10) = v28 ^ 0x41;
v41 = Buf2;
if (v87 > 0xF) v41 = (void**)Buf2[0];
*((_BYTE*)v41 + 11) = v29 ^ 0x24;
v42 = Buf2;
if (v87 > 0xF) v42 = (void**)Buf2[0];
*((_BYTE*)v42 + 12) = v27 ^ 0x77;
v43 = Buf2;
if (v87 > 0xF) v43 = (void**)Buf2[0];
*((_BYTE*)v43 + 13) = v28 ^ 0x7F;
v44 = Buf2;
if (v87 > 0xF) v44 = (void**)Buf2[0];
*((_BYTE*)v44 + 14) = v29 ^ 0x33;
v45 = Buf2;
if (v87 > 0xF) v45 = (void**)Buf2[0];
*((_BYTE*)v45 + 15) = v27 ^ 0x6B;
v46 = Buf2;
if (v87 > 0xF) v46 = (void**)Buf2[0];
*((_BYTE*)v46 + 16) = v28 ^ 5;
v47 = Buf2;
if (v87 > 0xF) v47 = (void**)Buf2[0];
*((_BYTE*)v47 + 17) = v29 ^ 0x64;
v48 = Buf2;
if (v87 > 0xF) v48 = (void**)Buf2[0];
*((_BYTE*)v48 + 18) = v27 ^ 0x21;
The rest of the function is just storing our input key into Buf1
and compare it against Buf2
using memcmp
.
We can easily write a Python script to construct the key!
Reconstructing The Key
import base64
encoded = "CcN6"
decoded = base64.b64decode(encoded)
xor_block = [decoded[0] ^ 0x1B, decoded[1] ^ 0xF7, decoded[2] ^ 0x2C]
xor_constants = [
0x46, 0x5B, 0x22, 0x73, 0x58, 0x3A, 0x6B,
0x67, 0x33, 0x71, 0x41, 0x24, 0x77, 0x7F,
0x33, 0x6B, 0x05, 0x64, 0x21
]
key_bytes = bytearray()
for i, constant in enumerate(xor_constants):
src_byte = xor_block[i % 3]
key_bytes.append(src_byte ^ constant)
print(key_bytes.decode("utf-8"))
We can get the key TotallySecureKey123
by running the script.
And… it’s correct!
The “Debugger” Way
We can also solve this crackme by just using a debugger. Here, I’m going to use Binary Ninja for this purpose. Let’s fire up Binary Ninja, head to the Debugger pane, and press Launch.
We’re going to navigate to the main()
function using the Symbols pane.
Skipping Anti-Debug Checks
We’re going to bypass the first check, IsDebuggerPresent
, located in the main()
function.
Let’s set a breakpoint right after xor r12d, r12d
, after the call to IsDebuggerPresent
. In my case, I’m going to set a breakpoint at 0x7ff71ac816d9
. Then type g
to execute the crackme. It breaks at the breakpoint.
>>> bp 00007ff71ac816d9
>>> g
Here, the result of IsDebuggerPresent
is loaded into eax
. Here, test eax, eax
sets the Zero Flag (ZF
) based on whether eax = 0
. If a debugger is present (eax ≠ 0
), ZF
becomes 0
, and the program jumps to 0x7ff71ac81dfe
for exiting the program.
Therefore we need to modify the value of eax
to 0
to bypass this check. We can do so by typing r eax=0
into the debugger console.
>>> r eax=0
After that, we should be able to skip the jne
at 0x7ff71ac816e1
.
Next, we’re going to bypass the CheckRemoteDebuggerPresent
check.
Once again, let’s set a breakpoint right after call qword [rel CheckRemoteDebuggerPresent]
. In my case, I’m going to set a breakpoint at 0x7ff71ac816fa
.
>>> bp 00007ff71ac816fa
>>> g
Here, the program calls the CheckRemoteDebuggerPresent
API, which returns a non-zero on success (eax ≠ 0
) and sets *pbDebuggerPresent
flag to 1
. The *pbDebuggerPresent
has the address of [rsp+0x78]
.
We’re going to set eax
to 1
and zero the flag at rsp+78
.
>>> r eax=1
>>> ed rsp+78 0
This should skip both the je
and jne
at 0x7ff71ac816fc
and 0x7ff71ac81703
!
Bypassing Timing Checks
Talking about the timing checks in more detail, the program just implements a massive loop and check the execution time of this loop. This loop should be executed in no more than about 1 second which is equals to 0x3b9acde8
(or 1e9
in decimal) nanoseconds.
We can easily skip this loop by setting a breakpoint after call _Query_perf_counter
. I did some further stepping and seems like we can safely set a breakpoint directly after sub rax, qword [rsp+0x50]
which compares the execution time against 0x3b9acde8
.
>>> bp 00007ff71ac81835
>>> g
Type r
into the debugger console to view the value of the registers. Notice that the program compares rax
to 0x3b9acde8
. If the value of the rax
register is less than 0x3b9acde8
(it should be since we skipped the loop!), continue stepping the code. Otherwise, set the value of rax
to something smaller.
Now we’ve bypassed the timing check and started going into the key construction logic!
Revealing The Key
After skipping all the checks, we can definitely continue stepping the code. In the Registers pane, the key TotallySecureKey123
is being constructed!
We solved it with just a debugger, no decompilation needed!
Tricking The Crackme
Can we still get the crackme to print Correct!
even if we use an incorrect key?
Let’s continue stepping the code until the crackme prompts us for input. We’ll type something in there and press return.
We should be able to continue stepping the code until… this cmp r8, r15
line. This is the one comparing the length of our input key against the constructed one (I did use the decompiler to verify this btw, so this is not exactly the using-only-the-debugger way).
Now we need to set a breakpoint at that cmp
instruction.
>>> bp 00007ff71ac81cd1
>>> g
Type r
into the debugger console to reveal the value of r8
and r15
.
Looking at the values, r8
does indeed store the length of our input password. I typed 123
so the value of r8
is 3
. We knew the correct key has characters, if we convert into hex it would be 0x13
which is also the value of r15
.
We can easily bypass this check by changing the value of r8
to 0x13
.
>>> r r8=13
We should be able to continue stepping through the code… until we meet our last obstable — the memcmp
call, which basically compares the content of our input against the correct key.
The memcmp
call writes the result into eax
. If the two memory blocks are equal, eax
should be 0
. Then test eax, eax
sets the Zero Flag (ZF
) to 1
. The jne
instruction only jumps to the “not equal” handling part when ZF = 0
, and we definitely don’t want that. Therefore we need to modify the value of eax
to 0
.
Let’s set a breakpoint at the test
instruction first:
>>> bp 00007ff71ac81ce0
>>> g
And then modify eax
:
>>> r eax=0
Now we should be able to bypass the jne
. After a lot of stepping, the crackme prints Correct!
to the console!
Thoughts
I learned so much from this crackme. I’ve been doing static analysis for solving challenges for a while, and this is the first time that I actually learn to use a debugger!
I’m still not sure if I’m using the debugger the right way though, it seems to me that I’m not taking full advantage of the debugger interface in Binary Ninja though. If anyone appears to read this post and know how to use the debugger properly, leave a comment to give me some tips!
Also special thanks to this write-up Crackme: git's simple crackme medium-hard which is the inspiration for me to solve this crackme without using a decompiler!