Firmadyne `inferNetwork.sh`
===
---
###### tags: `firmadyne` `security`
本篇紀錄閱讀 `inferNetwork.sh` source code 筆記
---
**目錄**
[TOC]
# /scripts/inferNetwork.sh
1. Source Code
```shell==
#!/bin/bash
set -e
set -u
if [ -e ./firmadyne.config ]; then
source ./firmadyne.config
elif [ -e ../firmadyne.config ]; then
source ../firmadyne.config
else
echo "Error: Could not find 'firmadyne.config'!"
exit 1
fi
if check_number $1; then
echo "Usage: inferNetwork.sh <image ID> [<architecture>]"
exit 1
fi
IID=${1}
if [ $# -gt 1 ]; then
if check_arch "${2}"; then
echo "Error: Invalid architecture!"
exit 1
fi
ARCH=${2}
else
echo -n "Querying database for architecture... "
ARCH=$(psql -d firmware -U firmadyne -h 127.0.0.1 -t -q \
-c "SELECT arch from image WHERE id=${1};")
ARCH="${ARCH#"${ARCH%%[![:space:]]*}"}"
echo "${ARCH}"
if [ -z "${ARCH}" ]; then
echo "Error: Unable to lookup architecture. \
Please specify {armel,mipseb,mipsel} as the second argument!"
exit 1
fi
fi
echo "Running firmware ${IID}: terminating after 60 secs..."
timeout --preserve-status --signal SIGINT 60 \
"${SCRIPT_DIR}/run.${ARCH}.sh" "${IID}"
sleep 1
echo "Inferring network..."
"${SCRIPT_DIR}/makeNetwork.py" -i "${IID}" -q -o \
-a "${ARCH}" -S "${SCRATCH_DIR}"
echo "Done!"
```
2. Explaining
```shell==21
if [ $# -gt 1 ]; then
```
判斷參數是否大於一個,以下是官方示範裡使用的方式
```shell
./scripts/inferNetwork.sh 1
```
所以這種用法會走 Line 28 的 else
---
在 Line 28 後的 else 中,有這麼一段
```shell==30
ARCH=$(psql -d firmware -U firmadyne -h 127.0.0.1 -t -q \
-c "SELECT arch from image WHERE id=${1};")
ARCH="${ARCH#"${ARCH%%[![:space:]]*}"}"
```
此段從 Database `firmware` 撈出 image id 為第一個參數的 arch
經實測,撈出的資料會類似以下

