Hexo
兄弟7180分析
Posted on: 2024-01-09 Edited on: 2024-03-14 In:  Views: 

1.固件提取

咸鱼上淘了一块板子,直接吹下flash提取,8pin SPI flash,16MB

2.固件分析

binwalk分析

image-20230729103800372

结果非常混乱,推测为RTOS固件

查看一下字节序

image-20230729104049274

大端序,再查看一下熵值

binwalk -E

image-20230730150705717

可以看到有几部分具有很高的熵值,可能是被压缩了,也可能是混淆状态。

被压缩的固件是无法直接运行的,固件中肯定存在某个部分是用于解压缩的,固件需要能够自举。

大的设备可能会有一个专门的flash用于存放bootloader进行初始化操作,其中就会包含解压缩或者解混淆等操作;而对于本设备只有一个flash,那么bootloader和业务逻辑的固件都是一起的。看到熵值图,其中也有不少熵值较低的。

直接用IDA打开固件

image-20230730152326616

IDA没有进行任何分析。注意,此时我们没有设置固件的加载地址,因此默认加载地址为0,在0地址处解析固件

image-20230730152934005

解析出了一些代码,不过这些代码是以0地址为基址解析出来的,不一定正确,需要进一步分析之后才能够确定。

0地址处是一个跳转指令,跳转到了0x40处执行,看到0x40处的函数

image-20230730154105496

这个函数中执行了大量的初始化操作,由于很多地址在IDA中没有映射,所以显示为红色

在最后面有几个函数

image-20230730160039485

首先看到sub_E54函数

image-20230730160106047

这个函数从0xA000BA78开始的0x6370个字节进行清空操作。

再看到sub_B46函数

image-20230730161217228

同样的函数调用了两次

看到sub_A94函数

image-20230730161323129

a2是0xA0001000,a3是0x7D6B,a1是0xF2C0xF2C指向的地址中存在大量数据

image-20230730161419895

这个函数执行的操作大致就是将0xF2C这个地址中的数据经过一系列操作后,存放到0xA0001000中,长度为0x7D6B

第二个sub_A94函数

image-20230730162225791

将0x8c9b处的数据处理后存放到0xA000B750,长度为unk_8C9A + (unk_8C99 << 8) + (unk_8C98 << 16) + (unk_8C97 << 24)

image-20230730162326899

经计算后得到长度为0x1c2.

我们需要想办法得到经sub_A94处理后的数据。通常而言,可以将处理数据的函数的伪代码抄下来稍微修改一下使其能够运行,不过由于这个函数有些复杂,修改伪c代码可能会导致函数失真,所以我采取另一种方法:使用qiling框架对代码进行部分模拟。

qiling框架是对unicorn的封装,用于模拟固件非常合适。

正常而言使用qiling模拟RTOS固件需要创建一个配置文件,用于指定各个区段,而且也不一定能运行成功;一种相对简单且成功率高的方法就是使用qiling运行一段目标固件架构的shellcode或者代码,将固件读取到内存中,添加区段映射,然后再从shellcode中跳转到固件中执行即可。

首先来确定我们要模拟的函数要从哪开始执行

image-20230730164222487

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 Qiling
from qiling.const import QL_VERBOSE
from qiling.const import QL_ARCH
from qiling.const import QL_ENDIAN
from qiling.const import QL_OS

decompress_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.

运行截图如下

image-20230730172412517

运行之后得到了两个文件

image-20230730165626572

然后在将得到的这两个文件附加到IDA中,并映射到指定地址

image-20230731203901787

然后继续往后看,看到sub_C98函数,sub_c98函数中调用的函数都是调用的我们刚刚解压出来的数据中的函数,所以如果我们没有完成上一步的解压操作那么就无法继续往后分析。

image-20230731204028500

不过很遗憾,这些函数相当难看,让人一头雾水,根据之前分析canon打印机的经验,在这段操作中应该会将另一部分固件解压缩到内存中的函数,但我翻了好几遍之后也没找到类似的函数,分析陷入僵局。

后面发现,程序调用了多次sub_A0008150函数

image-20230830171657691

这个函数有三个参数,主要功能就是将第一个参数的字符串和内存中的一块函数表的函数名进行对比,如果匹配就进行调用,函数表如下图所示

image-20230830171819827

一开始我没有意识到

image-20230830172004053

这些是函数指针,浪费了许多时间。

回看到最后一行代码

image-20230830172038973

PrgStart对应的函数如下图所示

image-20230830172233924

看到sub_A45C函数

image-20230830172410076

这个函数的i应该是一个整数,0x23c588,循环将0xA0850498开始,长度为0x23c588的内存清0

继续看到sub_A296函数

image-20230830172746289

又看到了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 Qiling
from qiling.const import QL_VERBOSE
from qiling.const import QL_ARCH
from qiling.const import QL_ENDIAN
from qiling.const import QL_OS
from unicorn import UC_PROT_ALL

decompress_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中,继续往下看

image-20230830173738021

看到sub_A4D0函数

image-20230830173937610

看到sub_A04210AC函数

image-20230830174005096

很明显的解压缩函数,这里实际上就是zlib的压缩方式

image-20230830200734337

开头是压缩数据的长度,后面跟着的就是zlib的压缩数据。

image-20230830200916526

这行代码猜测就是对上图中的zlib数据进行解压缩,然后加载到0xA042DBF4中

下面的image-20230830201201038也是一样的功能,其实这两块zlib压缩数据是连在一起的,前一块zlib压缩数据的起始地址加上长度就能找到第二块zlib数据,第二块的长度为0x10f53,解压缩后将被加载到0xA08180DC

未完待续……

--- 本文结束 The End ---