CTF丨Linux Pwn入門教程:針對函數重定位流程的相關測試(上)

Linux Pwn入門教程系列分享如約而至,本套課程是做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。html

教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。python

 

課程回顧>>數組

Linux Pwn入門教程第一章:環境配置安全

Linux Pwn入門教程第二章:棧溢出基礎函數

Linux Pwn入門教程第三章:ShellCode測試

Linux Pwn入門教程第四章:ROP技術(上)ui

Linux Pwn入門教程第四章:ROP技術(下)操作系統

Linux Pwn入門教程第五章:調整棧幀的技巧debug

Linux Pwn入門教程第六章:利用漏洞獲取libc設計

Linux Pwn入門教程第七章:格式化字符串漏洞

Linux Pwn入門教程第八章:PIE與bypass思路

Linux Pwn入門教程第九章:stack canary與繞過的思路

 

今天i春秋與你們分享的是Linux Pwn入門教程第十章:針對函數重定位流程的相關測試(上),閱讀用時約15分鐘。

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表取值取到的是真正的地址,從而避免重複進行重定位。

符號解析的過程

咱們經過調試已經大概搞清楚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)
#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位下該結構體爲:

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)會被解析成宏定義。

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。

 

 

以上是今天的內容,你們看懂了嗎?後面咱們將持續更新Linux Pwn入門教程的相關章節,但願你們及時關注。

相關文章
相關標籤/搜索