注意前面有空格
Line 32 主要用途是拿來消去所有 prefix 的空格
---
```shell==40
timeout --preserve-status --signal SIGINT 60 \
"${SCRIPT_DIR}/run.${ARCH}.sh" "${IID}"
```
若 $IID = 1 且 ARCH 為 mipseb,會 run /scripts/run.mipseb.sh 1
關於這個腳本解釋 請看 `run.mipseb.sh` 的 Source Code
---
經過 Line 40 讓 qemu 跑了 60 秒後,產生記錄檔
`scratch/${IID}/qemu.initial.serial.log` 檔案
(${IID} 為 Image ID)
這個檔案將在 `makeNetwork.py` 用到
```shell==47
"${SCRIPT_DIR}/makeNetwork.py" -i "${IID}" -q -o \
-a "${ARCH}" -S "${SCRATCH_DIR}"
```
若 $IID 為 1
$ARCH 為 mipseb
此指令代換後為
```shell==
"${FIRMWARE_DIR}/scripts/makeNetwork.py" -i "1" -q -o \
-a "mipseb" -S "${FIRMWARE_DIR}/scratch/"
```
接著請繼續看 `makeNetwork.sh`
# /scripts/run.mipseb.sh
1. Source Code
```shell==
#!/bin/bash
set -e
set -u
if [ -e ./firmadyne.config ]; then
source ./firmadyne.config
elif [ -e ../firmadyne.config ]; then
source ../firmadyne.config
else
echo "Error: Could not find 'firmadyne.config'!"
exit 1
fi
if check_number $1; then
echo "Usage: run.mipseb.sh <image ID>"
exit 1
fi
IID=${1}
WORK_DIR=`get_scratch ${IID}`
IMAGE=`get_fs ${IID}`
KERNEL=`get_kernel "mipseb"`
qemu-system-mips -m 256 -M malta -kernel ${KERNEL} \
-drive if=ide,format=raw,file=${IMAGE} \
-append "firmadyne.syscall=1 \
root=/dev/sda1 console=ttyS0 \
nandsim.parts=64,64,64,64,64,64,64,64,64,64 \
rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel \
print-fatal-signals=1" \
-serial file:${WORK_DIR}/qemu.initial.serial.log \
-serial unix:/tmp/qemu.${IID}.S1,server,nowait \
-monitor unix:/tmp/qemu.${IID},server,nowait \
-display none -net nic,vlan=0 -net socket,vlan=0,listen=:2000 \
-net nic,vlan=1 -net socket,vlan=1,listen=:2001 \
-net nic,vlan=2 -net socket,vlan=2,listen=:2002 \
-net nic,vlan=3 -net socket,vlan=3,listen=:2003
```
2. Explaining
```shell==21
WORK_DIR=`get_scratch ${IID}`
IMAGE=`get_fs ${IID}`
KERNEL=`get_kernel "mipseb"`
```
`get_scratch`, `get_fs`, `get_kernel` 都是定義在 `firmadyne.config` 的 function
主要用途是回傳相關檔案的位置
---
從 Line 25 之後是實際上 run qemu 的部分,以下解釋部分參數
- `-m 256 -M malta`
設定 ram size 以及模擬 malta motherboard
- `-serial file:${WORK_DIR}/qemu.initial.serial.log`
設定 console 輸出會存到的位置
`inferNetwork.sh` 會呼叫到 `makeNetwork.py`
而 `makeNetwork.py` 將會用到此檔案
# /scripts/makeNetwork.py
1. Source Code
```python=
#!/usr/bin/env python
import sys
import getopt
import re
import struct
import socket
import stat
import os
debug = 0
QEMUCMDTEMPLATE = """#!/bin/bash
set -u
ARCHEND=%(ARCHEND)s
IID=%(IID)i
if [ -e ./firmadyne.config ]; then
source ./firmadyne.config
elif [ -e ../firmadyne.config ]; then
source ../firmadyne.config
elif [ -e ../../firmadyne.config ]; then
source ../../firmadyne.config
else
echo "Error: Could not find 'firmadyne.config'!"
exit 1
fi
IMAGE=`get_fs ${IID}`
KERNEL=`get_kernel ${ARCHEND}`
QEMU=`get_qemu ${ARCHEND}`
QEMU_MACHINE=`get_qemu_machine ${ARCHEND}`
QEMU_ROOTFS=`get_qemu_disk ${ARCHEND}`
WORK_DIR=`get_scratch ${IID}`
%(START_NET)s
function cleanup {
pkill -P $$
%(STOP_NET)s
}
trap cleanup EXIT
echo "Starting firmware emulation... use Ctrl-a + x to exit"
sleep 1s
%(QEMU_ENV_VARS)s ${QEMU} -m 256 -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 rdinit=/firmadyne/preInit.sh rw debug ignore_loglevel print-fatal-signals=1 user_debug=31 firmadyne.syscall=0" \\
-nographic \\
%(QEMU_NETWORK)s | tee ${WORK_DIR}/qemu.final.serial.log
"""
def stripTimestamps(data):
lines = data.split("\n")
#throw out the timestamps
lines = [re.sub(r"^\[[^\]]*\] firmadyne: ", "", l) for l in lines]
return lines
def findMacChanges(data, endianness):
lines = stripTimestamps(data)
candidates = filter(lambda l: l.startswith("ioctl_SIOCSIFHWADDR"), lines)
if debug:
print("Mac Changes %r" % candidates)
result = []
if endianness == "eb":
fmt = ">I"
elif endianness == "el":
fmt = "<I"
for c in candidates:
g = re.match(r"^ioctl_SIOCSIFHWADDR\[[^\]]+\]: dev:([^ ]+) mac:0x([0-9a-f]+) 0x([0-9a-f]+)", c)
if g:
(iface, mac0, mac1) = g.groups()
m0 = struct.pack(fmt, int(mac0, 16))[2:]
m1 = struct.pack(fmt, int(mac1, 16))
mac = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", m0+m1)
result.append((iface, mac))
return result
# Get the netwokr interfaces in the router, except 127.0.0.1
def findNonLoInterfaces(data, endianness):
#lines = data.split("\r\n")
lines = stripTimestamps(data)
candidates = filter(lambda l: l.startswith("__inet_insert_ifa"), lines) # logs for the inconfig process
if debug:
print("Candidate ifaces: %r" % candidates)
result = []
if endianness == "eb":
fmt = ">I"
elif endianness == "el":
fmt = "<I"
for c in candidates:
g = re.match(r"^__inet_insert_ifa\[[^\]]+\]: device:([^ ]+) ifa:0x([0-9a-f]+)", c)
if g:
(iface, addr) = g.groups()
addr = socket.inet_ntoa(struct.pack(fmt, int(addr, 16)))
if addr != "127.0.0.1" and addr != "0.0.0.0":
result.append((iface, addr))
return result
def findIfacesForBridge(data, brif):
#lines = data.split("\r\n")
lines = stripTimestamps(data)
result = []
candidates = filter(lambda l: l.startswith("br_dev_ioctl") or l.startswith("br_add_if"), lines)
for c in candidates:
for p in [r"^br_dev_ioctl\[[^\]]+\]: br:%s dev:(.*)", r"^br_add_if\[[^\]]+\]: br:%s dev:(.*)"]:
pat = p % brif
g = re.match(pat, c)
if g:
iface = g.group(1)
#we only add it if the interface is not the bridge itself
#there are images that call brctl addif br0 br0 (e.g., 5152)
if iface != brif:
result.append(iface.strip())
return result
def findVlanInfoForDev(data, dev):
#lines = data.split("\r\n")
lines = stripTimestamps(data)
results = []
candidates = filter(lambda l: l.startswith("register_vlan_dev"), lines)
for c in candidates:
g = re.match(r"register_vlan_dev\[[^\]]+\]: dev:%s vlan_id:([0-9]+)" % dev, c)
if g:
results.append(int(g.group(1)))
return results
def ifaceNo(dev):
g = re.match(r"[^0-9]+([0-9]+)", dev)
return int(g.group(1)) if g else -1
def qemuArchNetworkConfig(i, arch, n):
if not n:
if arch == "arm":
return "-device virtio-net-device,netdev=net%(I)i -netdev socket,id=net%(I)i,listen=:200%(I)i" % {'I': i}
else:
return "-net nic,vlan=%(VLAN)i -net socket,vlan=%(VLAN)i,listen=:200%(I)i" % {'I': i, 'VLAN' : i}
else:
(ip, dev, vlan, mac) = n
# newer kernels use virtio only
if arch == "arm":
return "-device virtio-net-device,netdev=net%(I)i -netdev tap,id=net%(I)i,ifname=${TAPDEV_%(I)i},script=no" % {'I': i}
else:
vlan_id = vlan if vlan else i
mac_str = "" if not mac else ",macaddr=%s" % mac
return "-net nic,vlan=%(VLAN)i%(MAC)s -net tap,vlan=%(VLAN)i,id=net%(I)i,ifname=${TAPDEV_%(I)i},script=no" % { 'I' : i, 'MAC' : mac_str, 'VLAN' : vlan_id}
def qemuNetworkConfig(arch, network):
output = []
assigned = []
for i in range(0, 4):
for j, n in enumerate(network):
# need to connect the jth emulated network interface to the corresponding host interface
if i == ifaceNo(n[1]):
output.append(qemuArchNetworkConfig(j, arch, n))
assigned.append(n)
break
# otherwise, put placeholder socket connection
if len(output) <= i:
output.append(qemuArchNetworkConfig(i, arch, None))
# find unassigned interfaces
for j, n in enumerate(network):
if n not in assigned:
# guess assignment
print("Warning: Unmatched interface: %s" % (n,))
output[j] = qemuArchNetworkConfig(j, arch, n)
assigned.append(n)
return ' '.join(output)
def buildConfig(brif, iface, vlans, macs):
#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]
#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]
vlan_id = None
if len(vlans):
vlan_id = vlans[0]
return (ip, dev, vlan_id, mac)
def getIP(ip):
tups = [int(x) for x in ip.split(".")]
if tups[3] != 1:
tups[3] -= 1
else:
tups[3] = 2
return ".".join([str(x) for x in tups])
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}
"""
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) in enumerate(network):
output.append(template_1 % {'I' : i})
if vlan:
output.append(template_vlan % {'I' : i, 'VLANID' : vlan})
output.append(template_2 % {'I' : i, 'HOSTIP' : getIP(ip), 'GUESTIP': ip})
return '\n'.join(output)
def stopNetwork(network):
template_1 = """
echo "Deleting route..."
sudo ip route flush dev ${HOSTNETDEV_%(I)i}
echo "Bringing down TAP device..."
sudo ip link set ${TAPDEV_%(I)i} down
"""
template_vlan = """
echo "Removing VLAN..."
sudo ip link delete ${HOSTNETDEV_%(I)i}
"""
template_2 = """
echo "Deleting TAP device ${TAPDEV_%(I)i}..."
sudo tunctl -d ${TAPDEV_%(I)i}
"""
output = []
for i, (ip, dev, vlan, mac) in enumerate(network):
output.append(template_1 % {'I' : i})
if vlan:
output.append(template_vlan % {'I' : i})
output.append(template_2 % {'I' : i})
return '\n'.join(output)
def qemuCmd(iid, network, arch, endianness):
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")
return QEMUCMDTEMPLATE % {'IID': iid,
'ARCHEND' : arch + endianness,
'START_NET' : startNetwork(network),
'STOP_NET' : stopNetwork(network),
'QEMU_DISK' : qemuDisk,
'QEMU_NETWORK' : qemuNetworkConfig(arch, network),
'QEMU_ENV_VARS' : qemuEnvVars}
def process(infile, iid, arch, endianness=None, makeQemuCmd=False, outfile=None):
brifs = []
vlans = []
data = open(infile).read()
network = set()
success = False
#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)
deviceHasBridge = False
for iwi in ifacesWithIps:
#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)
#create a config for each tuple
network.add((buildConfig(iwi, dev, vlans, macChanges)))
deviceHasBridge = True
#if there is no bridge just add the interface
if not brifs and not deviceHasBridge:
vlans = findVlanInfoForDev(data, iwi[0])
network.add((buildConfig(iwi, iwi[0], vlans, macChanges)))
ips = set()
pruned_network = []
for n in network:
if n[0] not in ips:
ips.add(n[0])
pruned_network.append(n)
else:
if debug:
print("duplicate ip address for interface: ", n)
if makeQemuCmd:
qemuCommandLine = qemuCmd(iid, pruned_network, arch, endianness)
if qemuCommandLine:
success = True
if outfile:
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)
else:
print(qemuCommandLine)
return success
def archEnd(value):
arch = None
end = None
tmp = value.lower()
if tmp.startswith("mips"):
arch = "mips"
elif tmp.startswith("arm"):
arch = "arm"
if tmp.endswith("el"):
end = "el"
elif tmp.endswith("eb"):
end = "eb"
return (arch, end)
def main():
infile = None
makeQemuCmd = False
iid = None
outfile = None
arch = None
endianness = None
(opts, argv) = getopt.getopt(sys.argv[1:], 'f:i:S:a:oqd')
for (k, v) in opts:
if k == '-f':
infile = v
if k == '-d':
global debug
debug += 1
if k == '-q':
makeQemuCmd = True
if k == '-i':
iid = int(v)
if k == '-S':
SCRATCHDIR = v
if k == '-o':
outfile = True
if k == '-a':
(arch, endianness) = archEnd(v)
if not arch or not endianness:
raise Exception("Either arch or endianness not found try mipsel/mipseb/armel/armeb")
if not infile and iid:
infile = "%s/%i/qemu.initial.serial.log" % (SCRATCHDIR, iid)
if outfile and iid:
outfile = """%s/%i/run.sh""" % (SCRATCHDIR, iid)
if debug:
print("processing %i" % iid)
if infile:
process(infile, iid, arch, endianness, makeQemuCmd, outfile)
if __name__ == "__main__":
main()
```
2. Explainging
進入口為 Line 351 main()
```python=358
(opts, argv) = getopt.getopt(sys.argv[1:], 'f:i:S:a:oqd')
```
創造後面跟 1 個 option 的 arguments: `-f`, `-i`, `-S`, `-a`
以及後面不跟 option 的 arguments: `-o`, `-q`, `-d`
以下假設 `inferNetwork.sh` 是如此呼叫 `makeNetwork.py` 的:
```shell==
"${FIRMWARE_DIR}/scripts/makeNetwork.py" -i "1" -q -o \
-a "mipseb" -S "${FIRMWARE_DIR}/scratch/"
```
---
有被執行到的 if 在下面條列:
- `-i 1`
```python=367
if k == '-i':
iid = int(v)
```
- `-q`
```python=365
if k == '-q':
makeQemuCmd = True
```
- `-o`
```python=371
if k == '-o':
outfile = True
```
- `-a "mipseb"`
```python=373
if k == '-a':
(arch, endianness) = archEnd(v)
```
`archEnd` 只是很簡單的把 `"mipseb"` 切成 `"mips"`, `"eb"`
並存回 `(arch, endianness)`
- `-S "${FIRMWARE_DIR}/scratch/"`
```python=369
if k == '-S':
SCRATCHDIR = v
```
---
```python=376
if not arch or not endianness:
```
不會進if。
---
```python=379
if not infile and iid:
infile = "%s/%i/qemu.initial.serial.log" % (SCRATCHDIR, iid)
```
會進if。
設定 `infile = "${FIRMWARE_DIR}/scratch//1/qemu.initial.serial.log"`
此檔案在 run qemu 後產生
---
```python=381
if outfile and iid:
outfile = """%s/%i/run.sh""" % (SCRATCHDIR, iid)
```
會進if。
設定 `outfile = "${FIRMWARE_DIR}/scratch//1/run.sh"`
---
```python=383
if debug:
```
不會進if。
---
```python=385
if infile:
process(infile, iid, arch, endianness, makeQemuCmd, outfile)
```
會進 if。
參數代換後,如下
```python=
process("${FIRMWARE_DIR}/scratch//1/qemu.initial.serial.log", \
1, "mips", "eb", True, "${FIRMWARE_DIR}/scratch//1/run.sh")
```
接著繼續追看 process 的 Source Code (Line 280)
---
```python=283
data = open(infile).read()
```
將 infile 打開讀取
```python=287
#find interfaces with non loopback ip addresses
ifacesWithIps = findNonLoInterfaces(data, endianness)
```
```python=293
print("Interfaces: %r" % ifacesWithIps)
```
輸出結果如下

