PLC 固件分析。
Introduction
分析的对象是施耐德昆腾系列 PLC 的 NOE-711 以太网模块的固件:
进行逆向的固件是施耐德在 VxWorks 上进行的二次开发,所以分析过程中会涉及到很多操作系统中的知识。
特别感谢 Asa9ao 师傅的文章。
Preliminary Analysis
首先用 Binwalk 分析一下文件格式(先使用 binwalk -e
提取文件,再对提取出的文件进行分析),可以看到固件采用的操作系统(PowerPC big-endian)、内核版本(VxWorks 5)、符号表(起始地址为 0x301E74)等相关信息:
接下来载入 IDA,处理器选择 PowerPC big-endian 后直接加载:
PPC 中共有 32 个通用寄存器,各个寄存器的用途见附录。先在 IDA 中做一个简单的分析,看一下在 0x4C 处的一段汇编:
先看前两条指令,lis
用于加载立即数,将 16 位整型 1 传至 r1 并左移 16 位,即将寄存器的第 17 位设为 1(可能是因为大端序);addi
把 r1 的低 16 位加 0 后的结果再赋给 r1 的低 16 位。相当于将 r1 的值设置为 0x10000:
lis r1, 1
addi r1, r1, 0
接下来对 r3 做同样的操作,然后将 r1 的低 16 位减 0x10(开辟栈空间),最后 b
指令用于调用函数:
lis r3, 1
addi r3, r3, 0
addi r1, r1, -0x10
b loc_1CD94
根据对 r1 的操作可以判断出这里的一部分其实是对栈进行初始化,故固件的加载地址应该是前面为 r1 设置的 0x10000,则我们可以重新加载 IDA 并设置 ROM 和 RAM 的起始地址为 0x10000。接下来对符号表进行修复。用 010 Editor 打开固件,定位到符号表起始地址(之前 Binwalk 分析得到)。因为 VxWorks 5 的符号表比较特殊,16 字节为一组符号信息,分别表示符号字符串地址、符号所在地址、特殊标识(比如 0x0500 就是函数的意思)、0 填充位。根据 16 字节一组的规律,可以定位到 0x3293A4 为符号表的结尾:
根据上面的信息,编写 IDA-Python 脚本对代码重新进行分析:
from idaapi import *
loadAddress = 0x10000
eaStart = 0x301E64 + loadAddress
eaEnd = 0x3293A4 + loadAddress
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 += 0x10
脚本执行完成后,可以看到 IDA 通过符号表重新设置好了函数名:
如果遇到
MakeStr
函数报错,见这篇文章。
How VxWorks System Works?
经过上面的分析,已经对该固件有了初步的认识。接下来从 VxWorks 操作系统的角度来更进一步地了解固件。为了更好地分析,接下来转战 Ghidra,因为其支持 PPC 的反汇编。同时前面修复符号表的任务可以通过插件 vxhunter 来实现(具体操作见 README)。先顺着上面的分析,再看看 _sysInit
前面的部分,一开始的部分主要是 isync
指令(指令同步):
mfmsr r3
rlwinm r4, r3, 0, 17, 15
rlwinm r4, r4, 0, 28, 25
rlwinm r4, r4, 0, 26, 24
mtmsr r4
isync
接下来对 r4 进行操作,主要是通过 mtspr
将特殊寄存器 DC_CST
的值设置为 0xC0000。tlbia
指令则对应快表(TLB)的相关操作:
lis r4, 0x400
addi r4, r4, 0
mtspr IC_CSR, r4
mtspr DC_CST, r4
lis r4, 0xA00
addi r4, r4, 0
mtspr IC_CSR, r4
mtspr DC_CST, r4
lis r4, 0xC00
addi r4, r4, 0
mtspr IC_CSR, r4
mtspr DC_CST, r4
tlbia
然后看看这部分反编译的结果。大致上没有问题,就是 Ghidra 错把 r4 的值也当成了 usrInit
的参数:
void _sysInit(void)
{
instructionSynchronize();
TLBInvalidateAll();
usrInit(0,0xc000000);
return;
}
接下来就是 usrInit
函数:
一开始的部分是 PPC 下的 Function Prologs。先通过 stwu
把 r1 的值存到 local_18+r1 的内存地址上;mfspr
将 LR 寄存器的值(记录函数返回地址)赋给 r0;接下来再将 r31 的值存到 local_4+r1 的内存地址上,再把 r0 的值放到 local_res4+r1 的内存地址上(local_res4 是正数,意味着把 r0 放到了栈底往上的部分,其实就是把函数返回地址存到栈上);最后 or
将 r1 和自己按位或并把结果存入 r31,相当于 x86 下的 mov r31, r1
:
stwu r1, local_18(r1)
mfspr r0, LR
stw r31, local_4(r1)
stw r0, local_res4(r1)
or r31, r1, r1
下面的部分是一系列的函数调用,到最后是恢复堆栈以及 blr
返回到上一层函数:
stw r3, local_10(r31)
lwz r3, local_10(r31)
bl sysStart
li r3, 0x1
li r4, 0x1
bl cacheLibInit
bl excVecInit
bl sysHwInit
bl usrCacheEnable
bl wvLibInit
bl usrKernelInit
lwz r11, 0x0(r1)=>local_18
lwz r0, 0x4(r11)
mtspr LR, r0
lwz r31, -0x4(r11)
or r1, r11, r11
blr
反编译后得到大致的代码,接下来就主要根据反编译的代码来分析:
void usrInit(undefined4 param_1)
{
sysStart(param_1);
cacheLibInit(1,1);
excVecInit();
sysHwInit();
usrCacheEnable();
wvLibInit();
usrKernelInit();
return;
}
根据前面的分析,可以判断 sysStart
中传入的参数是 r3。首先调用 bzero
将两个参数(内存地址)之间的内存置 0;然后设置系统的启动类型 sysStartType
为传入的参数,其中启动类型包括有 BootRAM 启动和 ROM 启动,压缩式和非压缩式等;最后调用 intVecBaseSet
初始化系统的中断向量表的起始地址为 0:
void sysStart(undefined4 param_1)
{
bzero(&_func_smObjObjShow,0x157914);
sysStartType = param_1;
intVecBaseSet(0);
return;
}
接下来再看看 excVecInit
,总体上来说是在初始化中断向量表:
undefined4 excVecInit(void)
{
int *piVar1;
undefined4 *puVar2;
puVar2 = &DAT_0030a488;
if (PTR_excExcHandle_0030a490 != (undefined *)0x0) {
do {
(*(code *)puVar2[1])(*puVar2,puVar2[2]);
piVar1 = puVar2 + 5;
puVar2 = puVar2 + 3;
} while (*piVar1 != 0);
}
return 0;
}
具体根据 puVar2
指向的地址来看。puVar2[0]
为下标、puVar2[1]
指向函数地址(指向 excConnect
)、puVar2[2]
则指向另一个函数(指向 excExcHandle
)。3 个双字一组(12 个字节),每次检查下一组的 excExcHandle
是否为 0,如果是则结束对中断向量表的初始化过程:
接下来的 sysHwInit
用来将各种外设进行简单的初始化,同时让他们保持“沉默”。因为 CPU 通过中断来响应外设,但由于现在没完全建立起中断体,所以一旦产生中断,就会出现没有中断处理函数的情况,进而导致系统出错:
void sysHwInit(void)
{
uint uVar1;
int iVar2;
size_t sVar3;
undefined auStack24 [8];
int local_10;
local_10 = vxImmrGet();
MPC860ClocksInit();
CpicInit();
uVar1 = _GetMPC860Rev();
if (uVar1 < 0x89a) {
*(undefined2 *)(local_10 + 0x952) = 0;
*(undefined2 *)(local_10 + 0x950) = 0;
*(undefined2 *)(local_10 + 0x954) = 0;
*(undefined4 *)(local_10 + 0xabc) = 0;
*(undefined4 *)(local_10 + 0xab8) = 0;
*(undefined2 *)(local_10 + 0xac2) = 0;
*(undefined2 *)(local_10 + 0x962) = 0;
*(undefined2 *)(local_10 + 0x960) = 0;
*(undefined2 *)(local_10 + 0x964) = 0;
*(undefined2 *)(local_10 + 0x972) = 0;
*(undefined2 *)(local_10 + 0x970) = 0;
}
else {
*(undefined2 *)(local_10 + 0x952) = 0xf830;
*(undefined2 *)(local_10 + 0x950) = 0x830;
*(undefined2 *)(local_10 + 0x954) = 0;
*(undefined2 *)(local_10 + 0x956) = 0x200;
*(undefined4 *)(local_10 + 0xabc) = 0x80080000;
*(undefined4 *)(local_10 + 0xab8) = 0x10013e;
*(undefined2 *)(local_10 + 0xac2) = 0;
*(undefined2 *)(local_10 + 0x962) = 0xc;
*(undefined2 *)(local_10 + 0x960) = 0x10;
*(undefined2 *)(local_10 + 0x964) = 0;
}
*(undefined4 *)(local_10 + 0xaec) = 0;
ppc860IntrInit(9);
sysSerialHwInit();
iVar2 = sysNvRamGet(auStack24,6,0xfffffffa);
if (iVar2 != -1) {
iVar2 = strcmp(auStack24,s_50MHZ_00205568);
if (iVar2 != 0) {
sVar3 = strlen(s_fec(0,0)_labcomm1:smooney_100MB\_00205570);
sysNvRamSet(s_fec(0,0)_labcomm1:smooney_100MB\_00205570,sVar3 + 1,0);
sysNvRamSet(s_50MHZ_00205568,6,0xfffffffa);
}
iVar2 = strncmp(sysBootLine,&DAT_00205618,3);
if (iVar2 != 0) {
*sysBootLine = '\0';
}
}
sysCpmEnetDisable(0);
sysCpmEnetIntDisable(0);
vxPowerModeSet(1);
MPC860Init();
return;
}
像 usrCacheEnable
这样类似 xxxEnable
的函数都是“使能”的意思(数字电路中的使能端)。只有使能了,这个固件才可以使用:
undefined4 usrCacheEnable(void)
{
cacheEnable(0);
cacheEnable(1);
AisysMmuCache();
return 0;
}
最后也是最关键的 usrKernelInit
。前面 xxxLibInit
部分都是对函数库的初始化,qInit
以及 workQInit
是对队列的初始化;最后则调用了 kernelInit
函数:
void usrKernelInit(void)
{
undefined4 uVar1;
classLibInit();
taskLibInit();
qInit(&readyQHead,qPriBMapClassId,&readyQBMap,0x100);
qInit(&activeQHead,qFifoClassId);
qInit(&tickQHead,qPriListClassId);
workQInit();
uVar1 = sysMemTop();
kernelInit(usrRoot,20000,0x490d2c,uVar1,5000,0);
return;
}
在 kernelInit
函数中主要就是创建并执行了一个任务,同时设置了该任务的 TCB、栈、内存池等。这里创建的任务就是 usrRoot
:
void kernelInit(undefined4 param_1,int param_2,int param_3,uint param_4,int param_5,
undefined4 param_6)
{
uint uVar1;
uint uVar2;
int iVar3;
int iVar4;
int iVar5;
undefined auStack552 [516];
rootMemNBytes = param_2 + 7U & 0xfffffff8;
uVar1 = param_3 + 7U & 0xfffffff8;
uVar2 = param_5 + 7U & 0xfffffff8;
intLockLevelSet(param_6);
roundRobinOn = 0;
vxTicks = 0;
vxIntStackBase = uVar1 + uVar2;
vxIntStackEnd = uVar1;
bfill(uVar1,uVar2,0xee);
windIntStackSet(vxIntStackBase);
iVar3 = vxIntStackBase;
taskIdCurrent = (undefined *)0x0;
pRootMemStart = (param_4 & 0xfffffff8) - rootMemNBytes;
iVar5 = rootMemNBytes - 0x220;
iVar4 = pRootMemStart + iVar5 + 0x18;
bfill(auStack552,0x200,0);
taskInit(iVar4,s_tRootTask_0022bf08,0,6,iVar4,iVar5,param_1,iVar3,pRootMemStart -iVar3,0,0,0,0,0,
0,0,0);
taskIdCurrent = auStack552;
rootTaskId = iVar4;
taskActivate(iVar4);
return;
}
Create a new task —— usrRoot
在 usrRoot
中,调用了一系列函数对系统进行初始化,最终在 usrAppInit
中进入系统:
void usrRoot(undefined4 param_1,undefined4 param_2)
{
usrKernelCoreInit();
memPartLibInit(param_1,param_2);
memInit(param_1,param_2);
sysClkInit();
usrIosCoreInit();
usrKernelExtraInit();
usrIosExtraInit();
usrNetworkInit();
selectInit();
usrToolsInit();
cplusLibInit();
cplusDemanglerInit();
usrAppInit();
return;
}
接下来一个一个来看 usrRoot
中的函数。首先是 usrKernelCoreInit
,主要作用是对一些功能进行初始化,sem 开头的代表信号量;wd 即 Watch Dog,用于监测系统有没有严重到无法恢复的错误,有的话将重启系统;msgQ 则是消息队列;taskHook 则是和 hook 相关的内容:
void usrKernelCoreInit(void)
{
semBLibInit();
semMLibInit();
semCLibInit();
msgQLibInit();
wdLibInit();
taskHookInit();
return;
}
接下来调用 memPartLibInit
和 memInit
初始化系统的内存堆,这之后就能正常地调用 malloc 和 free 了:
int memPartLibInit(undefined4 param_1,undefined4 param_2)
{
int iVar1;
if ((DAT_0030b8fc == 0) &&
(iVar1 = classInit(memPartClassId,0x44,0,memPartCreate,memPartInit,FUN_0018c634), iVar1 ==0))
{
*(undefined **)(memPartClassId + 0x24) = memPartInstClassId;
classInstrument();
memPartInit(&DAT_0030b884,param_1,param_2);
DAT_0030b8fc = 1;
}
return -(uint)(DAT_0030b8fc == 0);
}
void memInit(undefined4 param_1,undefined4 param_2)
{
memLibInit();
memPartLibInit(param_1,param_2);
return;
}
然后调用 sysClkInit
初始化时钟,其中包括一些时钟中断系统的初始化:
void sysClkInit(void)
{
sysClkConnect(usrClock,0);
sysClkRateSet(0x3c);
sysClkEnable();
return;
}
剩下的部分主要再来看看网络的初始化。
Dive into PLC’s Network
在 usrNetworkInit
函数中包括加载网络设备、启动网络设备等等工作:
void usrNetworkInit(void)
{
usrNetProtoInit();
muxLibInit();
usrEndLibInit();
usrNetworkBoot();
usrNetRemoteInit();
usrNetAppInit();
return;
}
首先 usrNetProtoInit
函数是对网络协议的初始化,包括有 UDP、TCP、ICMP 等常见的网络协议:
void usrNetProtoInit(void)
{
usrBsdSockLibInit();
hostTblInit();
usrIpLibInit();
udpLibInit(&udpCfgParams);
udpShowInit();
tcpLibInit(&tcpCfgParams);
tcpShowInit();
icmpLibInit(&icmpCfgParams);
icmpShowInit();
igmpLibInit();
mCastRouteLibInit();
netLibInit();
tcpTraceInit();
netShowInit();
return;
}
在 usrNetworkBoot
中主要进行处理网络的地址、设备名:
void usrNetworkBoot(void)
{
usrNetBoot();
usrNetworkAddrInit();
usrNetmaskGet();
usrNetDevNameGet();
usrNetworkDevStart();
return;
}
接下来 usrNetRemoteInit
函数创建 remote 进程,设备连接至网络:
void usrNetRemoteInit(void)
{
usrNetHostSetup();
usrNetRemoteCreate();
return;
}
最后调用 usrNetAppInit
,其中会包括 TFTP(一种以 UDP 为基础的文件传输协议)、SNMP(简单网络管理协议)等的初始化:
void usrNetAppInit(void)
{
usrSecurity();
tftpdInit(0,0,0,0,0);
sntpcInit(0x7b);
pingLibInit();
usrSnmpdInit();
return;
}
除了其他函数外,usrSecurity
函数中主要创建了一个用户登录的表,在最后调用的 loginUserAdd
中会先去表中找用户名,如果存在会报错,如果没有就会添加到表中。这里出现一个大问题就是用户名和密码都是明文存储,逆向得到的数据可以直接拿来登录:
void usrSecurity(void)
{
if ((sysFlags & 0x20) == 0) {
loginInit();
shellLoginInstall(loginPrompt,0);
loginUserAdd(0,0);
}
return;
}
int loginUserAdd(undefined4 param_1,undefined4 param_2)
{
int iVar1;
uint uVar2;
undefined auStack24 [4];
byte local_14 [8];
iVar1 = symFindByName(DAT_0030c96c,param_1,auStack24,local_14);
if (iVar1 == 0) {
errnoSet(&DAT_00360002);
iVar1 = -1;
}
else {
uVar2 = symAdd(DAT_0030c96c,param_1,param_2,(uint)local_14[0],(uint)symGroupDefault);
iVar1 = (int)(((int)uVar2 >> 0x1f) - ((int)uVar2 >> 0x1f ^ uVar2)) >> 0x1f;
}
return iVar1;
}
交叉引用一下,定位到多处调用。这个漏洞就是 CVE-2011-4859(施耐德硬编码漏洞),攻击者可以获取到 FTP、TELNET 等协议的账号密码,并远程访问 PLC:
void usrAppInit(void)
{
...
printf(s_----->_Password:_%s_<-----_00205b30,auStack56);
loginDefaultEncrypt(auStack56,&DAT_00342044);
loginUserAdd(s_fwupgrade_00205b4c,&DAT_00342044);
loginUserAdd(s_sysdiag_00205b58,s_bbddRdzb9_00205b60);
loginUserAdd(s_fdrusers_00205b6c,s_bRbQyzcy9b_00205b78);
loginUserAdd(&DAT_00205b84,s_cdcS9bcQc_00205b8c);
loginUserAdd(s_ntpupdate_00205b98,s_See9cb9y99_00205ba4);
...
}
Appendix
PowerPC 寄存器
- r0:在 Function Prologs 时使用,一般不需要我们关心;
- r1:栈寄存器;
- r2:TOC 指针(Table of Contents),用于在系统调用时标识系统调用号;
- r3:存储函数返回值;
- r4-r10:参数,返回值较为特殊时(比如乘法导致一个寄存器放不下的时候),r4 也可以存放返回值;
- r11:在指针的调用和当作一些语言的环境指针;
- r12:在异常处理和 glink(动态连接器)代码;
- r13:保留作为系统线程 ID;
- r14-r31:存储本地变量。
References
https://www.anquanke.com/post/id/187792
https://bbs.pediy.com/thread-229574.htm
https://www.anquanke.com/post/id/188591
https://www.anquanke.com/post/id/189164
https://www.anquanke.com/post/id/190565
https://paper.seebug.org/771/
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!