首页
社区
课程
招聘
[原创] 看雪 2024 KCTF 大赛 第八题 星门
发表于: 2024-9-2 02:35 3741

[原创] 看雪 2024 KCTF 大赛 第八题 星门

2024-9-2 02:35
3741

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

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

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

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

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

因此执行shellcode时具备充分的权限
   
   
题目本身已经把解法告诉我们了:利用ptrace系统调用控制另一个进程做我们想做的事

手册对ptrace的说明如下:

在满足一定条件(手册的 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 看看有哪些进程在跑:

上面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进程中:

最后,需要考虑目标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的逻辑为:

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

目标shellcode:

不能简单的rename /bin/sh到/usr/sbin/chroot,因为xinetd调用chroot时有额外参数会造成干扰
末尾用了死循环保持目标进程不退出,因为这是docker内部的1号进程,如果它退出了整个容器都会退出
   
转为字节码:

最终payload:

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

转为字节码:

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

然后再正常连接:

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

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

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

最终flag:

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;
}
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-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
$ 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
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)
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)
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.
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

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2024-9-2 02:43 被mb_mgodlfyn编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (1)
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
你好,能分享下目标shellcode的字节码是如何生成的吗,我本地生成的不生效
2024-10-15 16:11
0
游客
登录 | 注册 方可回帖
返回
//