[noxCTF] Ra(p)hic

Challenge by -

Writeup for Rap(h)ic; Reversing 966

Intro

This is a hard Reversing challenge from noxCTF. The solver only gets an ELF file.

Description: There is nothing like overly designed design

The original file is attached at the bottom of the document.

Summary

We get an ELF file. Upon execution, it will load another ELF file from its memory into “/tmp/…” and execute it.

The processes use a socket to communicate with each other - The parent process sends addresses which the child process later executes and sends its return value back to the parent. Any return value other than 0 will cause both the parent and the child to exit.

The addresses sent from the parent to the child are functions which contains numerous constraints on the key. After meeting all of them we get the key “THIS_IS_THE_MOST_SECURE_KEY_a1”.

Executing “raphic” with this key will give us the flag: “noxCTF{Reversed_Pr3tty_Camel}“

Way of Work

We start by running “file” on raphic. It’s a statically linked, stripped 64 bit ELF file.

file

After executing it we get the message “Usage: ./raphic <key>”.

usage

Still nothing. We can run “strace” and see what it does.

strace

The program starts by writing some data to the file “/tmp/…” before using clone. It then opens a socket and sends some data through it. Let’s also trace the child using the flag “-f”.

strace_child

The child executes “/tmp/…” - The file that was created just now. It then unlinks itself (deletes the file after exit). The next action is to bind SIGTRAP with some function in the binary. Afterwards, the parent and child will communicate through a socket.

We understand that there is not a single binary to reverse but two of them. I chose to extract the inner file with “binwalk”.

binwalk

The file starting at offset 0 is raphic itself. Trying to open 0x9EF90 with IDA will tell us that the header entry size is invalid, and we are left with the file at 0xB20D0 - Our child process.

We can start by understanding our parent process.

Note: The binary is statically linked, we can understand what some of the functions are by diving into them and search for a syscall (that’s how I found write, read and a few more).

parent_main

The parent executes the child and then sleeps for a second - Probably waiting for the child to start. The next thing that it does is connect to a socket at /tmp/socket.socket (As seen before, it’s used to communicate between the parent and the child) and sends some data through it. Later on, it reads the data sent back from the child and exits if it isn’t 0 (”\x00”*8 is more accurate).

Let’s open the child in IDA and see what it does.

child_broken_main

This is not what we expect to see in the main function. You may have noticed that “jz; jnz” with the same destination. It’s a trick used to fool disassemblers. In order to fix it, we need to convert the following instruction directly to data, replace the first byte with 0x90(nop) and convert it back to code. After fixing it we can disassemble the entire main and treat it as a function which can then be decompiled.

child_main

First of all, we can see the unlink and a call to some function with sub_400BAB. This is the same address that we saw in strace earlier - Let’s rename it to sigtrap_handler.

After setting up the socket we face a loop. In each iteration, the first 8 bytes read from the socket are a function the child will later call. The second time read is called we get the key that was passed to the parent.

Our goal now is to understand the functions at the addresses sent from the parent and make them all return 0 - Otherwise, the program will exit.

This is the list of addresses:

0X400C59
0X400CF0
0X400D42
0X400D7C
0X400E08
0X400EDB
0X400E36
0X400F0B

First check:

check1

We face a debug break here (aka “int 3” or “0xCC”). We know that it will invoke a call to sigtrap_handler so we need to check it out as well.

sigtrap_handler

We can easily see that key[0:4]= “THIS” and key[12:16]=“MOST”. sub_400B5D simply xors each byte with 0x42. The following python line will give us the wanted value of our string.

''.join([chr(ord(x)^0x42) for x in '\x1D\x11\v\x1D'][::-1])

output: “_IS_”.

And we just found key[4:8].

check2

There are multiple choices that satisfy this, we can get them all with this python script.

import itertools
printable = [chr(x) for x in range(0x21,0x7f)]
correct_options = []
for option in itertools.product(printable, repeat=4):
     if not ((ord(option[0]) ^ ord(option[1])) != 28 or (ord(option[2]) ^ ord(option[3])) != 26):
             correct_options.append(''.join(option))

len(correct_options)

output: 8464.

We can print all of the options later, let’s just leave it for now.

check3

key[16] = ‘_’ , key[23]=’_‘

So far we have the following template:

THIS_IS_XXXXMOST_******_

Where X represents one of check2’s options and * an unknown character. There could be more characters following the key.

check4

The first condition is key[18] == key[22]. We then go into some sub-functions.

sub_402660

sub_402D20

Searching for the magic values as they are won’t produce anything useful but splitting it into DWORDS and searching for “67452301 EFCDAB89 98BADCFE 10325476” will give you md5 as the first result.

In addition, “MD5 part of OpenSSL” appears in the strings of the process.

It was also possible to use lscan which would identify libcrypto in the child. After adding the signature file to IDA you could see md5_block_asm_data_order is used inside one of the inner functions.

We could summarize sub_402660 as “store the md5 of a2 bytes, starting from key_17 into a3”.

The md5_sum is compared to “unk_4BEB04”, time to use MD5 Decrypter.

The following IDAPython script will get us the bytes, and our original string is SECURE.

print ''.join([hex(idc.Byte(x))[2:] for x in range(0x4BEB04, 0x4BEB04 + 16)])

Our key:

THIS_IS_XXXXMOST_SECURE_

check5

We don’t know which function is called exactly but can safely assume it’s strcmp. It mst return 0 so we can also be certain that this is the end of the string.

Almost complete key:

THIS_IS_XXXXMOST_SECURE_KEY_a1

func6

There is no validation here, just and md5 of our entire key saved into some global buffer (rename it to md5_of_key).

func7

Our input isn’t used here, we don’t actually need to understand this function (It’s AES-CTR decryption if you wanted to know).

And our last function, which is definitely a favorite:

print_flag

All that’s left for us now is to find the correct combination of check2.

After looking at the rest of the key we can assume that this part of the key ends with a ‘_’. Let’s filter our original results.

filtered = filter((lambda x: x[3]=='_'), correct_options)
for option in filtered:
     print ''.join(option)

We could either brute-force all of these options (There are only 92 of them) or we could look at the list and see the option “THE_” which makes perfect sense here.

Final key:

THIS_IS_THE_MOST_SECURE_KEY_a1

flag

The flag is: noxCTF{Reversed_Pr3tty_Camel}

Original file: raphic

Child: child

IDA databases: raphic.i64 child.i64