justCTF 2020 [debug_me_if_you_can-RE]

I had some time over the weekend and decided to try some reversing challenges from justCTF[*] 2020. Although I didn’t participate in the ctf seriously but solved an interesting challenge which deserves a writeup for sure.


Challenge

I bet you can’t crack this binary protected with my custom bl33d1ng edg3 pr0t3c70r!!!111oneoneone
Download : supervisor crackme.enc flag.png.enc


TL;DR

  • Protector uses Nanomite technique
  • Using strace to analyse execution
  • Code is decrypted on the fly and then again encrypted
  • Rebuilding a clean unprotected binary with IdaPython
  • Keygenning

Role of Father Process

The crackme.enc binary is encrypted. I tried forcing some of it to code but that didn’t help me enough. So it looks that it is only encrypted at some marked positions in the function. Also that int3 looks suspicious.

Encrypted child main function

So I shifted my focus to another binary from the provided files ie. supervisor.

1
2
3
4
5
└──╼ $./supervisor
Hello there!
Error! https://www.youtube.com/watch?v=Khk6SEQ-K-k
0xCCya!
: No such process

supervisor begins by initializing some data. Later it forks a child process and executes crackme.enc with ptrace(PTRACE_TRACEME) which means that crackme.enc is being traced by the supervisor.

supervisor main function

The child logic starts with continuing the child process until it encounters an interrupt.. ahh ok!. So simply after an interrupt is generated it transfers control to the father and father handles its execution from that point.
This is a typical case of nanomite.
https://www.apriorit.com/white-papers/293-nanomite-technology

Later it gets the rip from the child’s interrupt context and peeks 4 bytes from it. These can be either 13 37 BA BE or DE AD C0 DE and exits if it is neither of them.
If 13 37 BA BE, then it also peeks the other 4 bytes in succession.

Also if you aren’t much familiar with ptrace flags I recommend you to read this :
https://www.linuxjournal.com/article/6100

supervisor start

Then it notes the address of first occurences of bytes FE ED C0 DE and DE AD C0 DE.

supervisor peeking for feedc0de and deadc0de

It is often useful to display opcodes bytes along with the disassembly in IDA, especially in cases like these.
Options -> General -> Disassembly -> Number of opcode bytes (non-graph)

Now it decrypts some specific bytes after the noted FE ED C0 DE address, sets rip to it and continues execution.

supervisor decrypting data

So we arrive at the following code block structure:

Codeblock Illustration


Tracing Execution with strace

The execution flow can simply be traced with the help of strace. Since it does all of the work with ptrace we see a lot of calls in strace output.
As we found out through static analysis, it starts off by comparing 4 bytes after an interrupt with 1337BABE, and looks out for FEEDC0DE and DEADC0DE afterwards.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
wait4(-1, NULL, 0, NULL)                = 5962
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=5962, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
ptrace(PTRACE_CONT, 5962, NULL, 0) = 0
wait4(-1, NULL, 0, NULL) = 5962
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=5962, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
ptrace(PTRACE_GETREGS, 5962, NULL, 0x7ffdd357e660) = 0
//v18 = ptrace(PTRACE_PEEKTEXT, c_pid, v19, 0LL);
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7dd, [0x909003011337babe]) = 0
//v17 = ptrace(PTRACE_PEEKTEXT, c_pid, v19 + 4, 0LL);
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e1, [0x9090909090900301]) = 0
//v16 = peek_until_match(c_pid, v19, 0xFEEDC0DELL) + 4;
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7dd, [0x909003011337babe]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7de, [0x90909003011337ba]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7df, [0x9090909003011337]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e0, [0x9090909090030113]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e1, [0x9090909090900301]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e2, [0x9090909090909003]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e3, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e4, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e5, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e6, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e7, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e8, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7e9, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7ea, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7eb, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7ec, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7ed, [0x9090909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7ee, [0xde90909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7ef, [0xc0de909090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f0, [0xedc0de9090909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f1, [0xfeedc0de90909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f2, [0x48feedc0de909090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f3, [0x8d48feedc0de9090]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f4, [0x3d8d48feedc0de90]) = 0
//found feedc0de
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7f5, [0x8d3d8d48feedc0de]) = 0
//v7 = peek_until_match(c_pid, v19, 0xDEADC0DELL);
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b7dd, [0x909003011337babe]) = 0

Also if the 4 bytes after an interrupt is DEADCODE it starts encrypting the code previously decrypted. PTRACE_PEEKTEXT and PTRACE_POKETEXT are used consecutively for decryption/encryption.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ptrace(PTRACE_GETREGS, 5962, NULL, 0x7ffdd357e660) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b39a, [0x9090aabb1337babe]) = 0 //decryption
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b39e, [0x909090909090aabb]) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3b9, [0x5bb0b08e0fb609f6]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3b9, 0xb0335b0de435e275) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3bd, [0xc0410636b0335b0d]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3bd, 0x28c1eeb658b3b38d) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3c4, [0xe86a15c4607e2828]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3c4, 0x1ebfc4589ffc1a9) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3ba, [0xb658b3b38de435e2]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3ba, 0x3bbd3e560001b807) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3be, [0xc1a9c1ee3bbd3e56]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3be, 0xffffffb805eb0000) = 0
ptrace(PTRACE_GETREGS, 5962, NULL, 0x7ffdd357e660) = 0
ptrace(PTRACE_SETREGS, 5962, NULL, 0x7ffdd357e660) = 0
ptrace(PTRACE_CONT, 5962, NULL, 0) = 0
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_TRAPPED, si_pid=5962, si_uid=1000, si_status=SIGTRAP, si_utime=0, si_stime=0} ---
wait4(-1, NULL, 0, NULL) = 5962
ptrace(PTRACE_GETREGS, 5962, NULL, 0x7ffdd357e660) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3ce, [0x5dfc458bdeadc0de]) = 0 //encryption
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3b9, [0xeb00000001b80775]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3b9, 0x88eb88ea30ecfd) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3bd, [0xffffb8050088eb88]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3bd, 0x1774508ee8030303) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3c4, [0x1ebfc4589ffff17]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3c4, 0xe86115cf6075169d) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3ba, [0x8ee8030303ea30ec]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3ba, 0x3068eed8e04bd02) = 0
ptrace(PTRACE_PEEKTEXT, 5962, 0x56468f86b3be, [0x169d745003068eed]) = 0
ptrace(PTRACE_POKETEXT, 5962, 0x56468f86b3be, 0x28c04a0d3d5bb0b0) = 0
ptrace(PTRACE_GETREGS, 5962, NULL, 0x7ffdd357e660) = 0
ptrace(PTRACE_SETREGS, 5962, NULL, 0x7ffdd357e660) = 0

