工控安全入门(八)—— 设备驱动与通信分析

 

这篇文章中我们将简单了解Vxworks中的设备驱动,并完成固件主逻辑的全部逆向,同时复现该固件最后一个已知漏洞。

文章其实很早就写好了,但由于山东省省赛的原因拖了好几天……给大家道个歉。本篇将是施耐德NOE77101固件的完结篇,之后会分析其他固件,当然随着我的学习也可能对该固件做一些补全,建议阅读前首先阅读专栏其他逆向文章:

https://www.anquanke.com/subject/id/187300

 

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灯。

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

(完)