一次嵌入式固件逆向实践

文章目录
  1. 1. 0x0 简介
  2. 2. 0x01 确定加载基地址
    1. 2.1. 方法一.通过跳转表特征确定基地址
    2. 2.2. 方法二.通过字符串引用次数暴力搜索确定基地址
  3. 3. 0x02 IDA Pro反编译函数
    1. 3.1. 方法一.借用ghirda反汇编结果
    2. 3.2. 方法二.使用PowerPC函数序言prologue特征码
  4. 4. 0x03 通过sig优化库函数识别
  5. 5. 0x04 TODO
  6. 6. 0x05 参考

0x0 简介

本文已被看雪智能设备版本设置为精华文章:https://bbs.pediy.com/thread-266803.htm
获取到一个设备的固件,有6M,基本没在网上找到对该固件进行分析的文章,因此决定按照固件分析的一般思路对该固件进行逆向分析,实践学习下。主要用到的工具binwalk、ida pro 7.5、ghidra。首先使用binwalk获取固件基础信息,首先看下能不能提取出文件来:



失败,猜测可能是个单文件固件或者有没有可能是个加密的固件,但明文字符串信息分布比较均匀,应该不是加密固件,再使用-A选项看看固件中的字节码信息。



很明确了,扫描出来全是PowerPC大端指令,包括函数开始prologue、结束epilogue相关的指令。

0x01 确定加载基地址

首先使用32位ida pro对firmware进行反汇编,不过需要首先设定处理器类型,选择PowerPC big-endian。



然后一路点确定,很遗憾ida pro没有自动分析,没有一个函数被识别出来,试试binwalk扫描出的函数prologue地址0x2004,跳转到这个地址按C键反汇编,可以自动识别关联的一些函数,查看字符串窗口也无法查看到对字符串的引用,猜测可能有两个原因:

  • ida pro现在识别出的函数太少,很多函数还未识别出来,导致无引用
  • 加载基地址不正确,导致引用地址错误,无法形成交叉引用
    识别出来的函数太少,可以写一个脚本根据函数序言特征识别出函数头,再自动分析函数,特征码为94 21 FF ?? 7C 08 02 A6,binwalk中的识别码为0x7C0802A6(mflr r0),而ghidra可以直接自动分析。
1
2
ROM:000020EC 94 21 FF F8                             stwu      r1, back_chain(r1)       //开辟栈空间
ROM:000020F0 7C 08 02 A6 mflr r0 //


ghirda总共识别出来26000多个函数,应该识别出来差不多了,但是字符串仍然无法形成引用,要不然就是不正确,必须要解决基地址问题。

方法一.通过跳转表特征确定基地址

搜索powerpc固件加载基地址可以获取到一些信息,ppc_rebase运行可以得到一个基地址,但验证后发现不正确。通过参考其他资料,目前主要有四种固件基地址识别方法,具体参考ARM设备固件装载基址定位的研究_朱瑞瑾,然后通过学习PowerPC相关指令集PowerPC下C逆向指南,发现PowerPC switch语句汇编实现中存在一个跳转表,通过跳转表及函数语句地址之间的关系可以计算出PowerPC固件基地址,具体参考脚本。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#coding=utf-8
import os
import sys
import re
import struct
'''
get powerpc big endin base addr by switch case jmp_table
ROM:0000FAF4 28 03 00 07 cmplwi r3, 7
ROM:0000FAF8 54 63 10 3A slwi r3, r3, 2
ROM:0000FAFC 3D 83 00 07 addis r12, r3, 7
ROM:0000FB00 41 81 01 D0 bgt loc_FCD0
ROM:0000FB04 81 6C FB 10 lwz r11, -0x4F0(r12)
ROM:0000FB08 7D 69 03 A6 mtctr r11
ROM:0000FB0C 4E 80 04 20 bctr
ROM:0000FB0C # ---------------------------------------------------------------------------
ROM:0000FB10 00 06 FB 30 .long unk_6FB30
ROM:0000FB14 00 06 FB 54 .long unk_6FB54
ROM:0000FB18 00 06 FB 70 .long unk_6FB70
ROM:0000FB1C 00 06 FB A8 .long unk_6FBA8
ROM:0000FB20 00 06 FB C4 .long unk_6FBC4
ROM:0000FB24 00 06 FC 18 .long unk_6FC18
ROM:0000FB28 00 06 FC 34 .long unk_6FC34
ROM:0000FB2C 00 06 FC B8 .long unk_6FCB8
ROM:0000FB30 # ---------------------------------------------------------------------------
ROM:0000FB30 80 7F 01 68 lwz r3, 0x168(r31)
ROM:0000FB34 48 01 D3 85 bl sub_2CEB8
ROM:0000FB38 38 83 00 00 addi r4, r3, 0
ROM:0000FB3C 38 7F 00 00 addi r3, r31, 0

ida pro crtl+B "7D ?? 03 A6 4E 80 04 20" 匹配查找到类似代码
方法1:
bctr 根据ctr寄存器值跳转
mtctr r11 表示将r11的值加载到ctr寄存器
r11 = 0x70000+r3*4-0x4F0,可以计算得到 当r3为0时,r11为0x6FB10 则ctr寄存器值也为0x6FB10
则第一个跳转地址表实际所在的地址应该为0x6FB10, 0x6FB10 = base_addr + file_offset(0xFB10)
可以计算base_addr = 0x6FB10 - 0xFB10

方法2:
最后一个跳转地址后面应该是第一个case语句跳转地址,这里的文件偏移为0xFB30
在跳转地址表中找到最小的一个地址,这里为0x6FB30
实际这两个地址应该相等,则 base_addr = 0x6FB30 - 0xFB30

脚本实现的方法2,但是在某些固件中跳转地址表中的跳转地址不是绝对地址而是相对地址,脚本就无法通过方法二计算
需要手动根据方法1计算
'''