Also as you can observe the patched addresses are not in any order and overlap too.

Decryption at specific locations

So the final code flow can be depicted as follows:

Codeflow


Analysing Decryption Algorithm

The decryption algorithm is simple indeed!

It uses the qwords initialised earlier and references those according to the two specific bytes after 13 37 BA BE.
Each decryption cycle uses 5 specific qwords at a time.

The layout is as follows :

1
2
3
4
5
6
7
8
9
10
11
0x5607d9b68d60 0x0000000000007777
0x5607d9b68d68 0x0000000700000018
0x5607d9b68d70 0x0000000000000008
0x5607d9b68d78 0x0000000000000041
0x5607d9b68d80 0x0000000000000000

0x5607d9b68d88 0x0000000000007777
0x5607d9b68d90 0x0000007000000018
0x5607d9b68d98 0x0000000000000008
0x5607d9b68da0 0x0000000000000042
0x5607d9b68da8 0x0000000000000000

It uses some bytes from crackme and does simple xor operations with these qwords.
Here is the psuedocode implemented in python :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dec_key_data = [0x0000000000000301,
0x0000000700000018,
0x0000000000000008,
0x0000000000000041,
0x0000000000000000]

crackme_bytes = 0xffffffffbdb809a8

xor_elem = (dec_key_data[0] + dec_key_data[4])

x = dec_key_data[3] ^ (xor_elem & 0xff)
y = dec_key_data[3] ^ (xor_elem >> 8)

xor_res = x ^ (y<<8)\
^ (x << 16) ^ (y << 24)\
^ (x << 32) ^ (y << 40)\
^ (x << 48) ^ (y << 56)

dec_bytes = crackme_bytes ^ xor_res
print(hex(dec_bytes)) #0xbdbfbdbffff84be8

Writing an Unprotector

I thought of several ways to approach it such as :

  • Patching encryption function call in supervisor and use strace
  • Use a regex to filter out the good bytes from the strace log
  • Use IDAPython to patch some bytes and unprotect it
  • Use emulation to only decrypt code

For the first one, its not that easy there’s a catch, simply patching the encryption call will not work as the decryption key is also generated from the encrypted bytes afterwards.
The second one is a faster approach among the others and will surely give us the flag easily but its not fun and not the best way to solve it imo.
The last two would require us to understand the decryption process as well but will help us in the long run if we are given another binary protected with the same fancy bl33d1ng edg3 pr0t3c70r!!!
I’ll use the third one here.

We begin by finding all the 1337babes in the text section.

