We are given the option of unlocking 3 keys, and each key is unlocked using a different function.
If we unlock all the keys, we will be given access to arm the nuke, and if we arm the nuke, we can launch it and “win”.
While we do this, we also need to keep an eye out for ways to control EIP so that we can gain code exec and spawn a shell.

The first vulnerability we can observe is in the menu that is printed.

The address of our wopr chunk is leaked when LAUNCH SESSION - 1448484872 is printed.

We are asked to enter a launch key, and this launch key is strncmp()-ed with the string in GENERAL_HOTZ.key for not more than strlen(GENERAL_HOTZ.key) bytes.

If we look carefully, we can see there is an integer underflow vulnerability that allows us to trivially bypass this check!Specifically, we can trigger this integer underflow vulnerability if we set our user launch key to be a single null byte, or \x00.

When we set our user key to \x00, unsigned __int8 len_user_key will be set to 0xff when len_user_key = strlen(&user_key) - 1; is called, because 0-1 = 0xffffffff and an unsigned __int8 type is 8 bits.

Next, strncpy(keyauth_one, &user_key, len_user_key); is called, which copies 0xff bytes from user_key, to keyauth_one. However, we’ve only allocated 0x80 bytes for keyauth_one. keyauth_one+0x80 is where the actual data from GENERAL_HOTZ.key is stored, and when this strncpy() is called, the data there will be overwritten!

Interestingly, if we read the manpage for strncpy(), we can see the following.

If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written.

Therefore, even though the actual length of our user_key is 0 bytes, strncpy() will still fill the rest of the 0xff bytes with null bytes!

So the data stored in GENERAL_HOTZ.key will be overwritten with null bytes.

Finally, strncmp(), which is called next, contains the following statement in its manpage.

