【例题ctfshow pwn82】

# 1.用处:

在有溢出而无法泄露内容时可以利用ret2dlresolve来修改解析的函数,从而达到执行想要的函数的目的

# 2.原理:

程序通过动态链接来达到节省空间的目的,动态链接会有一个延迟绑定的特点,将连接的过程推迟到了程序运行时

动态链接中有个重要函数_dl_runtime_resolve(link_map_obj,reloc_index)来对动态链接的函数进行重定位

利用dl_resolve原因:
1、dl_resolve 函数不会检查对应的函数是否越界,它只会根据我们给定的数据来执行(也就是说其可以偏移到我们构造的地方)
2、dl_resolve 函数最后的解析根本上依赖于所给定的字符串(修改其要解析的字符串也就会调用修改后的函数)

dl_runtime_resolve函数只会在第一次调用这个动态链接的函数时其作用,第二次调用就不需要解析了,可以直接得到这个调用的函数的地址;
第一次调用时程序会查找需要链接的各种信息,再通过_dl_runtime_resolve将函数的真实地址写入got.plt表中

# 1.运行_dl_runtime_resolve

在第一次调用动态链接函数前程序执行的是*.plt函数,其内部会执行dl_runtime_resolve来进行解析找到真实地址

这里看一下第一次调用write函数的情况(实际上第一次调用的是write@plt的形式)利用命令objdump -d pwn82

得到第一次调用write的地址,在gdb中下断点b *0x80485a6 ,然后运行

si单步进入write@plt查看情况:

发现最后执行了_dl_runtime_resolvegot.plt内放的是要跳转执行的下一条汇编指令地址


