在run.sh中,FirmAE会检查当前固件是否在之前仿真成功过,如果是第一次仿真或者是之前仿真失败,FirmAE会重新开始创建镜像、生成qemu启动网络配置的工作;如果之前已经仿真成功了,则直接执行之前的启动命令。
run.sh:是否之前仿真成功 如下是关键代码,其中${WORK_DIR}
目录是工作目录,对应着实际的目录scratch/固件编号/
目录。${WORK_DIR}/web
是web仿真成功的标志文件,FirmAE仿真成功一个目录则会在其中写入true
。./scripts/makeImage.sh
是创建qemu镜像、将文件系统写入到镜像、并对文件系统做修改的脚本;./scripts/makeNetwork.py
则是负责生成qemu运行命令、配置qemu启动命令的参数,也是这篇文章将要简单说明的。
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 if (! egrep -sqi "true" ${WORK_DIR}/web); then # ================================ # make qemu image # ================================ t_start="$(date -u +%s.%N)" # 查询数据库 ./scripts/tar2db.py -i $IID -f ./images/$IID.tar.gz -h $PSQL_IP \ 2>&1 > ${WORK_DIR}/tar2db.log t_end="$(date -u +%s.%N)" time_tar="$(bc <<<"$t_end-$t_start")" echo $time_tar > ${WORK_DIR}/time_tar t_start="$(date -u +%s.%N)" # 制作qemu镜像 ./scripts/makeImage.sh $IID $ARCH $FILENAME \ 2>&1 > ${WORK_DIR}/makeImage.log t_end="$(date -u +%s.%N)" time_image="$(bc <<<"$t_end-$t_start")" echo $time_image > ${WORK_DIR}/time_image # ================================ # infer network interface # ================================ t_start="$(date -u +%s.%N)" echo "[*] infer network start!!!" # TIMEOUT is set in "firmae.config". This TIMEOUT is used for initial # log collection. TIMEOUT=$TIMEOUT FIRMAE_NET=${FIRMAE_NET} \ ./scripts/makeNetwork.py -i $IID -q -o -a ${ARCH} \ &> ${WORK_DIR}/makeNetwork.log # run_debug.sh等实际上都是./run.sh的软连接 ln -s ./run.sh ${WORK_DIR}/run_debug.sh | true ln -s ./run.sh ${WORK_DIR}/run_analyze.sh | true ln -s ./run.sh ${WORK_DIR}/run_boot.sh | true t_end="$(date -u +%s.%N)" time_network="$(bc <<<"$t_end-$t_start")" echo $time_network > ${WORK_DIR}/time_network else # 如果之前仿真成功过则直接仿真 echo "[*] ${INFILE} already succeed emulation!!!" fi
makeNetwork.py:生成最终的qemu启动命令 makeNetwork.py
是进行网络处理的python脚本,里面大概包含了首次通过命令启动qemu虚拟机、然后分析qemu虚拟机的启动日志、生成新的启动参数、通过新的qemu命令再次启动虚拟机,并检查虚拟机的web服务器启动状况。
makeNetwork.py
中调用的关键函数如下:
1 2 3 4 main -> process -> inferNetwork -> checkNetwork
inferNetwork函数:首次启动QEMU虚拟机并分析启动日志 1. 首次启动虚拟机 inferNetwork
函数会重新挂载qemu磁盘,获取磁盘文件系统中的一些启动服务,在文件系统中修改preInit.sh
脚本,并生成QEMU启动命令。QEMU启动命令会增加rdinit=/firmadyne/preInit.sh
参数,使得虚拟机启动后会首先去执行该脚本。
1 2 3 4 5 6 7 8 9 10 11 12 print ("Running firmware %d: terminating after %d secs..." % (iid, TIMEOUT))cmd = "timeout --preserve-status --signal SIGINT {0} " .format (TIMEOUT) cmd += "{0}/run.{1}.sh \"{2}\" \"{3}\" " .format (SCRIPTDIR, arch + endianness, iid, qemuInitValue) cmd += " 2>&1 > /dev/null" with open (SCRATCHDIR + "/" + str (iid) + "/qemu.init.cmd" , "w+" ) as out: out.write(cmd) os.system(cmd)
首次执行的qemu虚拟机启动命令如下,这次仿真所消耗的时间就是6分钟:
1 timeout --preserve-status --signal SIGINT 240 /home/utest/app/FirmAE/scripts/run.mipseb.sh "3" "rdinit=/firmadyne/preInit.sh" 2>&1 > /dev/null
我从一个已经启动的虚拟机中查看脚本内容如下:
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 #!/firmadyne/sh BUSYBOX=/firmadyne/busybox [ -d /dev ] || mkdir -p /dev [ -d /root ] || mkdir -p /root [ -d /sys ] || mkdir -p /sys [ -d /proc ] || mkdir -p /proc [ -d /tmp ] || mkdir -p /tmp mkdir -p /var/lock${BUSYBOX} mount -t sysfs sysfs /sys${BUSYBOX} mount -t proc proc /proc${BUSYBOX} ln -sf /proc/mounts /etc/mtabmkdir -p /dev/pts${BUSYBOX} mount -t devpts devpts /dev/pts${BUSYBOX} mount -t tmpfs tmpfs /run/sbin/init & /firmadyne/network.sh & /firmadyne/run_service.sh & /firmadyne/debug.sh /firmadyne/busybox sleep 36000
可以看到脚本会创建一些必备的目录以提高仿真生成率(来自FirmAE论文,有数据证实),并挂载一些设备。然后执行文件系统中的/sbin/init
,这个或许在真实设备中是首次执行的程序。最后会运行内置的一些脚本,启动debug.sh
是FirmAE较FirmAdyne所没有的。还有一个sleep命令,应该是为了等待启动成功。
2. 分析启动日志 虚拟机首次执行是有时间限制的,时间到后关闭虚拟机。随后inferNetwork
函数会分析qemu启动日志,从中得到例如开放端口、IP、MAC地址改变等信息,然后返回。FirmAE的内核源码是被修改过的,对一些关键系统调用做了hook,因此是可以在qemu启动日志中得到许多信息。这些信息随后会辅助生成最终的qemu启动命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 data = open ("%s/qemu.initial.serial.log" % targetDir, 'rb' ).read() ports = findPorts(data, endianness) ifacesWithIps = findNonLoInterfaces(data, endianness) macChanges = findMacChanges(data, endianness) print ('[*] Interfaces: %r' % ifacesWithIps)networkList = getNetworkList(data, ifacesWithIps, macChanges) return qemuInitValue, networkList, targetFile, targetData, ports
checkNetwork函数 继续返回到process
函数中,接下来会调用checkNetwork
函数。该函数的主要功能是从inferNetwork
函数提取的日志信息networkList
中分析虚拟机的网络类型。
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 if vlanNetworkList: print ("has vlan ethernet" ) filterNetworkList = vlanNetworkList result = "normal" elif ethNetworkList: print ("has ethernet" ) filterNetworkList = ethNetworkList result = "normal" elif invalidEthNetworkList: print ("has ethernet and invalid IP" ) for (ip, dev, vlan, mac, brif) in invalidEthNetworkList: filterNetworkList.append(('192.168.0.1' , dev, vlan, mac, brif)) result = "reload" elif brNetworkList: print ("only has bridge interface" ) for (ip, dev, vlan, mac, brif) in brNetworkList: if devList: dev = devList.pop(0 ) filterNetworkList.append((ip, dev, vlan, mac, brif)) result = "bridge" elif invalidBrNetworkList: print ("only has bridge interface and invalid IP" ) for (ip, dev, vlan, mac, brif) in invalidBrNetworkList: if devList: dev = devList.pop(0 ) filterNetworkList.append(('192.168.0.1' , dev, vlan, mac, brif)) result = "bridgereload"
test_emulation.sh:第二次启动虚拟机并分析网络仿真结果 通过首次仿真的结果,我们可以得到一系列的信息,例如网络列表、端口等等。这些信息将用于生成最终的qemu启动命令。如下还是process
函数中,根据首次仿真的日志得到一些关键信息,用于生成最后的仿真命令qemuCommandLine
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 qemuCommandLine = qemuCmd(iid, filterNetworkList, ports, network_type, arch, endianness, qemuInitValue, isUserNetwork) with open (outfile, "w" ) as out: out.write(qemuCommandLine) os.chmod(outfile, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH) os.system('./scripts/test_emulation.sh {} {}' .format (iid, arch + endianness)) if (os.path.exists(SCRATCHDIR + '/' + str (iid) + '/web' ) and open (SCRATCHDIR + '/' + str (iid) + '/web' ).read().strip() == 'true' ): success = True break
随后调用test_emulation.sh
去执行这条仿真命令,该虚拟机启动命令在后台执行:
1 2 3 4 echo "[*] test emulator" $ {WORK_DIR}/run.sh 2>&1 >${WORK_DIR} /emulation.log & sleep 10
脚本还会调用check_network
函数检查虚拟机的网络仿真状态,主要是通过ping和访问web服务端口来判断:
1 2 3 4 5 6 7 8 9 10 11 12 echo -e "[*] Waiting web service... from ${IPS[@]}" read IP PING_RESULT WEB_RESULT TIME_PING TIME_WEB < <(check_network "${IPS[@]}" false) if (${PING_RESULT}); then echo true > ${WORK_DIR}/ping echo ${TIME_PING} > ${WORK_DIR}/time_ping echo ${IP} > ${WORK_DIR}/ip fi if (${WEB_RESULT}); then echo true > ${WORK_DIR}/web echo ${TIME_WEB} > ${WORK_DIR}/time_web fi
check_network
函数的代码如下,循环通过ping判断虚拟机是否存活,以及通过curl判断WEB服务是否启动起来,然后写入到固件工作目录的状态文件:${WORK_DIR}/ping
和${WORK_DIR}/web
中。
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 check_network () { sleep 10 IPS=("${@}") DEBUG_MODE=${IPS[-1]} unset 'IPS[${#IPS[@]}-1]' PING_RESULT=false PING_TIME=-1 WEB_RESULT=false WEB_TIME=-1 RET_IP="None" START_TIME=$(date +%s | bc) CURRENT_TIME=$(date +%s | bc) t_start=$(date +%s.%N) while [ ${CURRENT_TIME} -le $[${START_TIME} + ${CHECK_TIMEOUT}] ] do for IP in "${IPS[@]}" do if (curl --max-time 2 --output /dev/null --silent http://${IP} || curl --max-time 2 --output /dev/null --silent https://${IP}); then t_end=$(date +%s.%N) if (! ${WEB_RESULT}); then WEB_TIME=$(echo "$t_end - $t_start" | bc) fi if (! ${PING_RESULT}); then PING_TIME=${WEB_TIME} fi PING_RESULT=true WEB_RESULT=true RET_IP=${IP} fi if (ping -c 1 ${IP} > /dev/null); then t_end=$(date +%s.%N) if (! ${PING_RESULT}); then PING_TIME=$(echo "$t_end - $t_start" | bc) fi PING_RESULT=true RET_IP=${IP} fi sleep 1 CURRENT_TIME=$(date +%s | bc) done if (${WEB_RESULT}); then break fi done echo "${RET_IP}" "${PING_RESULT}" "${WEB_RESULT}" "${PING_TIME}" "${WEB_TIME}" }
至此,关键脚本makeNetwork.py
分析完成。
小结 以前在看FirmAE论文的时候,论文中强调仿真采用了启发式的分析方法,其实这个启发式主要就是对固件的文件系统和网络进行分析,然后进行相应的patch。
对于文件系统,FirmAE会分析其中的web服务、服务程序中所需的文件和文件夹和设备、然后生成脚本在启动时创建相应的文件和文件夹,挂载相应的设备。对于网络配置,FirmAE会在分析阶段启动两次qemu虚拟机。第一次是为了获取到网络配置信息,然后生成新的qemu启动命令;第二次是为了判断虚拟机是否被启动、web服务是否被启动。
综合来说,FirmAE的时间消耗大头是在网络配置上,要是不顺利的话,第一次网络启动会默认消耗6分钟、第二次也会消耗6分钟。而且,FirmAE真正启动还会再执行一次qemu虚拟机的启动,也就是说,从固件到仿真成功,一共需要执行三次qemu虚拟机。
出于个人需求,FirmAE对我来说在判断逻辑上还可以改改。例如有时候WEB服务着实启动条件比较苛刻,需要对WEB程序进行patch,这种场景下我们实际上只想让FirmAE快速搭建起来一个qemu虚拟机,在网络判定的时候ping能够ping通就行,web服务可以自己连接到虚拟机上去手动启动。