0%

栈溢出ret2dlresolve

本文参照freebuf上的一篇文章,因为这篇文章比较透彻,此仅为自己的记录

最近空的时候也在刷Pwnable.kr的题,但很多题确实考的知识都不会,也不想看writeup,想着先把一些pwn的知识复习一下再说

ret2dlresolve
本文参照freebuf上的一篇文章,因为这篇文章比较透彻,此仅为自己的记录
https://www.freebuf.com/articles/system/170661.html

预备知识:延迟绑定,动态链接等

Test.c

1
2
gcc test.c -fno-stack-protector -m32 -o test
readelf -S test

.dynsym //动态链接符号表
.dynstr //动态链接的字符串
.rel.dyn //变量重定位
.rel.plt //函数重定位
.got //全局变量偏移表
.got.plt //全局函数偏移表

1
readelf -d test

显示test中的section

它的结构如下:

1
2
3
4
5
6
7
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;

一个 Elf_Dyn 是一个键值对,其中 d_tag 是键,d_value 是值。可以看到.dynamic中的JMPREL段地址与.rel.plt地址相对应,是用来保存运行时重定位表的,看一下该表的内容

可以看到read符号位于.rel.plt的第一个,也就是偏移为0×0的地方,这里的r_offset(偏移量)就是.got.plt的地址
然后关注一下.dynsym(对应SYMTAB )对应的内容

.symtab的内容不用关注,.dynsym的结构体为

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
Elf32_Word st_name; // Symbol name(对应于.dynstr中的索引)
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;
#define ELF32_R_SYM(info) ((info)>>8)
#define ELF32_R_TYPE(info) ((unsigned char)(info))
#define ELF32_R_INFO(sym, type) (((sym)<<8)+(unsigned char)(type))

通过ELF32_R_SYM(info) ((info)>>8) 可以得知,sym[num]中的num是通过((r_info)>>8)索引的(r_info?==>.rel.plt中的info的值)
因为.dynsym的地址为020c,又因为read函数对应的num为1,查看一下ndx name为read处的内存

解释一下这一串地址0x020c+0x10*1的意义
0x020c 对应.dynsym的地址
0×10 : 每一条symbol信息的大小在SYMENT中体现,为16 bytes (可以用readelf -d fun命令查看)
1 : num值为1

可以看到0x080481dc对应的第一个值为0x1a,这个值对应st_name 即read字符串在.dynstr中的偏移

0x029c dynstr 地址
0x1a偏移量

之前的分析忘记关pie了,现在关pie后在分析一下流程

1
gcc test.c -fno-stack-protector -m32 -o test -no-pie

第一步jmp to 0x804c00c,而0x804c00c中是下一步的地址,因为这个程序第一次运行所以got表中没有保存read函数的地址,所以程序又跳转会read@plt+6,所以紧接着会执行下一步

先将0×0压栈(0×0表示相对.rel.plt的偏移,通过上面分析我们可以知道,read符号在.rel.plt中的位置为第一个,所以偏移为0),又跳转到0x8049030,看一下该地处的内容。

会将0x804c004压栈,然后跳转到0x804c008处。0x804c004处对应一个指向内部数据结构的指针,类型是 link_map,在动态装载器内部使用,包含了进行符号解析需要的当前 ELF 对象的信息。在它的 l_info 域中保存了 .dynamic 段中大多数条目的指针构成的一个数组,我们后面会利用它。
0x0804c008 处为函数 dl_runtime_resolve(link_map,rel_offset)
所以会调用函数dl_runtime_resolve(link_map,0×0),解析出地址,然后写到对应位置因此如果我们伪造一个rel_offset,以及对应的其他结构体,便可以执行任意函数了
其实dl_runtime_resolve()函数中调用了dl_fixup()函数
首先我们看一下dl_runtime_resolve()函数的实现

在 0xf7fe44f0地址处调用了_dl_fixup()函数,并且采用寄存器传参
dl_fixup()是在dl-runtime.c中实现的,这里只展示主要的地方

1
2
3
4
5
6
7
8
9
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg)

const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);

逐行解释

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

这里面 link_map还是一开始传进来的link_map,但一开始传进来的rel_offset改为用reloc_arg表示:reloc_arg=reloffset

1
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

用来计算重定位入口reloc,JMPREL即.rel.plt地址,reloc_offset即reloc_arg

1
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

找到在.dynsym中对应的条目,[ELFW(R_SYM) (reloc->r_info)]就是为了找到对应的num[?]

1
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7

1
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);

根据st_name对应的偏移,去.dynstr(STRTAB)中查找对应的字符串,result为libc基地址

1
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);

value为函数的实际地址,在libc基地址的基础上加上函数在libc中的偏移

1
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);

将函数地址写到got表对应位置

攻击流程
流程如下当.dynamic可写时,可以将.dynstr地址改写为.bss地址,然后在bss段伪造我们想要的函数的字符串
当.dynamic不可写时, 上面我们讲完了函数的解析流程 主要是由dl_runtime_resolve(link_map,rel_offset),之所以它能解析不同函数的地址,以为我们传入的rel_offset不同,因此,把传入的rel_offset改为我们希望的函数的偏移,便可以执行我们希望的函数,新的问题来了,.rel.plt中不一定存在我们希望的函数,因此就需要我们伪造一个.rel.plt,将rel_offset修改为一个比较大的值,在.rel.plt+rel_offset的地方是我们伪造好的,结构跟.rel.plt相同的数据,这样我们就相当于伪造好了reloc(重定位入口),程序又会根据r_info找到对应的.dynsym中的symbols,我们再次伪造symbols的内容->st_name,使得到的str在我们的可控地址内,然后在.dynstr+st_name地址处放置库函数字符串例如:system。

让我们回到当前的情况再走一遍攻击流程
攻击前:事先准备和开辟一块用于伪造的bss_stage