之前我们学习了包括modbus、S7comm、DNP3等等工控领域的常用协议,从这篇开始,我们一步步开始,学习如何逆向真实的plc固件。
用到的固件为https://github.com/ameng929/NOE77101_Firmware
目前网上几篇对于该固件的分析都是以2018工控安全题目解题为主,并没有相应的知识和说明,这次我们不会做题目方面的说明,而是重点关注如何从零开始逆向固件。
本系列涉及到vxworks操作系统、PowerPC CPU架构的汇编语言、Ghrida的反编译问题,网上提供的资料较少,可能对于很多人来说是全新的知识。由于篇幅所限,也为了不让大家烦躁,我们暂且不对这两个新玩意儿进行长篇大论,我会将知识点穿插讲解。另外,建议大家使用windows系统,mac版的ida无法对PowerPC的固件进行函数识别,使用了好几个版本测试都存在该问题,原因暂时不明。
固件提取
这一步其实和路由器的固件逆向非常相似,厂商都会在官网提供固件的升级包,我们通过下载安装来更新设备的软件版本。不过路由器的版本更新相对于plc就频繁得多了,plc升级在大多数厂家的认识中是一件“麻烦”事(假如说一天一更新那工厂还干不干活了……),所以plc的固件是相对稳定的,我们选用的固件虽然看上去很“古老”,但如果考虑到我们现在不是进行漏洞挖掘,而是进行基础学习的话,就很合适了。
这里用到的工具就是大家熟知的binwalk,不过有所不同的是对于路由器来说,我们其实是“知根知底”的,因为架构和操作系统都还在我们的知识范围,而plc的操作系统和cpu就不那么友好了。
如图为binwalk操作某路由器固件,可以看到信息很多,包括文件系统等等一堆
如图为操作plc固件,基本可以说是啥信息都没有
直接binwalk -e NOE77101.bin提取即可,对分离出来的文件再一次binwalk就可以看到很多有用的信息了,包括固件采用的操作系统、内核版本、符号表等等信息
至此我们成功提取除了固件,要注意的是,这里操作系统是vxworks,cpu则是big endian的PowerPC,所以接下来分析工作并不是一帆风顺的,还需要经历诸多磨炼。
手动修复与自动修复
手动修复
我们将固件拖入ida准备开始分析
首先要修改处理器选项,因为ida并不能识别出来我们处理器的架构,我们需要将Processor type手动设置为PowerPC的big endian
接着ida还会让我们指定RAM和ROM的起始地址,在我们不知道的情况下,只能直接点击ok,剩下的选项都直接默认即可。其实这里已经有成熟的工具可以用来自动化识别了,但是考虑到我们是第一次实践,还是认认真真的尝试着走一遍。
进入后发现ida的函数少得可怜(mac版的就干脆一个也没有,笔者这里是在windows上保存了idb后在mac上操作的),而且一般的ida不支持对PowerPC的反编译,就很难受。当然,再难受也得硬着头皮看,先找到第一个函数。
看不懂?没事,我们一点点扣。首先这里大量使用到了r1处理器,在PPC(后文中PowerPC都会简称为PPC)中共有32个通用寄存器,虽然叫通用,但有一些寄存器和x86一样被“固化”了,比如r1就是栈寄存器,其余“固化”的还有:
- r0,函数序言(function prologs)阶段使用,一般不需要我们关心
- r2, toc指针,名字很高大上,其实就是系统调用时,标识系统调用号。
- r3,返回值
- r4-r10,参数,返回值较为特殊(比如乘法导致一个寄存器放不下的时候)时,r4也可以存放返回值
- r11,用在指针的调用和当作一些语言的环境指针。
- r12 ,用在异常处理和glink(动态连接器)代码。
- r13 ,保留作为系统线程ID。
- r14-r31,本地变量
还有好多好多寄存器,我们贯彻用到哪说哪的原则,这里就不再提了,需要我们再说。
lis r1,1
addi r1,r1,0
这两条指令可就有意思了,lis是加载立即数(load Immediate)的意思,它将16位的立即数传送到r1,然后再左移16位,在本条指令中就是把1放到了寄存器的第17位上,也就是0x10000了。
这里就冒出来问题了,我们很显然一般用的都是32位数(不管是编程还是寄存器长度,一般都是32位)啊,PPC的指令长度为4个字节,也就是32位,除去指令码和寄存器外,根本就放不下32位的数,难不成这里的1就是16位的1?
当然不是啦,这里就要看第二条指令了,addi的意思就是让将r1的低十六位+0的值给r1的低十六位,这样就是lis操作高十六位,addi再补上低十六位,成功实现了32位立即数的传送。
下面关于r3的操作同理,接着又是addi操作,将r1的值减了0x10,然后是b指令,b指令就是call了,调用一个函数的意思。还记得r1是干什么的吗?没错,r1是栈寄存器啊,这里开始将栈赋值,然后又对一个栈地址进行了减法操作意味着什么?
没错,一开始进行的实际上是栈的初始化,我们把它叫做initStack,接着的减法实际上就是在栈上开辟了空间(栈是高地址向低地址增长的),这里就应该是整个固件的初始化函数的一部分。
、
再查看官方手册,我们发现,初始化的栈的地址也就是System Image的地址,所以我们ida最开始选择的RAM、ROM的地址就应该是0x10000。我们重新用ida打开该程序,设置RAM、ROM,就可以发现识别的函数多了很多了。
这一步之后我们需要符号进行还原,毕竟谁也不愿意看着一堆sub_xxx来分析吧?这里我们就要用到binwalk告诉我们的符号表地址了(忘了吗?是0x301e74),我们用010editor打开固件,16进制形式分析,跳转到对应地址。
我们之前已经知道了这是vxworks5的固件,此类固件的符号表比较特殊,16字节为一组,分别表示的是符号字符串地址、符号所在地址、特殊标识(比如0x0500就是函数的意思)、0填充位,所以我们就可以按照这个规则来进行符号的修复。
脚本如下:(大家自行按照自己的固件地址对Start和end进行修改即可)
# coding:utf-8
from idaapi import *
import time
eaStart = 0x31eec4
eaEnd = 0x348114
ea = eaStart
while ea < eaEnd:
offset = 0
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 = ea + 16
print"ok"
修复完后ida的function栏可以说是相当友好了。
自动修复
上面我们对固件进行了繁琐的分析最终才完成了对于函数表的修复,实际上,已经有大神开发了一款插件可以对Vxworks的固件进行自动的修复和分析,这就是平安的银河安全实验室开发的vxhunter,大家可以去github自行下载(感谢大佬们的奉献)。
https://github.com/dark-lbp/vxhunter
之前我们手动分析用的是ida,自动部分我们就用Ghidra来进行(还有一个最大的好处是Ghidra支持对PowerPC的反汇编,所以之后的分析我们都采用Ghidra来进行)。
按照readme安装vxhunter,然后打开windows选项卡,选择scriptManager,运行脚本即可
耐心等待一段时间,即可完成
是不是非常简单?下一步就可以开始进行分析了。
小试牛刀
首先我们跳转到之前简单分析的initStack(要注意之前是0x4c,但是我们把基址设置为0x10000后地址应该是0x1004c)
可以看到在initStack操作后,进行了跳转,目标是usrInit,还记得参数的传递规则吗?没错,这里的r3就是为了传递参数的,可以看到,和我们之前说到的一样,还是使用lis和addi的搭配进行32位整数的传递
当然其实再往上看我们还可以发现r4寄存器的身影,因为PowerPC不像x86的stdcall,能通过push来判断哪些是函数的参数,所以我们只能是把r3~r10的身影都锁定,宁可错杀,不可放过。(我们也可以通过查看后续函数的调用来猜测,不过也是猜测,还是多注意为好)
这里可能有些人一看就慌神了,“我靠,这都不认识,怎么看啊?”,不慌不慌,其实这里的调用十分有规律,我们拆开看看
00010018 3c 80 04 00 lis r4,0x400
0001001c 38 84 00 00 addi r4,r4,0x0
00010020 7c 90 8b a6 mtspr IC_CSR,r4
00010024 7c 98 8b a6 mtspr DC_CST,r4
可以看到,实际上是进行了三组同样的操作,只不过是r4的值不一样罢了,我们选择一组来进行分析。
首先打头的还是lis和addi的组合,将r4的值设置为0x4000000,然后是mtspr指令,指令格式如下:
mtspr spc_reg,reg
spc_reg是特殊寄存器的意思,指令将reg寄存器的内容传个一个特殊寄存器,所以这条指令的意思就是将0x4000000赋给IC_CSR寄存器。可以看到,这个r4就是个中间商,那么和之后的参数传递应该是没有关系的。
看到这里有些同学可能就会说了:“你不是说Ghidra有反汇编功能吗?那你还费劲让我们看这个?”,哎,别急别急,我们就来看看Ghidra给我们的反汇编是什么样的
它就分析出了三条,instructionSynchronize()实际上对应的指令是isync,也就是指令同步的意思,并不关键,TLBInvalidateAll()实际上对应的指令是tlbia,也就是快表的相关操作(快表不知道的去翻大二专业课《操作系统》吧,文章内实在没地方讲了),而最后就是函数调用了。
有没有发现?这反编译显然是把我们的mtspr给落下了,直接将r4当成了参数,导致usrInit有了俩参数!其实我们在刚才的探索中很明显看到r4在这绝对没有起到参数传递的功能。这就说明了对于PowerPC的反编译,ida干不了,但Ghrida干得也不是很好,所以在之后的分析中我们要时刻留意,绝对不能只看反编译结果。
接着我们调到usrInit函数来看看,还是先看汇编部分
这部分是大家熟悉的函数序言,我们简单分析一下。
stwu r1,local_18(r1)
意思就是将r1的内容送到r1+local_18的地址中,r1我们说过是栈顶指针。
mfspr r0,LR
这句话在ida上会被翻译为mflr r0,就是讲LR的值给了r0,LR寄存器是记录函数返回地址的寄存器
stw r31,local_4(r1)
和第一句格式相同,是将r31的内容送到r1+local4的地址中
stw r0,local_res4(r1)
这里local_res4是个正数,也就是说将r0送到了一个栈基址往上的地方,在想到r0内容是函数返回地址,是不是就清晰了?实际上就是相当于x86中call指令将函数返回地址保存在栈上的操作。
or r31,r1,r1
or指令的操作是将第二个操作数与第三个操作数or后保存进第一个数中。那就有人要问了,r1和r1进行了or之后不还是r1吗?好问题,实际上这种格式就是PPC的mov指令,写成x86就是mov r31,r1,为什么这样大费周折呢?这里我猜测是为了提高效率。
说到这是不是大家已经脑补出了函数序言的基本行为?实际上和x86并没有什么本质上的区别。
继续来看该函数
又是一堆函数,那我们就先看看第一个吧,它以param1作为参数,而param1就是r3,前面分析了是0
首先是bzero操作,参数是开始地址和结束地址,将中间部分都置为0,之后又这段地址将用来保存数据。
sysStartType是系统的启动类型,包括有bootram启动和rom启动,压缩式和非压缩式等等,这里受篇幅所限,先不细讲了。
intVecBaseSet是非常非常非常重要的一个函数,不知道大家看到intVec有没有想起来在《Windows调试艺术》中我们说过有个东西叫中断向量表(Interrupt Vector Table),其实就是那玩意,这个函数就是用来设置起始地址的,这里的起始地址固定为0。
返回到usrInit我们又看见下面还一个名字中带Vec的啊,那正好再来看看这个伙计
是不是又觉得有点难了?别慌,慢慢来,首先是var2变成了地址,而后又大量使用var2的数组形式,那就让我们瞅瞅地址指向了啥
哦,var2[0]是个数,var2[1]是个地址,指向的是个函数,var2[2]是个地址,指向的是个结构体。
往后走首先是if检验excExhandle(也就是var2[2])是不是为NULL,不是继续,显然这里不是,那就继续。
接着是个非常怪异的循环,涉及到指针、地址、结构体、结构体指针的变换,可能会很难,我尽量说的简单一些。
首先找迭代变量,这里很显然是*var1,而var1是啥?var1是var2+5,var2加5(这里的加5是地址加5,不是真的+5),看看图中地址就知道了,它跑到了handle,也就是说var1在验证handle是否为空。
var2同样在迭代,不过它是每次+3,在看图,也就是到了0x200那个地方,下面又是一个相同的结构。
最终是以*var2、var2[2]为参数调用var[1],也就是excExcHandle(data,excExHandle),在迭代不断地进行该函数,完成Interrupt Vector Table的初始化工作。
总结
可以看到,plc固件的逆向涉及到了非常非常多的新知识,由于篇幅所限,这次我们仅仅是完成了最基本的工作,接下来我们会一点点深入,直到彻底吃透该固件。