5

Hack.LU 2013 CTF Wannabe Writeup Part Two: Buffer Overflow Exploitation

Introduction

This blogpost contains a writeup of the second phase of the Hack.LU 2013 Wannabe challenge. The first phase writeup can be found here: Hack.LU 2013 CTF Wannabe Writeup Part One: Web Exploitation

During the first phase, we managed to get ourselves a limited shell (www-data) on a webserver. In this phase, we had to exploit a custom C program compiled for Linux x64 which contained a couple of buffer overflow vulnerabilities. Because of some memory protection measures, a Return-Oriented Programming (ROP) approach was taken. The whole process is described in more detail below.

Goal

After briefly investigating the filesystem of the webserver, it became apparent what our next goal was. We located a file called ‘sign_key.flag’ in user ‘arthur’ his home directory, amongst some others. It was only readable by its owner, arthur. There was also a cronjob running a script called ‘inspector’ every 17 minutes. This script did nothing more than running a custom x64 ELF binary in /home/arthur/bin called ‘control’ with the argument ‘–clean’ and piping this to a log file ‘inst.log’. This binary had the suid bit set, meaning that it will be running under the privileges of its owner, arthur, when executed. We should leverage vulnerabilities in this binary to execute code in the context of the user ‘arthur’, in order to read the contents of the flag file.

Luckily, the source code of this custom binary was also present in the /home/arthur/bin directory (control.c), allowing us to investigate the code for vulnerabilities. Both the precompiled x64 ELF file  ‘control’ and its source code ‘control.c’ are provided as a download archive at the bottom of this post.

It was noticed that insp.log, the output file of every crontab execution of the custom control application readable by everyone including www-data, contained the flag as part of the output of a previous exploitation attempt. This may explain why 7 teams managed to solve this challenge the morning shortly before the CTF deadline… or not.

The x64 ELF control binary was examined for memory protection measures with the checksec script:

Our biggest concern will be the Non-eXecutable stack (NX). This will require us to take a ROP-approach. Luckily, the binary was not compiled as a Position-Independent-Executable (PIE), so its code section (executable by default) is not randomized and we can leverage it to find suitable gadgets.

Buffer Overflow Vulnerabilities

The application presents us with two different code paths that get triggered based on the first command line parameter: –clean or –sign. The sign option requires us to provide a password that is eventually compared with a string in the file /home/arthur/pass, which is not readable for user www-data, so this seems like a dead end:

Looking more into the clean option, its functionality became clear: it parses all files in the ./upload/ directory for lines containing “system();” strings, outputs positives to stdout, and then removes them. This keeps the upload directory of the web application clean (see also part one). However, the implementation is not perfect..

The clean_uploads function is the starting point and does nothing more than looping through all elements in the ./upload/ directory, calling the inspect function on each entry and remove them afterwards. When all elements are removed, the function log_result is called to print out the findings of the inspect function.

The inspect function takes the name of an element of the ./upload/ directory and attempts to parse this element (if it’s a readable file), looking for “system(argument);” strings on each line (see fgets). If there’s a hit, the string argument of the system call is taken and written to a global two-dimensial character array found[255][192]. Another global variable cnt is incremented since it is used to keep track of the number of hits. It also serves as an index in the found array (see line 69-70).

The log_result function prints each of the cnt elements of the global found variable to stdout, prefixed with some information about the call’s position and the total number of detected systemcalls. The prefix is concatenated with the system call argument by means of snprintf (see line 82-85), and copied into a local function variable buffer[224]. The number of bytes copied is equal to strlen(found[i])+32. Since found[i] contains maximum 192 bytes and the prefix string is always smaller than 32 bytes, this should never go wrong, would it?

Off-by-one

When the argument for an identified system(argument) call is longer than 192 bytes, the number of bytes that are written to the found[cnt][192] variable is exactly 192 (line 65-69), which leaves no room for a trailing null-byte (static variables contain zeroes by default in C). This can be leveraged in the log_result function, where strlen() is used on elements of the found[] array to determine the number of bytes to be written to a local buffer[224] variable (line 83). When we construct a file containing two lines with “system(arg);”, where arg is a string longer than 192 bytes, at least 192*2=384 bytes will be written to the local buffer variable, which only has room for 224 bytes. This implies that the off-by-one vulnerability in the inspect function can be exploited to smash the stack of the log_result function.

