一次嵌入式固件逆向实践
2021-04-10 18:58:00 Author: mp.weixin.qq.com(查看原文) 阅读量:249 收藏

本文为看雪论坛精华文章

看雪论坛作者ID:KenLi

0x0 简介

获取到一个设备的固件,有6M,基本没在网上找到对该固件进行分析的文章,因此决定按照固件分析的一般思路对该固件进行逆向分析,实践学习下。主要用到的工具binwalk、ida pro 7.5、ghidra。首先使用binwalk获取固件基础信息,首先看下能不能提取出文件来:
很明确了,扫描出来全是PowerPC大端指令,包括函数开始prologue、结束epilogue相关的指令。

0x01 确定加载基地址

首先使用32位ida pro对firmware进行反汇编,不过需要首先设定处理器类型,选择PowerPC big-endian。
然后一路点确定,很遗憾ida pro没有自动分析,没有一个函数被识别出来,试试binwalk扫描出的函数prologue地址0x2004,跳转到这个地址按C键反汇编,可以自动识别关联的一些函数,查看字符串窗口也无法查看到对字符串的引用,猜测可能有两个原因:
(1) ida pro现在识别出的函数太少,很多函数还未识别出来,导致无引用。
(2) 加载基地址不正确,导致引用地址错误,无法形成交叉引用。
识别出来的函数太少,可以写一个脚本根据函数序言特征识别出函数头,再自动分析函数,特征码为94 21 FF ?? 7C 08 02 A6,binwalk中的识别码为0x7C0802A6(mflr r0),而ghidra可以直接自动分析。
ROM:000020EC 94 21 FF F8                             stwu      r1, back_chain(r1)       //开辟栈空间ROM:000020F0 708 02 A6                             mflr      r0                       //
ghirda总共识别出来26000多个函数,应该识别出来差不多了,但是字符串仍然无法形成引用,要不然就是不正确,必须要解决基地址问题。

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

搜索powerpc固件加载基地址可以获取到一些信息,ppc_rebase:

https://github.com/ilovepp/ppc_rebase运行可以得到一个基地址,但验证后发现不正确。

通过参考其他资料,目前主要有四种固件基地址识别方法,具体参考ARM设备固件装载基址定位的研究_朱瑞瑾,然后通过学习PowerPC相关指令集PowerPC下C逆向指南,发现PowerPC switch语句汇编实现中存在一个跳转表,通过跳转表及函数语句地址之间的关系可以计算出PowerPC固件基地址,具体参考脚本。
#coding=utf-8import osimport sysimport reimport struct'''get powerpc big endin base addr by switch case jmp_tableROM:0000FAF4 28 03 00 07                             cmplwi    r3, 7ROM:0000FAF8 54 63 10 3A                             slwi      r3, r3, 2ROM:0000FAFC 3D 83 00 07                             addis     r12, r3, 7ROM:0000FB00 41 81 01 D0                             bgt       loc_FCD0ROM:0000FB04 81 6C FB 10                             lwz       r11, -0x4F0(r12)ROM:0000FB08 7D 69 03 A6                             mtctr     r11ROM:0000FB0C 4E 80 04 20                             bctrROM:0000FB0C                         # ---------------------------------------------------------------------------ROM:0000FB10 00 06 FB 30                             .long unk_6FB30ROM:0000FB14 00 06 FB 54                             .long unk_6FB54ROM:0000FB18 00 06 FB 70                             .long unk_6FB70ROM:0000FB1C 00 06 FB A8                             .long unk_6FBA8ROM:0000FB20 00 06 FB C4                             .long unk_6FBC4ROM:0000FB24 00 06 FC 18                             .long unk_6FC18ROM:0000FB28 00 06 FC 34                             .long unk_6FC34ROM:0000FB2C 00 06 FC B8                             .long unk_6FCB8ROM:0000FB30                         # ---------------------------------------------------------------------------ROM:0000FB30 80 7F 01 68                             lwz       r3, 0x168(r31)ROM:0000FB34 48 01 D3 85                             bl        sub_2CEB8ROM:0000FB38 38 83 00 00                             addi      r4, r3, 0ROM: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()

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

大概原理是首先获取固件中的字符串地址,然后通过设置不同的基地址测试该基地址下字符串的引用次数,引用次数越高说明该地址为基地址的概率越大,有一定的通用性。
在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函数脚本如下:
#coding=utf-8import csvimport ida_funcsimport 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()

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

大部分编译器编译生成的函数头可能会有一些固定的指令,如x86平台的mov edi, edi;push ebp,这种情况在PowerPc也存在PowerPC特征码为stwu rS,rD(n);mflr r0,我们可以利用这个特征编写ida python脚本使ida开始自动反编译固件生成函数。
#coding=utf-8import reimport ida_kernwinimport ida_funcsimport 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中一些字符串处理的函数,这里列以下尝试了哪些方法。
(1) 搜索获取powerpc相关sig库:
https://github.com/IridiumXOR/uclibc-sig
(2) 安装linux powerpc交叉编译库,提取lib。
(3) 根据固件中的Copyright string: "Copyright MGC 2004 - Nucleus PLUS - MPC860 Diab C/C++ v. 1.14"字符串,安装VxWorks Tornado开发环境,提取lib。

最后还是Tornado开发环境中提取的lib制作的sig有效,这里分享下Tornado.V2.2.POWERPC下载地址:
https://pan.baidu.com/s/17ulZN#list/path=%2F
我是xp环境才安装运行成功的,提取的路径如下C:\Tornado\host\diab\PPCCS

效果如下:

0x04 TODO

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

0x05 参考

1. Zyxel设备eCos固件加载地址分析

https://cq674350529.github.io/2021/03/04/Zyxel%E8%AE%BE%E5%A4%87eCos%E5%9B%BA%E4%BB%B6%E5%8A%A0%E8%BD%BD%E5%9C%B0%E5%9D%80%E5%88%86%E6%9E%90/

2. ARM设备固件装载基址定位的研究_朱瑞瑾

http://gb.oversea.cnki.net/KCMS/detail/detail.aspx?filename=1018812112.nh&dbcode=CDFD&dbname=CDFDREF

3. PowerPC下C逆向指南

https://bbs.pediy.com/thread-191928.htm

4. IOT设备逆向工程中的函数识别

http://blog.nsfocus.net/function-recognition-reverse-engineering-iot-equipment/

- End -

看雪ID:KenLi

https://bbs.pediy.com/user-home-627167.htm

  *本文由看雪论坛 KenLi 原创,转载请注明来自看雪社区。

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458383558&idx=1&sn=18508697479ea1fe115dc67283d1c026&chksm=b180c44c86f74d5a4e9d23d002e9d0768ea85a2cdd29063dcb91f66240c94ace6aeb1ec2e817#rd
如有侵权请联系:admin#unsafe.sh