分析具体调用方法:

  1. 进入write自己的表项后,有个push 0x20是dl_runtime_resolve(link_map_obj,reloc_index) 的第二个参数`reloc_index
  2. 进入公共plt表项(plt0)后,有个push <0x80498bc> 就是第一个参数link_map_obj

这里我们需要将plt0的地址覆盖到ret处,也就是执行了跳转,同时将dl_runtime_resolve的第一个参数push进栈【这么做的原因是因为我们需要利用dl_runtime_resolve函数解析执行我们需要的函数,直接ret进入会缺少第一个参数,用自己的plt表项覆盖ret就无法偏移到我们想要的地址】

这里利用ret0覆盖ret后,需要直接写入参数"reloc_index",因为这是通过模拟这种方式执行write_plt等函数(可以将这两个整体视为调用了函数),需要手动写入参数到栈上,后面就是调用write_plt的返回地址和参数
最终也就是:p32(plt0)+p32(reloc_index)+p32(返回地址)+p32(参数1)+p32(参数2)+p32(参数3)

# 疑问

为什么要以plt0覆盖ret,而不直接用dl_runtime_resolve来覆盖

因为需要填充dl_runtime_resolve的参数,而我们执行plt0就不需要填充第一个参数了,而第二个参数是我们需要控制的所以需要自己填充

# 2.执行_dl_runtime_resolve

在上面的两个push中将_dl_runtime_resolve的两个参数压入栈中,然后就进入_dl_runtime_resolve来执行

其内部过程为(借用大佬的图):

通过上面的图可以看到以第二个参数为准,然后一步一步相加计算得到要解析的函数名

# 第一步:得到reloc_index

利用第二个参数与.rel.plt相加,那么就可以得到调用的函数的具体.rel.plt结构体情况:

.rel.plt结构体:

typedef struct{
  Elf32_Addr r_offset;
  Elf32_Word r_info;
}Elf32_Rel

从上面的.rel.plt结构体来看,.rel.plt重定位表 是保持了所有函数的这个结构体信息(每个结构体8字节),而不仅仅是起到一个索引的作用

因此用.rel.plt的基地址加上偏移就为所要函数的Elf32_Rel指针,然后就得到了对应的r_offsetr_info

.rel.plt存放内容(命令readelf -x .rel.plt 程序名):

我们的重点就是在这个偏移如何得到:

上面可以知道在执行时,第二次跳转到plt0(.plt表的头部),而我们需要的是.rel.plt这里就有一个对应的关系(这里就要计算得函数是.plt的第几个结构体,)

上面可以看到,对应结构体开始在.plt中是从下标1开始,而在.rel.plt中是从下标0开始,也就是说如果write结构体是结构体5,那么在.rel.plt中就是结构体4

.plt存放内容(命令readelf -x .plt 程序名):

通过上面的对应关系知道,我们想要找write在.rel.plt重定位索引就要通过plt来得到,由.plt.rel.plt的对应关系知道:plt结构体-1=rel.plt结构体,所以这里计算在.plt中是第几个结构体:

write_plt-plt[0]=offset    //这里plt[0]地址可以通过pwntools得到
offset/16=是plt表中的第几个结构体
plt结构体-1=rel.plt结构体
因为.rel.plt结构体的大小为8字节,所以还要乘8(这里得到的值是相对偏移)

也就是:
write_index = [(write_plt-plt[0])/16 -1] * 8(write_index就是与.rel.plt的偏移,也就是reloc_index)【不过其实这个计算再后面伪造时用不到,因为偏移通过后面伪造的地址减基地址就得到了】

# 第二步:伪造.rel.plt结构体

上面知道write_index知道,这是与.rel.plt的偏移;但是在dl_resolve执行时并没有检查边界,所以我们可以将这个偏移任意修改,因此可以修改其偏移到到我们伪造的.rel.plt结构体,可以构建式子:

rel.plt+write_index=fake_struck_rel_plt

所以:
write_index=fake_strunk_rel_plt-rel.plt

然后需要考虑在偏移的地方来伪造对应的结构体:

typedef struct{
  Elf32_Addr r_offset;
  Elf32_Word r_info;
}Elf32_Rel

这个结构体有两个成员变量需要伪造,第一个是r_offset,这个可以通过pwntools的ELF功能自动获得,也就是write_got=elf.got["write"];第二个成员变量r_info需要自己查看readelf -a 程序名,如下:

看下面的图可以知道,r_info右移一个字节,以上图中的607的6作为.dynsym下标,寻找.dynsym的标号为6结构体(下标以0开始)

# 第三步:构造动态符号表 dynsym+(r_info>>8)

dynsym结构体:

typedef struct
{
  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; //对于导入函数符号而言,除st_name外其他字段都是0

所以根据该结构体,我们要伪造的结构体样子大致为[name偏移,0,0,0x12]

l利用命令readelf -a pwn82找到.dynsym符号表,可以看到write对应的是下标Num=6

接下来查看具体write的.dynsym结构体:

其结构体就是[0x4c,0,0,0x12](当然这里的0x4c可以被改变,这就达到解析其他字符串的目的),而在dynsym+(r_info>>8)也就是得到了该结构体,我们可以修改r_info使下标指向到我们伪造的dynsym结构体处

伪造.dynsym结构体:

因为.dynsym的每个结构体大小是16字节,那么我们就需要满足其是16字节对齐的(因为这里是以下标的形式索引每次只能16字节的查找,其他的直接用偏移所以不用担心对齐),利用公式使其满足16字节对齐:

fake_sym_addr = base_stage + 32 //伪造结构体的地址
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)   //计算是否是16字节对齐
fake_sym_addr = fake_sym_addr + align //使完成16字节对齐

# 疑问1

0x10 - ((0x8048a88 - 0x8048a00) & 0xf) = 0x10 - 0x8 = 0x8

距离初始的偏移,然后与0xf查看最后一16进制位的值,0x10来看其满足16进制对齐还有多少距离

接下来利用伪造的.dynsym结构体来反推r_info:

我们知道dynsym+(r_info>>8)=write结构体地址 ,而在我们伪造的结构体下已知了结构体地址,所以:

index_dynsym = (fake_sym_add-dynsym ) / 0x10(间隔的结构体个数)
r_info=(index_dynsym<<8)+0x07

这里得到的r_info就可以在前面伪造的.rel.plt结构体处使用这个值(r_offset不用改变)

# 第四步:构造动态字符表(.dynstr)

这里我们需要构造两部分

  1. 构造.dynstr动态字符表(也就是部署write\x00这个字符串去解析)
  2. 修改第三步中的.dynsym结构体的str_name这个偏移量(使能够找到write字符串)

首先就是部署write\x00字符串,得到这个的地址记为fake_write_addr,接下来计算偏移 str_name=fake_write_addr-dynstr ,最后将第三步中的dynsym结构体伪造成[str_name,0,0,0x12]

# 第五步:getshell

上面部署的是write\x00字符串,那么我们下面将其部署成system\x00就可以了,而write函数的三个参数中,将第一个参数修改为/bin/sh\x00就能getshell

# 3.总结:

ret2dlresolve是在无法泄露地址得到libc的情况下使用,这里是通过调用其他函数的plt,之后在解析时解析我们想要执行的函数名即可

# 执行流程:

1.首先通过重定位索引reloc_index来确定需要的函数在重定位表.rel.plt的偏移是多少,以此来找到对应的Elf32_Rel结构体

构造步骤:

  1. 部署字符串(要执行的函数名,如system\x00
  2. 通过字符串地址-dynstr基地址 得到 dynsym结构体中第一个成员变量的值(也就是字符串的偏移)
  3. 部署伪造的system的.dynsym结构体,其中第一个成员变量要用步骤2的值
  4. 由伪造的system结构体地址-dynsym基地址=偏移,利用该偏移得到r_info((偏移/0x10<<8)+0x7)
  5. 伪造rel.plt结构体,通过上面的r_info作为其第二个成员变量(第一个成员变量为write_got等)
  6. 利用伪造的rel.plt结构体地址-rel.plt基地址=reloc_index,得到的reloc_index就是dl_runtime_resolve第二个参数,第一个参数是link_map_obj在前面利用plt0覆盖ret后就直接push进栈了

# 疑问1

有个疑问,为什么要分成三个表,直接是动态字符串表加偏移不行吗

个人觉得这是为了节省一定的资源,能够缩小一定的查找范围

# 疑问2

.plt0.rel.plt.dynsym.dynstr的基地址怎么找:

plt0=elf.get_section_by_name(".plt").header.sh_addr
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr

# 疑问3

r_offset的作用是什么,好像除了保持函数在got表的偏移就没有用了,这个偏移也就是write_got = elf.got['write'] ,个人理解其作用是将write函数填入,不直接填入system是因为没有调用system_plt,通过这种方式进入dl_runtime_resolve最后改将解析的write变为system即可

# 疑问4

r_offset是write_got = elf.got['write']而再调用过write_plt和未调用过write_plt情况下write_got是否相同?

第一次调用时才进行解析,后面调用时write_got里直接存放的是真实地址,那么我认为r_offset保持的不是真实地址,而是第一次调用时需要的偏移(那么我们再构造的时候用write_got是不是就有问题,因为感觉不是第一次调用得到的偏移)

# 注意的点

.dynsym结构体下标以0开始(其实就是除了plt之外其他的结构体但是从0开始的,因为plt0相当于作为头部了)

flat函数是pwn模块的功能:将多个变量转化为二进制字符串

payload=pwn.flat([v1,v2,v3])

v1,v2,v3是加入到payload的变量,相当于v1+v2+v3

# 还要注意

我们执行的plt[0]等,需要是在迁移后的栈上执行,这里直接覆盖ret貌似有点问题

# 4.例题(pwn82)

# 1.程序信息:

main()函数

show()函数:

可以看到read函数能溢出,因为这道题有write函数,所以我们可以利用ret2libc,但是这里换个方式用ret2dlresolve解决

# 2.分析

通过上面的原理我们知道,需要构造一系列的结构体然后利用_dl_runtime_resolve的第二个参数reloc_index来最终通过构造的结构体找到想执行的函数名去解析 执行

而由于我们的构造的结构体内容需要指向后面构造的内容所以我们可以从后往前构造:

# 1.首先确定name_offset

将构造的system输入到bss段上

b"a"+read+p32(0)+p32(0x804+4)+p32(4)+read(0,98e0,len(dynstr))+read(0,8e0+0x100,len("/bin/sh"))+p32(0x8048376 plt0)+p32(0xdeadbeff)+p32(0x80498e0+0x100)