Stage2

  • sandbox bypass
    • Stage1已经实现了任意代码执行
  • 接下来就是sandbox部分
    • 目标是读取/home/tp/flag2

解析 - 整体构成

亲:

  • fork生成子A
  • alarm handler
  • 在随机位置设置子A的显示

子A:

  • seccomp
  • clone生成子B
  • 作为显示运行

子B:

  • 主程序(note管理类)

kernel:

  • seccomp过滤器

子A和子B的内存空间,fd等是公用的,寄存器数据通过pipe交换使用

子B运行system call时,如果被许可才会执行

如果不被许可(必要的额外检查),返回SIGSYG信号,调用SIGSYS handler,向显示传递数据

之后显示上进行过滤器,没问题的话会再次调用system call,这时seccomp过滤器会无条件通过

显示端的过滤条件导致进程强制结束

解析 - sandbox部分

  • 信号handler(SIGALRM)注册
  • 从0x10000000~0x700000000000的范围中,选择两个随机区域,作为g_trusted_code使用的code区域和stack区域
    • g_trusted_code是子A的独立过滤器(显示端)
  • 通过SIGALRM,10秒后结束
  • 因为随机生成的地址作为g_trusted_code,子A向该地址写入先使用的汇编代码
  • 因此ucontext使用的context的显示端,产生调用寄存器状况,会转送到子A
  • 注册SIGSYS handler
  • 亲级的随机地址是生成给g_trusted_code使用,预先申请所有可能的区域,以便可以在任何地方写入,并通知父级,已经写入完成
  • 子A的内存空间,亲把g_trusted_code的汇编代码写入完成后,通过ucontext来切换
  • g_trusted_code内执行clone(),线程的一端(子B)返回,子B开始执行do_command

事前准备

  • 依次调用一次那三个函数

显示端(独立过滤器)

写入pipe管道后,system call相关寄存器从中读取数据,并执行以下检查:

  • arg_checker()
    • system call是open时,arg1 == /home/tp/flag1的话,OK
    • system call是mmap/mprotect/munmap/brk/mremap的话,OK
    • 其他的NG,之后进程整体强制结束
  • outside_run_check_and_do_syscall()
    • stack上的返回地址是g_trusted_code范围以外的地址的话,NG
    • OK的话,执行system call
    • NG的话,进程整体强制结束

call_r12_munmap

  • 在已经整体获取到的区域中,释放未使用的区域
  • 生成pipe
  • 变更g_trusted_code的内存权限

call_r13_seccomp

  • 省略前半部分生成filter的过程,这里直接贴出生成后的filter
  • 右下角这一大块是设置filter,filter内容参见左上注释部分

call_r14_clone

  • 作为子A的显示端(独立过滤器)
  • 作为子B的context切换,do_command
  • 清空ucontext使用的context,通过clone()产生子A和子B的分歧,子A:显示端,子B:do_command

arg_checker & outside_run_check_and_do_syscall

  • arg_checker()
    • system call是open时,arg1 == /home/tp/flag1的话,OK
    • system call是mmap/mprotect/munmap/brk/mremap的话,OK
    • 其他的NG,之后进程整体强制结束
  • outside_run_check_and_do_syscall()
    • stack上的返回地址是g_trusted_code范围以外的地址的话,NG
    • OK的话,执行system call
    • NG的话,进程整体强制结束

子A的sighandler

  • 通过seccomp,system call执行失败时(收到SIGSYS),作为sighandler被调用
  • 经过pipe到显示端,然后重新挑战

sandbox类的问题点

事实上,这是一个二阶段的sandbox

  1. seccomp(kernel过滤器,不可bypass)
  2. 显示端 + seccomp
    • 如果1失败,则使用2
    • 在用户空间实施过滤器的seccomp
    • seccomp会对经过这里的调用全部许可(关注点)

因此重点关注seccomp的bypass

关于seccomp的判定

  • seccomp对二阶段显示端是否通过时如何判定的?
    • 这个读一下seccomp的过滤器代码就能知道
    • 看一下dump流程
      • seccomp过滤器只对应一个进程
      • 不是从子B,从子A开始调用的是怎样判定的
  • 在seccomp执行前的int 3断点patch
  • ASLR有效
  • int 3停止时,$r12是seccomp的过滤器,这里是0x21ee45b14ed0,从这里开始dump到整个内存页结束的内容
  • arch 检查(是不是x86_64)
  • 检查system call编号
  • 检查RIP(下位32bit,上位32bit,分两次比较,RIP == 0x32b9f368f0b2的话ALLOW)

通常allow的特殊地址:

  • 0x32b9f368f0b2(这个是每次都会变化的)
  • 这时,0x32b9f368f000~是g_trusted_code写入的区域
  • diff = 0xb2
  • g_trusted_code + 0xb2(显示端中间)
  • 指向存放ret地址的地方
  • 也就是说,我们要判断g_trusted_code + 0xb0 的syscall是否被调用

总结

  • 二阶段的sandbox
  • 第一阶段不可bypass
  • 第二阶段存在RIP检查
  • 会检查是否是从显示端内的syscall开始的调用
  • 如果是从显示端的syscall发起的调用,则seccomp会全部允许

sandbox问题攻略

  • 在stage1已经实现了任意代码执行
  • 如果能够直接jmp到显示端的syscall,不就可以绕过所有过滤器了吗?
  • 在子A内,有一个特殊的syscall可以完全通过seccomp
  • 子A和子B共享内存空间,因此可以从子B进行jmp来调用这个syscall

问题点

显示端在内存空间的哪里?

  • 运行时被设置在一个随机地址
  • 地址信息,在内存中也没有残留
    • 也就是说,不能通过内存泄漏来获取
  • 因此,需要某种技术从内存中获取显示端的地址

能够利用的syecall是下面这些:

  • clone, close, exit_group, read, write,
  • rt_sigprocmask, rt_sigreturn
  • mmap, mprotect, munmap, brk, mremap

内存空间大概是这样:

但是,实际上这些区域之间是有空隙的:

攻略idea

  • 以适当的size进行mmap,填充这些空隙
    • mmap是被允许的syscall
  • 空隙完美填充(后面是连续区域),那么后面就可能是显示端区域
    • 如何确定后面是否是连续区域?
  • 访问没有mapped的区域时,会发生SIGSEGV,但存在避免SIGSEGV的方法
  • 利用syscall判定
    • write(fd,buf,count) #read()也可以
    • mprotect(addr, len, prot)
  • 向write或者mprotect传递位置地址作为参数,检查返回值
  • 如果成功,就能够断定这个地址存在

算法

  1. 指定reserved size的一半,调用mmap
    • 不能指定从哪里取得
      • 也有可能得到的不在reserved区域范围内
    • 当然,如果没有能够mmap的空间,会失败
  1. 如果失败,size再次减半,重试
  2. 如果成功,对后面的地址进行write,判断区域是否存在
    • 区域可能存在,也可能不存在
    • 不存在的情况下,离开现在已经获取到的区域,同样的size重试(第2步)
  1. 如果存在,判断它是否是code
    • 开头四个字节是0x41 0xff 0xd4 0x48的话,可以判定是code
    • 如果不是code,离开现在已经获取到的区域,同样的size重试(第2步)
  1. 判定成功的话,它是code
    • 之后直接调用code + 0xb0的syscall,会允许所有的syscall
    • open(“/home/tp/flag2”)->read()->write()

exploit