(輸出的第一行為我個人多加的,方便觀察實際的參數值)
現在就繼續追 `findNonLoInterfaces` 的 Source code (Line 75)
---
```python=77
lines = stripTimestamps(data)
```
`stripTimestamps` 的功用簡單來說就是
將 data 切成一行行的 line
並將每行開頭的(若有的話) `[......] firmadyne:` 砍掉
並存回 lines 陣列回傳。
---
```python=78
candidates = filter(lambda l: l.startswith("__inet_insert_ifa"), lines) # logs for the inconfig process
```
將開頭是 "__inet_insert_ifa" 的 line 存進 candidates 中
---
```python=81
result = []
if endianness == "eb":
fmt = ">I"
elif endianness == "el":
fmt = "<I"
```
初始化 result 陣列
並依據 endianness 來決定如何 pack
詳情請見 python module `struct.pack`
---
先來看看若是 __inet_insert_ifa 開頭的 Log 會長怎樣

再看以下 code:
```python=86
for c in candidates:
g = re.match(r"^__inet_insert_ifa\[[^\]]+\]: device:([^ ]+) ifa:0x([0-9a-f]+)", c)
if g:
(iface, addr) = g.groups()
addr = socket.inet_ntoa(struct.pack(fmt, int(addr, 16)))
if addr != "127.0.0.1" and addr != "0.0.0.0":
result.append((iface, addr))
return result
```
---
簡單來說
`inferNetwork.sh` 先執行了 `run.${ARCH}.sh` 60秒
產生了 qemu.initial.serial.log
再呼叫 `makeNetwork.py` 去從 log 中找 `__inet_insert_ifa` pattern