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
- seccomp(kernel过滤器,不可bypass)
- 显示端 + 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传递位置地址作为参数,检查返回值
- 如果成功,就能够断定这个地址存在
算法
- 指定reserved size的一半,调用mmap
- 不能指定从哪里取得
- 也有可能得到的不在reserved区域范围内
- 当然,如果没有能够mmap的空间,会失败
- 不能指定从哪里取得
- 如果失败,size再次减半,重试
- 如果成功,对后面的地址进行write,判断区域是否存在
- 区域可能存在,也可能不存在
- 不存在的情况下,离开现在已经获取到的区域,同样的size重试(第2步)
- 如果存在,判断它是否是code
- 开头四个字节是0x41 0xff 0xd4 0x48的话,可以判定是code
- 如果不是code,离开现在已经获取到的区域,同样的size重试(第2步)
- 判定成功的话,它是code
- 之后直接调用code + 0xb0的syscall,会允许所有的syscall
- open(“/home/tp/flag2”)->read()->write()