如何魔改firmadyne来修复模拟崩溃
一、问题
用过firmadyne的大家都知道,对于某些固件,firmadyne并不能获取到正确网络接口,甚至不能正常启动qemu虚拟机。导致这种后果原因可能有很多种,其中一种是调用某些nvram函数不成功,导致某些init程序崩溃,无法正常初始化网卡等设备。
NVRAM:非易失性随机访问存储器 (Non-Volatile Random Access Memory),是指断电后仍能保持数据的一种RAM。在嵌入式系统领域内,可以直接理解成板子上的FLASH芯片,里面保存着代码数据,用户配置数据等,如UBOOT,kernel,rootfs,user data。数据多以key/value形式储存。
二、解决方案
对于nvram函数导致的问题,最有效的解决方法还是分析crash log,根据需要的参数自行修改libnvram源码并且重新编译。
firmadyne自带了修改过的libnvram.so,并且会在makeimage阶段设置劫持固件原本的libnvram.so。它可以根据key令nvram_get等函数返回用户事先定义好的key/value中的value,若key未定义,则返回null。
它的源码位于firmadyne/sources/libnvram/
nvram.c
文件定义了要劫持的库函数等。
alias.c
文件定义了一些函数的别名。
config.h
定义的是要初始化的所有key/value。
下图为config.h代码,可以看到默认情况下,firmadyne作者已经在其中增加了对很多型号设备的适配:
所以我们解决问题的方法就是修改上述文件,向config.h中添加我们需要的key/value,并且重新编译libnvram.so。
三、举个例子
路由器型号:DIR-2640-US
下载地址:https://support.dlink.com/productinfo.aspx?m=DIR-2640-US
固件版本:Firmware (1.01B04)
更新日期:02/07/20
1.获得固件
固件本身是经过加密的,不过加密方式是跟其他较新的路由器相同,所以也可以用其他路由器中的bin/imgdecrypt
来解密。
qemu-mipsel -L . ./bin/imgdecrypt DIR2640A1_FW101B04.bin
mv /tmp/.firmware.orig /path/DIR2640A1_FW101B04_decrypted.bin
之后binwalk -Me DIR2640A1_FW101B04_decrypted.bin
可以分离出fs
2.尝试模拟
我这里安装的是2020年的firmware-analysis-toolkit,链接:https://github.com/attify/firmware-analysis-toolkit
执行./fat.py DIR2640A1_FW101B04_decrypted.bin
3.定位crash
片刻后,喜闻乐见的Network interfaces为空。此时不要按回车开始模拟固件,先查看在获取网络配置阶段产生的log,位于firmadyne/scratch/ID/qemu.initial.serial.log
,这里ID为2。
可以看到nvram_daemon崩溃,ra和epc地址并不像在程序text段,更像是在lib中。
用ghidra分析一波bin/nvram_daemon
先,重点关注nvram相关函数。其中nvram_safe_set()
在alias.h和nvram.h中都没有被定义,后面需要我们手动添加到alias.h。
先看最基础的nvram_get , nvram_set函数xref:
随便定位到两处可以大概判断出函数原型
char *nvram_get(int idx , const char *key);
int nvram_set(int idx , const char *key , const char *value);
接着看看主函数:
FUN_00403038():
WRSConfigGet()是一个库函数,定义在lib/libwrscfg.so
,它实际上是调用libnvram.so
中的nvram_safe_get
来获取内容。
在nvram.c
中定义了nvram_safe_get
这个函数,所以它会被劫持。
正如前面提到的,在config.h
中并没有我们事先定义的factory_mode
这个key,所以WRSConfigGet("factory_mode")
返回值为空字符串.
因此函数最终会执行到TW_LoadDefaultConfig()
,这是一个定义在lib/librcm.so
中的库函数,它的作用是清空设备,重新把各项参数的默认值加载到nvram等设备中去:
loadDefault(0xb2c)
:
分析bin/rallink_init
:
nvram_clear()
也是固件原本libnvram.so中的库函数,但是firmadyne并没有劫持这个函数,所以会导致崩溃。
4.尝试修复
因此一个解决方法是让FUN_00403038()不能执行到TW_LoadDefaultConfig()
,也就是需要修改config.h
设置factory_mode的value不为空且不为1。并且添加原本需要设置默认值的key/value。
同时注意到firmadyne中定义的nvram_get和nvram_set函数原型跟上述两函数原型并不同:
这是固件中的函数原型
char *nvram_get(int idx , const char *key);
int nvram_set(int idx , const char *key , const char *value);
这是firmadyne中定义的函数原型
char *nvram_get(const char *key);
int nvram_set(const char *key, const char *val);
因此需要修改firmadyne中libnvram源码中的函数使其传参与固件相同,并且向alias.c中添加nvram_safe_set()
。
由于c语言不支持重载,用内联汇编的方式也不能很好的解决(firmadyne本身就尝试使用这种方式,效果不是很好)。这里选择大部分重写,在nvram_get和nvram_set两个函数里都增加一个idx参数,暂时无作用。
并且把nvram.c
,alias.c
中所有调用,定位到这两个函数的地方全部修改,下图是几个例子。
向alias.c中添加nvram_safe_set()
int nvram_safe_set(const char *key, const char *val) {
return nvram_set(1,key, val);
}
全部修改完后我们就可以重新编译,替换firmadyne/binaries/libnvram.so.mipsel
。
这里需要重新交叉编译,可以自建toolchain(https://github.com/firmadyne/firmadyne#compiling-from-source)
或者直接下载firmadyne提供的toolchains:https://cmu.app.boxcn.net/s/hnpvf1n72uccnhyfe307rc2nb9rfxmjp
下载后解压出mipsel-linux-musl.tar.xz
,将解压后的内容放到某个文件夹.
在firmadyne/sources/libnvram
下执行
make clean && CC=/opt/cross/mipsel-linux-musl/bin/mipsel-linux-musl-gcc make
mv libnvram.so ../../binaries/libnvram.so.mipsel
即可。
接着重新尝试模拟,不出意外上一次的crash不会出现,但还是会在其他地方crash,因为还没有添加出厂默认参数。
查看log,ra=00402090
0x402090:
未设置BssidNum的value,导致atoi参数为Null,函数报错导致crash,确实是有些key没有设置好。
重新看到TW_LoadDefaultConfig()
:
分别调用了三个文件,文件的内容都是key=value的形式,将其中的内容全部加入到config.h
,记得删除factory_mode=1
的键值对。
这里写了个简单的脚本来转换:
#!/usr/bin/env python3
from sys import argv
try:
f = open(argv[1],"r")
except:
print("Usage: ./genconfig.py filename")
exit()
f2 = open("result.txt","a")
for line in f.readlines():
if not line.startswith("#") and "=" in line:
if line.endswith("\n"):
line = line[:-1]
line = line.replace("\"","\\\"")
d = line.split("=")
f2.write("ENTRY(\""+d[0]+"\", nvram_set , \""+d[1]+"\") \\\n")
f.close()
f2.close()
多次尝试,还会有几次崩溃,到崩溃的地点确定key值,在config.h
中一一设置,直到没有crash即可,此时应该也可以看到成功获取到网络接口信息。
到此为止启动过程中crash,无法获得网络接口的问题应该是被解决。
5.更多
但启动后又发现有其他问题:
EXT2-fs error (device sda1): ext2_lookup: deleted inode referenced: 41076
经过一些排查,发现是qemu启动时使用image的问题,firmadyne整个过程会两次使用同一个image来启动qemu虚拟机,第一次用来获取网络接口。第二次是根据网络接口,添加与主机通信的网卡然后最终启动。
第一次获取完网络接口后,未按正确方式退出,而且firmadyne也没有将image复原,第二次使用的image是第一次修改过的,会导致某些固件的image在第二次mount时出现问题。
解决方法也很简单,在获取网络接口之前创建一个image的备份,获取结束后恢复image。
修改firmadyne/scripts/run.mipsel.sh
:
修改firmadyne/scripts/inferNetwork.sh
:
再次尝试,错误不再出现,使用admin/twsz@2018
可以登录shell