当前位置: 首页 > news >正文

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要与brdev进行匹配,优先返回网桥接口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仅支持mipsarm架构,对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。

startNetworkHOSTNETDEV_%(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.shTIMEOUT是针对推断过程而非测试模拟过程的,而测试模拟过程,正如下所示,只有一个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一起写出来。

相关文章:

  • 网站图片加载 优化/上海网站seo外包
  • 中山家居企业网站建设/seo公司 引擎
  • 丘里奇网站排名/广州网络优化最早的公司
  • 天津做国外网站/成都网站seo
  • 深圳做棋牌网站建设哪家公司便宜/职业培训学校
  • 网站开发技术交流/百度快速收录工具
  • DaVinci:限定器 - HSL
  • 2023年,KPI和OKR的“双轨制”绩效管理升级
  • MWORKS 2023a 已上线!
  • 夜深忽梦少年事,7年又一年,来看看95年那个小伙现在怎么样了
  • Linux——线程概念及私有数据和优缺点
  • 老杨说运维 | 2023,浅谈智能运维趋势(二)
  • 微信推送消息给女友提醒每天天气情况,本文讲解流程,附带代码,可快速上手。
  • Vite中如何更好的使用TS
  • 三、WEB框架介绍以及设计模式
  • [Leetcode] 股票的价格跨度(单调栈)
  • 【java入门系列四】java基础-数组
  • 万字长文--详解Git(快速入门)