Off-by-one (2)

However, the log_result function contains a custom canary stack smashing protection measure. In the beginning of the function, the value of the global cookie variable is assigned to a local variable overflow. Since this local variable overflow is declared before the local buffer[224] variable and thus located after this variable in memory (stack grows down), it is overwritten when the call to the snprintf function on line 83 overwrites an overly long string to this local buffer[224] in order to overwrite the return address of the function and smash the stack. This can be verified easily by debugging with gdb. First, put a file with two system()calls containing respectively 200 A’s and B’s in a local upload directory, and then execute the control binary in gdb, breakpointing after the loop of the log_result function. Inspect the variable contents:

Thus, in order to overwrite the stack, we must make sure the local overflow variable contains the same value as the global cookie variable after we overwrite its value with our own payload. This requires us to know the value of cookie beforehand, but unfortunately, it is initialized to a random value in the constructor of the program based on /dev/urandom, which is not guessable.

However, there is another off-by-one error that can be reached in the inspect function. The global cnt variable is of type unsigned char, which can take 256 values (0-255) by design. However, the global found[255] array only contains 255 elements (0-254). When we construct a file containing 256 valid system() calls, the argument of the 256th line will be written to element found[255], which is exactly the memory address of the cookie variable. This can also be verified empirically with GDB by constructing a file that contains 256 system() calls, the last one containing a string of which the first four bytes will overwrite the global cookie variable contents:

Also, note that the global unsigned char cnt variable wrapped around from 256 to 0 again, making the log_result function believe no elements are present in the global found variable.

Exploitation

We now have all ingredients to successfully overwrite the return address of the log_result function:

  1. We first put an arbitrary value in the global cookie variable by exploiting the second off-by-one vulnerability (256 lines with valid system() calls, the last one controlling the cookie value)
  2. We then exploit the first off-by-one vulnerability to overwrite the local buffer[224] variable on the stack and take ownership of the return address, making sure that we overwrite the local overflow variable with the same value as set in the previous step.
  3. We take a ROP-oriented approach (Non-eXecutable stack, remember?): the ultimate goal is to get a shell with privileges of user ‘arthur’.

Return-Oriented Programming

In order to successfully conduct step two, offsets to the overflow variable and the return address were calculated. Next, a suitable address to jump back to was located by examining the external functions that the control x64 ELF binary imports:

Especially the last import seemed interesting: system. It expects one string parameter containing the command it must execute on the underlying system – in our case this would be /bin/sh preferably. On the x64 Linux architecture, the calling convention states that the rdi register should hold the first paramater, in our case the address of the string “/bin/sh” in memory. So in order to get there, we have to:

  1. Introduce the string “/bin/sh” at a known location in memory
  2. Get the address of this string in register RDI via some gadget
  3. Return to address 0000000000400a20 (call system())

Get string in memory

To fulfill requirement one, we decided to put our string right after the overwritten global cookie variable. Since we know the location of this variable in memory (it is not randomized since binary is not PIE), we just add 0x4 to this address to get the address of our string:

So our string address is 000000000060dce0+0x4 = 000000000060dce4

Locate gadgets

We managed to find a gadget that pops a value from the stack into RDI and then returns with the ROPgadget github project:

This is actually a code splicing case (opcode 5f c3). The real command was pop r15 (opcode 41 5f c3):

Good job ROPgadget! If we now overwrite the return address with the following values onwards, we should be able to end up calling system(“/bin/sh”):

[0x0000000000401583] [000000000060dce4] [0000000000400a20]

However, as can be seen, we have a lot of null characters to write, which is a bad character since the length calculation we exploit is strlen().

Write null characters

