锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

工控安全PLC固件逆向三

时间:2022-12-13 09:00:00 tyco系列连接器tyco连接器176372

我们之前详细分析过bootram和Vxworks在本文中,我们转向了基本的启动过程plc网络部分,同时复制我们的第一、第二个工业控制安全漏洞。

一、VxWorks由网络设备驱动

一般来说,考虑到一些特殊设备的重要性和常用性,有三种设备:块设备、字符设备和网络设备,VxWorks设备驱动分为字符设备驱动、串口驱动、块设备驱动、Flash设备驱动,网络设备驱动,USB设备驱动,其实我们在上一篇文章中已经接触到了一些关于串口驱动的知识(还记得/tyCo/1和/tyCo/0吗?)。

由于网络设备和IO不打交道,有些人可能会疑惑,我们读写网络数据不就是对于网络设备的IO操作吗?是的,但是我们说的和IO不处理主要是指没有普通文件接口,或者根本没有相应的设备节点。我们不像操作磁盘,open一个设备,read数据,过数据socket操作,在socket再去read、write。

作为一种特殊的外设,网络设备享受和享受flash、磁盘等Vxworks除了基本的驱动程序外,常见的外设还在驱动程序和协议栈之间设置了不同的待遇MUX接口层。这样的设置使得驱动层不再需要关注协议层需要什么,只需提供最基本的读写界面MUX层,从MUX层读写数据;协议层不需要关心底层驱动是什么样的,有什么特殊性,只需要调用MUX层层给出的接口实现数据MUX读写就够了,这也是操作系统中常见的解决不了就加中间层的想法。

如下图所示:

其实在早期Vxworks中间采用协议栈和驱动直接交换数据的方式,但显然不好用,后期发展成这样的形式。当然,除了这个模型,还有满意度BSD socket等待网络模型,但考虑到MUX广泛使用,我们还在这里MUX为主。

对于网络设备驱动,该系统可分为以下两种:

  • END,Enhanced Network Driver,基于帧传输数据的增强网络驱动,实际上是我们的日常生活Windows、Linux上接触到的网络驱动相似。
  • NPT,Network Protocol Toolkit,相当于网络协议工具END改良版或进化版,不再保留链路层信息,以包的形式传递数据。

在这两种网络驱动的基础上,我们来了MUX层,虽说是MUX向上对接协议栈,但需要注意的是,我们提到的协议栈往往不包括链路层和物理层,我们更愿意将其视为驱动和设备要完成的功能。我们的协议栈是纯软件协议栈,如果非要拿的话TCP/IP说,我们的MUX更像是插在网络层和链路层之间,如下图所示:

当然,你不太明白这是怎么做到的(比如:ARP等等怎么办?),没关系,在以后的逆向过程中,我们将详细讨论这一部分。

为了更好地分析我们的固件,让我们大致看看标准的网络初始化过程,为以下逆向奠定良好的基础。

  • 由于网络设备的加载,mux层,所以我们需要将驱动程序注册到此处,这样网络设备的注册实际上就分了两个部分,一是设备的加载(驱动程序层),使用endLoad();一个是mux的加载,muxDevLoad()
  • 还需要再次驱动网络设备endStart(),在mux层muxDevStart()
  • 初始化协议栈据说是协议栈,但通常是TCP/IP了,通过usrNetProtoInit调用
  • 加载网络协议,我们正在完成设备和mux互动后,设备应与协议栈相连, ipAttach实现这一步

网络通信可在完成上述步骤后开始,通信调用链一般如下:

muxReceive()-> ipReceiveRtn()-> ip_input()->…-> tcp_input()-> recv()

send()->…->tcp_output()->…->ip_output()->ipOutput()->…->ipTxRestart()->ipTxStartup()->muxSend()->send()

2、固件主逻辑的所有逆向

网络初始化和隐藏的危险

我们从usrRoot进入usrNetworkInit函数,这个函数是所有网络初始化的开始,上述加载网络设备、启动网络设备等工作都在这里进行。

可以再调用一堆init函数,别担心,让我们一个一个地看。首先,我们谈到了协议的初始化。usrNetProtoInit函数。