def get_ppc_base_by_switch_table(image_data, start_addr, max_gap=1<<16):
'''
通过跳转表首地址获取跳转地址表、第一个case语句地址
跳转地址表中的地址应该是紧凑的,有一个地址范围差max_gap,通过该条件可以获取到所有跳转地址
与最后一个跳转地址相邻的是第一个case语句的地址,如果基地址正确则该地址应该和跳转地址表中最小的地址相等
这里设置的基地址为0,则这两个地址之间的差值即为基地址
'''

offset = start_addr
gap = 0
jmp_table_addr = struct.unpack_from(">i", image_data, offset)[0]
if jmp_table_addr == 0:
return -1
jmp_table_addrs = []
while gap < max_gap:
jmp_table_addrs.append(jmp_table_addr)
offset = offset + 4
addr = struct.unpack_from(">i", image_data, offset)[0]
gap = abs(addr - jmp_table_addr)
jmp_table_addr = addr
jmp_table_addrs.sort()
file_loc1_addr = offset
true_loc1_addr = jmp_table_addrs[0]
ppc_base = true_loc1_addr - file_loc1_addr
return ppc_base

def get_switch_code_addrs(image_data):
'''
#ida 7D ?? 03 A6 4E 80 04 20
7D ?? 03 A6 mtctr rS
4E 80 04 20 bctr
通过switch语句字节码匹配查找固件中switch case跳转表首地址
'''

re_switch_opcode = b"\x7d.{1}\x03\xA6\x4E\x80\x04\x20"
bytes_data = bytearray(image_data)
re_pattern = re.compile(re_switch_opcode)
addrs = []
for match_obj in re_pattern.finditer(bytes_data):
addrs.append(match_obj.start()+8) #7D ?? 03 A6 4E 80 04 20 len = 8
return addrs

def ppc_base_count(ppc_bases):
freq_dict = {}
for ppc_base in ppc_bases:
freq_dict[ppc_base] = freq_dict.get(ppc_base, 0) +1
return freq_dict

def print_success(ppc_bases):
ppc_base_freq = ppc_base_count(ppc_bases)
ppc_base_freq = sorted(ppc_base_freq.items(), key = lambda kv:(kv[0], kv[1]))
for base in ppc_base_freq:
print('%#x:%d'%(base[0], base[1]))
print("The rebase address is:%#x"%ppc_base_freq[0][0])

def find_ppc_rebase(firmware_path):
f = open(firmware_path, "rb")
image_data = f.read()
f.close()
addrs = get_switch_code_addrs(image_data)
if len(addrs) == 0:
print("[-] error find switch table addrs")
return
ppc_bases = []
for addr in addrs:
ppc_base = get_ppc_base_by_switch_table(image_data, addr)
if ppc_base < 0:
continue
ppc_bases.append(ppc_base)
if len(ppc_bases) > 0:
print(firmware_path + " firmware base addr:\n")
print_success(ppc_bases)
else:
print("find rebase address failed, you can see the fllow addr use ida pro:")
for inx, val in enumerate(addrs):
if inx > 5:
break
print("%#x"%(val-16))
print("press key C, find addi ra,rb, eg:addi r9, r11, 0x71A4 # 0x271A4")
print("base = \"0x271A4\" - %#x" %addrs[0])


def usage():
print("ppc_rebase.py firmware_path")

def main():
if len(sys.argv) < 2:
usage()
else:
firmware_path = sys.argv[1]
if not os.path.exists(firmware_path):
usage()
else:
find_ppc_rebase(firmware_path)

if __name__ == "__main__":
main()



关键是通过7D ?? 03 A6 4E 80 04 20特征码获取switch语句地址,然后根据地址关系可以计算出固件基地址为0x60000,设置基地址后字符串可以正常引用



