OneShell

I fight for a brighter tomorrow

0%

FirmAE:网络仿真配置

在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/mtab

mkdir -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
# 开始分析qemu虚拟机首次启动的日志
data = open("%s/qemu.initial.serial.log" % targetDir, 'rb').read()

# 寻找开放端口
ports = findPorts(data, endianness)

#find interfaces with non loopback ip addresses
ifacesWithIps = findNonLoInterfaces(data, endianness)
#find changes of mac addresses for devices
# 寻找MAC地址的变化
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)
# 重新生成了QEMU命令
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服务可以自己连接到虚拟机上去手动启动。