网络设备代码审计与漏洞挖掘(上)

Published: 二 14 九月 2021

In Misc.

由于以前很多分析没有写笔记,而且我讨厌做重复的事,所以下面有些地方没例子,等着以后遇到了再补充吧。。。

一、分析环境搭建

获取固件

这里的固件指磁盘镜像,一般设备就是一个linux系统,我们需要先将它的磁盘镜像一遍并使用模拟环境重新运行它,这样一方面方便分析,另一方面防止破坏原始设备,通常可通过如下方式获取设备或镜像:

  • 若是内部代码审计的话,当然是开发直接提供镜像咯!否则看下面
  • 设备供应商官网提供虚拟机或固件下载,如Cisco(Sinfor以前以前也提供后来被打了就下架了)
  • 论坛、网盘等提供虚拟机或固件下载,如emulatedlab也提供各类镜像下载
  • 购入设备,从咸鱼等二手市场上购入二手设备要便宜些
  • 租借设备

对于后两种方式,获取的是硬件设备(如服务器或小硬件盒子),因此需要额外的步骤提取固件:

  • 如果可以操作系统命令行(SSH、串口、调试口等),可通过 dd 命令拷贝磁盘。如果设备的磁盘太大而已使用空间很小,则可以使用tar命令去仅拷贝存在的文件,但是这种方式需要很细致,不要破坏文件的属性,也不要漏过一些特殊文件,否则可能出现不好排查的问题。
  • 可拆机物理设备,拆取存储介质然后挂载磁盘
  • 不可拆机物理设备,黑盒挖掘RCE漏洞获取操作系统命令行权限,再通过 dd 命令拷贝磁盘,此时一般有管理员权限,因此能挖掘所有的接口,一般系统设置(如网络设置),导入导出操作等容易出现问题,优先挖这些接口
  • 如果不能拆又挖不到洞可咋整?做安全的思想不能僵化要敢于打破规则,嗯意思就是悄悄地拆,此时需要对抗的就是易碎贴了,什么热风枪吹风机,或者淘宝仿制易碎贴啥的,开动自己的小脑瓜。

⚠️ 使用事项(以下两条是用钱换来的):

  • 收到设备录好开箱视频,收好所有零件和包装,返还设备同

  • 谨慎的对设备做写操作,尽量在内存文件系统上操作,谨慎的将dd命令的目标指定为设备的磁盘

运行固件

提取出固件后,为便于调试,添加硬件以及防止损坏设备,一般会使用虚拟机来运行固件,一般情况下可使用vmware workstation,virtualbox,parallels desktop等,它们的优点是使用方便速度快,但是不支持模拟其他架构,支持硬件较少,可配置性较弱,另外还有写使用类虚拟化导致启动失败,因此使用这些虚拟机无法成功运行固件时,一般会使用qemu。另外将提取出来的固件使用qemu来模拟运行,有时需要使用提取出来的文件重新制作文件系统,编译满足此系统运行的内核等。

Qemu

由于主要用qemu,此处简单记录几点Qemu常见的使用技巧:

1.调试:一般直接使用-s-S进行调试,前者是-gdb tcp::1234参数的简写,表示启动gdb stub监听1234端口,后者表示在CPU重置后即暂停,暂停在第一条指令处,可用于调试BIOS或BootLoader等。调试BootLoader一般在开始时在0x7c00处设置断点,在一般情况下BIOS将BootLoader加载到0x7c00开始的内存处然后跳转,并且一般情况下为16位程序,需要在gdb中设置set architecture i8086,之后若CPU切换了模式gdb未正确识别,可再次attach。

注:若gdb无法下断,可能是因为使用了硬件加速,去掉即可。

2.监控虚拟机:使用monitor进行客户机监控,如使用-monitor telnet::9000,server能在9000端口启动monitor,这会使qemu在启动时暂停,使用nc连接后客户机可继续执行,而nc终端可执行各种监视任务。也可以使用-vnc :60 -monitor stdio在当前交互启动monitor,使用vnc连接5960端口获取虚拟机的屏幕。

3.dump虚拟机内存:使用monitor可直接操作geust,如dump内存可使用dump-guest-memory mem.dump

4.加速:很多镜像必须使用加速运行才不会失败,一般来说都是使用kvm加速,它是Linux下的一个内核模块,有amd和intel的实现,它需要CPU支持虚拟化并在BIOS里将其开启,若linux安装在vmware等虚拟机内,需要在VMM里将其打开,如vmware如下: image.png 开启后进入Linux,使用lsmod查看系统是否已加载kvm,如下已加载amd版: image.png 若未加载可尝试使用modprobe加载,若出错可能在编译kernel时未编译该模块,需要重新编译。而想要在windows上直接加速,且为intel处理器,可尝试使用haxm驱动,强调下它的git仓库release了多个文件,别下错了。

5.网络a.user模式 user模式最简单,由于实际环境下vpn只会开放443端口,因此我们只需要将443端口映射出来,另外为了管理方便还映射了22与8080端口,需要注意qemu的user模式跨网段ping会失效,不应使用它验证网络连通性,它的默认网关是10.0.2.2,一般把它本身配置为10.0.2.15,如:

qemu-system-i386 -nographic -drive format=raw,file=tos -m 8G -smp cpus=2,cores=2 -accel hax \ 
-netdev user,id=n0,hostfwd=::10022-:22,hostfwd=::10443-:443,hostfwd=::18080-:8080,hostfwd=::13946-:13946 \
   -device e1000e,netdev=n0 
   -nic user

b.tap模式 tap模式使用网桥,windows下的操作步骤如下:

1.下载openvpn,安装后会出现新的tap网卡,将其重命名为eth0

2.修改配置如下:

qemu-system-i386 -nographic -drive format=raw,file=tos -m 8G -smp cpus=2,cores=2 -accel hax \
   -netdev tap,id=n0,ifname=eth0 \
   -device e1000e,netdev=n0

3.同时选中上网网卡与tap网卡,右键桥接,之后即可在内部使用上网网卡的配置了,可参考12。至于linux,它自带tap/tun,直接创建就好了。

EVENG

它其实也是用的Qemu,只是有的时候比较方便,使用方式如下:

  1. 首先从镜像论坛下载虚拟机镜像
  2. 从官网下载社区版并导入vmware,再添加一张网卡
  3. 初始化过程中配好IP与账户,默认console账户为root/eve,webui账户为admin/eve之后访问ssh,将下载的镜像解压后连同文件夹上传到/opt/unetlab/addons/qemu,如此处将pcs解压:
root@eve-ng:/opt/unetlab/addons/qemu# tree
.
`-- pulse-PCS-v-9.1r8.0-b7453-VT
    `-- virtioa.qcow2

1 directory, 1 file
  1. 再使用ifconfig与brctl查看网卡可见有9个网桥与两张网卡,其中网卡会与网桥一一对应:
root@eve-ng:/opt/unetlab/addons/qemu# brctl show
bridge name bridge id       STP enabled interfaces
pnet0       8000.000c2908f770   no      eth0
pnet1       8000.000c2908f77a   no      eth1
pnet2       8000.000000000000   no      
pnet3       8000.000000000000   no      
pnet4       8000.000000000000   no      
pnet5       8000.000000000000   no      
pnet6       8000.000000000000   no      
pnet7       8000.000000000000   no      
pnet8       8000.000000000000   no      
pnet9       8000.000000000000   no      
  1. 访问webui,新建实验,在空白处右键新建node,会发现pulse secure已经可识别,选中并添加:
image.png
  1. 同样右键新建network,选择cload1,即pnet1网络,接着光标指向新建的ps,在关机时它会有一个🔌标志,将其连到cload,它会出现三张网卡选中internal后开机如下:

image.png

  1. 此时光标移至ps在左下角可见其vnc连接地址,使用vnc客户端连接,可对其进行配置,由于之前的步骤已将虚拟机新加的网卡与ps的第一张网卡桥接了,因此配上同网段的IP即可通讯,如新加网卡网段为192.168.202.0/24,则在ps初始化时配置该网段未使用IP即可,配置完后进入如下界面:

image.png

其他工具

在挖掘网络设备时一般使用QEMU的系统级仿真,因为这些设备环境比较复杂,像嵌入式设备可使用emuxfirmadyneqiling去做,以后遇到再说...

沙盒破解

此处的沙盒和系统安全里的那个隔离沙盒不太一样,一般设备不会为用户提供全功能的shell(如bash),相反它们只会提供一个功能有限的shell,我们把它叫做沙盒,拥有设备最高权限的管理员也只能执行它所允许的操作,对设备进行各种运维配置(如配置网络,修改用户信息,修改策略等),如下图为某设备的沙盒shell: image.png 这些功能一般不能满足设备分析的需求,如无法使用ps,netstat等命令,无法任意传输文件或进行dbg,因此首先需要破解该沙盒,从而获取一个功能较完整的shell(越狱),在破解前需要先了解下设备是怎么实现这种功能的。首先把它的磁盘挂载到自己的系统(如Ubuntu),看看它的分区,此时几种情况:

  1. 可以看到所有分区:分区是正常的分区,里面的文件就不一定是正常的文件了,仔细分析它的文件。
  2. 只能看到部分分区:分区没文件系统或分区被加密了,需要进一步分析。

一般会遇到从Bootloader开始,各过程做定制化,做加密,所以需要先破解这些障碍,这里有个前提,系统能正常工作,不管它做了什么保护只要系统能正常工作它就必须代码里实现这些操作,此时一般有两条路: 1.老实人之正向分析:熟悉启动过程跟着分析就好了,这个过程单独写了《Linux启动过程之XXX》可以参考下。

2.小聪明之找各种小技巧:就是找各种关键点,此时binwalk将是你的好盆友,通过binwalk一般都能获取很多重要信息,再通过直接改文件,内存取证,直接改内存等方式获取shell。

其实能够调试就可以直接获取shell,想想调试时可以直接修改内存当然能执行任意代码了,之前想偏了认为上帝视角获取的是客户机的物理内存,需要手动找页表做反向映射,后来看这篇文章才想通其实上帝视角的调试器能够也实际获取的是逻辑地址,因为在中断时可以获取虚拟机的所有寄存器,于是通过段寄存器(平坦模型也不需要)与CR3即可获取到当前正在运行的任务/例程的逻辑地址空间,当知道调试器当前获取的是当前被中断的任务的空间,当然可以为所欲为而不必担心地址转换或修改其他任务的内存,不过还有个问题是上帝视角的调试器对内存的修改不会触发OS的写时复制机制,而Linux下fork机制会共享很多数据,此时若直接修改共享内存页将影响到整个系统范围,因此若要修改数据最好修改任务私有的内存,这包括运行过一段时间(已经发生过CoW)的数据区,栈区,堆区,而指令一般不必修改,寄存器可以任意修改。还有个问题是被中断的进程最好是用户态的,尽管内核态也可以调用系统调用,但内核态出现问题会直接使崩溃,判断的方法时看PC的值,高地址就是在内核态。

此处放两个例子吧:

🌰壹

这是某设备,开机后只有一个沙盒shell,因此需要先越狱: image.png 首先关机将磁盘挂载出来,使用如下命令:

