© Noiche

char nick[] = "N0iche";

printf("https://www.root-me.org/%s\n", nick);

printf("%s\x40protonmail\x2ecom\n", nick);

[AUCTF2020] - Reverse - Purple Socks

Apr 5, 2020 • Write-Up,AUCTF-2020,Reverse

Challenge details :

CTF Challenge Category Value Solves
AUCTF2020 Purple Socks Reverse 960 63

Description

We found this file hidden in a comic book store. Do you think you can reverse engineer it?
We think it’s been encrypted but we aren’t sure.
Author: nadrojisk
File : purple_socks

TL;DR

The purple_sock file was XOR encrypted, so I had to find the key in order to obtain an ELF binary.

The binary was asking for a user and then a password, and both were hardcoded srings.

A final password was asked to access flag.txt and it was also XOR encrypted with a seed pulled from a hardcoded array.

That seed was XOR encrypted itself by the user input !

Let’s get an ELF !

The challenge’s description told us that the file was encrypted, so I first had to get rid of the cipher.

Since the file was probably an ELF or a PE, I tried a common cipher : XOR. But how to get the key ?

ELF files start with the magic number 0x7fELF, and XOR is bijective, so I just XORed the first bytes of the file with that magic number.

Here is my code :

#!/usr/bin/env python3

key = [0x45, 0x7f, 0x46, 0x4C]

purple_sock = [0x0b, 0x31, 0x08, 0x02]

for i in range(0, 4):
    print(hex(key[i] ^ purple_sock[i]))

And the key was… 0x4e ! :D

At this point, I just needed to XOR every bytes of the file with that key, except 0x00 and 0x4e :

#!/usr/bin/env python3

key = int("0x4e", 16)

xored  = open("purple_socks", "rb").read()
output = open("clear.elf", "wb")

for x in xored:
    if x not in [0, key]:
        clear = x ^ key
    else:
        clear = x

    output.write(clear.to_bytes(1, 'little'))

And here it is, a nice ELF file !

file command on the clear.elf file

Reverse time ! First part

At this point I had an ELF, great, let’s run it and see what happen !

first run of the clear.elf file

Hum, user and password uh ? It’s time to use Radare2, here are some extra informations about the binary :

Radare2 iI command

There are also some interesting strings :

Radare2 strings

The main function basically asks for a username and a password, then compares our input to a hardcoded string with a call to strcmp:

Radare2 pdf command on the main

Since there was no antidebug technique, a simple breakpoint on the call strcmp instruction got the job done. Here is the stack view I got when I reached the breakpoint :

Radare2 stack view user:pass

OK, we got the user and the password !

user     = bucky
password = longing, rusted, seventeen, daybreak, furnace, nine, benign, homecoming, one, freight car

I used it to pass the user:password check, bringing me to a simple command prompt :

clear.elf prompt

Second part

The ? command told me that ls, read and quit commands were available…

We had nowhere to netcat for this challenge (in the description), but I remembered the strings.

It looked like I had to use the read command on challenges.auctf.com:30049, in order to read the flag.txt file.

Before connecting to the server, I created a local flag.txt file and I tried to read it using the challenge’s command prompt, but it asked me a password. Hum, let’s dig into Radare2 again and examine the instructions following the strcmp for the user bucky:

Radare2 VV command main

The function commandline is called, here is a graph view of that function.

Radare2 VV command commandline

As you can see on that picture, the commandline function was just displaying the command prompt, collecting my input and calling the function cmd_dispatch with my input as a parameter. So, what’s inside the cmd_dispatch function ?

Radare2 VV command dispatch

That function is a command dispatcher, it parses the user input and then call the function corresponding to the user’s command.

The picture shows the beginning of the function control flow, we can see that a 7+n words input wouldn’t be accepted, and the rest of the function just compares the input with hardocded string. When the input match one of the strings, the adress of the corresponding function is put into eax and a call eax brings us to the right place :