也是一堆调用,但这次比较有规律,大部分都是xxxLibInit我们首先调用格式usrBsdSockLibInit我们前面说的函数xxxLibInit一般是指库的初始化,但要注意一切usr我们尽量看开头的函数,因为用户很可能做了一些自定义的操作。

我们前面说过,当ioGlobalStdSet我们可以在将标准输入和输出重定向串口后使用它printf一类函数,所以这里报错不再是以前的了log或是专门的err函数,但通过打印字符串(当然,这部分错误可以显示给用户,如果更困难的错误仍然会被采用log在逆向过程中,这些字符串可以帮助我们推理函数的一般过程。

这里可见uVar2作为返回值,首先进行sockLib如果库的初始化失败,将打印相应的错误,并将uVar2设置为0xFFFFFFFF,下面同理,sockLibAdd事实上,它是根据用户的需求初始化的bsdSockLib。只有当所有步骤都成功时,它们才会成功uVar2置为0。

这里需要注意的是,返回值没有在父函数中进行测试。一开始我以为是Ghidra反汇编问题,但检查汇编后发现确实没有检查,查阅Vxworks发现源代码也没有检查,也就是说,这里只打印错误的信息,即使初始化失败,也不会影响系统的下一步运行。

回到usrNetProtoInit,往下是常规的初始操作,包括host table、udp等的lib,这里就不赘述了。回到上面usrNetworkInit,进入usrEndLibInit函数。

end这是上面提到的增强网络驱动,首先使用了muxAddrResFuncAdd添加了arpsolve函数作为地址分析功能,即实现plc的arp功能,所谓arp在网络中,将ip地址转换为mac我们可以通过地址协议arpsolve进一步分析arp这里就不赘述功能实现了。

向下是一个大循环,显然循环变量是endDev_Table,每次加6,也就是说这个Table它应该是五local_它看起来像一个普通的计数器。

而while在循环内,我们可以看到,muxDevLoad加载驱动函数mux层,其参数依次为table0、1、2、3、4项,所以我们可以把它作为突破点,让我们来看看函数的定义:

void * muxDevLoad     (     int                          unit,        /* unit number of device */     END_OBJ * (* endLoad) (char* ,     void*                        ),           /* load function of the driver */     char *                       pInitString, /* init string for this driver */     BOOL                         loaning,     /* we loan buffers */     void *                       pBSP         /* for BSP group */     ) 

显然table[0]除此之外,我们还应该注意驱动号,table[1]是驱动方法,table[2]是驱动方法,而在muxDevLoad装载成功后,将是table[5]设置为1,即标志位。然后调用muxDevStart来启动设备。

我们可以在汇编部分看到,实际驱动函数是Fec860EndLoad,Fec是fast Ethernet controller860指示我们的简写cpu型号。

向下走是usrNetworkBoot该函数主要处理网络的地址和设备名称。

前三个函数都很简单,分别是获取地址和掩码,usrNetDevNameGet用于获取网络设备名称的函数。最后调用usrNetworkDevStart启动设备。

主要是物理网络接口和本地电路接口。还包括读取用户设置。

回到usrNetworkInit,下一步将进行Remote主要是设置主机和创建Remote连接。

完成上述步骤后,我们的设备甚至连接到互联网。然后我们终于在网络初始化中与我们的用户最相关usrNetAppInit看吧,看看这个usrAppInit我们应该意识到这个名字是在Vxworks用户自定义的网络部分。例如,我们希望打开设备nfs(network file system 我们正在服务远程文件访问)Tornado中添加NFS组件,INCLUDE_NFS_SERVER,相关的初始化函数将在函数中自动生成。

rpc为Remote Procedure Call 远程过程调用,这是Vxworks毕竟,远程调用是系统提供的最基本的服务。

telnet协议是TCP/IP协议族的一员是Internet远程登录服务的标准协议和主要方式,同样是默认的。

ftp则是File Transfer Protocol,用于在网络传输文件。/p>

ping估计大家就更熟悉了,不再赘述。

snmp是Simple Network Management Protocol 简单网络管理协议,主要用来支持网络管理系统。

估计上面说的几个大家多少都听过,但像是sntpc这种估计就懵了,实际上这是Simple ntp client,ntp是最古老的网络协议之一,主要是用来同步时间的。

CVE-2011-4859、CVE-2011-4860漏洞出处

这些都是初始化一类的函数,显然不是我们该关注的,而这个usrSecurity就比较有意思了,我们点进去看看。

loginInit创建了一张login的表,用来保存后续的login信息,而shellLoginInstall则是类似hook的一种函数,它的第一个参数是一个函数,用来替换shell登录时的函数,我们可以简单看一下主要部分(为了方便大家观看我对部分函数进行了重命名,有兴趣的可以自己对这些函数进行逆向,并不困难)。

主要就是在时限内读取了login name和login pass,并检查是否正确,如果正确就登录成功了,当然中间有很多“插曲”,有兴趣有的可以自己探索一下。

最后usrSecurity调用了loginUserAdd。

首先去检查上面我们建立的usr表,如果有的话就直接报错,没有的话添加该用户到usr表里。这里就出现大问题了,由于loginUserAdd的参数都是明文字符串,那么我们只要找到登录的地方,是不是就可以直接按照该用户名和密码进行登录呢?

事实上确实是如此,我们暂时跳回到usrAppInit中,同样存在此类情况。

这就是CVE-2011-4859,著名的施耐德硬编码漏洞,如果我们通过后门账户进行登录,危害性可想而知。而这也是2018工控比赛的一道题目,有兴趣的朋友可以找找那场比赛的相关wp。 

 是不是很兴奋?经过我们不懈努力,我们终于成功找到了我们的第一个工控漏洞,虽然说漏洞年代有点久远,而且漏洞偏简单,但这也是巨大的收获。

如果你有这款plc设备的话,可以利用升级时的bug,来实现让plc瘫痪的功能。使用osLoader软件,该软件用来升级plc的固件版本,只需要输入设备的ip,然后会利用现有的账号密码(其实就是这些我们发现的后门账户)来尝试登录设备然后进行升级,我们只需要指定一个错误的固件,就可以实现plc宕机了。

觉得这样就够了?其实这张小小的一张截图中还有一个CVE!这就是CVE-2011-4860,图中ComputePassword函数存在的漏洞。

该函数涉及到了两个参数,我们首先向上看看这俩参数是何方神圣。

可以看到eth实际上是调用GetEthAddr,该函数如下:

检测标志位是否为-1,如果是就获取到了mac地址,这里的mac地址并不是我们熟悉的格式,而是数组的形式进行存储。

下面的设备创建、文件系统建立过程我们暂且略过(留到下一篇文章中),看到sprintf,将eth划分了六部分,按照”.2X“的格式排列,实际上就是格式化mac地址。

最后调用ComputePassword进行运算,参数1就是mac地址的数组,参数2保存运算后的密码。

可以看到逻辑非常简单将全局变量copy到pass,如下图所示,即开头为0x。

接着将数组的第三部分拼接到pass,然后调用strtoul,该函数将字符串转换为无符号整数,其中参数一为源,参数二为目标,参数三是基数,这里是0x10,也就是以16进制进行转换(这也就是为什么先把pass的开头部分置为0x的原因了)。

最终进行简单的位处理和异或操作,然后用sprintf将pass置为全局变量所给出的格式。

这就是最后的pass了,也就是说,我们只需要在知道mac地址的情况下只需要对该”算法“(简单到我都不知道能不能叫它算法)进行逆向即可得到密码。

mac地址的获取方法就多了,最简单的,知道ip了发送arp,即可得知设备的mac地址,然后就可以通过后门账户成功登陆了。

三、Vxworks的设备驱动

在逆向过程中,我们经常会遇见xxxCreate、xxxDrv、iosDrvInstall之类的函数,这其实都是在对设备进行操作,更恰当的说,是对设备及其驱动进行操作。之前我们详细说明了设备驱动中最为特殊的网络驱动,这篇中我们就对其余的驱动进行简单的介绍,当然我们只是为了方便逆向不是为了编写驱动,对驱动编写有兴趣的可以去查查相关资料。

这里的上层io主要就是我们经常使用的open、write等等已经完全告别底层的接口,io子系统则是分发器兼抽象器,对于不同设备的读写进行进一步分发,对于操作进行抽象后为上层提供好用的接口。向下就是具体的驱动了。

首先是我们最熟悉的字符设备,所谓字符设备即以字节流的方式来读写数据的设备,像是我们平常用的键盘就属于这一类,I2C、SPI、UART等接口也都可以作为字符设备驱动,字符设备受制于字节流的读写方式,我们可以很容易处理数据(都一个一个来了,每次顺序操作即可),所以Vxworks并没有再为我们提供中间层,直接由我们写的驱动来对设备进行操作。

串口设备,实际上串口这个概念非常大,在Vxworks的驱动部分,串口相当于“除了几类特殊串口以外的串口”,对于这类设备因为其广泛性与差异性(串口设备用的多、用法还各有不同),所以Vxworks设置了tty中间层,串口通过,当我们向设备发送数据时,io子系统并不会对我们的数据进行任何处理,而是转给tty中间层,再由该层去找对应的驱动程序来进行具体处理。说到这大家可能就注意到了,串口设备驱动应该要和tty层“认识”,要不然tty如何去找驱动呢?这其实就是一个注册的过程,涉及到了ttyOpen、ttyDevCreate等函数,第一个注册的设备往往就当做了标准输入输出(还记得我们之前分析过程中有个函数更改了标准输入输出后我们才能进行printf操作吗?)。

块设备,我们平常用的硬盘就是典型的扩设备,它以数据块的方式进行数据读取,我们往往是在这上面建立文件系统进而对设备进行操作的(像是Windows的ntfs、FAT,Linux的ext4等等),Vxworks主要提供了两种文件系统:

  • dosFs,即兼容MS-DOS的文件系统,我们看到的文件结构就类似于windows的形式。
  • rawFs,不做处理,相当于一整块硬盘就是一个文件。

Flash设备,是一种非易失的闪存技术,我们经常用它来存储代码(特别是嵌入式领域,如果你做过iot方面的开发对它就一定不陌生),它实际上还是属于块设备,但是由于其广泛性、特殊性和重要性(用的多、存代码、擦写方式与硬盘有所不同),所以Vxworks为它在块设备文件系统下又加了一个中间层——TFFS(True Flash File System)。

USB设备,这是非常麻烦的一种设备,因为这种设备往往需要双方协调(比如u盘,u盘内部也需要有硬件、驱动、软件系统,还要和主机端进行协调,最终才可以实现数据传输),我们需要针对硬件、软件做出适配。以下是usb设备的抽象结构:

假设我们用u盘插入设备,那就是如上图的两个USB设备在进行数据交换,很显然需要做好的是连接工作和usb的控制工作,连接我们不必考虑,那主要就是控制器了,我们需要对控制器进行驱动编写,然后往上建立USB栈,这样就完成了整个USB的驱动工作。

对于Vxwork来说,我们经常会看到对于设备的操作(比如xxxDevVCreate),实际上这都是设备和操作系统产生联系,也就是建立上图模型的过程,一般需要:

创建设备——设备初始化——使用设备

而在创建过程中对于不同的设备又包括了将设备注册到io子系统、建立文件系统等等操作,初始化中包含了可选项的初始化、基础设置等等,我们在逆向过程中会看到具体的代码。

逆向分析

我们从上一次继续,进入usrAppInit进行下一步的分析。

首先是各类的初始化,首先创建了ram设备,也就是我们上面说的块设备,然后在RAM1建立了文件系统,初始化相关设置。然后又建立了tffs设备,也就是flash设备,由于我们之后要从flash设备中读取运行程序、配置信息等重要文件(在后面会提到),一旦建立失败后果严重,所以失败了我们就直接死循环。

接着到了FTP_User_Add函数,我们进入查看,这里为了方便大家理解,我已经修改好了变量名。

可以看到首先打开了/FLASH0/ftp/ftp.ini文件,如果失败了,那就添加一个默认的账户(也就是说如果用户没有设置ftp.ini时,我们可以通过该后门用户直接登录)。然后去依次读取ini文件中的用户名、密码,最后进行添加和核对,如果过程中出现了错误就打印相关错误,但是没有设置检查,及时出错也不会导致系统出现问题。

在读取完ftp的ini文件后就会进行ftp的初始化操作,虽然这是Vxworks为我们提供的api,但实际上里面隐含了许多有趣之处,我们进去看看。

开始创建了socket,并对socket进行选项设置、bind、listen操作等等,当然还掺杂着一堆信号量的操作来保证同步与互斥,实际上就是实现了网络通信的基础。

然后会调用taskSpawn函数来创建新的任务,这个函数我们之前已经说过了,第六个函数就是创建的新任务的”main“函数。这里如果调用失败了debug输出错误信息。从这里开始我们就要”多线操作“了,逆向的工作会稍微复杂一点。

我们可以点进去简单看一下,首先是一堆变量的初始化,有常见的keepintvl、keepidle等等,都是tcp传输中的重要选项,往下则是一个大的while循环,里面还嵌套了一个循环。我们按照逆向循环的一般思路,首先找到循环变量,这里第一个循环是死循环,但是子循环中list_head在一直在通过lstNext在迭代,同时client_inet就等于list_head,也在同步迭代。并且每次拿到拿到client_inet后会做一定的处理。

大循环方面首先是debug输出等待用户连接的提示语,接着以client_info作为socket的连接对象进行accept操作(这里的socket编程就不再多做介绍了),然后会将client的ip转换为我们熟知的形式debug输出,然后到小循环进行下一步。到这我们就大致了解到这就是ftp的连接操作,进一步的分析这里就不再赘述了,毕竟咱是工控入门,不是协议分析。

往下走PortA_Init用来初始化端口,CrashLogStartup则是对于log的设置,rebootHookAdd是我们之前提到过的hook操作,它将重启的操作进行hook,resetHardWare就是hook后的函数,其中也主要是一些设置操作。回到usrAppInit,接着调用的是bpi_init函数。

关灯,然后初始化一堆东西,开灯,简洁明了。其中有我们前面非常熟悉的modbus的初始化,此外eos也值得关注,125则是个”特例“,这些东西后面我们会具体来查看。再回到”main“函数。 

将power_up_done置为0,然后循环执行new_poll_bp_token函数,power_up_done会在该函数内部进行变化(我们可以通过交叉引用来查看),所以这并不是死循环,我们进入该函数。

首先是获取信道2out的状态,和0xff进行”与“操作,看结果是不是0x40,这是在硬件编程中很常用的手段,0x40即对应0000 0100,同于”与“检查state的第三位是否为1,如果是则进入bp_isc_c,不是则调用process_modnet,我们先来看看不是的情况。

首先会取得信道1out的信息,然后做一系列时间的操作,最终输出收到modnet command的时间,然后就是上图中的内容,检查信道1内容的0xb的位置,如果为0x03则读取信息,如果是0x04则写信息,我们这里就选取读信息作为例子来看看,注意这里传入的参数是msg+8(下面使用msg_8表示)。

这里检查msg_8的2、5是否为0x5、0x00,如果不是的话直接调用mbus_err_resp__FP9ERROR_RSPUc,返回值固定为0xd,如果是的话则将msg_6作为地址,并用tickGet函数获取时间,打印相关信息。然后根据地址为1或3进行不同的操作,主要是一系列赋值,1的话会返回0x38,3的话为0x18。

回到new_poll_bp_token,这次我们进入bp_isr_c函数来进行研究。

可以看到通过程序通过token的type来进行分发处理,注意这里power_up_done会因为token_type=1而设置为1,打破了之前的while循环。这里涉及到了modbus、eos、user_logic等多个逻辑,要想全部写完,估计够写好几篇文章的了,我们这里就选择处理modbus消息的这一部分来做简单分析,有兴趣的可以自己尝试分析一下。

首先会检查nb_mb_port是否为0,这个变量代表的就是port的数量,不是的话则拿到端口的列表,循环,循环变量为用来计数的counter和port的结构体,循环内容是检查port_stru的第五项是否为0,并且第一项不能为login_prtnum(最开始为空,后面会变化),如果符合则以port的结构体为参数调用put_mbus_msg。

该函数首先通过dequeue函数,拿到了mbus_queue里的各个msg,操作类似于链表,mbus_queue的每个节点的[0]为之后的msg的数量,[1]为下一个节点,dequeue函数将[1]付给了mbus_msg,然后让mbus_queue的[1]再指向下一个节点。实现了msg的遍历,注意这里没有循环,因为循环的过程是在外面的函数进行的。

然后检查msg的[8]是否小于0x100,不是就直接将整成非法信息了。然后是一系列的检验,不论怎么样实际上我们都是要将传进来的port的结构体赋给局部变量。

往后是我们的port结构体的[6]设置为msg[0],然后用read_mbus_svars函数,访问内存,取出内容赋给msg。其余的操作较为繁琐,大致就是将消息给存到了别处,并没有做进一步处理。然后再通过port_stry_00作为参数,对该函数进行递归调用。

到这里我们就看完了modbus_port_FV函数,我们回到bp_isr_c,其实这个函数里包含了大量的消息处理函数,但是篇幅所限我们就不再做更进一步的说明了,建议大家可以继续研究。我们继续看”main“函数。

接着的几个函数也是用来做一些初始化工作的,当这些全部完成后,会打印”Starting Root Task.“,然后通过taskSpawn创建名为NOERoot新任务。

该任务首先会打开某个灯,然后会初始化Device Manager(设备管理器),新建一个DM的任务,同时执行用户指定的程序。

DM创建的任务会读取消息后按照消息的种类,实现停止所有指令、重启等操作,可以说是相当于是linux的root权限了。

首先是复制了一个路径,然后将NOEScript拼接,打开,成功了就执行该文件,也就是说该文件就是后面的具体操作了。到此,我们已经完成了对整个固件的主逻辑的全部探索,从一开始的开机、亮灯到最后的root,我们基本都做了介绍,当然还有很多很多地方我们没有分析到,有兴趣的同学可以继续探索。

modbus_125_handler

我们前面复现了两个洞,实际上该固件一共有三个CVE,但是由于第三个CVE并没有出现在主逻辑里,所以逆向过程中我们没有遇到。

CVE-2011-4861,SchneiderElectricQuantumEthernet模块中的modbus_125_handler函数中存在漏洞。远程攻击者可借助MODBUS125函数代码在TCP502端口上安装任意固件更新。写的很简单,我们来详细看看它到底是怎么回事。

首先会检查拿到消息的function code是不是125,不是的话会把board_id设置为0x1,是的话会使用switch对code的[2]进行分发,也就是说[2]代表的是子功能码,可以看到内容包括读取硬件id、进入内核模式等等权限极高的操作,但是这还不算是太大的危害,真正可怕的是后面的内容。

当检查到子功能码为6时,会检查指令是否为8,如果是的话即进行下面的操作:首先构造一个a.bin文件的目录的字符串,尝试open,成功则进行合法性校验,然后又开始读取该文件的header,失败则报错,接着尝试进入/FLASH0/wwwroot/conf/exec目录,成功后通过简单的检查确定文件是否为下载的内核bin,是的话进行改名,将a.bin改为新的kernel.bin,最后创建内核更新的任务,打开led灯。

更新任务就是打印了提示语句,然后对任务进行推迟,然后重启。可以看到在整个流程中并没有进行严格的用户检测和固件检测,我们仅仅需要对固件进行简单的处理,然后构造流量包,即可完成对于设备固件的替换,进而导致设备瘫痪或者刷入有漏洞的固件版本进行进一步攻击。

本篇将是施耐德NOE77101固件的完结篇。

锐单商城拥有海量元器件数据手册IC替代型号,打造电子元器件IC百科大全!

相关文章