# 加载nbd模块
modprobe nbd

# 映射文件,注:此处为新建的文件
qemu-nbd -c /dev/nbd0 /opt/unetlab/tmp/0/c43d9d9f-d79b-4d72-b603-de8eaed816b5/1/virtioa.qcow2

# 若使用ls /dev/nbd0看不到分区,可使用如下命令刷新
#partx -a /dev/nbd0

之后可见有如下分区:

root@eve-ng:/opt/unetlab/addons/qemu# fdisk -l /dev/nbd0
Disk /dev/nbd0: 40 GiB, 42949672960 bytes, 83886080 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x185e607a

Device       Boot    Start      End  Sectors  Size Id Type
/dev/nbd0p1              1    66527    66527 32.5M 83 Linux
/dev/nbd0p2          66528   133055    66528 32.5M 83 Linux
/dev/nbd0p3         133056   199583    66528 32.5M 83 Linux
/dev/nbd0p4         199584 83885759 83686176 39.9G 85 Linux extended
/dev/nbd0p5         199585  6491519  6291935    3G 83 Linux
/dev/nbd0p6        6491521 14881103  8389583    4G 83 Linux
/dev/nbd0p7       14881105 32356799 17475695  8.3G 83 Linux
/dev/nbd0p8       32356801 40746383  8389583    4G 83 Linux
/dev/nbd0p9       40746385 58222079 17475695  8.3G 83 Linux
/dev/nbd0p10      58222081 66414095  8192015  3.9G 82 Linux swap / Solaris
/dev/nbd0p11      66414097 83885759 17471663  8.3G 83 Linux

其中前三个分区大小一致,猜测为启动分区及备份,其他为主要工作分区,查看分区类型:

root@eve-ng:/tmp# blkid 
/dev/mapper/eve--ng--vg-root: UUID="9164c943-55e6-45c6-88a0-2dea4ca1a815" TYPE="ext4"
/dev/mapper/eve--ng--vg-swap_1: UUID="626a6157-4ae7-4138-978a-7de2ad9f5b52" TYPE="swap"
/dev/nbd0p1: UUID="39f25811-f73a-4634-a659-d2b9dab96ec8" TYPE="ext2" PARTUUID="185e607a-01"
/dev/nbd0p2: UUID="834bbeb9-028d-4f5d-97c1-43a8c9718d45" TYPE="ext2" PARTUUID="185e607a-02"
/dev/nbd0p3: UUID="45f2e82c-0613-4e68-abeb-577dfb155191" TYPE="ext2" PARTUUID="185e607a-03"
/dev/nbd0p5: UUID="wSy3Dh-1cNX-D5hf-sKTD-lOxp-LSBt-da1axE" TYPE="LVM2_member" PARTUUID="185e607a-05"
/dev/nbd0p6: UUID="iAFqTI-hLrP-v1Nb-48ap-65jA-6xln-EmsqiO" TYPE="LVM2_member" PARTUUID="185e607a-06"
/dev/nbd0p7: UUID="W1Ix6I-i2LH-SWM1-uE2z-iDs3-v3FB-jjxRH5" TYPE="LVM2_member" PARTUUID="185e607a-07"
/dev/nbd0p10: UUID="Glnag3-g1aM-ai9x-m1Bc-id7E-nbf8-7grZay" TYPE="LVM2_member" PARTUUID="185e607a-0a"
/dev/nbd0: PTUUID="185e607a" PTTYPE="dos"
/dev/nbd0p8: PARTUUID="185e607a-08"
/dev/nbd0p9: PARTUUID="185e607a-09"
/dev/nbd0p11: PARTUUID="185e607a-0b"

可知前几个分区可挂载,先挂载后查看内容:

root@eve-ng:/tmp# mkdir /tmp/nbd0p1 /tmp/nbd0p2 /tmp/nbd0p3
root@eve-ng:/tmp# mount /dev/nbd0p1 /tmp/nbd0p1/

root@eve-ng:/tmp/nbd0p1# ls
boot.b  coreboot.img  grub  kernel  log_coreboot  lost+found  map  VERSION