方法二.通过字符串引用次数暴力搜索确定基地址

大概原理是首先获取固件中的字符串地址,然后通过设置不同的基地址测试该基地址下字符串的引用次数,引用次数越高说明该地址为基地址的概率越大,有一定的通用性,在readme中说对查找ARM固件效果比较好,我测试了两个PowePC固件都能正确获取到基地址,不过需要设置好参数,特别是大小端、字符串长度范围。由于是暴力搜索计算出来的所以比较费CPU,计算一次至少半小时起步,项目地址:https://github.com/sgayou/rbasefind



0x02 IDA Pro反编译函数

设置正确的基地址后,ghirda基本可以正常进行静态反汇编分析了,也有伪代码功能,但是用得不熟悉、插件貌似也很少,还是习惯ida pro,但是ida不能自动反汇编,必须手动make code,下面通过两种方法使ida反编译函数。

方法一.借用ghirda反汇编结果

可以将ghirda反汇编得函数地址信息导出,然后使用脚本导入到ida中make code。
ghirda中导出函数列表方法:Window->Functions 在Functions窗口右键Export->Export to CSV保存。
ida中导入ghirda函数脚本如下:

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
#coding=utf-8
import csv
import ida_funcs
import ida_kernwin

'''
ida pro 7.5 python3
'''


def get_funcs_addr(csv_path):
starts = []
with open(csv_path, 'r') as file:
reader = csv.DictReader(file)
for row in reader:
start = int(row['Location'], 16)
starts.append(start)
return starts

def add_ida_funcs(starts):
for index, start in enumerate(starts):
ida_funcs.add_func(start)

def main():
file_path=ida_kernwin.ask_file(1, "*", "ghidra export functions csv file path")
starts = get_funcs_addr(file_path)
add_ida_funcs(starts)
print("[+] done")

if __name__ == "__main__":
main()

只需要填入ghirda导出的csv文件路径即可。



方法二.使用PowerPC函数序言prologue特征码

大部分编译器编译生成的函数头可能会有一些固定的指令,如x86平台的mov edi, edi;push ebp,这种情况在PowerPc也存在PowerPC特征码为stwu rS,rD(n);mflr r0,我们可以利用这个特征编写ida python脚本使ida开始自动反编译固件生成函数。

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
#coding=utf-8
import re
import ida_kernwin
import ida_funcs
import ida_ida

'''
ida pro 7.5 python3
'''


def find_func_prologue(file_path, pattern):
f = open(file_path, "rb")
image_data = f.read()
f.close()
bytes_data = bytearray(image_data)
re_pattern = re.compile(pattern)
addrs = []
for match_obj in re_pattern.finditer(bytes_data):
addrs.append(match_obj.start())
return addrs


def auto_make_function(prolog_addrs):
for addr in prolog_addrs:
ida_funcs.add_func(ida_ida.inf_get_min_ea() + addr)

def main():
ppc_prologue = b"\x94.{2}\xF8\x7C\x08\x02\xA6"
file_path=ida_kernwin.ask_file(1, "*", "firmware path")
addrs = find_func_prologue(file_path, ppc_prologue)
print("[+] find %d func prologue"%len(addrs))
auto_make_function(addrs)
print("[+] done")

if __name__ == "__main__":
main()

其实在github发现一个类似的脚本(https://github.com/maddiestone/IDAPythonEmbeddedToolkit/blob/master/define_code_functions.py),但是这个是针对ida 7.0编写的,本来想移植过来的,折腾了下最后还是自己写一个简单点。

0x03 通过sig优化库函数识别

由于固件文件并不像PE、ELF文件有导入表,ida中也没有内置的sig文件,所有的函数都必须靠自己人工识别,工作量太大了,不过强大的ida pro可以自己创建sig文件,经过一番折腾,可以识别libc中一些字符串处理的函数,这里列以下尝试了哪些方法。

  • 搜索获取powerpc相关sig库,https://github.com/IridiumXOR/uclibc-sig
  • 安装linux powerpc交叉编译库,提取lib
  • 根据固件中的Copyright string: “Copyright MGC 2004 - Nucleus PLUS - MPC860 Diab C/C++ v. 1.14”字符串,安装VxWorks Tornado开发环境,提取lib
    最后还是Tornado开发环境中提取的lib制作的sig有效,这里分享下Tornado.V2.2.POWERPC下载地址,我是xp环境才安装运行成功的,提取的路径如下C:\Tornado\host\diab\PPCCS
    效果如下:


0x04 TODO

由于并没有实际运行环境,这里并没有进行动态分析,分析起来难度较大,后续可能尝试能否使用qemu-system模式将固件运行起来进行动态调试。

0x05 参考

Zyxel设备eCos固件加载地址分析
ARM设备固件装载基址定位的研究_朱瑞瑾
PowerPC下C逆向指南
IOT设备逆向工程中的函数识别