1
2
3
4
5
6
7
8
9
10
11
def find_1337babes():
res = []
ea = idaapi.get_segm_by_name('.text').startEA
while True:
found_addr = FindBinary(ea, SEARCH_NEXT|SEARCH_DOWN, "BE BA 37 13")
if found_addr != 0xFFFFFFFFFFFFFFFF:
res.append(found_addr)
ea = found_addr
else:
break
return res

Those 1337babes addresses are used for identifying a code block as encrypted and then we look for first occurences of feedc0de and deadc0de.
I also wrote a short script to dump the qwords data from the supervisor binary.
Then we mark the addresses to be patched by our decrypt function.

1
patch_addr = leetbabe_addr + 4 + (qword[1] & 0xff) + (qword[1] >> 32)

And at the end we replace useless bytes with nops.

The patching function is as follows :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def start_patch(leetbabes):
dec_key_data = [['0x7777', '0x700000018', '0x8', '0x41', '0x0'], ['0x7777', '0x7000000018', '0x8', '0x42', '0x0'], ['0x7777', '0xdb00000018', '0x8', '0x43', '0x0'], ['0x7777', '0x3b00000018', '0x8', '0x27', '0x0'], ['0x7777', '0xa300000018', '0x8', '0x94', '0x0'], ['0x707', '0x700000014', '0x8', '0x41', '0x0'], ['0x707', '0x800000014', '0x8', '0x42', '0x0'], ['0x707', '0xb00000014', '0x8', '0x43', '0x0'], ['0x707', '0x700000014', '0x8', '0x27', '0x0'], ['0x707', '0x700000014', '0x8', '0x94', '0x0'], ['0xaabb', '0x700000014', '0x8', '0x41', '0x0'], ['0xaabb', '0xb00000014', '0x8', '0x42', '0x0'], ['0xaabb', '0x1200000014', '0x8', '0x43', '0x0'], ['0xaabb', '0x800000014', '0x8', '0x27', '0x0'], ['0xaabb', '0xc00000014', '0x8', '0x94', '0x0'], ['0x202', '0x700000018', '0x8', '0x41', '0x0'], ['0x202', '0x9000000018', '0x8', '0x42', '0x0'], ['0x202', '0x11b00000018', '0x8', '0x43', '0x0'], ['0x202', '0x4b00000018', '0x8', '0x27', '0x0'], ['0x202', '0xd300000018', '0x8', '0x94', '0x0'], ['0xabcd', '0x700000018', '0x8', '0x41', '0x0'], ['0xabcd', '0x9000000018', '0x8', '0x42', '0x0'], ['0xabcd', '0x11c00000018', '0x8', '0x43', '0x0'], ['0xabcd', '0x4b00000018', '0x8', '0x27', '0x0'], ['0xabcd', '0xd300000018', '0x8', '0x94', '0x0'], ['0x1234', '0x700000018', '0x8', '0x41', '0x0'], ['0x1234', '0x7700000018', '0x8', '0x42', '0x0'], ['0x1234', '0xe900000018', '0x8', '0x43', '0x0'], ['0x1234', '0x3e00000018', '0x8', '0x27', '0x0'], ['0x1234', '0xad00000018', '0x8', '0x94', '0x0'], ['0x301', '0x700000018', '0x8', '0x41', '0x0'], ['0x301', '0x7800000018', '0x8', '0x42', '0x0'], ['0x301', '0xec00000018', '0x8', '0x43', '0x0'], ['0x301', '0x3f00000018', '0x8', '0x27', '0x0'], ['0x301', '0xaf00000018', '0x8', '0x94', '0x0']]
dec_key_data = [[int(x,16) for x in lst] for lst in dec_key_data]

for leetbabe_addr in leetbabes:
feedcode_addr = FindBinary(leetbabe_addr, SEARCH_DOWN , "DE C0 ED FE")
deadcode_addr = FindBinary(leetbabe_addr, SEARCH_DOWN , "DE C0 AD DE")

print("1337BABE @ "+hex(leetbabe_addr))
print("FEEDC0DE @ "+hex(feedcode_addr))
print("DEADC0DE @ "+hex(deadcode_addr))

ref_bytes = Qword(leetbabe_addr+4) & 0xffff

for qword in dec_key_data:
if qword[0] == ref_bytes:
patch_addr = leetbabe_addr + 4 + (qword[1] & 0xff) + (qword[1] >> 32)
crackme_bytes = Qword(patch_addr)
dec_res = decrypt(crackme_bytes, qword)
print("Patching " + hex(crackme_bytes) + " with " + hex(dec_res) + " @ " + hex(patch_addr))
ida_bytes.patch_qword(patch_addr, dec_res)