root@eve-ng:/tmp/nbd0p1# file kernel 
kernel: Linux kernel x86 boot executable bzImage, version 2.6.32-00025-g841d072-dirty (slt_ec_builder@lxc-linux64-0005-sc, RO-rootFS, swap_dev 0x3, Normal VGA

root@eve-ng:/tmp/nbd0p2# cat lilo.4 
...
image=/tmp/boot/kernel
label=current
read-only
initrd=/tmp/boot/coreboot.img
append="console=ttyS0,115200n8 console=tty0 vm_hv_type=KVM    system=A "
...

挂载lvm卷:

pvscan --cache /dev/nbd0p* && vgscan && lvscan && vgchange -ay

查看类型为cryto_LUKS:

root@eve-ng:/tmp/nbd0p2# blkid 
....
/dev/mapper/groupA-home: UUID="522714dc-1044-43df-9a38-cec1d69e1eb6" TYPE="crypto_LUKS"
/dev/mapper/groupA-runtime: UUID="65247d13-1f39-44e5-9046-b793413a7b56" TYPE="crypto_LUKS"
/dev/mapper/groupZ-home: UUID="5622f1a9-b80b-4a9e-8fe5-46546215f457" TYPE="crypto_LUKS"
/dev/mapper/groupS-swap: UUID="ef3a5470-06b2-4dce-a0d9-3dc98cf26a49" TYPE="crypto_LUKS"

luks可以看我旧博客的内容,此处首先编辑eveng里的ps,添加监视器参数-monitor telnet::9000,server,之后启动ps并使用nc连接监视器,执行如下命令dump内存:

(qemu) dump-guest-memory mem.dump
dump-guest-memory mem.dump

使用findaes获取内存中所有的aes密钥:

❯ .\findaes.exe C:\Users\betamao\Desktop\mem.dump
Searching C:\Users\betamao\Desktop\mem.dump
Found AES-256 key schedule at offset 0x425caeac:
6d 68 c8 ee 96 2d cd a1 32 16 0b 3d 7a 32 01 ac b7 82 a0 ef 16 fb 57 8a 36 38 3e b9 d6 9d 9f d7
Found AES-256 key schedule at offset 0x425cafd4:
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
.....

通过密钥对比获取到三个主密钥:

/dev/mapper/groupA-home => b47eb4242d36a3b20f7759a80824d8ebde05609a5bc0c24f6de4048b7077677d
/dev/mapper/groupZ-home => 未获得
/dev/mapper/groupA-runtime => 9cfa99bbb06c44d779a72a03468429b54d322f19007972da579a715de59d6e19

解密磁盘,可以看到根文件系统了:

root@eve-ng:~# cryptsetup luksAddKey /dev/mapper/groupA-home --master-key-file <(echo 'b47eb4242d36a3b20f7759a80824d8ebde05609a5bc0c24f6de4048b7077677d' | xxd -r -p)
Enter new passphrase for key slot: toor
Verify passphrase: toor
root@eve-ng:~# cryptsetup luksAddKey /dev/mapper/groupA-runtime --master-key-file <(echo '9cfa99bbb06c44d779a72a03468429b54d322f19007972da579a715de59d6e19' | xxd -r -p)
Enter new passphrase for key slot: toor
Verify passphrase: toor

root@eve-ng:~# cryptsetup luksOpen /dev/mapper/groupA-home gah
Enter passphrase for /dev/mapper/groupA-home: 
root@eve-ng:~# cryptsetup luksOpen /dev/mapper/groupA-runtime gaz
Enter passphrase for /dev/mapper/groupA-runtime: 

root@eve-ng:~# mkdir /tmp/gah /tmp/gaz
root@eve-ng:~# mount /dev/mapper/gah /tmp/gah
root@eve-ng:~# mount /dev/mapper/gaz /tmp/gaz
root@eve-ng:~# cd /tmp/gah/root/
root@eve-ng:/tmp/gah/root# ls
2.6.32.358-x86_64  boot     data  etc   lib    mnt      opt  proc     sbin  tmp  va   webserver
bin                cgroups  dev   home  lib64  modules  pkg  runtime  sys   usr  var

若能直接利用漏洞进入,也能通过如下方式dump出密钥:

[Sif-YY]$ dmsetup table --target crypt --showkey 
sda8_crypt: 0 15618048 crypt aes-xts-plain64 ae35655cd789d40f1c959b79cb7846a07d956d17c6e41e48bd5b847c38316c5177ce228c2f3ea699a65a3d74dd14951c3fe776ddc31fc249dfaf40a917bf224f 0 8:8 4096
sda2_crypt: 0 19527680 crypt aes-xts-plain64 d5b57bb704dfc8f3f55100e2c26413bb39fe5a54fdb9ed3eab5398d0080adef5cbb122ae3b5c2431a36468c72f41ea444b4a086849cd457f11f2fe6dec86b935 0 8:2 4096
sda6_crypt: 0 19525632 crypt aes-xts-plain64 0316ebb8a814460401753fcd875921bcafb4b542e0ac16e0c9d9102b5bd6b66130d1216500053b83dcec3cf0f052fe6c5142d3e7832514eabb3a93c36ad38ab0 0 8:6 4096

解密磁盘后,分析/sbin/init程序发现最终会启动/home/bin/dsconfig.pl,于是将其替换为/bin/bash,另外/home/boot.pl中会调用/home/bin/check_integrity.sh对所有文件进行完整性校验,由于无私钥最简单的办法是让其返回恒0: image.png

🌰贰

启动后是沙盒shell: image.png 先试试挂载磁盘,它由两块磁盘,一块大小80G实际只使用了几M猜测为数据/日志盘,先分析另一块,使用fdisk可见有两个分区:

root@bm:/tmp/# fdisk -l /dev/sdb
Disk /dev/sdb: 2 GiB, 2147483648 bytes, 4194304 sectors
Disk model: VMware Virtual S
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000

Device     Boot  Start     End Sectors  Size Id Type
/dev/sdb1  *      2048  526335  524288  256M 83 Linux
/dev/sdb2       526336 4194303 3667968  1.8G 83 Linux

将它们都挂在出来,查看sdb1的内容:

root@bm:/tmp/sdb1# ls -lah
total 63M
drwxr-xr-x  9 root root 4.0K Jul 23 07:13 .
drwxrwxrwt 14 root root 4.0K Jul 27 01:10 ..
drwxr-xr-x  2 root root 4.0K Jul 14 22:10 bin
-rw-r--r--  1 root root    1 Jul 14 22:10 boot.msg
drwxr-xr-x  2 root root 4.0K Jul 14 22:10 cmdb
drwxr-xr-x  2 root root 4.0K Jul 26 09:02 config
-rw-r--r--  1 root root    0 Jul 23 11:14 dhcp6s_db.bak
-rw-r--r--  1 root root    0 Jul 23 11:14 dhcpddb.bak
-rw-r--r--  1 root root    0 Jul 23 11:14 dhcp_ipmac.dat.bak
drwxr-xr-x 14 root root 4.0K Jul 26 16:58 etc
-rw-r--r--  1 root root  169 Jul 23 11:14 extlinux.conf
-rw-r--r--  1 root root   53 Jul 14 22:10 filechecksum
-rw-r--r--  1 root root 4.0M Jul 14 22:10 flatkc
-rw-r--r--  1 root root  256 Jul 14 22:10 flatkc.chk
-r--r--r--  1 root root 120K Jul 14 22:10 ldlinux.c32
-r--r--r--  1 root root  68K Jul 14 22:10 ldlinux.sys
drwxr-xr-x  2 root root 4.0K Jul 23 11:14 lib
drwx------  2 root root 4.0K Jul 23 11:14 log
drwx------  2 root root  16K Jul 14 22:10 lost+found
-rw-r--r--  1 root root  58M Jul 14 22:10 rootfs.gz
-rw-r--r--  1 root root  256 Jul 14 22:10 rootfs.gz.chk

.chk的是对应文件的校验和,看大小是256字节,共1024位没看出是什么算法生成的:

root@bm:/tmp/sdb1# ls -lah flatkc.chk 
-rw-r--r-- 1 root root 256 Jul 14 22:10 flatkc.chk

ldlinux是syslinux loader的内容:

root@bm:/tmp/sdb1# file ldlinux.sys 
ldlinux.sys: SYSLINUX loader (version 6.04)

root@bm:/tmp/sdb1# file ldlinux.c32 
ldlinux.c32: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, stripped

root@bm:/tmp/sdb1# cat extlinux.conf 
DISPLAY boot.msg
TIMEOUT 10
TOTALTIMEOUT 9000
DEFAULT flatkc ro panic=5 endbase=0xA0000 console=ttyS0,9600 root=/dev/ram0 ramdisk_size=65536 initrd=/rootfs.gz maxcpus=1

flatkt是kernel:

root@bm:/tmp/sdb1# file flatkc
flatkc: Linux kernel x86 boot executable bzImage, version 3.2.16 (root@build) #2 SMP Wed Jul 14 21:06:17 UTC 2021, RO-rootFS, swap_dev 0x3, Normal VGA

然后另起一台linux挂在磁盘,两个分区,看了看核心应该在rootfs里面,解压后发现没啥线索,只能继续接呀它里面的压缩包。这里面的压缩包无法直接解压鬼知道是不是做了什么修改,那就用它自带的工具解压吧,执行chroot . sbin/xz -d bin.tar.xz 然后打开康康,嚯,很多命令都指向了一个55M的init程序,除此啥都木有呜呜呜,先自己编个busybox,嗯整静态链接的,细节不多讲:

cp -P * /tmp/rootfs/sbin

对initrd进行重打包的脚本:

MOUNT_DIR1=/tmp/fortigate-vm-sdb1
#MOUNT_DIR2=/tmp/fortigate-vm-sdb2

ROOTFS_DIR=/tmp/rootfs
ROOTFS_FILE="$MOUNT_DIR1"/rootfs.gz
FILECKSUM_FILE="$MOUNT_DIR1"/filechecksum
unpack(){
  if [ ! -d "$MOUNT_DIR1" ]; then
    mkdir "$MOUNT_DIR1"
  fi
  #if [ ! -d "$MOUNT_DIR2" ]; then
  #  mkdir "$MOUNT_DIR2"
  #fi

  if ! (mount | grep "$MOUNT_DIR1" > /dev/null);then
      mount /dev/sdb1 "$MOUNT_DIR1"
    fi
    rm -rf "$ROOTFS_DIR"
    mkdir "$ROOTFS_DIR"
    cd "$ROOTFS_DIR" || exit
    gzip -cd "$ROOTFS_FILE" | cpio -imd --quiet
    chroot . sbin/xz -d bin.tar.xz
  chroot . sbin/ftar -xf bin.tar
  return 0
}

pack(){
  cd "$ROOTFS_DIR" || exit
  chroot . sbin/ftar -cf bin.tar bin/
  chroot . sbin/xz -z bin.tar
  rm "$ROOTFS_DIR"/bin/*
  find . | cpio --quiet -H newc -o | gzip -9 -n > "$ROOTFS_FILE"
  crc32=$(gzip -1 -c <"$ROOTFS_FILE" | tail -c8 | hexdump -n4 -e '"%x"')
  echo -e "/rootfs.gz,CRC32,0x${crc32}\n/flatkc,CRC32,0xffffffff" > "$FILECKSUM_FILE"
}

option="$1"
case "$option" in
pack) pack ;;
unpack) unpack ;;
*) echo "$0 pack|unpack" && exit 1 ;;
esac

重打包发现启动不了,经分析存在签名校验,因此直接把它pass掉: image.png 首先找一个点去打补丁,找了会儿感觉这个关机的点不错,通过这个字符串定位到函数: image.png 把原来的输出改为执行busybox的sh,并去掉关机操作: image.png 插电!开机!执行失败: image.png 根据这个字符串发现是sysctl文件,这就很奇怪咯,沉着分析,冷静思索,不管了直接把它替换了,应该影响不大!插电!开机!成功获取shell: image.png image.png

注意不要看到不是标准shell就觉得要越狱,有些设备直接提供了命令可以进入到系统的shell。

搭建调试环境

当固件能成功运行并获取到root shell时,首先需要关注它的运行时环境:

  1. 基于什么系统与发行版:unix(freebsd, ),linux(ubuntu, centos, suse等),这将直接决定后续调试的复杂度。
  2. 内核版本号:低版本容易存在已知漏洞(如提权),且默认支持的缓解措施较少(如aslr),但是由于ABI兼容性问题编译软件较麻烦,高版本恰好相反。
  3. 是否自定义内核,内核模块是否静态编译进内核:若内核被修改过,如分析程序发现一些不存在于标准里的系统调用,发现一些奇怪的内核模块,则内核被定制过,这部分可能需要分析,另外将该固件里的程序提取到其他同(高)版本的内核里可能无法正常运行。
  4. 文件系统与分区挂载:观察磁盘有哪些分区,已挂载和未挂载的文件系统,是否使用了特殊文件系统,如rootfs是否是内存文件系统,是否使用了sysfs,procfs,tmpfs,cgroup,udev等,它们各自会提供特殊的功能。

调试时,需要充分利用系统与应用提供的各种机制:

  1. 使用ps, netstat等工具查看进程信息,充分利用/proc等目录的信息。
  2. 使用strace, ltrace,tcpdump等动态追踪或抓包工具可整体了解程序运行机制,从而快速定位到关键点。
  3. 研究程序的日志机制,调试日志可能通过编译预处理去除,也可能编译在程序中,通过特定配置,指定参数,指定信号激活,详细的调试日志方便分析程序流程。
  4. 选择合适的调试器,如ida的调试器,gdb与合适的插件,frida动态插桩等。

一般来说ida调试服务端适合与ida反编译工具配合使用,但是它依赖高版本的libc,libc++库,因此在旧内和尚一般需要先编译高版本的libc。gdb适用范围更广,包括非x86架构,非linux系统,一般是自己编译上带python扩展的版本再配合一些插件(gef,peda,pwndbg)使用。frida可作为strace的增强,能快速的对任意函数hook,但环境要求更多。 一般情况下,设备的环境里只会存在设备运行所必需的工具,因此很多分析所需工具(如nc, gdb, libc)需要自己安装。

  1. 当设备环境是发行版(如ubuntu 16.04)时,可尝试直接配置软件源进行软件安装,当发行版太旧已停止维护也可尝试查找archive源。
  2. 当设备环境是裸核或不存在包管理工具时,需要安装一台基于(或低于)该内核版本的发行版系统,在该系统上编译所需的软件并上传到设备环境里,这里最好使用静态编译(-static, --enable-static, --disable-dynamic),否则依赖的动态库(可用ldd查看)也需要被上传,为防止冲突可使用LD_LIBRARY_PATH环境变量指定库路径。

交叉编译环境

一般情况下设备运行环境中是不会存在开发工具的,而且一般也不存在包管理工具,因此需要在其他环境中编译必要的软件并上传到设备中,由于ABI(指令集/系统调用/大小端/文件格式)不同,需要用到交叉编译,一般需要两步,编译交叉编译器,再用交叉编译器编译得到目标程序,其中涉及三个概念:

  1. --build:指编译交叉编译器时使用的平台,不指定一般工具也能推测出
  2. --host:指编译出的交叉编译器需要运行的平台,不指定时和build一致
  3. --target:编译出的交叉编译器编译代码后生成的可执行文件运行的平台

如有一台超强的mips主机,但是它上面没有编译环境。有一台很菜的x86主机,它上面有gcc。现在编译个巨大无比的运行在arm平台上的程序,于是用--build x86 --host mips --target arm参数编译gcc,这样能生成一个mips版的gcc,它运行在mips主机上并能编译出在arm上执行的程序。当有了交叉编译器,只需要指定build和host参数即可。有一些工具能方便的构建交叉编译器,如buildroot和crosstool-ng。

buildroot

如它的名字,它是用于构建完整rootfs的,使用它可以很容易生成一个嵌入式linux系统环境,包括指定的内核镜像,同样也有针对该环境进行开发所需的交叉编译工具,在生成交叉编译工具时,而且它有glibc/uClibc-ng/musl三种库可选。它的使用方法如下:

  1. 下载最新稳定版的压缩包:https://buildroot.org/download.html
  2. 安装必要的工具软件:https://buildroot.org/downloads/manual/manual.html#requirement
  3. 使用make menuconfig生成配置文件,重点关注target optionstoolchain,前者选择架构,后者选择系统版本号和c库,此处选择的系统版本必须不大于设备系统版本
  4. 使用make编译,编译后结果在output目录,编译工具链在其host子目录中。

在使用它构建kernel时,需要先配置kernel的构建选项,可手动进入output/build里响应的linux源码目录,复制arch下的默认配置,或者使用make menuconfig生成配置。

crosstool-ng

相比于buildroot,它是更纯粹的交叉编译器生成工具,它可针对bare-metal或linux生成工具,并且支持glibc和uClibc-ng作为c库,不过它默认支持的系统版本较少,低版本需要使用git等方式手动配置,它的使用方法如下:

  1. 下载最新的代码:git clone https://github.com/crosstool-ng/crosstool-ng
  2. 安装必要的软件:apt-get install bison flex texinfo automake m4 libtool-bin libncurses-dev bison flex texinfo automake help2man,剩下的之后报错再补
  3. 进入目录运行./bootstrap,它会生成automake配置
  4. 执行./configure --prefix=/opt/crosstool-ng && make && make install生成并安装ct-ng
  5. 新建目录,在该目录下生成交叉编译配置:/opt/crosstool-ng/bin/ct-ng menuconfig,它的配置和buildroot类似,看菜单和对应的帮助即可
  6. 执行编译命令:/opt/crosstool-ng/bin/ct-ng build
  7. 之后即可指定x-tools下的程序为编译工具,对于使用动态链接的文件,需要把sysroot下的依赖库拷贝到目标系统。

构建通用分析工具

像GitHub的gdb binary项目,可以通过交叉编译工具构建一批兼容性强的分析工具,包括:

  1. gdbgdbserver,一般需要带python扩展,也就是python顺便也要来一份,方便使用如GEFpwndbg插件。
  2. 有些时候不好用gdbserver,如unix环境,需要编lldb
  3. 像ida在做伪代码调试时挺好用的,但是它的server是动态链接的而且libc版本还挺高,需要构建多版本的libc库。
  4. 很多环境里只有服务运行必须的工具,因此扔一个busybox很好用,所以也要储备着。
  5. 网络的netcat,tcpdump,sshd,隧道工具等,也是常用的,还有strace,ltrace等工具...

这些工具就是积累,用到了就把它保留好,把它们变成祖传的,它们的编译方法大同小异,这里随便找个例子,在分析产品时,想使用strace,它不自带因此需要自己编译,使用uname可知kernel为2.6.32:

bash-4.1# uname -a
Linux localhost2 2.6.32-00025-g841d072-dirty #1 SMP Mon Jul 20 17:51:26 EDT 2020 x86_64 x86_64 x86_64 GNU/Linux

创建交叉编译工具链,使用ct-ng menuconfig配置编译目标: image.png 配置操作系统,此处版本存在的话可以直接选择,否者可以指定从git的某个版本或本地的版本,这里指定内核头文件为2.6.33之前的版本(为了通用可以稍微选个小一点的版本): image.png 这里可以选择使用glibc或是uClibc及其版本: image.png 此处选择gcc的版本: image.png 编译普通程序只需指定Linux内核版本与运行目标平台,编译内核的话对gcc版本会有要求,配置好后使用ct-ng build编译即可,之后即可编译程序:

# 在ubuntu20上下载源码,指定工具链并编译
wget  https://github.com/strace/strace/releases/download/v5.13/strace-5.13.tar.xz
tar xf strace-5.13.tar.xz && cd strace-5.13
export PATH=$PATH:/home/bm/x-tools/x86_64-unknown-linux-uclibc/bin
./configure --host=x86_64-unknown-linux-uclibc CFLAGS=-std=c99 --prefix=/opt/strace
make && make install 

之后把编译好的二进制文件传入设备里即可,此处使用了默认的编译选项,即动态链接,因此还需要sysroot里的动态库,此处存放于/opt/lib目录下:

curl http://192.168.202.133:8000/strace > strace  # 传输strace
curl ....   > /opt/lib/...  # 传输必要的依赖
alias strace='LD_LIBRARY_PATH=/opt/lib /opt/strace/strace' 

一般应选择静态链接,除非静态链接比较困难。

授权破解

破解授权不是我们的目的,我们不是为了销售盗版而是为了挖掘它的安全漏洞,因此破解是不得已而为之的事,在设备运行后,我们需要首先尝试使用我们感兴趣的功能,如它的VPN功能是否能正常使用,若只有数量限制速率限制等,或者能够试用一段时间,那么就可以跳过这一步。但是有时一些设备的特定或全部功能是需要购买授权才能使用的,此时为了漏洞挖掘覆盖的全面性,我们必须要破解它授权;

  1. 分析授权逻辑,尝试直接打补丁(爆破)绕过授权验证。这种方式优点是简单粗暴,缺点是若未找到最佳的补丁点可能导致未完全破解进而程序功能异常。
  2. 逆向授权程序,伪造授权文件破解授权。通过分析授权逻辑编写注册机是最好的方式,若程序未正确使用公钥密码,可实现不打补丁直接生成合法的证书,否则一般是替换程序的公钥为自己生成的,或直接打补丁跳过公钥签名验证环节,然后自己生成符合格式的证书并自签名。这里若存在合法的证书则可以轻易伪造证书,否则需要仔细分析证书验证环节逆向出合法的证书格式。 授权函有多种形式,如单注册码或带授权信息的证书,以后者为例,一个典型的证书生成过程如下:

image.png 这里选两个典型的例子进行演示:

🌰壹

这个例子不需要写注册机,某设备试用版授权无法正常使用某功能,为了分析该功能只能破授权,通过分析里面的二进制文件发现和授权有关的部分出现了很多flexlm,还有很多lm相关的文件,搜索发现它用的是某三方授权方案: image.png image.png 再搜索发现该方案已经被分析很深了(神奇的是一家做授权的公司在早起产品中竟然没用公钥机制,导致可以不打补丁直接写注册机),那么此处就不必再深入研究它的实现细节了,根据yangmyron的分析,先找到它的damon文件,直接给它来个补丁,使用SIGN%s=进行字符串搜索定位到关键函数,将其返回值改为1,以及最后的或改为1:: image.png 再看nslicense里解析为具体的feature,根据它的需要编lic文件内容,如

SERVER this_host fa163ec85d89
VENDOR CITRIX
USE_SERVER
INCREMENT CNS_SEE_SERVER CITRIX 2021.0602 permanent 1 \
        VENDOR_STRING=;LT=Retail;GP=720;CL=SSE;SA=1;ODP=0 \
        ISSUED=10-jan-2021 NOTICE="sangfor...." \
        SN=LA-0123456789-25002:FID__788fcbbf_c5858b8918f_6dfe \
        START=14-jan-2016 SIGN="0514 8058 0055 3CB8 7F18 70D1 A5CF \
        CBD5 9A05 352C 15AE F733 F297 5144 7C57 137B 41EF 7101 C134 \
        B7C5 3989 99D5 0499 26D2 EF7C 55E8 FE25 5555 01CF E346"
...

由于根文件系统为内存文件系统,重启后改动失效,因此将修改后的文件放入/var目录,并在每次启动时做替换,将/etc/rc.conf文件复制到/nsconfig目录下,在它里面写CITRIX替换指令:

rm -rf /netscaler/CITRIX
cp /var/CITRIX /netscaler
chmod a+x /netscaler/CITRIX

插电!开机!轻轻松松: image.png

🌰贰

这是一个写注册机的例子,同样某设备试用版授权无法正常使用某功能,于是只能破授权了,IDA打开,搜license字符串,找到一个可疑点: image.png

点进去分析,在sub_22D47E0函数里,看到它首先读取了vm.lic文件,之后对一个0xA8大小的结构体qword_D2C2C00进行了初始化,之后sub_22D7460做的事应该是解析文件内容并将解析后的内容置于qword_D2C2C00里: image.png 进一步分析sub_22D7460,由于直接用了openssl库,所以逻辑很容易分析,首先获取证书部分并base64解码: image.png 之后使用RSA解密头部,获取AES密钥: image.png 如果RSA解密失败,它会直接将头部作为AES密钥,之后会使用此密钥解密后续数据: image.png 解密后,循环解析并设置结构体: image.png 这样就能分析出结构体如下:

struct lic{             // 0xa8
   void* SERIALNO;  // 0x00  FGVMEV
   void* CERT;              // 0x08 
   void* KEY;               // 0x10 
   void* CERT2;             // 0x18 
   void* KEY2;              // 0x20 
   void* CREATEDATE;        // 0x28  "%a %b %e %H:%M:%S %Y"
   void* UUID;              // 0x30 
   void* CONTRACT;              // 0x38
   int USGFACTORY;              // 0x40
   int LENCFACTORY;             // 0x44
   int CARRIERFACTORY;              // 0x48
   int EXPIRY;                      // 0x4c
   int Model;                   // 0x50
   char pad1[0x04];          // 0x54
   __int64 createtime;      // 0x58
   int sSERIALNO;           // 0x60
   int sCERT;
   int sKEY;
   int sCERT2;
   int sKEY2;
   int sCREATEDATE;
   int sUUID;
   int sCONTRACT;
   int sUSGFACTORY;
   int sLENCFACTORY;
   int sCARRIERFACTORY;
   int sEXPIRY;             
   char pad2[0x10];     // 0x90
   __int8 valid;        // 0xa0
    char pad3[0x07];    // 0xa8
}

把结构体分析完之后就可以很好的进行交叉引用来获取所有对证书的使用点,并分析系统是怎么使用这些域的,这样就能知道怎么为这些域赋值,之后又是一通分析,不细述,只有一个点需要注意,它的代码有点奇怪导致ida无法正确识别函数: image.png 最终根据上述过程,可写出如下代码:

import base64
import struct


def serialize(dic: dict):
    buf = b''

    def serialize_key(s: str):
        s = s.encode() + b'\0'
        return struct.pack('<B', len(s)) + s

    def serialize_val(obj):
        if isinstance(obj, int):
            return b'n' + struct.pack('<HI', 4, obj)
        elif isinstance(obj, str):
            obj = obj.encode() + b'\0'
            return b's' + struct.pack('<H', len(obj)) + obj
        else:
            raise Exception('???')

    for key, val in dic.items():
        buf += serialize_key(key)
        buf += serialize_val(val)
    return buf


AES_BLOCK = 16
MAGIC = 0x13A38693
LINE_SIZE = 64
AES_KEY = b'\0' * 32


def gen_lic():
    lic_str = {
        "SERIALNO": "FGVMPG",
        # "CERT": "",
        # "KEY": "",
        # "CERT2": "",
        # "KEY2": "",
        "CREATEDATE": "1631013609",  # 20210907   Sun Jan 1 17:21:54 2021
        # "UUID": "",       # 32字节,可以没有
        # "CONTRACT": "",
        "USGFACTORY": 16,
        "LENCFACTORY": 16,
        "CARRIERFACTORY": 16,
        "EXPIRY": 60 * 60 * 24 * 365,  # 一年
    }
    buf = serialize(lic_str)
    buf = struct.pack('<III', 0, MAGIC, 0) + buf
    pad_chr = AES_BLOCK - (len(buf) % AES_BLOCK)
    buf += bytes(chr(pad_chr) * pad_chr, 'latin1')

    # 若未打补丁,需要对buf进行aes加密aes.enc(key=AES_KEY,iv=AES_KEY[:16],buf)

    lic = struct.pack('<I', len(AES_KEY)) + AES_KEY + struct.pack('<I', len(buf)) + buf
    header = b'-----BEGIN FGT VM LICENSE-----'
    foot = b'-----END FGT VM LICENSE-----'
    data = base64.b64encode(lic)
    lines = [header, ]
    for i in range(0, len(data), LINE_SIZE):
        lines.append(data[i:i + LINE_SIZE])
    lines.append(foot)
    content = b'\r\n'.join(lines)
    with open('Fortigate.lic', 'wb') as f:
        f.write(content)


if __name__ == '__main__':
    gen_lic()

从上面分析,甚至不用打补丁就能写KeyGen,不过python Crypto AES和Openssl那种调法似乎不兼容,所以还是直接打个补丁吧! image.png 然后,破解成功! image.png

代码保护破解

有些厂商会对脚本语言写的代码做一些保护,这就要具体问题具体分析了,因为这不是重点因此不要过多分析:

  1. 有破解方法:如PHP使用了ioncube这些,有工具的上工具,没工具的给钱让别人破。
  2. 没破解方法:评估下有没有必要咯,首先肯定下保护越多别人挖的概率越小,因此有漏洞的可能越大,只是要评估投入产出比。没破解方法,如果是它自己实现的算法那一般都可以破解,分析扩展库或解释器就行,如果是商业方案的话就看情况了。