顾名思义,考勤机是员工考勤管理的智能化解决方案,门禁考勤机则是在考勤机的基础上增加了对门禁的管理。几乎所有企事业单位都有管理员工考勤的需求,因此考勤机/门禁考勤机成了大多数单位的标配,而这类设备如果存在安全问题,会导致的后果有:
(1)扰乱考勤记录,增加企事业单位的管理成本;
(2)对于门禁考勤机,攻击者可以控制门禁的开启,影响企事业单位的实体安全;
(3)当前市面上的考勤机/门禁考勤机普遍采用指纹、人脸等生物识别方式,攻击者可能利用设备中的安全问题获取到员工的隐私信息;
(4)目前已经有不少设备可以通过局域网、互联网进行管理,它们需要接入办公网,攻击者控制设备后能以此为跳板对办公网进行进一步攻击。
本文以考勤机/门禁考勤机为分析对象,首先介绍了典型的使用和管理方式,随后对使用和管理环节中可能出现的攻击面进行了整理和分析,并选取了市场占有率及知名度较高的几个品牌作为实际案例,详细展示其中安全问题的发现和利用过程。
根据操作权限的不同,我们将用户划分成了两种角色:
(1)使用者:包括员工与访客,普通员工只有使用权限,可以通过人脸、指纹、IC/ID卡、密码等方式进行打卡或开启门禁,访客则只有门禁的临时使用权限;
(2)管理员:管理员除了兼具使用者角色的权限之外,还负责对考勤机进行管理和维护,诸如增加或删除员工信息、获取考勤记录等;
对设备内存储的员工信息进行查询、修改等操作,以及对设备本身的管理和维护操作,我们称之为对设备的管理。现在市面上的考勤机,管理方式一般有三种:本地管理、局域网管理、互联网管理。
本地管理指通过物理接触考勤机/门禁考勤机的方式对设备进行管理,如通过考勤机的按键、屏幕或USB接口进行管理。
本地管理只有管理员才能操作,通常可以通过指纹、人脸、管理密码等方式验证管理员的身份。
如下图所所示为某门禁考勤机的主菜单,通过该菜单可以对管理员和普通用户进行增、删、改、查,可以将门禁和考勤记录导出至U盘,可以设置设备的网络信息(如本机IP地址、网关等),也可以通过U盘对设备的固件进行更新。
图2-1 对设备进行本地管理时的主菜单
现在市面上已经有相当多的考勤机/门禁考勤机具备了局域网管理功能,通常设备的生产厂商会配套提供Windows系统下的管理软件,管理员可以在软件中通过设备IP地址直接连接并进行管理。
在我们目前已经分析过的设备中,所有本地管理能够执行的操作,局域网管理均可以执行,此外,门禁考勤机的管理软件中通常可以直接执行门禁的开启操作。
相比于本地管理,局域网管理方式更加方便快捷。数据查询只需要点点鼠标,员工信息的增加也可以预先拍好照片直接添加到设备中,而且管理软件通常可以直接对考勤、门禁数据进行处理和展示,大大减少了考勤统计时的工作量。
与本地管理类似,局域网管理中设备也会对管理员进行身份认证,目前普遍采用的方案是通信密码(与2.1节中提到的"管理密码"是两个独立的密码),即设备端预先设置一个密码,管理软件中必须填入相同的密码,才能正确地与设备进行通信,如下图为某品牌设备中通信密码的设置及使用。
图2-2A 设备端通信密码设置 图2-2B 管理软件中填写通信密码的窗口
互联网管理方式最常见的应用案例就是飞书/钉钉/企业微信,增加员工只需要由管理员将新员工的账号加入单位群组,员工信息可以通过员工各自的账号自行填写并同步至云端,考勤数据也会由设备上传至云端并由云端进行后续的处理和管理。下图为互联网管理方式常见的网络架构。
图2-3 互联网管理方式的网络架构
互联网管理方式下的考勤机/门禁考勤机设备必须连接到互联网才能正常工作,联网可以采用有线和无线两种方式,采用无线方式联网时,配网过程目前有两种方式:一是直接通过设备端的按键和屏幕进行配置,二是通过手机端App与设备进行BLE通信进行配置。
此外,大多数设备为了防止断网时无法使用,会将员工的人脸等信息同步到设备中,员工打卡时会采用离线验证的方式,打卡的记录暂时存储在本地,等重新建立与云端的连接后上传,离线记录的存储数量因设备而异,有几千到上万条不等。
用户打卡或开启门禁时使用的密码、人脸、指纹、IC/ID卡信息可能会被攻击者复制,用于干扰正常的考勤秩序或开启门禁。
管理密码通常为6位数字,若设置为弱密码,那么攻击者可能通过猜测或侧信道攻击的方式得到管理密码,从而获得设备的管理权限。
部分考勤机/门禁考勤机支持使用U盘导入导出用户数据或直接通过USB接口向设备发送指令。U盘内数据的解析、USB接口的驱动、USB通信协议都有可能存在安全问题,攻击者利用这些问题可能获取到设备的管理和控制权限。
通过局域网管理的设备,对管理员的身份认证方式就是通信密码,因此讨论通信密码的安全性,实际上也就是讨论设备对管理员身份认证过程的安全性。
常见的通信密码位6~8位数字,对于弱密码或6位数字的通信密码,可以直接尝试爆破。此外,部分通信密码使用时采用了弱加密方式,攻击者可能通过某种方式计算得到设备当前设置的通信密码。
攻击者可以嗅探或中间人攻击的方式,得到设备与管理端的通信数据。攻击者可以从这些通信数据中获得大量信息,甚至直接篡改通信数据,方便攻击者的下一步行动。
局域网管理协议通常是各生产厂商自定义的协议,在协议数据解析过程中可能存在安全问题,使攻击者能够通过发送特定数据达到特定目的,如绕过身份认证、使设备崩溃或完全控制设备。
部分通过互联网管理的设备,可以由管理员在手机App中使用BLE通信对设备进行配网,这个过程中通常会出现的安全问题包括:
(1)BLE通信数据以明文传输,其中包括了设备的重要信息;
(2)配网完成后BLE功能并未关闭,攻击者可以通过重放通信数据的方式,对设备的部分配置再次进行修改。
与局域网管理方式类似,通过互联网管理的设备也面临着通信嗅探和中间人攻击的威胁。
用户对设备的操作大都是通过云端转发的,因此云端对用户的身份认证和权限管理环节可能出现如非管理员账号操作设备、更改其他用户信息、获取设备敏感信息等问题。
设备登录到云端的身份凭证通常是设备自身的SN码或设备内置的一个ID,如果攻击者获取了该SN或ID,就有可能伪造设备身份接收到云端下发的信息,或向云端发送虚假数据。
由于"设备的管理者"这一角色拥有更高的权限,所以在实际场景中我们会着重对这部分功能的安全性进行分析,截至目前,我们在市场占有率最高的四个品牌的设备中都发现了一些安全问题,本报告挑选了一些比较有代表性的案例进行展开。
设备厂商:(non-disclosure)
设备型号:(non-disclosure)
设备功能:门禁考勤机,可以使用IC卡、指纹、密码打卡,支持本地管理和局域网管理
操作系统:RTEMS
本节以开启门禁的命令为例,分析设备端处理USB通信的流程,如下图是使用PC端管理软件发送的开启门禁的数据包。
图4-1 通过USB开启门禁时的抓包数据
该数据包长度固定为512,但是除了前五个字节外,其他都为0x00。事实上,PC端管理软件发送给设备的USB消息有两部分组成,前512个字节是命令的数据包header,512字节之后则是命令的payload,设备接收到命令后,会统一由下图中的函数处理。
图4-2 USB命令处理入口函数
这个函数有两个参数,第一个参数就是数据包的header,第二个参数是payload。入口函数的功能很简单,就是检查header的起始字节是否为0x66和0xbb,如果检查通过,则交由CommandProc函数处理,该函数中对于开启门禁的命令处理代码如下图:
图4-3 开启门禁命令的处理
这段代码逻辑也很简单,header的第三个字节即为命令ID,开启门禁的命令ID为0xE1,对应的处理分支就是将header的第4~7字节做字节序的转换,从图4-1来看转换后的结果应该3,随后该数值被传入SetLockControl函数对门禁进行控制。
从以上分析可以看除,当设备收到PC端管理软件发来的消息时,没有进行任何身份认证就对命令进行了处理。经过实际实验也可以发现,只要我们重放图4-1中抓到的数据包,就可以控制设备开启门禁。
4.1.2.1节中,我们通过抓取USB通信数据包的方式分析了开启门禁命令的内容,由于设备固件中有固件更新相关的处理代码,但是PC端管理软件没有固件更新的功能,所以本节通过逆向的方式来分析固件更新流程。
整体上来说,固件更新流程可以分为两步:
(1)接收固件数据。设备端接收到文件内容之后,会先将这些内容存储到堆上。相关代码如下图所示。
图4-4 接收固件数据
(2)固件全部数据接收完毕后,设备会将固件存储到Flash中。相关代码如下图所示。
图4-5 固件被写入Flash
根据代码中的函数名来推测,设备应该是支持以二进制形式替换整个固件、替换固件中的指定文件这两种更新格式,但是UpdateFileFw函数的中文件替换的流程有问题,所以最终我们通过UpdateBinaryFw函数对固件的rootfs进行了覆盖。
设备厂商:(non-disclosure)
设备型号:(non-disclosure)
设备功能:考勤机,可以使用人脸、指纹、密码打卡
操作系统:Android
在本考勤机中,我们发现了三个问题:
(1)设备的更新包没有签名校验,可以进行任意修改;
(2)对设备进行局域网管理时的通信密码认证可以绕过;
(3)设备在开机时会访问一个特定的HTTP URL请求新版本固件,我们可以通过DNS劫持,欺骗设备连接到指定服务器并更新恶意固件。
利用以上(1)+(2)两个问题,可以随时从局域网向设备植入恶意固件,利用(1)+(3)两个问题,可以在设备发起更新查询时向设备植入恶意固件。
在考勤机的UI中查看它的各个菜单时,发现有使用U盘固件升级的选项,在官网上没找到固件包,所以就和客服要了一个过来。收到的固件包是一个压缩包,解压后内容如下图所示。
图4-6 固件包内容
其中:
(1)overlay目录中有负责设备主要逻辑功能的程序"app"及其配置文件config.txt,通过逆向"app"可以分析设备的各个逻辑功能;
(2)boot.img是Android系统的boot image,binwalk可以直接提取其中的根文件系统,在根文件系统中发现设备的busybox阉割掉了telnetd,但是内置了对adb功能的支持;
(3)system.tar则是Android文件系统下system目录中的内容,其中的build.prop文件记录了系统的相关信息,如sdk版本号和Android系统版本号;
(4)固件更新时,设备会重启一次,重启之后会执行post_install.sh脚本,因此我们可以将需要执行的命令写到这个脚本里。
基于以上分析,我们可以通过修改post_intall.sh脚本,开启设备的adb服务,此外还可以再加一个telnetd作为Plan B,为此我们对固件更新包做出以下修改:
(1)在overlay目录下增加一个运行在Android系统下功能齐全的busybox;
(2)在overlay目录下增加一个脚本,该脚本内容如下
图4-7 开启telnetd和adb服务的脚本
(3)在post_install.sh中增加对overlay目录下新增脚本和busybox的处理。
之所以需要新增上图中的脚本,是因为post_install.sh运行时,Android系统的初始化尚未完全完成,此时无法开启telnetd和adb服务,因此最终的处理方法是在post_install.sh中后台运行图中脚本,该脚本先睡眠60秒以等待系统初始化完成。
考勤机的管理员可以通过"xxxx考勤管理系统"这一客户端,对设备进行局域网管理,设备监听了5005端口用于接收客户端发来的通信数据。
客户端在绑定设备时需要输入通信密码,绑定后客户端每次向设备发起连接时,第一个数据包就会先发送通信密码用于身份认证,相关的数据包、数据结构及处理流程如下图所示。
图4-8 局域网管理协议的身份认证
上图中Commkey指的就是上文中的通信密码,82(即16进制的0x0052)是这个命令的cmd id,由客户端发送给设备的CommKey是数字123456(0x0001e240),这段代码有两个问题:
无论如何,逆向固件和实际的实验都证明了CommKey的检查是可以绕过的。
后续对其他cmd id的分析中,未发现代码层面的漏洞,但是可以利用考勤机本身支持的管理功能对考勤机做出一些修改,例如下发固件更新指令、修改考勤机的通信参数(是否开启DHCP、网关地址、云端服务器的地址和端口等)。
通过5005端口进行固件更新总共需要发送三组数据:发送cmd id = 0x60A的命令、发送固件长度、发送固件文件内容
设备出厂时默认DNS是空的,为了方便测试,这里我们把设备的DNS服务器设置成了笔记本的IP地址192.168.137.1,然后本机上将域名kq.nbdeli.com解析为192.168.137.1。
设备在收到kq.nbdeli.com的回复后,会提取其中的url、md5和version三个字段,这三个名字很直观,分别是新固件的url地址、固件文件的md5校验以及新固件的版本,我们构造好json字符串后返回给考勤机,如下图所示。
图4-9 考勤机http固件更新响应
考勤机收到返回后就会向xxxxxx.com:4890/firmware请求新的固件文件了,此时只需要构造一个HTTP响应,把4.2.1.2.1节中修改的固件发给考勤机即可。
设备厂商:(non-disclosure)
设备型号:(non-disclosure)
设备功能:考勤机,可以使用人脸、密码打卡
操作系统:Linux
9922端口用于管理软件连接考勤机,该端口由TCPServer类来监听,TCPServer::run会调用DealClient函数完成消息处理,该函数内容如下图所示。
图4-10 DealClient函数
上图中,Do_Recv函数用于接收消息并对消息进行解密,当解密结果不为0时进入local_do_recv对命令进行处理。
Do_Recv的解密流程如下图所示。
图4-11 通信解密过程
上图中可以看到,只要解密后的数据中含有字符")",Do_recv函数返回值就不为0,也就是会进入后续的命令处理流程,而通信数据的加解密算法仅仅是一个异或算法,我们可以构造一个发给设备的数据包,使其包含(0~0xFF)^0x29(即")"的ASCII码)的所有结果,设备就会进入local_do_recv函数。由于我们不知道通信密码,所以发过去的命令必然是没有意义的,那么local_do_recv函数会发送命令执行失败的返回数据,如下图所示。
图4-12 命令执行失败的返回数据
假如通信数据的加密用Plain^CommKey=Cipher来表示,其中Plain就是上图中的字符串,其内容是固定的,Cipher则是我们收到的数据,这两部分都是已知数据,那么我们直接使用Cipher^Plain就能够计算得到设备中的CommKey了。
设备在处理多个局域网管理命令时,都使用了localGetParam和localGetContent函数来获取命令中的参数及其内容,如下图所示。
图4-13 localGetParam函数的使用
上图中,localGetParam获取某个参数的名称,cmdData是设备收到的数据,paramCnt则是表示参数的序号,取出的参数名称会放到buffer中,这是一个栈上的地址,而localGetParam内部使用了strncpy将接收到的消息直接复制到了buffer中,如果命令中的某个参数名称过长,那么经由localGetParam函数处理后,就会造成栈溢出。
这个栈溢出由strncpy造成,而程序基址为0x00400000,溢出时数据会被0x00截断,因此只能控制一次ra寄存器,需要结合其他手段才能实现任意代码执行。进一步分析后,在设置时间日期SetDateTime命令的响应函数中,发现一个位置可以利用栈溢出实现命令注入,该函数整体逻辑如下图。
图4-14 SetDateTime命令的响应函数
上图中v21与v30~v40都是栈上的数据。正常情况下SetDateTime命令用于设置日期或时间,设备接收到该命令后,会使用localGetParam函数依次遍历所有参数,如果存在名称为date或time的参数,则取出其中的内容并格式化到v30~v40中(这不是我们主要关注的内容,所以在图中以"…"省略掉了),并使用格式化后的日期或事件,在label9中构造date命令,随后使用system函数执行该命令设置系统时间,而如果PC端管理软件下发的SetDateTime命令中未发现date或time参数时,由于函数一开始就将v30~v40设置为0了,所以label9应该只执行date命令获取系统时间。
这里我们就可以利用之前在localGetParam函数中发现的栈溢出问题,该函数的栈如下图所示。
图4-15 SetDateTime函数的栈结构
结合图4-14来看,localGetParam函数会将参数名称放到v21中,如果在这里构造一个足够长的参数名字符串,那么就能够从v21(sp+0x28)一直覆盖到v40(sp+0x6C8),也就是控制了图4-14 LABEL9处构造的date命令字符串,从而实现命令注入。
设备厂商:(non-disclosure)
设备型号:(non-disclosure)
设备功能:考勤机,主要用于辅助某App进行急速打卡,仅支持互联网管理
操作系统:FreeRTOS
考勤机在使用前,需要先通过其App对设备进行配网及绑定操作,随后设备与云端建立连接,管理员可以在App中对考勤机进行进一步配置。
App与设备之间通信数据的结构如下图所示。
图4-16 BLE指令中header的结构
上图中固定字符串的内容为"com.xxxx.android.xxxx.btinterface.BleInterface",这是App处理BLE消息的接口的类名,token是由App端生成的,设备向App回复时必须携带与对应指令相同的token,指令名称是我们最关心的部分,不同的指令名称也对应了不同的payload,部分指令如下图所示:
图4-17 部分BLE指令的作用
经过我们的分析和测试,当设备处于已激活状态时,控制设备切换wifi需要先发送handshake后再发送connectWifi指令,而重置设备的resetDevice指令是不需要handshake的,所以这里就存在一个未授权重置设备的问题。需要注意的是,resetDevice指令必须携带payload,payload的内容可以是任意数据,但是不能为空。
设备重置后,我们就可以重新控制考勤机连接到指定的WiFi或绑定到任意App账号下。
上一小节中我们已经分析了一个未授权重置设备的问题,重置后的设备可以再次进行配网和激活操作,也就是说我们可以通过BLE通信控制设备连接到指定的WiFi。此外,经过验证,设备和云端的通信虽然全程有TLS加密保护,但是在设备端并没有对服务器进行证书校验,所以只要考勤机连接到我们控制的WiFi,就可以通过中间人攻击对设备与云端的通信进行嗅探和篡改。
在中间人攻击时,我们可以通过修改设备发送给云端的固件版本号来触发OTA流程,如下图所示。
图4-18 服务器下发的固件更新指令
上图有四个关键点:
(1)第一个红框是设备发送给云端的数据,包括了本地的固件版本号,当这个版本号小于云端最新版本号时,返回的数据中才会包含包含固件升级的相关信息,这里打印是原始数据,实际我们在中间人转发时会将该数据进行更改;
(2)第二个红框表示云端最新的固件版本,当云端返回固件更新指令时,设备会比较本地的固件版本和云端最新固件的版本号,所以图中返回的数据是无法触发设备执行OTA流程的,如果要植入固件,需要将这个版本号修改至高于1.4.3.54,;
(3)第三个红框则是一个HTTP的URL,就是固件的下载链接,直接访问这个URL就可以获取到固件升级包,这个URL里也包含了固件的版本信息,索性一起改掉;
(4)第四个红框是固件文件的MD5校验,植入恶意固件时需要修改这个值。
设备中处理atmStateSync请求的返回数据的关键流程如下图所示。
图4-19 设备开始OTA之前的判断
上图的内容表示,当设备向云端发送atmStateSync请求并收到云端返回的固件更新指令后,还需要以下两个条件满足其一才会真正执行OTA流程:
(1)is_ota_download_ready函数(这是日志中的名称,不是我随便起的)返回不为0,即当前系统时间处于凌晨2点~4点之间;
(2)need_ota_flag_count大于等于18,设备每收到一次固件更新指令,need_ota_flag_count这个变量就会执行自加一的操作,也就是设备发送atmStateSync请求且云端返回固件更新指令总计超过18次。
综合以上分析可以推测出,设备会多次发送atmStateSync请求向云端确认是否有固件需要更新,最多发送18次请求后会连接到指定URL获取固件文件。
要利用OTA向设备植入任意修改后的固件,需要解决的主要问题是三个校验值。
1)HTTP Response中的校验
OTA流程触发后,设备会向指定的URL发出HTTP请求获取固件文件,服务器对此HTTP请求的响应有如下图所示的header。
图4-20 返回固件文件时的http header
上图红框中的内容,包括了固件文件的长度,文件MD5校验的base64编码,以及http返回的数据类型,如果我们要想设备植入固件必须按照修改后的固件实现这三个http header。
固件升级包下载完成后,设备会计算整个升级包的长度和MD5值,如果与HTTP Response中的数值不同,OTA流程就会中断。
2)升级包解包时的校验
下载完成后,设备会解析升级包的内容,在升级包的0x146AB0地址处存储着镜像数据的sha256校验值,此校验的比较如下图所示,图中我们只修改了的镜像的内容,并没有修改对应的sha256校验结果,因此a10寄存器(升级包中保存的校验结果)和a11寄存器(根据镜像内容计算得到的校验值)的内容并不相同,此时更新会失败。
图4-21 固件更新时进行sha256校验
3)设备重新启动时的校验
通过以上两项校验后,设备会将OTA镜像烧录到Flash中之后,并自动重启尝试加载新的固件内容,启动时可以由串口看到校验失败的信息,如下图所示。
图4-22 设备启动时对OTA镜像的校验
该校验由bootloader完成,ESP32的SDK中提供了bootloader的源码,相关的部分如下图。
图4-23 bootloader源码中的校验
上图中bootloader_flash_read函数即是在读取该校验的内容,校验存储的位置是data->start_addr + unpadded_length,在OTA镜像中的位置如下图,其实就是图4-21 sha256校验的前一个字节。图4-22已经打印出来正确的校验数值是多少了,直接覆盖这个字节的内容即可。
图4-24 设备启动时的校验
上文中提到,设备获取固件升级包是通过访问一个HTTP RUL完成的,而返回的HTTP Response中除了OTA镜像的内容外,还包含了HTTP Response Header,即图4-20的内容,固件中处理Header的部分代码如下图所示
图4-25 http header的处理
上图中首先调用strstr函数在Header中找到Content-Type字段,随后使用scanf函数取出Content-Type字段的内容放到a12指向的位置,即a1+4,a1寄存器在xtensa架构中(M1考勤机的处理器ESP32采用了xtensa架构)被用作sp指针,所以这里最终会在未做长度检查的情况下把一段字符串放到栈空间中。
可以使用如下图POC验证该漏洞,这个函数的栈大小为0xE0,返回的Content-Type字段数据中字符串长度为0x200,设备收到此数据包后就会崩溃。
图4-26 HTTP Header栈溢出的POC
即便是知名品牌的产品,也难免会出现安全问题,因此我们针对用户和厂商分别提出以下建议。
针对用户,建议设置复杂的管理密码和通信密码,并及时将设备固件更新至最新版本,日常做好资产盘点,便于内网出现异常时快速定位流量来源。
针对厂商,建议做到:(1)进行严格的身份认证,包括对管理员的认证和对云端服务器的认证,以保证未经授权的用户无法操作设备。(2)增强对通信数据的保护,所有通信数据都应该采用规范的加密传输算法,以确保通信的安全性。