print("Patching unnecessary code with NOPS :)")
ida_bytes.patch_bytes(leetbabe_addr-1, "\x90"*7) # 0xcc + 1337babe + 2 bytes
ida_bytes.patch_bytes(feedcode_addr, "\x90"*4) # feedc0de
ida_bytes.patch_bytes(deadcode_addr-1, "\x90"*5) # 0xcc + deadc0de


Analysing Unprotected Crackme

Lets verify if we have successfully patched it.

1
2
3
└──╼ $./crackme.enc
Hello there!
Error! https://www.youtube.com/watch?v=Khk6SEQ-K-k

Clean! We can now start working on the crackme.
The crackme looks for a file named secret_key, checks it and then later decryptd the flag.png.enc file with AES.

Crackme main patched

If there are some errors in file operations we get an error.

File operations

The key checking algorithm is not too complex either. It looks like it is somewhat similar to a custom binary to decimal conversion algo. It looks for a ? that indicates a decimal has been processed. The result is then used as an index to some predefined bytes which it later compares with another index(1-127).

Key checking algo

I dumped those bytes and wrote a short keygen script.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#empty string => 0
bin_dic = {0:''}
j = 1
#generates all binary strings of n bits
for n in range(1,7):
maxint = int('1'*n,2)
for i in range(maxint+1):
data = (bin(i)[2:].rjust(n,'0'))
bin_dic[j] = data
j+=1


bin_list = [0x1b, 0x59, 0x29, 0x4c, 0x3d, 0x6f, 0x22, 0x7f, 0x26, 0x1c, 0x2c, 0x2f, 0x07, 0x4e, 0x17, 0x1e, 0x61, 0x0a, 0x53, 0x10, 0x34, 0x65, 0x4a, 0x42, 0x58, 0x08, 0x1d, 0x60, 0x33, 0x55, 0x37, 0x44, 0x52, 0x39, 0x2e, 0x72, 0x0f, 0x6e, 0x7e, 0x3f, 0x32, 0x47, 0x5a, 0x13, 0x19, 0x06, 0x7a, 0x51, 0x18, 0x1a, 0x63, 0x48, 0x02, 0x77, 0x3e, 0x54, 0x35, 0x16, 0x04, 0x5e, 0x4f, 0x49, 0x30, 0x03, 0x15, 0x71, 0x4d, 0x11, 0x38, 0x12, 0x05, 0x45, 0x27, 0x68, 0x3a, 0x75, 0x09, 0x20, 0x01, 0x40, 0x69, 0x23, 0x6a, 0x3b, 0x41, 0x5f, 0x7b, 0x57, 0x3c, 0x1f, 0x66, 0x56, 0x5c, 0x0c, 0x36, 0x73, 0x2d, 0x67, 0x43, 0x5d, 0x4b, 0x28, 0x76, 0x78, 0x7d, 0x31, 0x6d, 0x25, 0x14, 0x74, 0x5b, 0x6b, 0x0d, 0x50, 0x70, 0x64, 0x0e, 0x62, 0x2b, 0x0b, 0x46, 0x2a, 0x7c, 0x79, 0x6c, 0x24, 0x21]
key = ""

for i in range(1,128):
num = bin_list.index(i)
key += bin_dic[num]+'?'

print(key)
1
2
3

001111?10101?000000?11011?000111?01110?101?1010?001101?0010?111000?011110?110001?110101?00101?0100?000100?000110?01100?101101?000001?11010?111?10001?01101?10010??010?1011?0000?011010?001110?111111?11?010010?111110?101100?001?001001?100110?1?111010?110111?011?100001?00011?100?11111?101010?01001?1101?0101?11001?011111?1111?000101?00010?001011?010100?011001?01?10111?01000?010000?010101?1000?100011?00000?001000?111001?01010?10100?11110?0111?100101?00?000011?110?11101?110010?10000?00001?0011?11000?1110?011100?011000?1001?0?01011?101111?011101?100100?11100?010110?1100?0001?110110?10011?110100?0110?011011?100010?001010?010001?010011?110000?111101?101011?00110?10?110011?000010?00100?100000?101110?001100?100111?10110?101000?111100?01111?010111?111011?101001?00111?000?

1
2
3
4
└──╼ $./crackme.enc
Hello there!
Decoding done!
Check out flag_decoded.png

And finally we have the flag! 0xCC~0xCC

flag_decoded.png


Solution Files

You can find the unprotector script along with the idb files on my github here :
mrT4ntr4/Challenge-Solution-Files/justCTF_2020_debugme

This was an awesome crackme with a clever design and can prove a good resource for practicing tooling too..
I hope we get to see bl33d1ng edg3 pr0t3c70r v2, the next year!