AUCTF Reversing Writeups

I thought it was time for a reversing writeup involving a little Python and Cutter (radare2 GUI) legwork; so I picked 2 binaries I did during AUCTF 2 weeks ago. I think you can still get the binaries from the site.

1. Sora

A nice little Kingdom Hearts reference to start us off. This binary was pretty simple; it asks for a key as input, mashes the key up in an encrypt function, and compares it to a ‘secret.’

It’s good practice for making keygens, or really easy if you have a template and decompiler. Let’s start by analyzing the main function:

Graph View.
Decompiler View (love that cutter uses the Ghidra decompiler).

So as you can see, we want to get to the print_flag function. Thus we want a return value from the encrypt function that isn’t zero.

Let’s take a closer look at encrypt in the decompiler:

Looks kind of nasty. We have our input string arg1, this thing obj.secret, a lot of arithmetic transformation, and 2 possible return values. There’s also variable var_18, which is the iterator. Var_18 keeps incrementing, but as we can see, if it makes it past uVar1, which is the length of obj.secret, we get a return 1 (which we want).

Let’s examine the break condition:

We don’t want to break out of the loop because that causes a return 0. It’s a little hard to read, so let’s break it into pieces (or if you follow the complicated arithmetic, feel free to skip to The Secret):

(char *)(arg1) – this is the first character of our input string

(char *)(arg1 + (int32_t)var_18h) – this is a character in our string chosen by the iterator var_18h; if our string is “ABCD” and var_18h is 2, the current value of this expression is “C”.

(char *)(arg1 + (int32_t)var_18h) * 8 + 0x13) % 0x3d + 0x41 – the character in our input string gets multiplied by 8, that product gets added to 0x13, the result modulo’d by 0x3d and that result added to 0x41.

(int32_t)*(char *)(arg1 + (int32_t)var_18h) * 8 + 0x13) % 0x3d + 0x41 != (int32_t)*(char *)(int32_t)var_18h + _obj.secret))

The statement above is the full expression. The character in our input string is transformed by those operations and compared to the character at the same position in the secret. If the two do not match on any character, we break the loop and fail.

Sorry if all those steps convoluted the problem, but I think it’s good to write for beginners.

The Secret

This secret’s pretty easy to find: switch to the disassembly and double click on the use of _obj.secret:

So we have the secret; it’s “aQLpavpKQcCVpfcg”. We need a string, that when mangled in the way described, matches this secret. So let’s make a keygen.

The Keygen

I don’t know how other people make keygens, but I usually use a while loop and make an alphabet, input the secret, and have an empty string that becomes the key. We’ll iterate through the alphabet, mangle each character according to the algorithm, and check to see if it matches the current character in the secret. If it does, we add it as an element in the key, and keep going until our key is the same length as the secret.

Since sora is an interactive binary, I’m gonna assume that only printable characters can be inputted. So I used the string.printable constant from the string module.

Okay, enough teasing; here’s the code:

#!/usr/bin/python
import string

alphabet = string.printable
ciphertext = "aQLpavpKQcCVpfcg"
decrypted = ""
i=0
while True:
        if (len(ciphertext)<1):
                break
        x = ord(alphabet[i]) #ord turns the char into a number, then we mangle it
        x*=8
        x+=0x13
        x%=0x3d
        x+=0x41
        if (chr(x)==ciphertext[0]): #don't forget to turn the number back into a char
                decrypted+=alphabet[i] #add the matched char to the key
                ciphertext = ciphertext[1:] #I remove the front char from the ciphertext to increment 
        i+=1
        if (i>=len(alphabet)):
                i=0
print(decrypted)

So there it is. I prefer to remove the first character of the ciphertext with string slicing (string[1:]) each time a match is found, so I don’t have to iterate both the alphabet and the ciphertext.

When we run our keygen sorakey.py, it spits out a key pretty much immediately, and we can test our key against the sora binary:

The text we get back from sora means the key was accepted! And it works on the server; I’ve tested. So that’s one challenge down 🙂

2. Don’t Break Me

The next challenge is similar but a bit more involved.

It also looks for a key to validate. I know what you’re thinking: Are those hex bytes the key? Sadly, no. But they do make a cool message:

So if we examine the main function for dont_break_me, we see that there’s more going on than last time:

So in brief, our input is scanned into acStack8224, stripped of its newline, encrypted and then compared to the result of get_string. This function takes a pointer to arg_8h and fills its buffer with the secret. But if we look at get_string, the secret string is built at runtime and we can’t see it in a disassembler:

There’s a debugger check too, so if we debug it we’ll have to patch some jumps. Right? Well, fortunately there’s a way around it, and that way is called ltrace. ltrace runs binaries and intercepts calls to imported libraries; in this case, the output of strcmp is especially useful to us:

It might be hard to read, but I input “test”, it’s mangled into “VAEV” and compared with the string “SASRRWSXBIEBCMPX”. That’s our ciphertext. So ltrace saved us a lot of time!

2 Roads: Encrypt or Decrypt?

Finding the ciphertext was the easy part. Now we have to examine the encrypt function to see how input is mangled. But before we do that, a little Easter egg from the challenge creators. They included a decrypt function! It’s never referenced/used by the code, so it really is just extra. What we were going to do was make a keygen to find the winning combo, but we could just take the ciphertext and rewrite decrypt in Python. We’ll do that at the end.

Encrypt vs Decrypt

You’ll see that the while loop in encrypt looks pretty similar to sora. The iterator var_ch increments up till the length of our input string. Characters in our input string are transformed. This time, instead of checking against the character in another string, each mangled character is just appended to an output string (iVar3). But how is it mangled?

Keygen Against the Ciphertext

One complicating factor is that encrypt uses arguments passed in from main (see the use of the highlighted arg_10h and also arg_ch:

We need to go back to main to find out what values are passed:

So, arg_ch is 0x11 and arg_10h is 0xC. Now can substitute these values into the keygen.
Let’s redo our keygen from sora and change the arithmetic transformations:

#!/usr/bin/python
import string

alphabet = string.printable
ciphertext = "SASRRWSXBIEBCMPX"
decrypted = ""
i=0
while True:
        if (len(ciphertext)<1):
                break
        x = ord(alphabet[i]) # changes start here
        x-=0x41
        x*=0x11 # this was arg_Ch
        x+=0xc # this was arg_10h
        x=x+int(x/0x1a)*(-0x1a)+0x41 # changes end here
        if (chr(x)==ciphertext[0]):
                decrypted+=alphabet[i]
                ciphertext = ciphertext[1:]
        i+=1
        if (i>=len(alphabet)):
                i=0
print(decrypted)

And see if we get our key!

Well, that’s not the prettiest key, but it works. The thing about a keygen is that multiple values may be accepted. You can constrain the value to just letters or numbers or any smaller set by changing the alphabet you use, but keep in mind there may not be a key in those constraints. But anyways, let’s try the other route; re-implementing decrypt function in python.

Decrypt the Ciphertext

When we re-examine decrypt, one additional call that was not in encrypt stands out (see the highlighting):

We see that arg_ch is passed into this new function called inverse, and the result (iVar3) is used in the arithmetic transformation. So in order to re-implement decrypt, we’ll have to re-implement inverse(arg_ch).

I honestly have no idea why it’s called an inverse function and didn’t want to spend a ton of time on math. But regardless, this function processes arg_ch, which is the value 0x11. Once all the pieces are put together, it looks like this:

The Decryptor

#!/usr/bin/python

secret = "SASRRWSXBIEBCMPX"
decrypted = ""
def invert(x):
        i=0
        j=0
        while (j<0x1a):
                if ((x*j)%0x1a==1):
                        i = j
                j+=1
        return i

for i in secret:
        y = ord(i)
        y+=0x41
        y-=0xc
        y*=invert(0x11)
        intermediate = y+int(y/0x1a)*(-0x1a)+0x41
        decrypted+=chr(intermediate)
print (decrypted)

It’s a shotgun script for sure, but simple enough. Let’s see what happens when we decrypt the secret with our script decrypt.py:

Ooh and that’s a much more satisfying key, IKILLWITHMYHEART. And, you probably guessed it: if we constrain our keygen to using the alphabet string.ascii_uppercase, we’ll get this key generated 🙂

Well that’s it! A bit of a long blog post for 2 fairly simple rev challenges, but I’m just happy to be posting again. I’ve been doing a lot of forensics lately, so I’ll likely be posting rev and malware for the next couple of weeks. Thanks for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *