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;
}

接下来调用 memPartLibInitmemInit 初始化系统的内存堆,这之后就能正常地调用 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/


ics re

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Machine Learning & SVM
Experiments of Modbus Protocol