-
-
[原创]施耐德控制器固件逆向分析实践
-
发表于: 2024-3-27 17:34 5893
-
针对施耐德NOE 771固件进行逆向分析,并获取到隐藏在汇编代码中的后门账号。
理解固件逆向对于安全工作的意义;熟悉主要的固件逆向工具和方法;掌握IDA Pro进行静态分析的使用方式;掌握Binwalk对文件和固件的一般分析方法;
在计算机领域中固件(firmware)是一种为设备提供控制、监控、数据操作等功能的微型系统。嵌入式设备就是一些典型的含有固件的设备,BIOS也是固件的一种,同样的我们经常使用的硬盘设备也有其自己的固件。
本次实验的施耐德NOE771固件所使用的底层操作系统为Vxworks 5系列,CPU结构为PowerPC。
VxWorks操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS),它以其良好的可靠性和卓越的实时性被广泛地应用在通信、军事、航空及航天等高精尖技术领域中。甚至连1997年4月在火星表面登陆的火星探测器、2008年5月登陆的凤凰号,和2012年8月登陆的好奇号也都使用到了VxWorks系统。
固件的识别和解压,可以借用一些成熟的工具软件,如:Binwalk、BAT(Binary AnalysisToolkit)等。Binwalk和BAT均为比较流行的固件映像提取和分析工具。Binwalk以MIT License发布,BAT以GPL License发布。它们支持的固件映像解压格式对比表如下:
对于常见的嵌入式设备固件可以使用Binwalk或BAT来解压并提取固件文件。对于无法自动解压的固件,可以尝试以下方法分析:使用文件分析工具获得固件映像文件的基本数据类型。
使用字符串打印工具提取文件中所包含的明码字段,寻找是否有引导装载程序以及操作系统内核的信息。
使用十六进制转储工具(如hexdump)分析为了对齐固件文件空间分段而放入的连续填充字节,文件系统标识有可能紧跟其后。
文件系统有可能使用非标准的特征符,如果发现可疑特征符字段,可以替换为标准特征符,再尝试由固件解压工具进行识别。
固件静态分析指的是在不对嵌入式系统进行实际运行的情况下,通过对固件文件进行逆向解析,分析固件中各代码的调用关系及代码内容从而发现嵌入式系统可能存在的漏洞及后门或针对已发现的漏洞进行定位的一种技术手段。固件静态分析过程中将会涉及到固件提取、加载地址分析等各种分析技术。
固件解压之后的分析主要集中在对常见漏洞入口进行针对性的静态分析,包括:密码、默认开启的服务、端口、配置文件等。分析方法如下:
1)尝试提取文件中包含的明码字段是否存在硬编码密码等。
2)发掘固件的关联性,包括分析固件作者、库使用、目录结构、配置文件关键字、定制默认密码等信息。
3)对二进制可执行文件进行反汇编分析,可以借用一些成熟的工具软件,如:IDA Pro、Capstone等。对特定的嵌入式系统(如VxWorks)的登录模块进行反汇编分析,获取其登录密码的哈希算法等信息。
4)如果发现包含密码哈希的文件,可考虑使用John the Ripper或Hash Suite等工具进行暴力破解。前者有版本支持GPU加速(支持CUDA和OpenCL)。使用暴力破解工具可以利用前述步骤中提取的关键字,显著加快运行效率。
IDA Pro是应用最广泛的静态反汇编工具,它支持对大量的CPU架构进行逆向分析,包括X86、MIPS、PowerPC及Arm等。
Capstone是一个反汇编框架,它支持多种平台,能够运行在Windows、Mac OS X、Linux、FreeBSD、OpenBSD和Solaris中。Capstone可反汇编ARM、ARM64(ARMv8)、MIPS、PPC和X86架构下的应用。
主机操作系统:Windows、Linux、macOS选其一
主机虚拟机:VMware、VirtualBox、KVM选其一
Windows XP:IDA Pro
Kali Linux:Metasploit、Python、Capstone等
准备好部署有IDA Pro的Windows虚拟机,以及可以使用binwalk、python、十六进制编辑器的环境,例如Kali虚拟机,确保虚机可以方便的进行文件拷贝,也可以使用文件服务器的方式进行共享。准备Vxworks相关文献资料,必要的时候可以直接进行查阅。
在通常情况下,我们通过官网获取到的固件升级包,是一个包含了嵌入式固件的一个升级包。我们需要从该升级包中将真正的固件提取出来进行分析。
从施耐德官方网站下载固件升级包,从该升级包中提取固件文件。NOE 771的固件文件名为NOE77101.bin。
提取固件通常的方法是使用Binwalk等二进制分析工具对固件升级包进行自动化分析,待确认升级包类型后在进行解压或其他操作。
NOE771的固件文件名为NOE77101.bin,首先我们需要使用Binwalk来确认该文件的类型。通过使用Binwalk进行自动分析可以发现,该固件升级包包含了一个zlib压缩的文件。
Binwalk支持提取zlib压缩的文件,我们可以通过如下命令进行解压
解压后的文件385将存储在_NOE77101.bin.extracted目录中,并以文件在固件升级包中的起始位置来命名。
下图为解压固件升级包结果。
通过继续使用Binwalk对解压后的385文件进行分析可见,Binwalk已成功的分析出了固件中的一些路径名等信息,由此可见该文件的确是NOE771所加载的固件文件。
下图查看固件主要文件路径和程序结构以及版本信息。
由于嵌入式系统的固件需要加载到内存中的特定位置进行运行,这个特定的位置就叫做固件加载地址(base address)。
嵌入式系统的固件内的一些绝对地址调用(例如字符串)均是基于固件加载地址所计算出的内存地址,而不是固件文件中的偏移量地址。例如某函数调用了内存位置为0x30000的字符指针,且我们的固件加载地址为0x10000,那么该字符串在固件文件中的实际位置就是“内存位置”-“固件加载地址”,也就是0x20000。
因此为了使IDA能够正确的对代码进行解析,我们需要分析出固件的加载地址,否则所有涉及到绝对地址调用的代码解析都将是错误的。
某些固件会采用一些封装,比较典型的封装方法是使用ELF封装。通过封装后的ELF文件头部有特定的数据位记录了该固件的加载地址,因此针对该情况我们可以直接读取ELF文件头,从而直接获取到固件的加载地址。
ELF的全称是Executable and Linkable Format,常被称为ELF格式。通过使用greadelf之类的工具可以轻松的获取ELF文件的信息。
由于NOE771的固件并没有采用任何封装,因此针对NOE771的固件我们将通过分析固件头部的代码来大致猜测固件的加载地址。
使用IDA Pro分析嵌入式系统固件的话,首先我们需要确认目标的CPU类型从而选择正确的反汇编引擎。我们可以通过使用Binwalk–A命令来分析。
在得知了目标CPU架构是PowerPC big endian后我们就可以只用IDA加载并进行初步分析。
IDA Pro进行正确的固件加载默认加载后的IDA界面看起来像是这样的。仅仅分析出了极少数的函数
虽然现在我们可以成功反编译,但我们还需要确定固件的代码段基址才能重构符号表
确定基址的思路是寻找一条相对寻址方式的lis指令。
在IDA中使用ALT+T直接搜lis指令,CTRL+T进行向下(上)搜索,发现在000009F8处的lis指令
观察地址后面的@ha确定基址为0x10000,这也是固件常用基址
对@h和@ha的问题,可以看下图
这是powerPC汇编的特性问题,可以参考以下这篇文章:
现在我们就需要验证0x10000是否是我们真正的加载地址了。我们需要重新使用IDAPro加载固件文件,并按照下图进行配置。
配置正确的内存开始和加载地址完成后我们就可以看到IDA Pro能够正常的分析固件的函数调用关系了。
如之前所看到的,IDA虽然成功分析出了函数的调用关系。但是尚无法自动识别出函数的名字,这对我们进行分析造成了很大的阻碍。因此我们需要查看固件是否包含了符号表,如固件包含了符号表那么我们就可以利用符号表中的内容来修复IDA中所显示的函数名。
符号表(Symbol table)是一种用于语言翻译器中的数据结构,在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。在Vxworks中如果没有符号表我们将不能在shell中执行任何命令,因为Vxworks不知道命令名所对应代码的具体位置。
由于Vxworks系统的符号表包含了函数及函数名的对应关系,因此我们的第一步就是要找到符号表在固件中的位置。
之前使用Binwalk分析固件时,已经发现了固件中的符号表位置为0x301E74。
在获取了符号表在固件中的位置后,我们就可以使用16进制编辑器对固件进行查看从而确认Binwalk分析出的地址是否正确。
Vxworks 5系列的符号有他独特的格式,他以16个字节为一组数据,前4个字节是0x00,之后是函数名的内存地址,后4个字节是函数的内存地址,最后以4个特征字节数据结尾。
通过查看Binwalk分析出的地址位置可见,这个地址的确是符号表但是Binwalk的检测结果有一些偏移实际的符号表起始位置是0x301e60。
由于符号表有他自己的特征,因此能够通过遍历的方式快速的锁定符号表的起始及结束地址。我们所测试的固件的符号表起始地址为0x301e60+0x10000,结束地址为0x3293b0+0x10000。
在得到了符号表的位置后,我们就需要使用IDA的api来修复函数名,这里将使用如下的Python脚本。
在IDA的脚本中运行
运行之后发现旁边函数列表大多函数都找到了符号表中对应的名称
在固件预处理完成后,我们就可以通过查看固件的服务加载过程查看初始化时所添加的账号等信息。查看usrAppinit函数,可以发现大量的loginUserAddd调用。(多个后门函数)
而密码则是经过loginDefaultEncrypt函数哈希加密
结合vxworks5的源码来看
结合源码看汇编就非常清晰了
加密过程:
1.在第一个for循环中密码字符串逐字节与位置下标相乘再按位进行异或操作,然后将每一个字符的运算结果累加起来算出passwdInt。
2.passwdInt值与magic相乘再转化为String类型。
3.字符串逐字符与’3’、’7’、’9’进行比较,加相应的值。
可以通过随机生成密码来构建一个序列化的输入密码和passwdInt的对应表,同时passwdInt与输出密码之间也可构建对应表,这样输入密码和输出密码讲通过长度有限的Int类型passwdInt打通,这样我们就能通过查表的方式由输出密码得到输入密码。相比于MD5、SHA1等加密算法,vxencrypt加密算法由于加密方式问题导致密文长度受限,以至于存在弱点。
Rapid7 研究员HD Moore曾经发现VxWorks 5.x系统默认加密方式存在缺陷的研究文章
解密程序
这里贴上C的
成功找到隐藏在固件封装程序中的若干个后门账号和密码
(1)如果固件的内存加载地址并没有采用一般默认的0x10000,我们有哪些可能的办法去寻找其加载地址呢?
(2)除了本实验中提到的办法,还有什么其他思路可以找到固件的符号表地址?
(3)尝试下载工业控制安全教学实验箱中相关设备的固件进行后门分析,包括交换机、工业防火墙、施耐德PLC、西门子PLC和HMI。
ba4K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4c8Q4x3X3g2U0L8W2)9J5c8W2u0V1e0#2u0g2g2$3)9`.
Binwalk
-
e NOE77101.
bin
Binwalk
-
e NOE77101.
bin
# coding:utf-8
from
idaapi
import
*
# 符号表间隔
symbol_interval
=
16
load_address
=
0x10000
symbol_table_start
=
0x301e60
+
load_address
symbol_table_end
=
0x3293b0
+
load_address
ea
=
symbol_table_start
while
ea < symbol_table_end:
# 循环遍历修复函数名
offset
=
4
MakeStr(Dword(ea
+
offset), BADADDR)
sName
=
GetString(Dword(ea
+
offset),
-
1
, ASCSTR_C)
print
(sName)
if
sName:
eaFunc
=
Dword(ea
+
offset
+
4
)
MakeName(eaFunc, sName)
MakeCode(eaFunc)
MakeFunction(eaFunc, BADADDR)
ea
+
=
symbol_interval
# coding:utf-8
from
idaapi
import
*
# 符号表间隔
symbol_interval
=
16
load_address
=
0x10000
symbol_table_start
=
0x301e60
+
load_address
symbol_table_end
=
0x3293b0
+
load_address
ea
=
symbol_table_start
while
ea < symbol_table_end:
# 循环遍历修复函数名
offset
=
4
MakeStr(Dword(ea
+
offset), BADADDR)
sName
=
GetString(Dword(ea
+
offset),
-
1
, ASCSTR_C)
print
(sName)
if
sName:
eaFunc
=
Dword(ea
+
offset
+
4
)
MakeName(eaFunc, sName)
MakeCode(eaFunc)
MakeFunction(eaFunc, BADADDR)
ea
+
=
symbol_interval
/******************************************************************************
*
* loginDefaultEncrypt - default password encryption routine
*
* This routine provides default encryption for login passwords. It employs
* a simple encryption algorithm. It takes as arguments a string <in> and a
* pointer to a buffer <out>. The encrypted string is then stored in the
* buffer.
*
* The input strings must be at least 8 characters and no more than 40
* characters.
*
* If a more sophisticated encryption algorithm is needed, this routine can
* be replaced, as long as the new encryption routine retains the same
* declarations as the default routine. The routine vxencrypt
* in \f3host/<hostOs>/bin\fP
* should also be replaced by a host version of <encryptionRoutine>. For more
* information, see the manual entry for loginEncryptInstall().
*
* RETURNS: OK, or ERROR if the password is invalid.
*
* SEE ALSO: loginEncryptInstall(), vxencrypt
*
* INTERNAL
* The encryption is done by summing the password and multiplying it by
* a magic number.
*/
STATUS loginDefaultEncrypt
(
char
*in,
/* input string */
char
*out
/* encrypted string */
)
{
int
ix;
unsigned
long
magic = 31695317;
unsigned
long
passwdInt = 0;
if
(
strlen
(in) < 8 ||
strlen
(in) > 40)
{
errnoSet (S_loginLib_INVALID_PASSWORD);
return
(ERROR);
}
for
(ix = 0; ix <
strlen
(in); ix++)
/* sum the string */
passwdInt += (in[ix]) * (ix+1) ^ (ix+1);
sprintf
(out,
"%u"
, (
long
) (passwdInt * magic));
/* convert interger
to string */
/* make encrypted passwd printable */
for
(ix = 0; ix <
strlen
(out); ix++)
{
if
(out[ix] <
'3'
)
out[ix] = out[ix] +
'!'
;
/* arbitrary */
if
(out[ix] <
'7'
)
out[ix] = out[ix] +
'/'
;
/* arbitrary */
if
(out[ix] <
'9'
)
out[ix] = out[ix] +
'B'
;
/* arbitrary */
}
return
(OK);
}
/******************************************************************************
*
* loginDefaultEncrypt - default password encryption routine
*
* This routine provides default encryption for login passwords. It employs
* a simple encryption algorithm. It takes as arguments a string <in> and a
* pointer to a buffer <out>. The encrypted string is then stored in the
* buffer.
*
* The input strings must be at least 8 characters and no more than 40
* characters.
*
* If a more sophisticated encryption algorithm is needed, this routine can
* be replaced, as long as the new encryption routine retains the same
* declarations as the default routine. The routine vxencrypt
* in \f3host/<hostOs>/bin\fP
* should also be replaced by a host version of <encryptionRoutine>. For more
* information, see the manual entry for loginEncryptInstall().
*
* RETURNS: OK, or ERROR if the password is invalid.
*
* SEE ALSO: loginEncryptInstall(), vxencrypt
*
赞赏
- [原创]施耐德控制器固件逆向分析实践 5894
- [原创]工业控制互联网组态王软件漏洞测试验证与利用 11780
- [原创]libFuzzer使用总结教程 27313
- [原创]ASX to MP3 Converter本地代码执行漏洞 16338
- [原创]符号执行Symcc与模糊测试AFL结合实践 26769