高级ROP链构造方法(二)-- ret2dl_runtime_resovle
前面已经讲完了PLT和GOT表的作用,在那儿我留了一手,并没有讲清楚
_dl_runtime_resolve_
这个函数具体实现,还记得当时程序往栈上push了两个参数就进入_dl_runtime_resolve_
了吗?不记得可以回去翻下
我们回忆一下当时那两个参数是什么,没错,一个是0
,一个是0x804a004
,这里先讲他们是啥,0呢,我们先理解成一个Index,而0x804a004
是一个函数地址叫link_map
,其实_dl_runtime_resolve_
是通过link_map
来找到.dynamic
的
而.dynamic
是什么呢?我们随便拉一个程序进IDA来看,我们可以看到里面有一些结构体,其实和_dl_runtime_resolve_
紧密相关的只有三个:DT_STRTAB
, DT_SYMTAB
,DT_JMPREL
这三个根据地址我们可以发现它们指向了.dynstr
,.dynsym
,.rel.plt
link_map+0x8
的位置就是它要找的.dynamic
,接下来我们再通过.dynamic
找到.dynstr
, .dynsym
,`.rel.plt
.dynstr
的地址是 .dynamic + 0x44 -> 0x0804821c
.dynsym
的地址是 .dynamic + 0x4c -> 0x080481cc
.rel.plt
的地址是 .dynamic + 0x84 -> 0x08048298
我们看上面的IDA也是这个地址
到这里另外一个push
到栈上的参数就起作用了
将.rel.plt
的地址加上参数 reloc_arg
,即0x8048298+0x0 = 0x8048298
这里就是重定位表项,这里也是一个结构体,将r_info>>8
,即0x00000107>>8 = 1
作为.dynsym
中的下标
1 | typedef struct |
1 | typedef struct |
.dynstr + st_name
就是这个函数的符号名字符串
0x0804821c+0x1a = 0x8048236
IDA上的字符串表
最后在动态链接库查找这个函数的地址,并且把地址赋值给rel->r_offset
,即GOT
表就可以了
前面已经讲解完了_dl_runtime_resolve_
函数的执行流程,下面从源码层面来解析,不关键的代码已经删除
在进入_dl_runtime_resolve_
函数之后它又call _dl_fixup_
下面是_dl_fixup_
的源码
1 | _dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) |
通过讲解_dl_runtime_resolve_
函数,我们也很容易的想到利用点:
1.通过控制reloc_arg来实现伪造最终获取的函数字符串
2.修改.dynamic内存区域实现伪造最终获取的函数字符串
3.伪造link_map的地址
用一道经典例题讲解第一种情况
XDCTF2015_bof
先看看保护,我们看到开启了Partial RELRO
,所以咱修改不了.dynamic
,另外还开启了NX
保护,也写不了shellcode
到栈上
程序的流程十分的简单,打印出Welcome to XDCTF2015~!
,紧接着就是一个溢出函数
我们按照正常的思路走一遍,一般拿到栈溢出,我们首先就是看有无libc
,这题没有给出,然后就是后门函数,system( )
函数,/bin/sh
字符串,很遗憾都没有,看到这一般都会想到ret2libc
,但是当我去查看ROPgadget
的时候,发现这个动态链接的文件它的gadget
少的离谱,所以ret2libc
这条路也走不通了,那只能没有轮子咱们造一个轮子了—-ret2dl_runtime_resovle
先通过cyclic
计算一下溢出的偏移位置
从源码中也可以看出它并没有对reloc_arg
参数进行边界校验,导致我们即使传进去的reloc_arg
再大,它也不会报错,所以我们就传入一个大的reloc_arg
,将重定位表劫持到一个我们可控的地方,比如.bss
段,所以很容易就可以想到,通过栈迁移将栈迁移到.bss
段
根据上面的源码,我们可以知道,要构造的有3样东西.rel.plt
,.dynstr
,.dynsym
我们一步步来,先伪造.rel.plt
,.rel.plt
有两个结构体成员,第一个是r_offset
,为函数的GOT
表地址,我们将其填写成write
的GOT
表地址,第二个是r_info
,这个就比较重要了,我们先来回忆一下它是怎么被索引到的
1 | .rel.plt + reloc_arg = r_offset |
我们刚刚说到源码中没有对reloc_arg
参数进行边界校验,那我们就可以伪造r_offset
和r_info
到一个我们可控的地方
1 | .rel.plt + fake_arg = fake_r_offset |
代码如下,我们先不关注(fake_sym_addr - dynsym)/16
是什么我们先留个印象,这里为什么要加上7呢?因为要绕过这个assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
1 | relo_offset = elf.got['write'] |
接下来就是.dynsym
,它怎么索引到.dynsym
的呢,是不是relo_info >> 8
作为.dynsym
中的下标,所以我们要在relo_info
上做手脚,通过(fake_sym_addr - dynsym)/16
来让它索引到fake_dynsym
,除于16
是因为一个Elf32_Sym
大小是16
字节,这里还要注意的是要进行对齐!!!
代码如下:
1 | fake_sym_addr = bss_addr + 36 |
最后就是.dynstr
了,这个就很简单了,只需要构造st_name
让他找到我们伪造的字符串,它原来是通过.dynstr + st_name
来索引的:
1 | .dynstr + st_name = func_str |
代码:
1 | fake_str_addr = fake_sym_addr + 16 |
完整EXP:
1 |
|