最近有一个需求,如何对FirmAE仿真成功的设备中的固件进行修改,本质就是在仿真前修改固件的文件系统。背景是这样的,我在调试一个httpd的时候,它会创建大量的进程,这些进程手动进行kill的话太繁琐,使用gdbserver对其进行attach主进程,其他的进程也会影响主进程的调试,因此就想,能不能将httpd的自动启动关停,通过gdbserver手动启动程序并调试,这样进程状态比较可控。
这就需要从源码上分析一下FirmAE是如何从设备固件中提取固件、进行patch、使用qemu仿真的。 分析过程主要是从run.sh
入手,主要涉及到的源码文件有:
scripts/extractor.py
:提取固件中的文件系统和操作系统内核。
scripts/makeImage.sh
:创建qemu磁盘镜像,将固件文件系统进行修改,添加相关的工具、文件目录、设备等并写入到镜像中。
scripts/inferFile.sh
:分析固件文件系统的启动项,分析文件系统中的http server。
scripts/fixImage.sh
:修改文件系统,添加必要的目录,添加相关工具,根据文件系统中的应用程序创建相应的目录。
FirmAE创建qemu磁盘镜像的大概思路就是:
先从固件中提取文件系统和操作系统内核。
创建一个磁盘镜像,将其挂载,并将提取出来的文件系统复制到磁盘镜像中。
对挂载镜像的目录进行修改,分析文件系统启动项、http server等,并根据目标架构添加响应的工具和脚本到文件系统中。
写入磁盘镜像并取消挂载。
固件文件系统提取 分析run.sh
,FirmAE会使用scripts/extractor.py
解压出来固件中的文件系统和操作系统内核,将文件系统打包成tar.gz压缩包形式。extractor.py
的主要原理是调用binwalk的API,根据UNIX目录规范对binwalk解压的目录进行搜索,找到其中的文件系统并压缩保存到images
文件夹。
进一步分析qemu的镜像制作过程,主要逻辑在scripts/makeImage.sh
中,因此随后的分析都是以makeImages.sh
为主要代码进行分析,如果被这些代码调用绕糊涂了也可以不管具体的代码文件,看关键命令和关键流程的实现。
1 ./scripts/makeImage.sh $IID $ARCH $FILENAME \
创建磁盘镜像 首先将压缩的文件系统从原来的"${TARBALL_DIR}/${IID}.tar.gz"
复制到"${WORK_DIR}/${IID}.tar.gz"
,也就是从images
目录复制到scratch
目录中。复制后的文件系统压缩包使用完毕后,会被删除。原始的固件文件系统压缩包有且仅有一份在images
目录中,如果要对FirmAE进行二次开发,则可以直接在该目录中去获取文件系统,就不用单独再用binwalk对固件进行解压。
1 cp "${TARBALL_DIR}/${IID}.tar.gz" "${WORK_DIR}/${IID}.tar.gz"
使用qemu-img创建磁盘镜像,磁盘镜像是存放在scratch
目录下对应的固件文件夹(数字序号)中的image.raw
。FirmAE管理固件的方式就是在images
目录下存放固件中提取出来的文件系统和操作系统内核,在scratch
目录下以数字递增的序号表示可被仿真的设备相关文件。 如下是从固件中提取的操作系统内核和固件文件系统压缩包:
1 2 3 4 5 6 7 8 9 10 $ tree -L 1 images images ├── 1.kernel ├── 1.tar.gz ├── 2.kernel ├── 2.tar.gz ├── 3.kernel └── 3.tar.gz 0 directories, 6 files
如下是scratch
目录,包含了仿真过程中的一些标志文件,仿真状态例如web服务是否可达、是否可ping等。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ tree -L 2 scratch scratch ├── 1 │ ├── architecture │ ├── brand │ ├── current_init │ ├── emulation.log │ ├── fileList │ ├── fileType │ ├── image │ ├── image.raw │ ├── init ......
回归到镜像创建的过程中:
1 2 qemu-img create -f raw "${IMAGE}" 1G chmod a+rw "${IMAGE}"
给磁盘镜像创建分区,完整示例命令如/sbin/fdisk /home/utest/app/FirmAE/scratch/3/image.raw
,根据echo
设置的输入,该命令大致的流程是:
o
:创建一个空白的DOS分区表
n\np\n1
:添加一个主分区primary,分区的数量设置为1
\n\n
:创建第一个大小为2MB的扇区sector,剩下的作为第二个扇区。
w
:将分区表写入到磁盘中1 echo -e "o\nn\np\n1\n\n\nw" | /sbin/fdisk "${IMAGE}"
使用fdisk
查看磁盘镜像的属性如下:1 2 3 4 5 6 7 8 9 10 11 12 Device Boot Start End Sectors Size Id Type image.raw1 2048 2097151 2095104 1023M 83 Linux Command (m for help): Disk image.raw: 1 GiB, 1073741824 bytes, 2097152 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: 0x88272c5c Device Boot Start End Sectors Size Id Type image.raw 2048 2097151 2095104 1023M 83 Linux
挂载磁盘镜像 然后就是调用firmae.config
中的add_partition
函数,将该磁盘镜像进行挂载,并返回挂载后的设备目录。如下,函数的主要功能就是将之前创建的镜像文件,使用losetup
命令进行挂载,然后通过遍历、比较losetup
命令的结果,来获取磁盘镜像挂载的目录:
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 add_partition () { local IFS=$'\n' local IMAGE_PATH local DEV_PATH="" local FOUND=false losetup -Pf ${1} while (! ${FOUND}) do sleep 1 for LINE in `losetup` do IMAGE_PATH=`echo ${LINE} | awk '{print $6}'` if [ "${IMAGE_PATH}" = "${1}" ]; then DEV_PATH=`echo ${LINE} | awk '{print $1}'`p1 if [ -e ${DEV_PATH} ]; then FOUND=true fi fi done done while (! ls -al ${DEV_PATH} | grep -q "disk") do sleep 1 done echo ${DEV_PATH} }
如下是使用losetup
命令显示的结果,此时是仿真结束后的结果,所以不包含磁盘镜像的挂载目录,仅供参考:
1 2 3 4 5 6 7 8 9 10 11 $ losetup NAME SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE DIO LOG-SEC /dev/loop1 0 0 1 1 /var/lib/snapd/snaps/bare_5.snap 0 512 /dev/loop8 0 0 1 1 /var/lib/snapd/snaps/gnome-3-38-2004_115.snap 0 512 /dev/loop6 0 0 1 1 /var/lib/snapd/snaps/gtk-common-themes_1535.snap 0 512 /dev/loop4 0 0 1 1 /var/lib/snapd/snaps/gnome-3-38-2004_119.snap 0 512 /dev/loop2 0 0 1 1 /var/lib/snapd/snaps/core20_1778.snap 0 512 /dev/loop0 0 0 1 1 /var/lib/snapd/snaps/core20_1738.snap 0 512 /dev/loop7 0 0 1 1 /var/lib/snapd/snaps/snap-store_638.snap 0 512 /dev/loop5 0 0 1 1 /var/lib/snapd/snaps/snap-store_599.snap 0 512 /dev/loop3 0 0 1 1 /var/lib/snapd/snaps/snapd_17883.snap 0 512
如下是真实仿真过程中,命令的执行结果。可以看到最终返回了磁盘镜像的挂载目录/dev/loop9p1
1 2 3 4 5 6 7 8 9 10 ++ IMAGE_PATH=/home/utest/app/FirmAE/scratch/3/image.raw ++ '[' /home/utest/app/FirmAE/scratch/3/image.raw = /home/utest/app/FirmAE/scratch/3/image.raw ']' +++ echo '/dev/loop9 0 0 0 0 /home/utest/app/FirmAE/scratch/3/image.raw 0 512' +++ awk '{print $1}' ++ DEV_PATH=/dev/loop9p1 ++ '[' -e /dev/loop9p1 ']' ++ FOUND=true ...... ++ echo /dev/loop9p1 + DEVICE=/dev/loop9p1
创建文件系统 使用mkfs.ext2
在镜像挂载目录创建ext2
文件系统(实际上就是在磁盘镜像上创建文件系统)
在scratch
的固件编号目录中创建一个image
文件夹,例如mkdir /home/utest/app/FirmAE/scratch/3/image/
,然后将该文件夹也挂载到之前镜像的挂载目录上,例如mount /dev/loop9p1 /home/utest/app/FirmAE/scratch/3/image/
。
1 2 3 4 5 6 7 8 9 echo "----Making QEMU Image Mountpoint----" if [ ! -e "${IMAGE_DIR}" ]; then mkdir "${IMAGE_DIR}" chown "${USER}" "${IMAGE_DIR}" fi echo "----Mounting QEMU Image Partition----" sync mount "${DEVICE}" "${IMAGE_DIR}"
解压从固件中提取的文件系统。原本提取出来的被压缩的文件系统在FirmAE的根目录images
中,先前是将压缩文件系统复制到了固件目录scratch/固件编号/
中,因此解压完毕之后,需要将固件工作目录中的压缩文件系统删除。
1 2 3 echo "----Extracting Filesystem Tarball----" tar -xf "${WORK_DIR}/$IID.tar.gz" -C "${IMAGE_DIR}" rm "${WORK_DIR}/${IID}.tar.gz"
关键的执行命令如下:
1 2 mount /dev/loop9p1 /home/utest/app/FirmAE/scratch/3/image/ tar -xf /home/utest/app/FirmAE/scratch/3/3.tar.gz -C /home/utest/app/FirmAE/scratch/3/image/
至此,将相当于将固件中提取出来的文件系统给复制到了磁盘镜像中。
修改文件系统 在磁盘镜像中创建firmadyne
相关的目录。FirmAE是在FirmAdyne的基础上进行二次开发的,因此沿用了后者的一些设计逻辑。如果是使用debug模式启动的FirmAE,可以进行模拟器的shell中,查看firmadyne
中实际上包含了许多有用的工具和脚本,例如gdbserver
、gdb
、strace
和完整版的busybox
等。
1 2 3 mkdir "${IMAGE_DIR}/firmadyne/" mkdir "${IMAGE_DIR}/firmadyne/libnvram/" mkdir "${IMAGE_DIR}/firmadyne/libnvram.override/"
inferFile.sh 随后,将一个静态编译的bash-static
复制到image
目录中,例如cp /usr/bin/bash-static /home/utest/app/FirmAE/scratch/3/image/
,然后在宿主机上使用chroot
执行inferFile.sh
,例如chroot /home/utest/app/FirmAE/scratch/3/image/ /bash-static /inferFile.sh
。inferFile.sh
的主要功能就是分析固件的文件系统中所包含的启动程序和http server。例如寻找启动程序preinitMT
、preinit
、rcS
等等,并将得到的结果写入到/firmadyne/init
中,寻找http serveruhttpd
、httpd
、goahead
、alphapd
、boa
、lighttpd
。这些都是IoT设备中常见的http server,如果在仿真模拟一个设备失败的时候不妨检查一下设备固件中的http server是否包含在其中。如下是inferFile.sh
的代码,逻辑还是比较简单。
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 BUSYBOX="/busybox" $ {BUSYBOX} touch /firmadyne/init if (${FIRMAE_BOOT}); then arr=() if [ -e /kernelInit ]; then for FILE in `${BUSYBOX} strings ./kernelInit` do FULL_PATH=`${BUSYBOX} echo ${FILE} | ${BUSYBOX} awk '{split($0,a,"="); print a[2]}'` arr+=("${FULL_PATH}") done fi # kernel not handle this program if [ -e /init ]; then if [ ! -d /init ]; then arr+=(/init) fi fi for FILE in `${BUSYBOX} find / -name "preinitMT" -o -name "preinit" -o -name "rcS"` do arr+=(${FILE}) done if (( ${#arr[@]} )); then # convert to the unique array following the original order uniq_arr=($(${BUSYBOX} tr ' ' '\n' <<< "${arr[@]}" | ${BUSYBOX} awk '!u[$0]++' | ${BUSYBOX} tr '\n' ' ')) for FILE in "${uniq_arr[@]}" do if [ -d ${FILE} ]; then continue fi if [ ! -e ${FILE} ]; then # can't found original file (symbolic link or just file) if [ -h ${FILE} ]; then # remove old symbolic link ${BUSYBOX} rm ${FILE} fi # find original program from binary directories FILE_NAME=`${BUSYBOX} basename ${FILE}` if (${BUSYBOX} find /bin /sbin /usr/sbin /usr/sbin -type f -exec ${BUSYBOX} grep -qr ${FILE_NAME} {} \;); then TARGET_FILE=`${BUSYBOX} find /bin /sbin /usr/sbin /usr/sbin -type f -exec ${BUSYBOX} egrep -rl ${FILE_NAME} {} \; | ${BUSYBOX} head -1` ${BUSYBOX} ln -s ${TARGET_FILE} ${FILE} else continue fi fi if [ -e ${FILE} ]; then ${BUSYBOX} echo ${FILE} >> /firmadyne/init fi done fi fi $ {BUSYBOX} echo '/firmadyne/preInit.sh' >> /firmadyne/init if (${FIRMAE_ETC}); then if [ -e /etc/init.d/uhttpd ]; then echo -n "/etc/init.d/uhttpd start" > /firmadyne/service echo -n "uhttpd" > /firmadyne/service_name elif [ -e /usr/bin/httpd ]; then echo -n "/usr/bin/httpd" > /firmadyne/service echo -n "httpd" > /firmadyne/service_name elif [ -e /usr/sbin/httpd ]; then echo -n "/usr/sbin/httpd" > /firmadyne/service echo -n "httpd" > /firmadyne/service_name elif [ -e /bin/goahead ]; then echo -n "/bin/goahead" > /firmadyne/service echo -n "goahead" > /firmadyne/service_name elif [ -e /bin/alphapd ]; then echo -n "/bin/alphapd" > /firmadyne/service echo -n "alphapd" > /firmadyne/service_name elif [ -e /bin/boa ]; then echo -n "/bin/boa" > /firmadyne/service echo -n "boa" > /firmadyne/service_name elif [ -e /usr/sbin/lighttpd ]; then # for Ubiquiti firmwares echo -n "/usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf" > /firmadyne/service echo -n "lighttpd" > /firmadyne/service_name fi fi
fixImage.sh 随后继续按照类似的逻辑chroot
执行fixImage.sh
,例如chroot /home/utest/app/FirmAE/scratch/3/image/ /busybox ash /fixImage.sh
。fixImage.sh
的主要功能是对镜像中的文件系统进行修补。 将静态编译的busybox
复制到镜像的文件系统中:
1 2 3 4 5 if (${FIRMAE_BOOT}); then if [ ! -e /bin/sh ]; then ${BUSYBOX} ln -s /firmadyne/busybox /bin/sh fi $ {BUSYBOX} ln -s /firmadyne/busybox /firmadyne/sh
在FirmAE论文中提到,使用FirmAdyne仿真模拟失败很大一部分原因是没有创建相应的文件夹例如/proc
、/tmp
、/var
等等,此处就手动创建常见的文件夹。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mkdir -p "$(resolve_link /proc)" mkdir -p "$(resolve_link /dev/pts)" mkdir -p "$(resolve_link /etc_ro)" mkdir -p "$(resolve_link /tmp)" mkdir -p "$(resolve_link /var)" mkdir -p "$(resolve_link /run)" mkdir -p "$(resolve_link /sys)" mkdir -p "$(resolve_link /root)" mkdir -p "$(resolve_link /tmp/var)" mkdir -p "$(resolve_link /tmp/media)" mkdir -p "$(resolve_link /tmp/etc)" mkdir -p "$(resolve_link /tmp/var/run)" mkdir -p "$(resolve_link /tmp/home/root)" mkdir -p "$(resolve_link /tmp/mnt)" mkdir -p "$(resolve_link /tmp/opt)" mkdir -p "$(resolve_link /tmp/www)" mkdir -p "$(resolve_link /var/run)" mkdir -p "$(resolve_link /var/lock)" mkdir -p "$(resolve_link /usr/bin)" mkdir -p "$(resolve_link /usr/sbin)"
初次之外,还会分析应用程序中所依赖的目录,并进行创建:
1 2 3 4 5 6 7 8 9 for FILE in `${BUSYBOX} find /bin /sbin /usr/bin /usr/sbin -type f -perm -u+x -exec ${BUSYBOX} strings {} \; | ${BUSYBOX} egrep "^(/var|/etc|/tmp)(.+)\/([^\/]+)$"` do DIR=`${BUSYBOX} dirname "${FILE}"` if (! ${BUSYBOX} echo "${DIR}" | ${BUSYBOX} egrep -q "(%s|%c|%d|/tmp/services)");then ${BUSYBOX} echo "${DIR}" >> /firmadyne/dir_log mkdir -p "$(resolve_link ${DIR})" fi done fi
同样也会在/etc
目录创建一些必要的文件,例如时区TZ、host文件、passwd。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mkdir -p "$(resolve_link /etc)" if [ ! -s /etc/TZ ]; then mkdir -p "$(dirname $(resolve_link /etc/TZ))" echo "EST5EDT" > "$(resolve_link /etc/TZ)" fi if [ ! -s /etc/hosts ]; then mkdir -p "$(dirname $(resolve_link /etc/hosts))" echo "127.0.0.1 localhost" > "$(resolve_link /etc/hosts)" fi if [ ! -s /etc/passwd ]; then mkdir -p "$(dirname $(resolve_link /etc/passwd))" echo "root::0:0:root:/root:/bin/sh" > "$(resolve_link /etc/passwd)" fi
创建/dev
目录下的一些设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 mkdir -p "$(resolve_link /dev)" FILECOUNT="$($BUSYBOX find /dev -maxdepth 1 -type b -o -type c -print | $BUSYBOX wc -l)" if [ $FILECOUNT -lt "5" ]; then echo "Warning: Recreating device nodes!" if (${FIRMAE_ETC}); then TMP_BUSYBOX="/busybox" else TMP_BUSYBOX="" fi ${TMP_BUSYBOX} mknod -m 660 /dev/mem c 1 1 ${TMP_BUSYBOX} mknod -m 640 /dev/kmem c 1 2 ${TMP_BUSYBOX} mknod -m 666 /dev/null c 1 3 ${TMP_BUSYBOX} mknod -m 666 /dev/zero c 1 5 ${TMP_BUSYBOX} mknod -m 444 /dev/random c 1 8 ${TMP_BUSYBOX} mknod -m 444 /dev/urandom c 1 9 ${TMP_BUSYBOX} mknod -m 666 /dev/armem c 1 13 ......
添加实用工具到文件系统 这一步则主要是将针对目标架构静态编译的实用工具和实用脚本添加到/firmadyne
目录中。如下,先判断目标系统的架构,然后复制静态编译的实用程序到/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 26 27 28 29 for BINARY_NAME in "${BINARIES[@]}" do BINARY_PATH=`get_binary ${BINARY_NAME} ${ARCH}` cp "${BINARY_PATH}" "${IMAGE_DIR}/firmadyne/${BINARY_NAME}" chmod a+x "${IMAGE_DIR}/firmadyne/${BINARY_NAME}" done mknod -m 666 "${IMAGE_DIR}/firmadyne/ttyS1" c 4 65 cp "${SCRIPT_DIR}/preInit.sh" "${IMAGE_DIR}/firmadyne/preInit.sh" chmod a+x "${IMAGE_DIR}/firmadyne/preInit.sh" cp "${SCRIPT_DIR}/network.sh" "${IMAGE_DIR}/firmadyne/network.sh" chmod a+x "${IMAGE_DIR}/firmadyne/network.sh" cp "${SCRIPT_DIR}/run_service.sh" "${IMAGE_DIR}/firmadyne/run_service.sh" chmod a+x "${IMAGE_DIR}/firmadyne/run_service.sh" cp "${SCRIPT_DIR}/injectionChecker.sh" "${IMAGE_DIR}/bin/a" chmod a+x "${IMAGE_DIR}/bin/a" touch "${IMAGE_DIR}/firmadyne/debug.sh" chmod a+x "${IMAGE_DIR}/firmadyne/debug.sh" if (! ${FIRMAE_ETC}); then sed -i 's/sleep 60/sleep 15/g' "${IMAGE_DIR}/firmadyne/network.sh" sed -i 's/sleep 120/sleep 30/g' "${IMAGE_DIR}/firmadyne/run_service.sh" sed -i 's@/firmadyne/sh@/bin/sh@g' ${IMAGE_DIR}/firmadyne/{preInit.sh,network.sh,run_service.sh} sed -i 's@BUSYBOX=/firmadyne/busybox@BUSYBOX=@g' ${IMAGE_DIR}/firmadyne/{preInit.sh,network.sh,run_service.sh} fi
解除挂载 通过将镜像文件挂载到目录,写入文件系统,修改文件系统完毕后,则解除挂载。然后重新挂载镜像文件,并使用e2fsck
进行检查,最后解除挂载。
1 2 3 4 5 6 7 8 9 10 echo "----Unmounting QEMU Image----" sync umount "${IMAGE_DIR}" del_partition ${DEVICE:0:$((${#DEVICE}-2))} DEVICE=`add_partition ${IMAGE}` e2fsck -y ${DEVICE} sync sleep 1 del_partition ${DEVICE:0:$((${#DEVICE}-2))}
至此,将固件中的文件系统写入到磁盘镜像并修改相关文件以提高仿真成功率的工作完毕,得到一个qemu可使用的磁盘镜像。 那么回归到我之前的问题,如何在FirmAE仿真过程中修改固件的文件系统,此时就有如下的方案:
第一种方案是在文件系统从固件提取的过程中进行修改:
在extractor.py
提取文件系统成功后,暂停,解压并修改文件系统,然后重新打包成压缩包形式
如果要http server不启动的话,则需要修改相关的判定标志,使得web检查通过或者取消web检查改为使用者手动启动http server
第二种方案是在创建磁盘镜像后,对磁盘镜像中的文件系统进行修改:
将磁盘镜像进行手动挂载到宿主机,然后进行修改
同样修改完毕后,如果不想http server启动,也需要修改源码中对web检查的部分。