首页
社区
课程
招聘
[原创]Qiling框架模拟运行固件配合IDA动态调试
发表于: 2022-10-23 14:22 24155

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

2022-10-23 14:22
24155

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

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

Qiling官网: https://qiling.io/

Unicorn官网: https://www.unicorn-engine.org/

下面以我上个帖子 一个简单的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的包

对照代码样例,读入需要模拟的固件

接下来看原代码的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)来使用

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

接着,仿照样例生成Qiling对象,代码如下,因为这里模拟运行的用到了thumb指令,所以需要指定参数thumb=True。因为这里默认模拟的是小端序,而固件恰好是小端序固件,所以可以不指定端序,但是如果模拟的为大端序的固件,则需要指定参数endian=QL_ENDIAN.EB。更多参数用法详见官方文档

定义模拟运行的起始地址和终止地址,这里因为只模拟运行sub_800E288函数,所以设为sub_800E288函数起始地址和终止地址即可

因为模拟的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"

然后是启用debugger,Qiling默认是不启用debugger的,如下代码,启用gdb debugger并监听相应IP和端口。详细说明见官方文档:https://docs.qiling.io/en/latest/debugger/

在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有相应的函数实现后就不在需要了

重新运行后,出现如下信息,说明成功模拟运行,并启用了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",如下图,输出与预期一致

执行ql.run之前加入以下代码,可以动态解决这个问题,下面代码作用相当于注释掉qiling/debugger/gdb/utils.py中dbg_hook函数的前两行代码。同样地,待qiling后续版本修复这个问题后就不再需要这部分代码了

from qiling.core import *
from qiling.const import *
from qiling.os.const import *
from unicorn.arm64_const import *
from unicorn import *
from qiling.core import *
from qiling.const import *
from qiling.os.const import *
from unicorn.arm64_const import *
from unicorn import *
filepath='stm32f103RCT6.bin'
with open(filepath, 'rb') as fp:
    fw = fp.read()
filepath='stm32f103RCT6.bin'
with open(filepath, 'rb') as fp:
    fw = fp.read()
# 加载地址
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)
# 加载地址
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)
ql = Qiling(code=fw, archtype="arm", ostype="blob", profile="ql-config.ql", thumb=True)
ql = Qiling(code=fw, archtype="arm", ostype="blob", profile="ql-config.ql", thumb=True)
begin =0x800E288
end = 0x800E296
begin =0x800E288
end = 0x800E296
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)
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)
ql.debugger = 'gdb:0.0.0.0:9999'
ql.debugger = 'gdb:0.0.0.0:9999'
class MyMapper:
    def add_fs_mapping(self, ql_path, real_dest):
        pass
 
ql.os.fs_mapper = MyMapper()
class MyMapper:
    def add_fs_mapping(self, ql_path, real_dest):
        pass

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

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

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

最后于 2022-10-24 08:37 被wuzhouzcx编辑 ,原因:
2022-10-24 08:37
0
雪    币: 16495
活跃值: (6347)
能力值: ( LV13,RANK:923 )
在线值:
发帖
回帖
粉丝
6
wuzhouzcx IDA不是有个模拟器插件啊。。。我都是直接调试运行。
Emu 是吧,不好用,unicorn 也能开发个ida调试的插件就好了
2022-10-24 11:20
0
雪    币: 41
活跃值: (823)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
好文,楼主威武!
2022-10-26 08:35
0
雪    币: 13992
活跃值: (17371)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2022-10-26 09:36
0
雪    币: 1401
活跃值: (3801)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
9
ida中下断点,好像不能断下来吧。
2022-10-26 14:52
0
雪    币: 2028
活跃值: (1563)
能力值: ( LV7,RANK:144 )
在线值:
发帖
回帖
粉丝
10
方向感 ida中下断点,好像不能断下来吧。

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

2022-10-26 18:51
0
雪    币: 1777
活跃值: (3950)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
感谢分享
2022-10-26 22:44
0
雪    币: 1401
活跃值: (3801)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
12
烧板侠 thumb确实断不下来,之前arm测试完没问题就没测thumb下断点的情况,经过测试在python的site-packages目录中找到qiling/debugger/gdb/utils.py,把db ...
还有qiling不支持ppc,看源码少一部分内容,unicorn已经支持ppc了。
2022-10-28 13:22
0
雪    币: 1421
活跃值: (162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
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__
2022-11-9 17:23
0
雪    币: 2028
活跃值: (1563)
能力值: ( LV7,RANK:144 )
在线值:
发帖
回帖
粉丝
14
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
2022-11-9 21:01
0
游客
登录 | 注册 方可回帖
返回
//