The strncmp()function compares not more than n characters. Because strncmp() is designed for comparing strings rather than binary data, characters that appear after a `\0' character are not compared.

So, because both our user_key and hotz_key buffers contain a \x00 as their first byte, nothing after it will be compared.
And because \x00 == \x00, we will pass this check and unlock key 1 :)

KEY 3

Unlocking keys 3 and 2 is a little trickier.
And the reason I say keys 3 and 2, instead of 2 and 3, is because I unlocked key 3 before I unlocked key 2 using a use-after-free vulnerability I found, that overlayed a key 2 object over a key 3 object.

Let’s break this function down.
keyauth_two() first takes both a 16-byte AES-128 crypto key and 16 or 32-bytes of data from the user. The user provided key is then used to AES encrypt the data using a static initializtion vector (IV), 0xfeedfacfdeadc0debabecafe0a55b00b.

The same data is also encrypted in the same way except using the actual data from GENERAL_CROWELL.key.

The ciphertext of the data that is encrypted using the actual crowell_key is stored at offset keyauth_two_chunk+0x54, and the ciphertext that was generated using our user provided key is stored at offset keyauth_two_chunk+0x74.

If we consider the fact that we can free the key 3 object and allocate a key 2 object over this, our goal becomes clear.
We need to generate a ciphertext using our user-provided AES crypto key in such a way that 0x31337 is written to offset keyauth_two_chunk+0x84. If we do this, then the next time we attempt to unlock key 3, we will pass the check at the end of the key 3 validation function, and unlock key 3!

We can easily find the plaintext that will generate a ciphertext that meets this condition using the python library, PyCrypto.

We first set our key to be 16 0x0’s. Then we set our ciphertext to contain 0x31337 at offset ciphertext+0x10.

key="\x00"*(16)cipher="\x00"*0x10+p32(0x31337)+"\x00"*0xc

obj=AES.new(key,AES.MODE_CBC,IV)plaintext=obj.decrypt(cipher)

When we decrypt it, we are able to produce a plaintext that encrypts to a ciphertext with 0x31337 at offset ciphertext+0x10!
After using this plaintext as the data to encrypt in keyauth_two(), we simply run the keyauth_three() method again, providing any input to pass the check and unlock key 3!

KEY 2

In order to unlock key 2, we need to exploit the use-after-free again, this time using key 3 to produce useful leaks about key 2.

Additionally, we need to understand a bit of crypto.

Let’s start with what happens when we submit a valid session ID when authenticating key 3.

When we do this, 0x40 bytes of data starting at keyauth_three+0x4 are xor’d with randomly generated bytes.

RESPONSE MUST BE ENTERED IN HEX FORMAT EXAMPLE: 4C304C4343444331534630524E75427A

TIME NOW: 1488756683 YOUR RESPONSE:

There are a couple things to notice here.
First, if we look again at the how the struct for the keyauth_two chunk is defined, we observe that the 16 byte crowell_key exists at offset keyauth_two+0x34, which aligns with the end of the 64-byte challenge starting at keyauth_three+0x4 that is printed out in keyauth_three().

00000034 crowell_key db 16 dup(?)

00000004 challenge db 64 dup(?)00000044 response db 64 dup(?)

Secondly, we can observe in the output that the current system time is leaked.

Additionally, because we are using a PRNG, the “randomly” generated bytes that are used for the xor operations are not truly random. If we seed our PRNG twice with the same seed, the same “random” bytes will be generated by rand() in the exact same order! PRNGs are only as random as the seed used to initialize them!

And how is our PRNG seeded?

In the main() function, our PRNG is seeded using the address of our wopr heap chunk, and also the current calendar time.

Well, we already know what the value of wopr_self is because it is printed out in the menu. And although we don’t know what the current system time is, we can brute force it by using the leak of the system time we got in keyauth_three() and decrementing the time from there
Therefore, we can recover the seed that was used to initialize our PRNG and reliably predict the bytes that are generated each time rand() is called!

To do this, I actually wrote a separate helper program to print out the starting bytes for different seeds start at wopr+time and working my way down.

I did this after re-running rpisec_nuke and attempting to decrypt data using a random key 2 and blank data. This is so keyauth_three+0x4 through keyauth_three+0x24 will be filled with null bytes, allowing us to compare the resulting rand() generated bytes with our brute force results.

Putting everything together, this is the flow we will use to crack key 2.

allocate key 3 chunk

free key 3 chunk

allocate key 2 chunk with blank data

attempt to validate key 3 again

view challenge output & compare to expected rand() bytes

In our run session of rpisec_nuke, we get the following challenge output:

Essentially, each dword of our inputted nuke code is xor’d against the checksum in the first dword of our nuke object, and the result replaces our current checksum, which is subsequently xor’d with the next dword in order to calculate the new checksum and so on and so forth until the checksum is finally xor’d with the 0x454e44 dword at offset nuke+0x204.

With this in mind, it is trivial to come up with a nuke code that passes this check.

LAUNCHING THE NUKE

Once we have armed the nuke, we can finally launch it.
However, we still haven’t found a way to control EIP yet.
Forunately, there is a way in the launch_nuke() function.

We can see that it initializes a target pointer that initially points to nuke+0x208.
It then iterates through each byte in our nuke_code_hex buffer that we set when we armed our nuke in the program_nuke() function.
Based on the byte it reads from our buffer, different actions, which are decided by a switch statement, are performed.

0x52 = reprogram nuke

0x53 = write to target

0x49 = target++;

0x4f = print target

0x44 = Disarm or Detonate

0x45 = END

If it encounters a byte not included in this list, it simply does nothing and moves onto the next byte.

Additionally, we notice that there is pointer to the function, disarm_nuke() at offset nuke+0x288 and a pointer to the function detonate_nuke() at offset nuke+0x28c.

On a related note, if the string, “DOOM”, is encountered, the function pointer to detonate_nuke() will be called.

(*(void(__cdecl**)(int))(nuke+0x28C))(nuke+0x208);

Using a combination of the actions provided affords us two exploit primitives to work with.

the ability to leak the address of the executable

the ability to overwrite the function pointers at nuke+0x288 and nuke+0x28c

Additionally, if we are able to leak the address of the executable, we can use the leak to also calculate the base address of libc, since Ubuntu’s ASLR sucks, and the distance between the base addr of libc and the base addr of the executable does not change.

After we perform the leak, we can actually reprogram the nuke to reset target and have it perform a different set of actions. We will need to do this is we want to both leak and overwrite nuke+0x28c.

Initially I leaked libc to calculate the address of system@libc which I then wrote to nuke+0x28c while also writing a “/bin/sh\0” string to nuke+0x208, but this gave me a troll shell when I triggerd the function call.*

$ whoamishitshell

So to get around this, I had to write my own ROP chain to manually perform the syscall for execve("/bin/sh\0");
I used both gadgets from libc and a stack pivot gadget from the ELF executable to generate my ROP chain.

Putting everything together, we get a shell using the following exploit.

defleak_elf():r.recvuntil("STATUS:")tmp_leak=r.recv(0xd)elf_leak=(int("0x"+tmp_leak[10],16)*0x10)+int("0x"+tmp_leak[11],16)r.recvuntil("STATUS:")tmp_leak=r.recv(0xd)elf_leak+=(int("0x"+tmp_leak[10],16)*0x1000)+(int(tmp_leak[11],16)*0x100)r.recvuntil("STATUS:")tmp_leak=r.recv(0xd)elf_leak+=(int(tmp_leak[10],16)*0x100000)+(int(tmp_leak[11],16)*0x10000)r.recvuntil("STATUS:")tmp_leak=r.recv(0xd)elf_leak+=(int(tmp_leak[10],16)*0x10000000)+(int(tmp_leak[11],16)*0x1000000)returnelf_leak-0x4021# get elf_base

*After I got the flag, I found out from Doom that they had overloaded one of the _libc_* bootstrapping routines to add their own code which hooks system@libc. The intent was to force students to write their own ROP chain. Very evil of them, but it was a good exercise :)