首页
论坛
课程
招聘
[原创]Qiling框架模拟运行固件配合IDA动态调试
2022-10-23 14:22 13928

[原创]Qiling框架模拟运行固件配合IDA动态调试

2022-10-23 14:22
13928

前言

  • 实际分析嵌入式固件过程中,经常会遇到各种非Linux和非常见系统的固件,这类固件往往就是一个"裸"的二进制程序,而不像ELF和PE这些有特定结构的可执行程序,对于这类固件往往很难对它进行动态调试,而只能静态分析。最近一段时间,学习了很强大的Qiling框架,它除了可以模拟运行各种可执行程序,还提供了Debugger接口,于是乎我就想着能不能用Qiling配合IDA动态调试固件。遗憾的是网上关于Qiling的教程大部分都是模拟ELF和PE这些相对来说比较标准的程序,而模拟像这种就一个"裸"的二进制程序的资料却很少,这里分享下我的摸索过程

  • 关于Qiling和Unicorn这里就不多介绍了,详见官网

  • 下面以我上个帖子 一个简单的STM32固件分析 用到的固件stm32f103RCT6.bin来介绍如何用Qiling框架模拟运行指定函数并启用Debugger,然后使用IDA进行动态调试

开始

  • 首先时qiling官方github的有一个example引起了我的注意,如下图,qiling的示例有一个模拟arm下的uboot。uboot据我所知是一个bootloader,它在固件中就是一个"裸"的二进制程序,于是这里面可能就有我想要的样例

  • 查看hello_arm_uboot.py代码,一直拉到最后,如下图,这部分代码就是模拟运行"裸"的二进制程序的一个例子

  • 接下来就是照猫画虎,仿照着这个来尝试模拟运行stm32f103RCT6.bin固件中的XTEA函数。这里简单介绍下这个stm32f103RCT6.bin固件,它的加载基地址为0x8000000,从前一篇的分析可以知道它在地址0x800E288有个XTEA函数,这个函数就是接下来需要模拟调试运行的函数

  • 首先是导入qiling和unicorn的包

    1
    2
    3
    4
    5
    from qiling.core import *
    from qiling.const import *
    from qiling.os.const import *
    from unicorn.arm64_const import *
    from unicorn import *
  • 对照代码样例,读入需要模拟的固件

    1
    2
    3
    filepath='stm32f103RCT6.bin'
    with open(filepath, 'rb') as fp:
        fw = fp.read()
  • 接下来看原代码的Qiling对象生成方式,第一个code=uboot_code[0x40:],剔除了前0x40字节的原因应该是,uboot固件的前0x40字节不加载进内存,这里的stm32固件是整个都加载进内存的,所以可以直接传整个读入的fw。有个参数需要注意的是profile="uboot_bin.ql",看起来是还有一个配置文件"uboot_bin.ql"

  • 在同级目录下,可以找到这个uboot_bin.ql,那么接下来,需要简单理解下这个配置文件的各个参数的意义

  • 源码中搜索"heap_size",如下图,可以在qiling/loader/blob.py中找到关于这几个参数的含义

  • 根据上面代码可画出下图内存映射,"entry_point"这里为内存加载地址"load_address"而不是代码入口点,Qiling会根据entry_point和ram_size大小分配一块内存,然后将代码code写入,需要注意的是默认初始栈寄存器SP指向这块内存end_address - 0x1000的位置,如果模拟运行前不做修改,需要将ram_size预留出一定的栈空间的大小,不然往栈内存写数据时会覆盖code内存数据。堆内存heap的起始地址就是entry_point + ram_size,下图虚拟线表示默认不会直接映射堆内存,如果需要使用这块内存,需要先执行ql.os.heap.alloc(size)来使用

  • 接下来就可以生成配置文件了,代码如下

    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
    # 加载地址
    load_addr=0x8000000
     
    # 栈大小
    stack_size = 0x20000
     
    # 堆大小
    heap_size = 0x20000
     
    # 计算固件大小 0x1000对齐
    fw_size = math.ceil(len(fw)/0x1000)*0x1000
     
    # 初始分配的内存大小包括 固件和栈空间大小,+0x1000是为了使可用的栈大小与stack_size保持一致
    ram_size = fw_size + stack_size + 0x1000
     
    cfg_str = f"""
    [CODE]
    ram_size = {ram_size}
    entry_point = {load_addr}
    heap_size = {heap_size}
    [MISC]
    current_path = /
    """
    # 保存配置文件
    with open('ql-config.ql', 'w') as fp:
        fp.write(cfg_str)
  • 接着,仿照样例生成Qiling对象,代码如下,因为这里模拟运行的用到了thumb指令,所以需要指定参数thumb=True。因为这里默认模拟的是小端序,而固件恰好是小端序固件,所以可以不指定端序,但是如果模拟的为大端序的固件,则需要指定参数endian=QL_ENDIAN.EB。更多参数用法详见官方文档

    1
    ql = Qiling(code=fw, archtype="arm", ostype="blob", profile="ql-config.ql", thumb=True)
  • 定义模拟运行的起始地址和终止地址,这里因为只模拟运行sub_800E288函数,所以设为sub_800E288函数起始地址和终止地址即可

    1
    2
    begin =0x800E288
    end = 0x800E296

  • 因为模拟的sub_800E288函数有3个参数,所以还需要给函数传参

  • 这里简单介绍下ARM中常见的函数传参规范:对于函数参数不超过4个参数时用r0,r1,r2,r3寄存器来传参,对于函数参数超过4个参数时,前4个依旧用r0,r1,r2,r3寄存器传参,往后的参数以压入栈的方式传参。类似地函数如果有返回值,约定以r0寄存器返回。这些规范主要是为了不同程序或模块之间相互调用各自的函数而不出错,因为是规范,所以也就可以不遵循这个规范,比如说你自己用汇编写的程序的话,想怎么传参就怎么传参,只要你自己不限入混乱,程序不出错即可

  • 废话不多说,回到正题,这里获取unicorn对象来为函数传参(unicorn对象可以读写各个寄存器)。从上面可知,需要模拟的函数的3个参数都是指针,所以需要给r0,r1,r2 这3个寄存器写入3个内存地址,而初始SP寄存器(栈寄存器)和heap之间有块0x1000的内存没有用到,因此这里可以用sp+0x100,sp+0x200,sp+0x300,这3个地址作为函数参数传入。不过单单将3个地址写入r0,r1,r2寄存器还不够,还要在相应地内存写入数据,这样才是完整的传参过程。因为第3个参数是加密结果的输出,所以可以不往该地址写数据。如下代码,为r0传入密钥"BA 2F 96 A9 BA 2F 96 A9 BA 2F 96 A9 BA 2F 96 A9",为r1传入明文"10 BE 62 F8 E8 DC 34 46"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    sp = ql.arch.regs.sp
     
    uc=ql.uc
     
    #为第1个参数key传参
    uc.reg_write(UC_ARM_REG_R0, sp+0x100)
    uc.mem_write(sp+0x100, bytes.fromhex("BA 2F 96 A9 BA 2F 96 A9 BA 2F 96 A9 BA 2F 96 A9"))
     
    #为第2个参数in传参
    uc.reg_write(UC_ARM_REG_R1, sp+0x200)
    uc.mem_write(sp+0x200, bytes.fromhex("10 BE 62 F8 E8 DC 34 46"))
     
    #为第3个参数out传参
    uc.reg_write(UC_ARM_REG_R2, sp+0x300)
  • 然后是启用debugger,Qiling默认是不启用debugger的,如下代码,启用gdb debugger并监听相应IP和端口。详细说明见官方文档:https://docs.qiling.io/en/latest/debugger/

    1
    ql.debugger = 'gdb:0.0.0.0:9999'
  • 在qiling-1.4.4中,设置了debugger后且ostype为"blog"的情况下,直接运行ql.run会报"AttributeError: 'QlOsBlob' object has no attribute 'fs_mapper'"错

  • 在源码中找到了相应的描述,如下图,QlOsBlob类中有几个函数还没有实现,详见:qiling/os/blob/blob.py

  • 不过好在,经过测试,在执行ql.run之前,可以按以下方式,规避下这个问题。这段代码作用很简单,就是给个空壳给ql.os.fs_mapper。Python一个非常好的特性就是可以像下面这样,可以很容易地对各种库进行动态修改,而不用去修改库的原文件。下面这部份代码,等以后Qiling有相应的函数实现后就不在需要了

    1
    2
    3
    4
    5
    class MyMapper:
        def add_fs_mapping(self, ql_path, real_dest):
            pass
     
    ql.os.fs_mapper = MyMapper()
  • 重新运行后,出现如下信息,说明成功模拟运行,并启用了debugger并监听了相应的IP和端口。需要注意的是,如果在生成Qiling对象是指定了参数verbose=QL_VERBOSE.OFF,那么运行时不会有任何log信息

  • 接下来,介绍IDA中如何连接到这个server进行动态调试

  • IDA中选择Debugger > Select debugger或者快捷键F9,选择Remote GDB debugger

  • 选择Debugger > Process options

  • 输入运行Qiling机器的IP和端口,如果运行Qiling和IDA是同一机器同一系统内,则IP填127.0.0.1即可

  • 接着选择Debugger > Debugger options

  • 勾选如下两个即可

  • 然后选择Debugger > Manual memory regions

  • 按照下图,新建几个内存映射,否则调试时,IDA可能不能跳转到栈内存中,也不能查看栈内存的数据

  • 这里选择添加两个映射分别是栈内存和栈内存到堆内存之间的一小部分,堆内存(heap)因为这里没有用到,所以可以省略,code因为IDA分析的固件就是这部分内存,所以也可以省略

  • 最后选择Debugger > Attach to process,会出现一个PID为0的进程,点击OK即可

  • 最后如下图可以看到,IDA进入了调试模式,且停在sub_800E28函数开始的位置,接下来就可以使用IDA进行动态调试了

  • 调试结束后,可以回到Qiling中,读取相应地址的结果。从我上一个帖子可知预期的密文为"8C 79 F5 D1 5E A9 46 2D",如下图,输出与预期一致

    1
    ql.mem.read(sp+0x300, 8).hex()

最后


2022-10-27更新

  • qiling模拟thumb时,IDA下断点,停不下来,原因是IDA下的断点地址没有+1,但是qiling判断时却将当前地址+1了
  • 执行ql.run之前加入以下代码,可以动态解决这个问题,下面代码作用相当于注释掉qiling/debugger/gdb/utils.py中dbg_hook函数的前两行代码。同样地,待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
    56
    57
    58
    59
    60
    61
    import types
     
    def dbg_hook(self, ql, address: int, size: int):
        #if ql.arch.type == QL_ARCH.ARM and ql.arch.is_thumb:
        #        address += 1
     
        # resuming emulation after hitting a breakpoint will re-enter this hook.
        # avoid an endless hooking loop by detecting and skipping this case
        if address == self.last_bp:
            self.last_bp = None
     
        elif address in self.bp_list:
            self.last_bp = address
     
            ql.log.info(f'gdb> breakpoint hit, stopped at {address:#x}')
            ql.stop()
     
        # # TODO: not sure what this is about
        # if address + size == self.exit_point:
        #     ql.log.debug(f'{PROMPT} emulation entrypoint at {self.entry_point:#x}')
        #     ql.log.debug(f'{PROMPT} emulation exitpoint at {self.exit_point:#x}')
     
    def run(self, begin: Optional[int] = None, end: Optional[int] = None, timeout: int = 0, count: int = 0):
            """Start binary emulation.
     
            Args:
                begin   : emulation starting address
                end     : emulation ending address
                timeout : limit emulation to a specific amount of time (microseconds); unlimited by default
                count   : limit emulation to a specific amount of instructions; unlimited by default
            """
     
            # replace the original entry point, exit point, timeout and count
            self.entry_point = begin
            self.exit_point = end
            self.timeout = timeout
            self.count = count
     
            # init debugger (if set)
            debugger = select_debugger(self._debugger)
     
            if debugger:
                debugger = debugger(self)
                debugger.gdb.dbg_hook = types.MethodType(dbg_hook, debugger.gdb)
     
            # patch binary
            self.do_bin_patch()
     
            if self.baremetal:
                if self.count <= 0:
                    self.count = -1
     
                self.arch.run(count=self.count, end=self.exit_point)
            else:
                self.write_exit_trap()
                # emulate the binary
                self.os.run()
     
            # run debugger
            if debugger and self.debugger:
                debugger.run()
  • 附件代码已更新

[2023春季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2022-10-27 22:39 被烧板侠编辑 ,原因:
上传的附件:
收藏
点赞10
打赏
分享
最新回复 (13)
雪    币: 560
活跃值: 活跃值 (836)
能力值: ( LV4,RANK:44 )
在线值:
发帖
回帖
粉丝
Ysiel 活跃值 2022-10-23 21:17
2
0
逆二进制固件用unicorn会不会更好一点?qiling的优势在哪呢
雪    币: 2025
活跃值: 活跃值 (1028)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
烧板侠 活跃值 2 2022-10-23 22:29
3
0
Ysiel 逆二进制固件用unicorn会不会更好一点?qiling的优势在哪呢
优势在于qiling提供了debugger接口,可以使用IDA或者gdb进行动态调试。另一方面qiling是基于unicorn开发的,qiling中获取到unicorn对象很简单,完全可以把qiling当unicorn用
雪    币: 560
活跃值: 活跃值 (836)
能力值: ( LV4,RANK:44 )
在线值:
发帖
回帖
粉丝
Ysiel 活跃值 2022-10-23 23:47
4
0
烧板侠 优势在于qiling提供了debugger接口,可以使用IDA或者gdb进行动态调试。另一方面qiling是基于unicorn开发的,qiling中获取到unicorn对象很简单,完全可以把qilin ...
好的,谢谢解答,unicorn确实还得自己写调试器
雪    币: 4057
活跃值: 活跃值 (774)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wuzhouzcx 活跃值 2022-10-24 08:37
5
0

IDA不是有个模拟器插件啊。。。我都是直接调试运行。

最后于 2022-10-24 08:37 被wuzhouzcx编辑 ,原因:
雪    币: 14682
活跃值: 活跃值 (4035)
能力值: ( LV13,RANK:840 )
在线值:
发帖
回帖
粉丝
大帅锅 活跃值 4 2022-10-24 11:20
6
0
wuzhouzcx IDA不是有个模拟器插件啊。。。我都是直接调试运行。
Emu 是吧,不好用,unicorn 也能开发个ida调试的插件就好了
雪    币: 51
活跃值: 活跃值 (248)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
金奔腾 活跃值 2022-10-26 08:35
7
0
好文,楼主威武!
雪    币: 10267
活跃值: 活跃值 (11001)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2022-10-26 09:36
8
0
感谢分享
雪    币: 991
活跃值: 活跃值 (1732)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
方向感 活跃值 2022-10-26 14:52
9
0
ida中下断点,好像不能断下来吧。
雪    币: 2025
活跃值: 活跃值 (1028)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
烧板侠 活跃值 2 2022-10-26 18:51
10
0
方向感 ida中下断点,好像不能断下来吧。

thumb确实断不下来,之前arm测试完没问题就没测thumb下断点的情况,经过测试在python的site-packages目录中找到qiling/debugger/gdb/utils.py,把dbg_hook的前两行代码注释掉,IDA中调试thumb即可断点成功,不过这样改的话gdb调试thumb不知道会不会出现问题,如果只用IDA连接qiling的调试器可以暂时这么改

雪    币: 995
活跃值: 活跃值 (1902)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 活跃值 2022-10-26 22:44
11
0
感谢分享
雪    币: 991
活跃值: 活跃值 (1732)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
方向感 活跃值 2022-10-28 13:22
12
0
烧板侠 thumb确实断不下来,之前arm测试完没问题就没测thumb下断点的情况,经过测试在python的site-packages目录中找到qiling/debugger/gdb/utils.py,把db ...
还有qiling不支持ppc,看源码少一部分内容,unicorn已经支持ppc了。
雪    币: 1421
活跃值: 活跃值 (162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tearorca 活跃值 2022-11-9 17:23
13
0
fs_mapper缺少open大佬有没有遇到过
 with self.ql.os.fs_mapper.open('/proc/self/auxv', 'rb') as infile:
AttributeError: 'MyMapper' object has no attribute 'open'

和add_fs_mapping一样加了一个open还是报错
    with self.ql.os.fs_mapper.open('/proc/self/auxv', 'rb') as infile:
AttributeError: __enter__
雪    币: 2025
活跃值: 活跃值 (1028)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
烧板侠 活跃值 2 2022-11-9 21:01
14
0
tearorca fs_mapper缺少open大佬有没有遇到过 with self.ql.os.fs_mapper.open('/proc/self/auxv', 'rb') as infile: Attrib ...
你是gdb连接的调试端口?目前直接用gdb命令连接调试还是不能避免报错,gdb命令连接调试会用到那个功能,qiling对于ostype为'blob'还没有具体的实现,我帖子的代码也只是给了个空的实现,所以还是会出错。IDA连接这个调试端口是没有问题的,貌似IDA连接gdb并不会去调用那个接口,所以可以用文中的方法规避,你可以用IDA连接试试。或者可以试试这个基于unicorn开发的类gdb server:https://github.com/bet4it/udbserver,这个的用法可以参考原作者发在论坛的帖子:https://bbs.pediy.com/thread-272605.htm
游客
登录 | 注册 方可回帖
返回