We clearly need the ability to write null characters to arbitrary locations in our buffer. To achieve this, we can exploit the fact that the specification of snprintf states that ‘A terminating null character is automatically appended after the content written’, combined with the knowledge that we keep overwriting the same buffer in memory in our loop. Arbitrary null character writes can be achieved with the following algorithm:

  1. Locate the rightmost null character in our payload of length n. Let’s call this position pos. If not found, jump to 5
  2. Write an arbitrary string of length pos+1 concatenated with the rightmost part of payload that does not contain a null character, payload,substring(pos+1,n), to the stack buffer.
  3. Redefine payload as payload.substring(0,pos)
  4. Go back to step 1
  5. Write payload string to stack buffer

This algorithm works because every non-null part of the payload eventually ends up as the rightmost part of payload that gets written to memory in step 2 or 5. This part will never be overwritten by a next memory write, since the length of payload is decreased in step 3. On the contrary, the arbitrary string in step 2 is overwritten with a new arbitrary string and a part of of the real payload in a next iteration, including the null character that was handled in the previous iteration. Note that the maximum number of loops must be lower than 256 in this case, otherwise the global cnt variable will wrap over.

More bad characters

So we finally thought we were ready to get a shell and compiled our exploit:

This only provided us a segmentation fault upon execution control –clean, not more:

After a lot of debugging, we figured out it had to do with the system@plt address, 0x400a20. When replacing this value with the address of puts@plt, 0x4009d0, the string “/bin/sh” was spit out as expected before crashing:

Finally, it became clear: the system address contained a bad character, namely 0x0a. This is interpreted as a newline by fgets, and it will stop parsing the current line and never detect the vital system(arg); string on this line. So this address was not usable anymore, since there is no way to get around the fgets specification. We could have tried to find some more rop gadgets to calculate this address on-the-go and then jump to it, but we chose a different path.

We figured that, if the binary imports the system function, it probably uses it somewhere too. Near the end of the sign function, we have a call to system:

We located the corresponding assembly code in gdb:

By replacing the address of the direct call to system with the address where the sign function calls system (0x401087), we finally got our shell:

No more gadgets

We can even eliminate the gadget by leveraging the fact that the legitimate code that calls system() also must prepare rdi to hold the command address. As can be seen in the excerpt from the sign function above, at address 0x40107d, the rax register is populated with address [rbp-0xd0] via a LEA assembly command. Hereafter rdi is populated with the value of the rax register, and then system is called. Since we also control the rbp register after smashing the log_result function, we can adapt our exploit. We make sure that the value we overwrite rbp with is equal to the address of our “/bin/sh” string added with this offset: 0x60dce4 + 0xd0: 0x60ddb4. Here’s the final python code that creates the exploit file:

Notice that this final exploit only uses 22 system() strings to encode the payload, as opposed to 32 in the previous exploit: the gadget address containing 5 null characters is removed.

Conclusion

The Hack.LU 2013 Wannabe CTF challenge of the Fluxfingers team consisted of two major parts: the first part required exploiting three vulnerabilities (SQL injection, PHP loose comparison, and PHP preg_replace remote command execution) in a custom web application to obtain a limited user shell on the target system. The second part required constructing an exploit that bypasses a Non-eXecutable stack (ROP) and a custom stack canary protection for a bespoke C application on a Linux x64 platform that contained two off-by-one vulnerabilities, in order to elevate privileges necessary for reading the flag.

I personally learned a new PHP vulnerability related to “loose comparison”, as well as how to write (ROP-based) buffer overflows for the x64 Linux operating system. I must say bravo to the Fluxfingers team, as I really had to give my maximum in order to solve all the different exploitation steps they meticulously prepared. Great job! Until next year…

Downloads

Wannabe Custom Binary and source

Belgian. IT Security. Bug Bounty Hunter.

5 Comments

  1. Very nice post where can i learn more about exploitation in the 64bit world 😀 trying to find papers so far nowhere

    • Hi,

      Personally, I haven’t read any dedicated 64-bit exploitation tutorials before tackling this challenge and writing this blogpost. I merely adapted the public 32-bit exploitation techniques to the 64-bit assembly language, as this is basically all there is to it. I’m sure that after time elapses, more and more 64-bit papers will be published. Good luck!

      Arne

Leave a Reply

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