Radare2 call to eax

I quickly found these functions when looking at the symbols, and I confirmed my thougths using some breakpoints :

Command Corresponding function
? cmd_helpmenu
LS cmd_list
READ cmd_read
QUIT cmd_quit

At this point, i decided to take a look at the cmd_read function and to skip the others.

The cmd_read function was basically checking the input (is a filename following the command read ?) in order to call cmd_print_file.

OK… Time to look cmd_print_file then !

Here it is :

Radare2 call to encrypt

If we follow the control flow, we notice that this function open the file and exit if an error occurs.

If everything is OK, then the file name is compared with the string flag.txt and the function encrypt is called if it’s a match.

At the end, we rather have an error or the content of the file being displayed.

The encrypt function was the last step of the journey : for this part, I took a look at the control flow graph in order to have a nice overview.

I saw 2 interesting blocks, the first was xoring the input with a hardcoded seed, and the second one was comparing the xored input with a hardcoded encoded secret :

Radare2 encryption of the input

Radare2 compares

Here is a decompilator output so you can have a better comprehension of what’s happening in this function :

Radare2 decompilator encrypt

As we can see, the function XOR the input with the seed, but I had to be carefull : the XOR seed was overwritten with the resulting bytes !

In order to get the encoded secret and likewise for the seed, I decided to break on the strcmp call and on the xor so I could grab all the bytes I needed.

#!/usr/bin/env python3

array = [0x0e, 0x05, 0x06, 0x1a, 0x39, 0x7d, 0x60, 0x75, 0x7b, 0x54, 0x18, 0x6a]
seed  = [0x61, 0x75, 0x63, 0x74, 0x66] # "auctf"

flag = ""

for i in range(len(array)):
        flag += chr( array[i] ^ seed[i % 5] )

print(flag)

XORing the secret with the seed gave me the password open_ 2y… Hum, something went wrong.

At this point, I noticed that the secret was messed up starting from the 6th letter… Then i remembered that the seed was 5-bytes long and that each byte was overwritten after being used. That means that our first 5 first letters are good, but I forgot to update the seed and that’s why the rest of the secret was wrong.

Here is the correct script :

#!/usr/bin/env python3

array = [0x0e, 0x05, 0x06, 0x1a, 0x39, 0x7d, 0x60, 0x75, 0x7b, 0x54, 0x18, 0x6a]
seed  = [0x61, 0x75, 0x63, 0x74, 0x66] # "auctf"

flag = ""

for i in range(len(array)):
        flag += chr( seed[i % 5] ^ array[i] )
        seed[i % 5] = array[i]

print(flag)

And here it is ! open_sesame\n !

And giving that password gives me… the flag :D

Bonus

I realised that, since we can overwrite the seed, we could also pass the password check with a custom password ;)

(Or at least, for the 4 first letters if we want to keep it simple)

For the fun, I wanted to flag this challenge using the password STO\n

Here is my PoC :

#!/usr/bin/env python3

array = [0x0e, 0x05, 0x06, 0x1a, 0x39, 0x7d, 0x60, 0x75, 0x7b, 0x54, 0x18, 0x6a]
seed  = [0x61, 0x75, 0x63, 0x74, 0x66]

PoC   = "STO\n"

wanted_seed = []

for i in range(4):
        wanted_seed.append( array[i] ^ ord(PoC[i]) ) # We compute the seed that we need and that will overwrite the initial seed

first_input = ""

for i in range(4):
        first_input += chr(wanted_seed[i] ^ seed[i]) # We compute the input to provide to get that wanted_seed

print("You can now bypass the password check : submit \"{}\\n\" and then the password \"STO\\n\" will grant you the access".format(first_input, PoC))

As you can see, the idea is to overwrite the seed with a first input, so the second input can be different than “open_sesame” but still match the encoded secret.

Here is the flag !

auctf{encrypti0n_1s_gr8t_12921}