之前使用 firmware-analysis-toolkit 一键式生成了固件的分析环境。这篇文章分析一下它的工作流程和原理。
1 ./fat.py ../DIR-815A1_FW101SSB03.bin
从 fat.py 开始:
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 # fat.py ... ... def main(): show_banner(). # 打印一个好看的 LOGO parser = argparse.ArgumentParser() parser.add_argument("firm_path", help="The path to the firmware image", type=str) parser.add_argument("-q", "--qemu", metavar="qemu_path", help="The qemu version to use (must exist within qemu-builds directory). If not specified, the qemu version installed system-wide will be used", type=str) args = parser.parse_args() qemu_ver = args.qemu qemu_dir = None if qemu_ver: ... ... image_id = run_extractor(args.firm_path) # important! if image_id == "": print ("[!] Image extraction failed") else: arch = identify_arch(image_id) make_image(arch, image_id) infer_network(arch, image_id, qemu_dir) final_run(image_id, arch, qemu_dir) if __name__ == "__main__": main()
大致的流程就是:
提取镜像,返回镜像 id (第 16 行)
识别固件系统架构(21 行)
制作镜像 (22 行)
添加网络接口(23 行)
运行 QEMU 模拟(24 行)
这个函数非常粗暴地调用了 firmadyne/sources/extractor/extractor.py 来分析。
执行的语句是:
1 firmadyne/sources/extractor/extractor.py -np -nk '../DIR-815A1_FW101SSB03.bin' './firmadyne/images'
这个 extractor.py 也是 firmadyne 的工具,用于提取文件系统:
所以意思就是不并行操作,也不提取内核,只要文件系统的意思吧。
执行完之后,就成功地在 firmadyne/images 下生成了一个压缩文件:
1 2 $ ls firmadyne/images/DIR-815A1_FW101SSB03.bin_db2ac42c5d50fb5bd52999275f6d315b.tar.gz README.md
然后 FAT 将其重命名为 ${iid}.tar.gz
,即以依次命名为 1.tar.gz,2.tar.gz,后面来的以此类推。这个 iid 就是 main 函数中返回的 image_id 。
identify_arch —— 架构识别 这个函数也是类似的,直接调用 firmadyne/scripts/getArch.sh。
1 2 3 $ cd firmadyne/$ scripts/getArch.sh images/1.tar.gz ./bin/busybox: mipsel
看起来,该脚本可以输出文件系统类型和架构。简单研究了脚本,它是先解压了 1.tar.gz,然后看解压生成的文件有没有包含 busybox,alpha 等字段,然后再去分析解压出来的文件是什么格式(用 file 命令)。具体细节不再研究了。总之就是能用。
make_image —— QEMU image 制作 同样的,是调用 firmadyne/scripts/makeImage.sh 实现的。
1 scripts/makeImage.sh 1 mipsel
这个比较重要,所以单步调试一下。
初始化 首先是加载 firmadyne.config 文件,里面包含了一些函数和环境变量。用到了再查。scripts 目录下的 bash 脚本都包含了这一部分。
1 2 3 4 5 6 7 8 9 # !/bin/bash if [ -e ./firmadyne.config ]; then source ./firmadyne.config elif [ -e ../firmadyne.config ]; then source ../firmadyne.config else echo "Error: Could not find 'firmadyne.config'!" exit 1 fi
然后进行了一波检查,包括是不是 root 权限,以及传进来的架构(这里是 mipsel)是否合法。
下面就开始正式运行,先初始化一些变量,然后创建工作目录 frmadyne/scratch/${IID}
1 2 3 4 5 6 7 8 9 10 11 12 echo "----Running----" WORK_DIR=`get_scratch ${IID}` # 这里找到了 firmadyne/scratch/${IID} 目录,不存在的话会创建 IMAGE=`get_fs ${IID}` # firmadyne/scratch/${IID}/image.raw IMAGE_DIR=`get_fs_mount ${IID}` # firmadyne/scratch/${IID}/image/ CONSOLE=`get_console ${ARCH}` # firmadyne/binaries/console.mipsel ,暂时不清楚用途 LIBNVRAM=`get_nvram ${ARCH}` # firmadyne/binaries//libnvram.so.mipsel echo "----Creating working directory ${WORK_DIR}----" mkdir -p "${WORK_DIR}" chmod a+rwx "${WORK_DIR}" chown -R "${USER}" "${WORK_DIR}" chgrp -R "${USER}" "${WORK_DIR}"
创建 image 并初始化分区表 然后准备创建 image。首先计算创建 image 的大小。条件是必须比 sizeof(1.tar.gz) + 10MB 大,也必须是 8388608 的倍数 (8 MB 的倍数),不知道这个是什么道理。反正这一段代码计算出来就是一个 size。
1 2 3 4 5 6 7 8 TARBALL_SIZE=$(tar ztvf "${TARBALL_DIR}/${IID}.tar.gz" --totals 2>&1 |tail -1|cut -f4 -d' ') # 这个就是 1.tar.gz 的大小 MINIMUM_IMAGE_SIZE=$((TARBALL_SIZE + 10 * 1024 * 1024)) echo "----The size of root filesystem '${TARBALL_DIR}/${IID}.tar.gz' is $TARBALL_SIZE-----" IMAGE_SIZE=8388608 while [ $IMAGE_SIZE -le $MINIMUM_IMAGE_SIZE ] do IMAGE_SIZE=$((IMAGE_SIZE*2)) done
创建 image,并创建分区表。
1 2 3 4 5 6 7 8 9 10 echo "----Creating QEMU Image ${IMAGE} with size ${IMAGE_SIZE}----" qemu-img create -f raw "${IMAGE}" $IMAGE_SIZE chmod a+rw "${IMAGE}" echo "----Creating Partition Table----" echo -e "o\nn\np\n1\n\n\nw" | /sbin/fdisk "${IMAGE}" echo "----Mounting QEMU Image----" DEVICE=$(get_device "$(kpartx -a -s -v "${IMAGE}")") # '/dev/mapper/loop25p1' sleep 1
这个分区表我没太看懂。
echo -e
的意思是后面的字符串可以使用转义字符。我简单搜索了 fdisk 的用法,发现这个和代码里的完全对应:https://superuser.com/questions/332252/how-to-create-and-format-a-partition-using-a-bash-script
For example, the following clears the partition table, if there is one, and makes a new one that has a single partition that is the; entire disk: ( echo o # Create a new empty DOS partition table echo n # Add a new partition echo p # Primary partition echo 1 # Partition number echo # First sector (Accept default: 1) echo # Last sector (Accept default: varies) echo w # Write changes ) | sudo fdisk
总之就是清除分区表,然后创建一个单分区的新分区表。总觉得之前没有这个操作也没啥问题。
挂载镜像并写入文件系统 这个 get_device 我也看了半天,搜索了一下 kpartx 命令。我个人的理解是,对于多分区的磁盘镜像,不能直接挂载,需要将其先用 kpartx -av
处理,在 /dev/mapper 下生成设备,然后再将该设备 mount 到某个目录下,这样就可以直接操作这个分区的数据了。(参考 http://blog.chinaunix.net/uid-1911213-id-3387288.html 和 https://www.bbsmax.com/A/RnJW0eWR5q/ )
这里先执行了 $(kpartx -a -s -v "${IMAGE}")
,在 /dev/mapper 下创建了 loop25p1 设备。输出的内容是:'add\ map\ loop25p1\ \(253:5\):\ 0\ 63488\ linear\ 7:25\ 2048'
。所以 get_device 执行的 echo "/dev/mapper/$(echo $1 | cut -d ' ' -f 3)"
就是相当于 python 的 s.split(' ')[2]
,恰好把设备名 loop25p1
取出来。
随后创建文件系统,并挂载该设备,挂载到 frmadyne/scratch/${IID}/image/
1 2 3 4 5 6 7 8 9 10 11 12 echo "----Creating Filesystem----" mkfs.ext2 "${DEVICE}" sync # 用于刷新缓冲区 echo "----Making QEMU Image Mountpoint at ${IMAGE_DIR}----" if [ ! -e "${IMAGE_DIR}" ]; then mkdir "${IMAGE_DIR}" chown "${USER}" "${IMAGE_DIR}" fi echo "----Mounting QEMU Image Partition 1----" mount "${DEVICE}" "${IMAGE_DIR}"
然后就是常规地将文件系统写入进去,这里不仅解压了 1.tar.gz,还拷贝了 firmadyne 自己的一些文件:
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 echo "----Extracting Filesystem Tarball to Mountpoint----" tar -xf "${TARBALL_DIR}/${IID}.tar.gz" -C "${IMAGE_DIR}" echo "----Creating FIRMADYNE Directories----" mkdir "${IMAGE_DIR}/firmadyne/" mkdir "${IMAGE_DIR}/firmadyne/libnvram/" mkdir "${IMAGE_DIR}/firmadyne/libnvram.override/" echo "----Patching Filesystem (chroot)----" cp $(which busybox) "${IMAGE_DIR}" cp "${SCRIPT_DIR}/fixImage.sh" "${IMAGE_DIR}" chroot "${IMAGE_DIR}" /busybox ash /fixImage.sh rm "${IMAGE_DIR}/fixImage.sh" rm "${IMAGE_DIR}/busybox" echo "----Setting up FIRMADYNE----" cp "${CONSOLE}" "${IMAGE_DIR}/firmadyne/console" chmod a+x "${IMAGE_DIR}/firmadyne/console" mknod -m 666 "${IMAGE_DIR}/firmadyne/ttyS1" c 4 65 cp "${LIBNVRAM}" "${IMAGE_DIR}/firmadyne/libnvram.so" chmod a+x "${IMAGE_DIR}/firmadyne/libnvram.so" cp "${SCRIPT_DIR}/preInit.sh" "${IMAGE_DIR}/firmadyne/preInit.sh" chmod a+x "${IMAGE_DIR}/firmadyne/preInit.sh"
这里需要注意的是,13 行使用 chroot 来执行了 fixImage.sh 。才疏学浅我太能理解它的功能,看起来像是修复文件系统的部分内容,比如软链接,比如 /etc 目录的部分文件。至于为什么要用 chroot 和 busybox 我就不明白了。先不管。
最后收尾,unmount 并删除 device mapper:
1 2 3 4 5 6 7 echo "----Unmounting QEMU Image----" sync umount "${DEVICE}" echo "----Deleting device mapper----" kpartx -d "${IMAGE}" losetup -d "${DEVICE}" &>/dev/null dmsetup remove $(basename "$DEVICE") &>/dev/null
infer_network —— 网络添加 调用 firmadyne/scripts/inferNetwork.sh 实现:
1 2 cd firmadyne scripts/inferNetwork.sh 1 mipsel
这个部分也比较重要,所以也单步调试一下。inferNetwork.sh 的重点只有几行:
1 2 3 4 5 6 7 8 9 10 ... ... echo "Running firmware ${IID}: terminating after 60 secs..." timeout --preserve-status --signal SIGINT 60 "${SCRIPT_DIR}/run.${ARCH}.sh" "${IID}" sleep 1 echo "Inferring network..." "${SCRIPT_DIR}/makeNetwork.py" -i "${IID}" -q -o -a "${ARCH}" -S "${SCRATCH_DIR}" echo "Done!"
timeout --preserve-status --signal SIGINT 60
将保持运行 60s,超时后发送 sigint。运行的脚本是 run.mipsel.sh,其实内容就是启动 qemu 的语句。其实这里我还不太理解,添加网卡为啥要先启动 qemu。但是看到后面 makeNetwork.py 的时候就知道了。(此外,timeout 运行会阻塞。)
1 2 3 # scripts/run.mipsel.sh ... ... qemu-system-mipsel -m 256 -M malta -kernel ${KERNEL} -drive if=ide,format=raw,file=${IMAGE} -append "firmadyne.syscall=1 root=/dev/sda1 console=ttyS0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1" -serial file:${WORK_DIR}/qemu.initial.serial.log -serial unix:/tmp/qemu.${IID}.S1,server,nowait -monitor unix:/tmp/qemu.${IID},server,nowait -display none -netdev socket,id=s0,listen=:2000 -device e1000,netdev=s0 -netdev socket,id=s1,listen=:2001 -device e1000,netdev=s1 -netdev socket,id=s2,listen=:2002 -device e1000,netdev=s2 -netdev socket,id=s3,listen=:2003 -device e1000,netdev=s3
然后又套娃,运行 python 脚本 makeNetwork.py 来创建网卡:
1 2 cd firmadyne/ scripts/makeNetwork.py -i 1 -q -o -a mipsel -S scratch/
主要的功能由 process() 函数负责。下面分析 process 函数的代码:
1 2 3 4 5 6 7 8 9 10 11 12 # makeNetwork.py ... ... def process(infile, iid, arch, endianness=None, makeQemuCmd=False, outfile=None): ... ... data = open(infile).read() ... ... #find interfaces with non loopback ip addresses ifacesWithIps = findNonLoInterfaces(data, endianness) #find changes of mac addresses for devices macChanges = findMacChanges(data, endianness)
这里的这个 infile 是 firmadyne/scratch/1/qemu.initial.serial.log
,也就是前一步运行 qemu 时生成的日志文件。它记录了 qemu 的输出。findNonLoInterfaces 和 findMacChanges 函数都用到了这个日志文件。
text 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [ 0.000000] Linux version 2.6.39.4+ (ddcc@ddcc-virtual) (gcc version 5.3.0 (GCC) ) #2 Tue Sep 1 18:11:28 EDT 2020 [ 0.000000] bootconsole [early0] enabled [ 0.000000] CPU revision is: 00019300 (MIPS 24Kc) [ 0.000000] FPU revision is: 00739300 [ 0.000000] Determined physical RAM map: [ 0.000000] memory: 00001000 @ 00000000 (reserved) [ 0.000000] memory: 000ef000 @ 00001000 (ROM data) [ 0.000000] memory: 0065e000 @ 000f0000 (reserved) [ 0.000000] memory: 0f8b2000 @ 0074e000 (usable) [ 0.000000] debug: ignoring loglevel setting. [ 0.000000] Wasting 59840 bytes for tracking 1870 unused pages [ 0.000000] Initrd not found or empty - disabling initrd [ 0.000000] Zone PFN ranges: [ 0.000000] DMA 0x00000000 -> 0x00001000 [ 0.000000] Normal 0x00001000 -> 0x00010000 [ 0.000000] Movable zone start PFN for each node ... ...
找到网卡设备 先看 findNonLoInterfaces 函数,字面意思是找到非 lo 网卡。通过分析,找到了两张网卡 br0 和 br1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def findNonLoInterfaces (data, endianness ): lines = stripTimestamps(data) candidates = filter (lambda l: l.startswith("__inet_insert_ifa" ), lines) ... ... for c in candidates: g = re.match (r"^__inet_insert_ifa\[[^\]]+\]: device:([^ ]+) ifa:0x([0-9a-f]+)" , c) if g: (iface, addr) = g.groups() addr = socket.inet_ntoa(struct.pack(fmt, int (addr, 16 ))) if (not addr.startswith("127." )) and addr != "0.0.0.0" : result.append((iface, addr)) return result
下面是 findMacChanges 函数,功能也是类似的,看起来是找到 ioctl 相关的内容。不过对于这个固件来说没有找到。先不管了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def findMacChanges (data, endianness ): lines = stripTimestamps(data) candidates = filter (lambda l: l.startswith("ioctl_SIOCSIFHWADDR" ), lines) ... ... for c in candidates: g = re.match (r"^ioctl_SIOCSIFHWADDR\[[^\]]+\]: dev:([^ ]+) mac:0x([0-9a-f]+) 0x([0-9a-f]+)" , c) if g: (iface, mac0, mac1) = g.groups() m0 = struct.pack(fmt, int (mac0, 16 ))[2 :] m1 = struct.pack(fmt, int (mac1, 16 )) mac = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB" , m0+m1) result.append((iface, mac)) return result
识别网络信息 再回到 process 函数,下面对每个 iwi 进行操作 for iwi in [('br0', '192.168.0.1'), ('br1', '192.168.100.1')]
。找到和这两个网卡桥接的网卡,添加到 network 集合中。
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 ifacesWithIps = findNonLoInterfaces(data, endianness) macChanges = findMacChanges(data, endianness) print ("Interfaces: %r" % ifacesWithIps) deviceHasBridge = False for iwi in ifacesWithIps: brifs = findIfacesForBridge(data, iwi[0 ]) if debug: print ("brifs for %s %r" % (iwi[0 ], brifs)) for dev in brifs: vlans = findVlanInfoForDev(data, dev) network.add((buildConfig(iwi, dev, vlans, macChanges))) deviceHasBridge = True if not brifs and not deviceHasBridge: vlans = findVlanInfoForDev(data, iwi[0 ]) network.add((buildConfig(iwi, iwi[0 ], vlans, macChanges)))
添加网络设备 最后,network 里只添加了一张网卡信息:{('192.168.0.1', 'eth0', None, None)}
这个是固件暴露在外的网卡。我们要想访问这个被模拟的固件,就需要添加一条路由信息,到达这个 192.168.0.1 。qemuCmd 函数负责生成的 run.sh 的内容里包含下面语句:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # run.sh ... ... TAPDEV_0=tap${IID}_0 HOSTNETDEV_0=${TAPDEV_0} echo "Creating TAP device ${TAPDEV_0}..." sudo tunctl -t ${TAPDEV_0} -u ${USER} echo "Bringing up TAP device..." sudo ip link set ${HOSTNETDEV_0} up sudo ip addr add 192.168.0.2/24 dev ${HOSTNETDEV_0} echo "Adding route to 192.168.0.1..." sudo ip route add 192.168.0.1 via 192.168.0.1 dev ${HOSTNETDEV_0}
这里涉及 tap 设备的概念:
https://zhuanlan.zhihu.com/p/388742230 在计算机网络中,TUN 与 TAP 是操作系统内核中的虚拟网络设备 。不同于普通靠硬件网路板卡实现的设备,这些虚拟的网络设备全部由软件实现,并向运行于操作系统上的软件提供与硬件的网络设备完全相同的功能。 TAP 等同于一个以太网设备,它操作第二层数据包如以太网数据帧。TUN 模拟了网络层设备,操作第三层数据包比如 IP 数据封包。 相比于物理网卡负责内核网络协议栈和外界网络之间的数据传输,虚拟网卡的两端则是内核网络协议栈和用户空间 ,它负责在内核网络协议栈和用户空间的程序之间传递数据:
创建了 tap 设备之后,还把它作为参数传递给 qemu
1 -netdev tap,id=nettap0,ifname=${TAPDEV_0},script=no -device e1000,netdev=nettap0
关于 qemu 网络设备的使用,我还比较迷茫。先贴几个链接以后看看。
final_run —— 启动 QEMU 直接运行前面生成的 scratch/${iid}/run.sh 。