FirmAE源码粗读(四)
文章目录
- 简介
- 参考阅读
- QEMUCMDTEMPLATE
- process
- inferNetwork
- preInit.sh &run_service.sh
- inferDefault.py
- find系列函数
- getNetworkList
- buildconfig
- checknetwork
- qemuCmd
- startNetwork & stopNetwork
- qemuNetworkConfig & qemuArchNetworkConfig
- test_emulation.sh
- 感言
简介
大家好啊,我是你们的周更 啊不 月更 错误的 年更up主xyzmpv,欢迎一键三连,点赞数超过100写下一篇
咳咳咳,第四篇力,今天要端上来的是谁呢,让我康康…
Makenetork.py,就决定是你了!堂堂连载!
————————————————————————————————————————
以上颠佬发言请自行无视…
本周给大家带来的是makeNetwork.py
的分析,主要涉及qemu模拟与网络构建部分。这里也包括了论文中重要的预模拟部分,有不少函数就是用于执行预模拟以及用于从预模拟的log中提取信息。
该脚本中依然有不少类似语法糖的命令封装函数,例如mount/umountImage、checkVariable、stripTimestamps
等。
至于其他较为重要的函数,依然按照惯例,一个个进行介绍。
参考阅读
redhat的network interface介绍
openwrt developer guide的network interface介绍(重要)
FirmAE论文节译(重点在其中的网络仲裁)
qemu网络wiki
QEMUCMDTEMPLATE
预制菜
一个以字符串形式存储的,用于启动qemu模拟的sh脚本。总体上没啥新东西,只有写入/firmadyne/debug.sh
中的反连命令以及结尾的qemu模拟命令行需要注意。因为是字符串存储,所以使用了格式化字符串以便于在不同情况下使用。
qemu命令行参数可以去查阅qemu官方文档(需要去对应条目下查询),qemu模拟中的kernel cmdline的含义参见kernel官方文档以及金步国前辈的远古翻译
(反连命令中nc
带循环,telnet
没有,可能这也是为什么debug
模式下nc
容易连上而telnet
容易连不上,不过二者用的都是firmadyne
的新busybox
,按理不至于出问题啊)
#!/bin/bash
set -e
set -u
ARCHEND=%(ARCHEND)s
IID=%(IID)i
if [ -e ./firmae.config ]; then
source ./firmae.config
elif [ -e ../firmae.config ]; then
source ../firmae.config
elif [ -e ../../firmae.config ]; then
source ../../firmae.config
else
echo "Error: Could not find 'firmae.config'!"
exit 1
fi
RUN_MODE=`basename ${0}`
IMAGE=`get_fs ${IID}`
if (echo ${ARCHEND} | grep -q "mips" && echo ${RUN_MODE} | grep -q "debug"); then
KERNEL=`get_kernel ${ARCHEND} true`
else
KERNEL=`get_kernel ${ARCHEND} false`
fi
if (echo ${RUN_MODE} | grep -q "analyze"); then
QEMU_DEBUG="user_debug=31 firmadyne.syscall=32"
else
QEMU_DEBUG="user_debug=0 firmadyne.syscall=1"
fi
if (echo ${RUN_MODE} | grep -q "boot"); then
QEMU_BOOT="-s -S"
else
QEMU_BOOT=""
fi
QEMU=`get_qemu ${ARCHEND}`
QEMU_MACHINE=`get_qemu_machine ${ARCHEND}`
QEMU_ROOTFS=`get_qemu_disk ${ARCHEND}`
WORK_DIR=`get_scratch ${IID}`
DEVICE=`add_partition "${WORK_DIR}/image.raw"`
mount ${DEVICE} ${WORK_DIR}/image > /dev/null
echo "%(NETWORK_TYPE)s" > ${WORK_DIR}/image/firmadyne/network_type
echo "%(NET_BRIDGE)s" > ${WORK_DIR}/image/firmadyne/net_bridge
echo "%(NET_INTERFACE)s" > ${WORK_DIR}/image/firmadyne/net_interface
echo "#!/firmadyne/sh" > ${WORK_DIR}/image/firmadyne/debug.sh
if (echo ${RUN_MODE} | grep -q "debug"); then
echo "while (true); do /firmadyne/busybox nc -lp 31337 -e /firmadyne/sh; done &" >> ${WORK_DIR}/image/firmadyne/debug.sh
echo "/firmadyne/busybox telnetd -p 31338 -l /firmadyne/sh" >> ${WORK_DIR}/image/firmadyne/debug.sh
fi
#nc和telnet反连的代码,本质上使用的是firmadyne自带的busybox进行反连
chmod a+x ${WORK_DIR}/image/firmadyne/debug.sh
sleep 1
sync
umount ${WORK_DIR}/image > /dev/null
del_partition ${DEVICE:0:$((${#DEVICE}-2))}
%(START_NET)s
echo -n "Starting emulation of firmware... "
%(QEMU_ENV_VARS)s ${QEMU} ${QEMU_BOOT} -m 1024 -M ${QEMU_MACHINE} -kernel ${KERNEL} \\
%(QEMU_DISK)s -append "root=${QEMU_ROOTFS} console=ttyS0 nandsim.parts=64,64,64,64,64,64,64,64,64,64 %(QEMU_INIT)s rw debug ignore_loglevel print-fatal-signals=1 FIRMAE_NET=${FIRMAE_NET} FIRMAE_NVRAM=${FIRMAE_NVRAM} FIRMAE_KERNEL=${FIRMAE_KERNEL} FIRMAE_ETC=${FIRMAE_ETC} ${QEMU_DEBUG}" \\
-serial file:${WORK_DIR}/qemu.final.serial.log \\
-serial unix:/tmp/qemu.${IID}.S1,server,nowait \\
-monitor unix:/tmp/qemu.${IID},server,nowait \\
-display none \\
%(QEMU_NETWORK)s | true
#模拟命令行,-serial是将虚拟串口输出重定向,这里重定向至文件,输出与开机过程中硬件串口输出类似,都是BOOT LOG
#重定向monitor,ignore_loglevel print-fatal-signals=1是为了输出所有内核日志,便于排查模拟信息
#nandsim 虚拟nand设备
#注意这里没有& 也就是qemu相当于运行在前台,脚本会停在这里,直到qemu停止才会继续运行
%(STOP_NET)s
echo "Done!"
process
脚手架函数,看个启动流程好了。基本上就是预模拟(使用原始内核进行模拟)——获取网络信息————修补镜像、创建网络配置参数——格式化qemu命令行——启动测试模拟并进行检查。函数的outfile
一般为工作目录下的run.sh
,由QEMUCMDTEMPLATE
格式化而来,这一文件被test_emulation.sh
启动,用于测试与最终模拟(最终模拟是在主目录下的run.sh
中调用这里生成的工作目录下的run.sh
),test_emulation.sh
输出重定向到emulation.log
,最终模拟的qemu串口信息为工作目录下的qemu.final.serial.log
不过由于test_emulation.sh
也会调用工作目录下的run.sh
,会导致qemu.final.serial.log
提前生成,不过会被最终模拟的输出覆盖)。
所有的推断过程基本都是基于预模拟生成的qemu.initial.serial.log
(包括网络和nvram
).
注意其中的for循环,也就是会针对每一个init
进行预模拟与真实模拟。
def process(iid, arch, endianness, makeQemuCmd=False, outfile=None):
success = False
global SCRIPTDIR
global SCRATCHDIR
for init in open(SCRATCHDIR + "/" + str(iid) + "/init").read().split('\n')[:-1]:
with open(SCRATCHDIR + "/" + str(iid) + "/current_init", 'w') as out:
out.write(init)
qemuInitValue, networkList, targetFile, targetData, ports = inferNetwork(iid, arch, endianness, init)
'''遍历init列表中的每一项作为init进行预模拟,以生成log,寻找网络信息'''
print("[*] ports: %r" % ports)
# check network interfaces and add script in the file system
# return the fixed network interface
print("[*] networkInfo: %r" % networkList)
filterNetworkList, network_type = checkNetwork(networkList)
'''检查、修复网络信息'''
print("[*] filter network info: %r" % filterNetworkList)
# filter ip
# some firmware uses multiple network interfaces for one bridge
# netgear WNDR3400v2-V1.0.0.54_1.0.82.zip - check only one IP
# asus FW_RT_AC87U_300438250702
# [('192.168.1.1', 'eth0', None, None), ('169.254.39.3', 'eth1', None, None), ('169.254.39.1', 'eth2', None, None), ('169.254.39.166', 'eth3', None, None)]
'''上面是filterNetworkList的格式,基本就是提取出来的网络信息,下面遍历并存储这些信息'''
if filterNetworkList:
ips = [ip for (ip, dev, vlan, mac, brif) in filterNetworkList]
ips = set(ips)
with open(SCRATCHDIR + "/" + str(iid) + "/ip_num", 'w') as out:
out.write(str(len(ips)))
'''网络接口绑定的ip数'''
for idx, ip in enumerate(ips):
with open(SCRATCHDIR + "/" + str(iid) + "/ip." + str(idx), 'w') as out:
out.write(str(ip))
'''网络接口'''
isUserNetwork = any(isDhcpIp(ip) for ip in ips)
'''判断DHCP,这里是用ip地址判断的,开头10.0.2或结尾.190'''
with open(SCRATCHDIR + "/" + str(iid) + "/isDhcp", "w") as out:
if isUserNetwork:
out.write("true")
else:
out.write("false")
qemuCommandLine = qemuCmd(iid,
filterNetworkList,
ports,
network_type,
arch,
endianness,
qemuInitValue,
isUserNetwork)
'''利用信息格式化qemutemplate,得到真正的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
'''进行测试模拟,如果成功则直接返回'''
# restore infer network data
# targetData is '' when init is preInit.sh
if targetData != '':
targetDir = SCRATCHDIR + '/' + str(iid)
loopFile = mountImage(targetDir)
with open(targetFile, 'w') as out:
out.write(targetData)
umountImage(targetDir, loopFile)
return success
inferNetwork
预模拟+网络推断函数,用于生成内核BOOT log,以提取网络配置信息。关于kernel的init
过程可以看这篇和这篇,都是比较简略的介绍。
预模拟的log存在qemu.initial.serial.log
中,而真实模拟的log存在qemu.final.serial.log
中。
似乎firmdyne/FirmAE
没有特别分析linuxrc
,怪哉(可能是因为不需要)。
这个预模拟函数中有run.sh
设置的TIMEOUT
限制
def inferNetwork(iid, arch, endianness, init):
global SCRIPTDIR
global SCRATCHDIR
TIMEOUT = int(os.environ['TIMEOUT'])
targetDir = SCRATCHDIR + '/' + str(iid)
loopFile = mountImage(targetDir)
fileType = subprocess.check_output(["file", "-b", "%s/image/%s" % (targetDir, init)]).decode().strip()
print("[*] Infer test: %s (%s)" % (init, fileType))
'''检查init文件类型'''
with open(targetDir + '/image/firmadyne/network_type', 'w') as out:
out.write("None")
qemuInitValue = 'rdinit=/firmadyne/preInit.sh'
if os.path.exists(targetDir + '/service'):
webService = open(targetDir + '/service').read().strip()
else:
webService = None
'''获取之前提取的service列表'''
print("[*] web service: %s" % webService)
targetFile = ''
targetData = ''
out = None
if not init.endswith('preInit.sh'): # rcS, preinit
if fileType.find('ELF') == -1 and fileType.find("symbolic link") == -1: # maybe script
'''init为脚本文件,此时'''
targetFile = targetDir + '/image/' + init
targetData = readWithException(targetFile)
out = open(targetFile, 'a')
# netgear R6200
elif fileType.find('ELF') != -1 or fileType.find("symbolic link") != -1:
'''init为ELF或符号链接'''
qemuInitValue = qemuInitValue[2:] # remove 'rd'
targetFile = targetDir + '/image/firmadyne/preInit.sh'
targetData = readWithException(targetFile)
out = open(targetFile, 'a')
out.write(init + ' &\n')
else: # preInit.sh
out = open(targetDir + '/image/firmadyne/preInit.sh', 'a')
if out:
out.write('\n/firmadyne/network.sh &\n')
if webService:
out.write('/firmadyne/run_service.sh &\n')
out.write('/firmadyne/debug.sh\n')
# trendnet TEW-828DRU_1.0.7.2, etc...
out.write('/firmadyne/busybox sleep 36000\n')
out.close()
'''处理逻辑大概是如果init为脚本,直接在原始init里面追加firmadyne的工具脚本;
如果init是ELF文件或符号链接,在/firmadyne/preInit.sh里面补充上init路径后再补充firmadyne的工具脚本;
如果init=preInit.sh,直接在/firmadyne/preInit.sh上追加工具脚本。
(因为这个preInit.sh是之前inferFile.sh加的,所以不用管)
另一个点是在init为脚本或ELF的情况下,使用的初始命令行不同。论文里面提到这是为了在init执行前挂载文件系统,以适应固件操作(也就是可能有多个init,在不同启动阶段生效)
'''
umountImage(targetDir, loopFile)
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"
os.system(cmd)
'''预模拟,使用的init为调整完的/firmadyne/preInit.sh,预模拟脚本也在script目录下'''
loopFile = mountImage(targetDir)
if not os.path.exists(targetDir + '/image/firmadyne/nvram_files'):
print("Infer NVRAM default file!\n")
os.system("{}/inferDefault.py {}".format(SCRIPTDIR, iid))
'''推测nvram的值'''
umountImage(targetDir, loopFile)
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
macChanges = findMacChanges(data, endianness)
print('[*] Interfaces: %r' % ifacesWithIps)
'''寻找端口、网络接口+绑定的Ip、分配给网络接口的MAC'''
networkList = getNetworkList(data, ifacesWithIps, macChanges)
return qemuInitValue, networkList, targetFile, targetData, ports
'''返回值只有networkList和ports是提取出来的,剩下的qemuInitValue是预定义字符串,
targetFile、targetData是原始init路径和内容'''
在使用的预模拟脚本(runxx.sh
)上,FirmAE
也做了小的改动(主要在网络配置方面,qemu网络相关的内容参见官方wiki)。
虽然与firmadyne
一样在网络配置上使用了user mode
(意味着仅支持从主机到qemu的ping,反过来不行),但FirmAE
使用了默认的网络连接方式,而firmadyne
使用的则是socket
模式,应该是为了支持并行模拟。
总共设置了四个网络后端设备(qemu backends
),应该是为了满足提供论文中谈到的不同设备的以太网接口数目要求(Ethernet interface
)。
关于qemu网络的其他资料可以参考这个
if (${FIRMAE_NET}); then
QEMU_NETWORK="-device e1000,netdev=net0 -netdev user,id=net0 -device e1000,netdev=net1 -netdev user,id=net1 -device e1000,netdev=net2 -netdev user,id=net2 -device e1000,netdev=net3 -netdev user,id=net3"
else
QEMU_NETWORK="-device e1000,netdev=net0 -netdev socket,id=net0,listen=:2000 -device e1000,netdev=net1 -netdev socket,id=net1,listen=:2001 -device e1000,netdev=net2 -netdev socket,id=net2,listen=:2002 -device e1000,netdev=net3 -netdev socket,id=net3,listen=:2003"
fi
preInit.sh &run_service.sh
这两个基本都没啥东西…
preInit.sh
只是创建目录,挂载目录(应该是为了防止读关键目录时空目录)
run_service.sh
只是根据服务名启动对应的二进制,再检查是否启动成功
#!/firmadyne/sh
#preInit.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
——————————————————————————————————————————————————————————————————————————————————————
#!/firmadyne/sh
#run_service.sh
BUSYBOX=/firmadyne/busybox
BINARY=`${BUSYBOX} cat /firmadyne/service`
BINARY_NAME=`${BUSYBOX} basename ${BINARY}`
if (${FIRMAE_ETC}); then
${BUSYBOX} sleep 120
$BINARY &
while (true); do
${BUSYBOX} sleep 10
if ( ! (${BUSYBOX} ps | ${BUSYBOX} grep -v grep | ${BUSYBOX} grep -sqi ${BINARY_NAME}) ); then
$BINARY &
fi
done
fi
inferDefault.py
根据预模拟log推断nvram
值,似乎是FirmAE
新加的。流程大概是先从log中获取报错的nvram key
,再去image中全局搜索对应的key
,并把可能的的含key文件名
和含key数
写入统计文件。
#!/usr/bin/env python3
import sys
import os
import subprocess
def GetKeyList(IID):
keyList = []
for i in open('./scratch/{}/qemu.initial.serial.log'.format(IID), 'rb').read().split(b'\n'):
'''遍历每一行'''
if i.startswith(b'[NVRAM]'):
'''查找nvram相关的报错'''
l = i.split(b' ')
if len(l) < 3: continue
if not l[1].decode().isnumeric(): continue
keyLen = int(l[1].decode())
key = l[2][:keyLen]
'''对报错格式进行检查,仅提取包含完整名称和长度的NVRAM KEY
但是NVRAM 报错格式是不是固定为[NVRAM] length context 这样 不好说'''
# TODO: if you can parse null seperated key/value data in the binary, then remove equal character
#key = (i.split(b':')[1].strip())
try:
key.decode()
except:
# invalid key info
continue
if key not in keyList:
keyList.append(key)
'''更新KEY列表'''
return keyList
def GetDefaultFiles(keyList):
default_list = []
for dir_name, dir_list, file_list in os.walk('./scratch/{}/image'.format(IID)):
if dir_name.find('/firmadyne') != -1: continue
'''遍历跳过firmadyne目录'''
for file_name in file_list:
count = 0
if not os.path.isfile(dir_name + '/' + file_name): continue
data = open(dir_name + '/' + file_name, 'rb').read()
for key in keyList:
if data.find(key) != -1:
count += 1
# TODO: adjust approximate value
# TODO: need to save the start index of the nvram data to parsing from the binary
if count > len(keyList) // 2:
default_list.append((dir_name + '/' + file_name, count))
return default_list
'''本质上就是在镜像的全部文件中搜索key字符,将含key字符的文件名和key数目加入default_list'''
def Log(default_list):
# logging found nvram keys
with open('./scratch/{}/nvram_keys'.format(IID), 'w') as out:
out.write(str(len(keyList)) + '\n')
for i in keyList:
out.write(i.decode() + '\n')
'''写入报错的key数目和key内容'''
# logging default nvram files
if default_list:
with open('./scratch/{}/nvram_files'.format(IID), 'w') as f:
for i, j in default_list:
path = i.split('image')[1]
output = subprocess.check_output(['file', i]).decode()[:-1]
fileType = output.split(' ', 1)[1].replace(' ', '_')
if fileType.find('symbolic') == -1:
f.write('{} {} {}\n'.format(path, j, fileType))
os.system('cp ./scratch/{}/nvram_files ./scratch/{}/image/firmadyne/'.format(IID, IID))
'''将含nvram keys的文件目录和keys数写入统计文件'''
if __name__ == "__main__":
# execute only if run as a script
IID = sys.argv[1]
keyList = GetKeyList(IID)
if len(keyList) < 10:
exit(0)
defaultList = GetDefaultFiles(keyList)
Log(defaultList)
find系列函数
以findPorts
函数为例。
顾名思义,查找端口的函数。基本就是用re库去搜索log,找进程网络连接相关的内容。
这里有个小问题。它的查询格式是ip:port 0x xxx:xxxx
,但据我观察有的log只有port
不含ip
,所以导致匹配出来都是空的 2333(增加判断也不困难)
至于find里面的正则表达式为什么这么写,可以参考这一篇的系列
def findPorts(data, endianness):
lines = stripTimestamps(data)
'''stripTimestamps直接把log开头的时间的firmadyne去掉了'''
candidates = filter(lambda l: l.startswith(b"inet_bind"), lines) # logs for the inconfig process
'''data就是读的qemu.initial.serial.log,这里是遍历每一行以inet_bind开头的,实际上就是看init时进程启动的网络连接信息'''
result = []
if endianness == "eb":
fmt = ">I"
elif endianness == "el":
fmt = "<I"
prog = re.compile(b"^inet_bind\[[^\]]+\]: proto:SOCK_(DGRAM|STREAM), ip:port: 0x([0-9a-f]+):([0-9]+)")
'''注意看这个正则表达式里面的ip:port格式'''
portSet = {}
for c in candidates:
g = prog.match(c)
if g:
(proto, addr, port) = g.groups()
proto = "tcp" if proto == b"STREAM" else "udp"
addr = socket.inet_ntoa(struct.pack(fmt, int(addr, 16)))
'''将十六进制地址按大小端解包再转换为ip'''
port = int(port.decode())
if port not in portSet:
result.append((proto, addr, port))
portSet[port] = True
return result
剩下函数基本类似,只是正则表达式应该是管用的,都是在bootlog
中提取网络信息。
顺便,似乎查找的log内容(串口输出)是内核运行时的输出,不知道是不是这样的。
getNetworkList
利用搜集到的ifacesWithIps
(所有网络接口+ip)和macChanges
(网络接口+变更后的mac)进行二次查找,将结果封装为网络配置列表并返回。(顾名思义,这里的macChanges
只有在通过网络设置改变了mac的情况下才能查到,应该是对应添加网络接口的过程)。
不过它这个遍历逻辑似乎有点问题,如果遍历过了网桥接口,就会将deviceHasBridge
置为True,这种情况下不会遍历在设置完网桥及接入网桥的接口之后,可能出现的,不在网桥上的单独的网络接口。
def getNetworkList(data, ifacesWithIps, macChanges):
global debug
networkList = []
deviceHasBridge = False
for iwi in ifacesWithIps:
'''输入的ifacesWithIps看起来是所有被设置的网络接口+ip
这里是把每一个网络接口都先当成网桥接口进行遍历,再判断是不是真的网桥接口'''
if iwi[0] == 'lo': # skip local network
'''跳过本地环回接口'''
continue
#find all interfaces that are bridged with that interface
brifs = findIfacesForBridge(data, iwi[0])
'''查找连在网桥上的网络接口(除了被遍历的可能网桥接口自身)'''
if debug:
print("brifs for %s %r" % (iwi[0], brifs))
for dev in brifs:
#find vlan_ids for all interfaces in the bridge
vlans = findVlanInfoForDev(data, dev)
'''根据接口名查找接口的vlan id'''
#create a config for each tuple
config = buildConfig(iwi, dev, vlans, macChanges)
'''对每一个连在网桥上的网络接口生成一个networklist
iwi为网桥+ip,dev是连在网桥上的接口'''
if config not in networkList:
networkList.append(config)
deviceHasBridge = True
#if there is no bridge just add the interface
if not brifs and not deviceHasBridge:
'''被遍历的接口不是网桥,且已遍历的网络接口无网桥(防止重复遍历接口)
这里的dev使用的直接是ifacesWithIps里的dev'''
vlans = findVlanInfoForDev(data, iwi[0])
config = buildConfig(iwi, iwi[0], vlans, macChanges)
if config not in networkList:
networkList.append(config)
if checkVariable("FIRMAE_NET"):
return networkList
'''正常情况这里就返回了'''
buildconfig
组装配置,返回的mac
要与br
和dev
进行匹配,优先返回网桥接口br
对应的mac
地址。
独立接口与网桥桥接的接口会返回networklist
,网桥接口自身不清楚会不会返回(取决于br_dev_ioctl/br_add_if
是否会操作网桥接口本身),实例似乎是会的。
有了解的可以留言告诉我一下。
def buildConfig(brif, iface, vlans, macs):
'''
brif 可能的网桥接口+ip
iface 网桥上的网络接口/非网桥接口(此时与brif[0]一致)
vlans 可能的接口vlan id
mac 网络接口+mac地址
'''
#there should be only one ip
ip = brif[1]
br = brif[0]
#strip vlanid from interface name (e.g., eth2.2 -> eth2)
dev = iface.split(".")[0]
'''忽略iface里面的vlanid'''
#check whether there is a different mac set
mac = None
d = dict(macs)
if br in d:
mac = d[br]
elif dev in d:
mac = d[dev]
'''有网桥mac优先用网桥mac,单独的接口才使用自身的mac'''
vlan_id = None
if len(vlans):
vlan_id = vlans[0]
return (ip, dev, vlan_id, mac, br)
'''
返回值有两种类型:独立接口与网桥上的接口
对网桥上的接口:
ip、mac、br都是网桥接口的,自身的参数只有vlan_id和接口名dev
对独立接口:
五个参数都是自身的,且dev==br
'''
checknetwork
另一个核心函数,通过收集好的networklist
进一步筛选出合适的网络配置,包含了网桥与DHCP
的处理以及对网桥桥接设备的重新绑定。注释中提到的networklist
实例也值得一看。
这里的if-elif
分支结构也对应了对不同类型网络的处理,不过我觉得其实可以遍历一下networklist
,万一匹配到的可用网络类型不止一个呢?
def checkNetwork(networkList):
filterNetworkList = []
devList = ["eth0", "eth1", "eth2", "eth3"]
'''初始设置的四个网络后端接口'''
result = "None"
if checkVariable("FIRMAE_NET"):
devs = [dev for (ip, dev, vlan, mac, brif) in networkList]
devs = set(devs)
ips = [ip for (ip, dev, vlan, mac, brif) in networkList]
ips = set(ips)
'''使用集合排除重复项'''
# check "ethX" and bridge interfaces
# bridge interface can be named guest-lan1, br0
# wnr2000v4-V1.0.0.70.zip - mipseb
# [('192.168.1.1', 'br0', None, None, 'br0'), ('10.0.2.15', 'eth0', None, None, 'br1')]
# R6900
# [('192.168.1.1', 'br0', None, None, 'br0'), ('20.45.150.190', 'eth0', None, None, 'eth0')]
if (len(devs) > 1 and
any([dev.startswith('eth') for dev in devs]) and
any([not dev.startswith('eth') for dev in devs])):
'''检查是否同时含有桥接与非桥接的网络接口'''
print("[*] Check router")
'''检查是否提取到信息'''
# remove dhcp ip address
networkList = [network for network in networkList if not network[1].startswith("eth")]
'''networklist就是buidlconfig封装好的网络信息列表,network[1]就是网桥接口dev,
这里直接去掉了eth开头的网络配置,论文中提到是为了减少以太网接口数目来模拟arm设备'''
# linksys FW_LAPAC1200_LAPAC1750_1.1.03.000
# [('192.168.1.252', 'eth0', None, None, 'br0'), ('10.0.2.15', 'eth0', None, None, 'br0')]
elif (len(ips) > 1 and
any([ip.startswith("10.0.2.") for ip in ips]) and
any([not ip.startswith("10.0.2.") for ip in ips])):
'''同理,检查是否同时含有两种ip,再去掉DHCP默认ip的网络配置
不过依然不懂为什么dev和ip这两种情况互斥'''
print("[*] Check router")
# remove dhcp ip address
networkList = [network for network in networkList if not network[0].startswith("10.0.2.")]
# br and eth
if networkList:
vlanNetworkList = [network for network in networkList if not network[0].endswith(".0.0.0") and network[1].startswith("eth") and network[2] != None]
ethNetworkList = [network for network in networkList if not network[0].endswith(".0.0.0") and network[1].startswith("eth")]
invalidEthNetworkList = [network for network in networkList if network[0].endswith(".0.0.0") and network[1].startswith("eth")]
brNetworkList = [network for network in networkList if not network[0].endswith(".0.0.0") and not network[1].startswith("eth")]
invalidBrNetworkList = [network for network in networkList if network[0].endswith(".0.0.0") and not network[1].startswith("eth")]
'''
上面五条实际上就是根据信息提取出固件中的不同网络类型
含本地ip xxx.0.0.0 的被归为invaild
接口名含eth以及有vlan id的归为vlan类型
接口名含eth的直接归为独立的eth类型
接口名不含eth是直接归为桥接类型
'''
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"
'''对无效ip,强行绑定ip到192.168.0.1,论文提到了'''
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))
'''将dev强行绑定为我们设置的网络接口之一'''
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"
else:
print("[*] no network interface: bring up default network")
filterNetworkList.append(('192.168.0.1', 'eth0', None, None, "br0"))
'''啥也没找到,直接用默认网络配置'''
result = "default"
else: # if checkVariable("FIRMAE_NET"):
filterNetworkList = networkList
return filterNetworkList, result # (network_type)
qemuCmd
格式化qemu
命令行,实际上FirmAE
仅支持mips
与arm
架构,对ppc
以及aarch64
暂时没有加以支持,命令行内容可以与openwrt的qemu页面对比。
格式化后的结果会被写入run.sh
(工作目录下),在test_emulation.sh
与主目录下的run.sh
中被启动。
注意其中被函数startNetwork、stopNetwork、qemuNetworkConfig
嵌入的脚本内容。
def qemuCmd(iid, network, ports, network_type, arch, endianness, qemuInitValue, isUserNetwork):
'''
network实际上用的就是filterNetworkList,剩下的ports、network_type、isUserNetwork
是在之前checkNetwork里面提取的,i id、arch、endianness在提取阶段确定了,qemuInitValue是写死的值
参见inferNetwork
'''
network_bridge = ""
network_iface = ""
if arch == "mips":
qemuEnvVars = ""
qemuDisk = "-drive if=ide,format=raw,file=${IMAGE}"
if endianness != "eb" and endianness != "el":
raise Exception("You didn't specify a valid endianness")
elif arch == "arm":
qemuDisk = "-drive if=none,file=${IMAGE},format=raw,id=rootfs -device virtio-blk-device,drive=rootfs"
if endianness == "el":
qemuEnvVars = "QEMU_AUDIO_DRV=none"
elif endianness == "eb":
raise Exception("armeb currently not supported")
else:
raise Exception("You didn't specify a valid endianness")
else:
raise Exception("Unsupported architecture")
for (ip, dev, vlan, mac, brif) in network:
network_bridge = brif
network_iface = dev
'''网桥 桥接设备 根据论文里面写的只用一个桥接网口以模拟arm设备'''
break
return QEMUCMDTEMPLATE % {'IID': iid,
'NETWORK_TYPE' : network_type,
'NET_BRIDGE' : network_bridge,
'NET_INTERFACE' : network_iface,
'START_NET' : startNetwork(network),
'STOP_NET' : stopNetwork(network),
'ARCHEND' : arch + endianness,
'QEMU_DISK' : qemuDisk,
'QEMU_INIT' : qemuInitValue,
'QEMU_NETWORK' : qemuNetworkConfig(arch, network, isUserNetwork, ports),
'QEMU_ENV_VARS' : qemuEnvVars}
'''格式化脚本,关键步骤,特别注意startNetwork、stopNetwork、qemuNetworkConfig三个函数'''
startNetwork & stopNetwork
在宿主机(host)上配置网络的脚本,使用TAP技术,相关内容参见这一篇、这一篇和这一篇。命令行参考这一篇。
convertToHostIp
就是检查ip
的最后一部分,为0则加1,大于0则减1。
startNetwork
中HOSTNETDEV_%(I)i
是提取出来的网络接口,TAPDEV_%(I)i
是为该接口创建的TAP网卡。遍历提取出来的网络配置,每一个均设置了一个对应的TAP
虚拟网络接口。
stopNetwork
与之类似,用于停用并删除TAP
虚拟网络接口设备。
def startNetwork(network):
template_1 = """
TAPDEV_%(I)i=tap${IID}_%(I)i
HOSTNETDEV_%(I)i=${TAPDEV_%(I)i}
echo "Creating TAP device ${TAPDEV_%(I)i}..."
sudo tunctl -t ${TAPDEV_%(I)i} -u ${USER}
"""
'''创建tap虚拟网卡'''
if checkVariable("FIRMAE_NET"):
template_vlan = """
echo "Initializing VLAN..."
HOSTNETDEV_%(I)i=${TAPDEV_%(I)i}.%(VLANID)i
sudo ip link add link ${TAPDEV_%(I)i} name ${HOSTNETDEV_%(I)i} type vlan id %(VLANID)i
sudo ip link set ${TAPDEV_%(I)i} up
"""
'''将设备接口连接到tap网卡上,同时设置其vlan id'''
template_2 = """
echo "Bringing up TAP device..."
sudo ip link set ${HOSTNETDEV_%(I)i} up
sudo ip addr add %(HOSTIP)s/24 dev ${HOSTNETDEV_%(I)i}
"""
'''启动设备接口,为接口分配ip。
注意看一下HOSTNETDEV_%(I),在没有vlan的时候就是TAPDEV_%(I)i
但是在有vlan时是${TAPDEV_%(I)i}.%(VLANID)i
这也是为什么template_vlan里面起的是${TAPDEV_%(I)i}'''
else:
'''这两个应该是firmadyne的原始脚本,一般不会用到'''
template_vlan = """
echo "Initializing VLAN..."
HOSTNETDEV_%(I)i=${TAPDEV_%(I)i}.%(VLANID)i
sudo ip link add link ${TAPDEV_%(I)i} name ${HOSTNETDEV_%(I)i} type vlan id %(VLANID)i
sudo ip link set ${HOSTNETDEV_%(I)i} up
"""
template_2 = """
echo "Bringing up TAP device..."
sudo ip link set ${HOSTNETDEV_%(I)i} up
sudo ip addr add %(HOSTIP)s/24 dev ${HOSTNETDEV_%(I)i}
echo "Adding route to %(GUESTIP)s..."
sudo ip route add %(GUESTIP)s via %(GUESTIP)s dev ${HOSTNETDEV_%(I)i}
"""
output = []
for i, (ip, dev, vlan, mac, brif) in enumerate(network):
output.append(template_1 % {'I' : i})
if vlan != None:
output.append(template_vlan % {'I' : i, 'VLANID' : vlan})
output.append(template_2 % {'I' : i, 'HOSTIP' : convertToHostIp(ip), 'GUESTIP': ip})
'''这里的GUESTIP是firmadyne中用来添加路由规则的,在FirmAE中废弃'''
return '\n'.join(output)
qemuNetworkConfig & qemuArchNetworkConfig
封装模拟的qemu
网络参数。核心逻辑是遍历真机的网卡接口序号(这里默认从0开始,且开发者按顺序命名,如br0、br1、eth0
),在匹配到的情况下将封装的配置加入output
,并针对一个序号多接口的可能情况做了针对处理。
均未匹配到的情况下使用firmadyne
式的默认网络配置。
def qemuNetworkConfig(arch, network, isUserNetwork, ports):
output = []
assigned = []
interfaceNum = 4
if arch == "arm" and checkVariable("FIRMAE_NET"):
interfaceNum = 1
'''arm强行设置interfaceNum=1以保证模拟'''
for i in range(0, interfaceNum):
for j, n in enumerate(network):
# need to connect the jth emulated network interface to the corresponding host interface
if i == ifaceNo(n[1]):
''',n是network,j是网络配置在network列表中的排序,对应TAP网卡下标
n[1]是dev,ifaceNo返回接口名里面的数字,一般也就是真机的网卡编号'''
output.append(qemuArchNetworkConfig(i, j, arch, n, isUserNetwork, ports))
assigned.append(n)
break
'''检测到网卡接口序号被占用后直接遍历下一个序号
不过未考虑rth0+br0这种一个序号多接口的情况,所以后面进行了补充'''
# otherwise, put placeholder socket connection
if len(output) <= i:
output.append(qemuArchNetworkConfig(i, i, arch, None, isUserNetwork, ports))
'''未匹配到的情况下使用firmadyne默认的socket配置'''
# find unassigned interfaces
for j, n in enumerate(network[:interfaceNum]):
if n not in assigned:
# guess assignment
print("Warning: Unmatched interface: %s" % (n,))
output[j] = qemuArchNetworkConfig(j, j, arch, n, isUserNetwork, ports)
assigned.append(n)
'''补充可能的单序号多接口情况'''
return ' '.join(output)
qemuArchNetworkConfig
,根据架构选择不同的网络命令行,也根据是否为UserNetwork
做了不同处理(UserNetwork
的判断依据就是是否使用DHCP
技术)。这一部分也建议参考我的论文节译。
在使用DHCP时网络类型设置为了user
(防止在TAP
网络中因为DHCP
获取不到ip
而导致网络错误,配合之前checkNetwork
中对invalid
类型的处理让动态获取ip
的接口强行绑定到192.168.0.1
),但将之前捕获到的端口(ports
)通过hostfwd
进行了端口映射,使相应服务可以被主机访问(不过相应的也就不能ping
了,TODO
正是在考虑弥补这一点)。
在没有使用DHCP
时就是直接使用TAP
网络的方式进行配置了。
def qemuArchNetworkConfig(i, tap_num, arch, n, isUserNetwork, ports):
if arch == "arm":
device = "virtio-net-device"
else:
device = "e1000"
if not n:
return "-device %(DEVICE)s,netdev=net%(I)i -netdev socket,id=net%(I)i,listen=:200%(I)i" % {'DEVICE': device, 'I': i}
else:
(ip, dev, vlan, mac, brif) = n
vlan_id = vlan if vlan else i
mac_str = "" if not mac else ",macaddr=%s" % mac
if isUserNetwork: # user network dhcp server
'''启用DHCP时,使用usernetwork配置'''
# TODO: get port list (inet_bind)
# TODO: maybe need reverse ping checker
#portfwd = ",hostfwd=udp::67-:67"
#portfwd += ",hostfwd=udp::68-:68" # icmp port cannot foward
portfwd = "hostfwd=tcp::80-:80,hostfwd=tcp::443-:443,"
for (proto, ip, port) in ports:
if port in [80, 443]:
continue
portfwd += "hostfwd=%(TYPE)s::%(PORT)i-:%(PORT)i," % {"TYPE" : proto, "PORT" : port}
return "-device %(DEVICE)s,netdev=net%(I)i -netdev user,id=net%(I)i,%(FWD)s" % {'DEVICE': device, 'I': i, "FWD": portfwd[:-1]}
else:
return "-device %(DEVICE)s,netdev=net%(I)i -netdev tap,id=net%(I)i,ifname=${TAPDEV_%(TAP_NUM)i},script=no" % { 'I' : i, 'DEVICE' : device, 'TAP_NUM' : tap_num}
test_emulation.sh
测试模拟脚本,qemu命令行在qemuCMD
输出的run.sh
中。剩下的部分就是模拟结果写入(check_network
函数在firmae.config
中)。
有一个奇怪的点:主目录下的run.sh
中TIMEOUT
是针对推断过程而非测试模拟过程的,而测试模拟过程,正如下所示,只有一个sleep 10
和一些读取操作,就会直接开始检查模拟的各种信息,而后直接终止。(可能是开发者认为与最终模拟一致的测试模拟需要一定的速度?)
#!/bin/bash
if [ $# -ne 2 ]; then
echo $0: Usage: ./test_emulator.sh [iid] [arch]
exit 1
fi
set -e
set -u
if [ -e ./firmae.config ]; then
source ./firmae.config
elif [ -e ../firmae.config ]; then
source ../firmae.config
else
echo "Error: Could not find 'firmae.config'!"
exit 1
fi
IID=${1}
WORK_DIR=`get_scratch ${IID}`
ARCH=${2}
echo "[*] test emulator"
${WORK_DIR}/run.sh 2>&1 >${WORK_DIR}/emulation.log &
'''run.sh就是之前qemuCmd封装好的模拟脚本'''
sleep 10
echo ""
IPS=()
if (egrep -sq true ${WORK_DIR}/isDhcp); then
IPS+=("127.0.0.1")
echo true > ${WORK_DIR}/ping
else
IP_NUM=`cat ${WORK_DIR}/ip_num`
for (( IDX=0; IDX<${IP_NUM}; IDX++ ))
do
IPS+=(`cat ${WORK_DIR}/ip.${IDX}`)
done
fi
echo -e "[*] Waiting web service... from ${IPS[@]}"
read IP PING_RESULT WEB_RESULT TIME_PING TIME_WEB < <(check_network "${IPS[@]}" false)
'''调用check_network判断模拟结果'''
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
'''储存测试模拟信息'''
kill $(ps aux | grep `get_qemu ${ARCH}` | awk '{print $2}') 2> /dev/null | true
'''终止'''
sleep 2
感言
拖更 堂堂完结!
想看懂的话有必要滚回去再看看论文和项目了…推断式代码阅读不可取
通篇读下来的感受是这里是网络接口写作network interface
读作网卡(什么栈和堆栈)…
这一部分是FirmAE
代码中的珠穆朗玛峰(之一),所以写起来格外麻烦…
而且之前对计网这一块也不够了解,写到一半东拉西扯补了亿点点知识才敢接着写…
加上期末考试也就导致了更新直接跨年。
这一部分里面的网络设置基本都是在宿主机(host_system
)上进行的,而客系统(guest_system
)上的网络配置应该会在下一篇中,争取搭着libnvram
一起写出来。