1.固件提取 咸鱼上淘了一块板子,直接吹下flash提取,8pin SPI flash,16MB
2.固件分析 binwalk分析
结果非常混乱,推测为RTOS固件
查看一下字节序
大端序,再查看一下熵值
binwalk -E
可以看到有几部分具有很高的熵值,可能是被压缩了,也可能是混淆状态。
被压缩的固件是无法直接运行的,固件中肯定存在某个部分是用于解压缩的,固件需要能够自举。
大的设备可能会有一个专门的flash用于存放bootloader进行初始化操作,其中就会包含解压缩或者解混淆等操作;而对于本设备只有一个flash,那么bootloader和业务逻辑的固件都是一起的。看到熵值图,其中也有不少熵值较低的。
直接用IDA打开固件
IDA没有进行任何分析。注意,此时我们没有设置固件的加载地址,因此默认加载地址为0,在0地址处解析固件
解析出了一些代码,不过这些代码是以0地址为基址解析出来的,不一定正确,需要进一步分析之后才能够确定。
0地址处是一个跳转指令,跳转到了0x40处执行,看到0x40处的函数
这个函数中执行了大量的初始化操作,由于很多地址在IDA中没有映射,所以显示为红色
在最后面有几个函数
首先看到sub_E54
函数
这个函数从0xA000BA78
开始的0x6370
个字节进行清空操作。
再看到sub_B46
函数
同样的函数调用了两次
看到sub_A94
函数
a2是0xA0001000
,a3是0x7D6B
,a1是0xF2C
,0xF2C
指向的地址中存在大量数据
这个函数执行的操作大致就是将0xF2C
这个地址中的数据经过一系列操作后,存放到0xA0001000
中,长度为0x7D6B
第二个sub_A94
函数
将0x8c9b处的数据处理后存放到0xA000B750,长度为unk_8C9A + (unk_8C99 << 8) + (unk_8C98 << 16) + (unk_8C97 << 24)
经计算后得到长度为0x1c2
.
我们需要想办法得到经sub_A94
处理后的数据。通常而言,可以将处理数据的函数的伪代码抄下来稍微修改一下使其能够运行,不过由于这个函数有些复杂,修改伪c代码可能会导致函数失真,所以我采取另一种方法:使用qiling框架对代码进行部分模拟。
qiling框架是对unicorn的封装,用于模拟固件非常合适。
正常而言使用qiling模拟RTOS固件需要创建一个配置文件,用于指定各个区段,而且也不一定能运行成功;一种相对简单且成功率高的方法就是使用qiling运行一段目标固件架构的shellcode或者代码,将固件读取到内存中,添加区段映射,然后再从shellcode中跳转到固件中执行即可。
首先来确定我们要模拟的函数要从哪开始执行
在0x00000434
处执行了BLX sub_B46
指令,跳转执行sub_B46
,并切换为thumb模式;下一行指令的地址为0x00000438
,也就是执行完数据处理函数后会跳回来继续执行,所以要模拟的函数的开始地址为0x00000434
,结束地址为0x0000438
。
编写如下脚本
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 52 53 from pwn import *context.arch='arm' context.endian='big' from qiling import Qilingfrom qiling.const import QL_VERBOSEfrom qiling.const import QL_ARCHfrom qiling.const import QL_ENDIANfrom qiling.const import QL_OSdecompress_func=0x00000434 def hook1 (ql:Qiling ): ql.arch.regs.arch_pc=decompress_func return def hook2 (ql:Qiling ): length=ql.arch.regs.read('r2' ) ql.log.info("size:{}" .format (hex (length))) return def hook3 (ql:Qiling ): with open ("ram1.bin" ,"wb" ) as f: data=ql.mem.read(0xA0001000 ,0xaa80 ) f.write(data) with open ("ram2.bin" ,"wb" ) as f: data=ql.mem.read(0xA000B750 ,0x1000 ) f.write(data) ql.stop() if __name__ == '__main__' : shellcode=asm(shellcraft.thumb.infloop()) ql=Qiling(code=shellcode,ostype=QL_OS.LINUX,archtype=QL_ARCH.ARM,endian=QL_ENDIAN.EB ,verbose=QL_VERBOSE.DEBUG,thumb=True ) with open ('./brother_7180.BIN' ,'rb' ) as f: firm=f.read() firm_size=len (firm) ql.log.info("firmware size: {}" .format (hex (firm_size))) ql.mem.map (0 ,firm_size,info="[flash]" ) ql.mem.write(0 ,firm) ql.mem.map (0xA0000000 ,0x1000000 ,info="[ram]" ) ql.mem.map (0x20000000 , 0x10000000 , info="[STACK]" ) for info_line in ql.mem.get_formatted_mapinfo(): ql.log.debug(info_line) ql.hook_address(hook1,0x011ff000 ) ql.hook_address(hook2,0x00000B90 ) ql.hook_address(hook3,0x00000438 ) ql.arch.regs.arch_sp=0x2f000000 ql.run()
使用pwntools生成一段arm的thumb模式的shellcode,让qiling运行这段shellcode,将固件映射到0地址处,在0xA0000000
处映射一段内存,用于存放数据,在0x20000000
处也映射一段内存,用于栈空间。
设置了3个hook函数,hook1用于从shellcode跳转到固件中执行;hook3用于在执行完毕后将数据从内存中读出来并写入到文件中,然后结束运行。
需要注意的是,由于是thumb模式,函数地址需要比真实地址+1.
运行截图如下
运行之后得到了两个文件
然后在将得到的这两个文件附加到IDA中,并映射到指定地址
然后继续往后看,看到sub_C98
函数,sub_c98函数中调用的函数都是调用的我们刚刚解压出来的数据中的函数,所以如果我们没有完成上一步的解压操作那么就无法继续往后分析。
不过很遗憾,这些函数相当难看,让人一头雾水,根据之前分析canon打印机的经验,在这段操作中应该会将另一部分固件解压缩到内存中的函数,但我翻了好几遍之后也没找到类似的函数,分析陷入僵局。
后面发现,程序调用了多次sub_A0008150
函数
这个函数有三个参数,主要功能就是将第一个参数的字符串和内存中的一块函数表的函数名进行对比,如果匹配就进行调用,函数表如下图所示
一开始我没有意识到
这些是函数指针,浪费了许多时间。
回看到最后一行代码
PrgStart
对应的函数如下图所示
看到sub_A45C函数
这个函数的i应该是一个整数,0x23c588,循环将0xA0850498开始,长度为0x23c588的内存清0
继续看到sub_A296函数
又看到了sub_A1E4这个解压缩函数,我们依然使用qiling进行模拟执行
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 52 53 54 55 from pwn import *context.arch='arm' context.endian='big' from qiling import Qilingfrom qiling.const import QL_VERBOSEfrom qiling.const import QL_ARCHfrom qiling.const import QL_ENDIANfrom qiling.const import QL_OSfrom unicorn import UC_PROT_ALLdecompress_func_call_addr=0x0000A297 def hook1 (ql:Qiling ): ql.arch.regs.arch_pc=decompress_func_call_addr return def hook2 (ql:Qiling ): length=ql.arch.regs.read('r2' ) ql.log.info("size:{}" .format (hex (length))) return def hook3 (ql:Qiling ): with open ("ram3.bin" ,"wb" ) as f: data=ql.mem.read(0xA0420000 ,0xdbe8 ) f.write(data) with open ("ram4.bin" ,"wb" ) as f: data=ql.mem.read(0xA042DBE8 ,0x1000 ) f.write(data) ql.stop() if __name__ == '__main__' : shellcode=asm(shellcraft.thumb.infloop()) ql=Qiling(code=shellcode,ostype=QL_OS.LINUX,archtype=QL_ARCH.ARM,endian=QL_ENDIAN.EB ,verbose=QL_VERBOSE.DEBUG,thumb=True ) with open ('./brother_7180.BIN' ,'rb' ) as f: firm=f.read() firm_size=len (firm) ql.log.info("firmware size: {}" .format (hex (firm_size))) ql.mem.map (0 ,firm_size,info="[flash]" ) ql.mem.write(0 ,firm) ql.mem.map (0xA0000000 ,0x10000000 ,info="[ram]" ) ql.mem.map (0xe0000000 ,0x10000000 ,info="[ram]" ) ql.mem.map (0x20000000 , 0x10000000 , info="[STACK]" ) for info_line in ql.mem.get_formatted_mapinfo(): ql.log.debug(info_line) ql.hook_address(hook1,0x011ff000 ) ql.hook_address(hook2,0x0000A2BC ) ql.hook_address(hook2,0x0000A2E4 ) ql.hook_address(hook3,0x0000A2E8 ) ql.arch.regs.arch_sp=0x2f000000 ql.run()
然后将生成的固件加载到IDA中,继续往下看
看到sub_A4D0函数
看到sub_A04210AC
函数
很明显的解压缩函数,这里实际上就是zlib的压缩方式
开头是压缩数据的长度,后面跟着的就是zlib的压缩数据。
这行代码猜测就是对上图中的zlib数据进行解压缩,然后加载到0xA042DBF4中
下面的 也是一样的功能,其实这两块zlib压缩数据是连在一起的,前一块zlib压缩数据的起始地址加上长度就能找到第二块zlib数据,第二块的长度为0x10f53,解压缩后将被加载到0xA08180DC
未完待续……