首页
社区
课程
招聘
Unidbg的底层支持-Unicorn
发表于: 2021-10-25 17:41 32060

Unidbg的底层支持-Unicorn

2021-10-25 17:41
32060

https://github.com/unicorn-engine/unicorn

Unicorn的官网简介很简单明了:Unicorn是一个基于Qemu的轻量级的多平台、多架构的 CPU 模拟器框架。一句话让我们明白了它是做什么的。在Unidbg对一个Elf文件进行模拟执行的时候,我们一般是在跨平台运行的,所以就底层就需要一个模拟器Backend。其中一种就是Unicorn,下面我们就来看看Unicorn的使用

我们使用一个例子,就可以讲述Unicorn的使用。

Unicorn是开源的,可自行编译。凯神已经编译好了,可以直接使用

我们先来写一段简单的Thumb汇编代码

这三行汇编如果被CPU执行,结束后R0寄存器的值应该为5对吧。那么我们就看一下用Unicorn如何来模拟执行这三条指令

Unicorn是不认识汇编代码的,它跟CPU一样,只认识机器码,所以我们可以去下面的网站在线转换成机器码:

https://armconverter.com/

转换成机器码后

我们来看下执行结果, 符合我们的预期

对于上面代码的补充:在mem_map的时候,参数一起始地址必须是1K(32位下)/4K(64位下)对齐的,否则会抛出Invalid argument (UC_ERR_ARG)异常,参数三的操作权限可参考下表:

如果我们执行一段有跳转的汇编(后面我们测试也使用这段汇编代码)

那么我们再来看BlockHook的结果

我们执行上面的汇编指令,输出结果。就说明在0x1200地址上,有对内存的读取操作

MemHook就是ReadHook跟WriteHook的结合体,就不单独介绍了

加一条 svc #0 指令,产生异常

我们加段指令,来读取0x2000(此处未map)的内容

接下来我们介绍下Keystone跟Capstone这对兄弟,先来看Keystone。

上面我们使用在线网站进行汇编代码与机器码的相互转换,使用起来非常的麻烦,我们就可以借助Keystone这个项目来帮助我们对汇编代码,直接转换成机器码

Capstone跟Keystone是相反的

我们跟CodeHook结合来看下Capstone如何使用,也顺便看下它们的结合使用

上面我们介绍了Unicorn的详细使用,有了Unicorn的加持,我们就可以模拟Arm架构的指令集,从而执行So中的各种指令,当然多条指令组合成的函数都是可以的。我们可以看到Unicorn本身提供了丰富的API供我们使用,它本身提供的Hook,覆盖了我们需要实现的所有场景。最后介绍了Keystone跟Capstone这对兄弟,那么本次的分享就结束啦,对本块内容有兴趣的朋友可以加个VX一起学习呀: roy5ue

 
<dependency>
    <groupId>com.github.zhkl0228</groupId>
    <artifactId>unicorn</artifactId>
    <version>1.0.12</version>
</dependency>
<dependency>
    <groupId>com.github.zhkl0228</groupId>
    <artifactId>unicorn</artifactId>
    <version>1.0.12</version>
</dependency>
movs r0, #3
movs r1, #2
add  r0, r1
movs r0, #3
movs r1, #2
add  r0, r1
 
 
0x0320
0x0221
0x0844
0x0320
0x0221
0x0844
public class UnicornTest {
 
    long BASE = 0x1000;
 
    @Test
    public void test(){
        // 先来定义我们的机器码
        byte[] code = new byte[]{
            0x03,0x20,
            0x02,0x21,
            0x08,0x44};
 
        // 创建 Unicorn 对象, 它就像一个CPU, 我们可以使用它的接口来操作这个CPU
        // 参数一:架构
        // 参数二:运行模式(在ARM、Thumb中无关紧要,最终还是靠运行时判断)
        Unicorn unicorn = new Unicorn(UnicornConst.UC_ARCH_ARM,UnicornConst.UC_MODE_THUMB);
 
        // mem_map 进行内存映射,在使用Unicorn提供的内存时,必须先进行映射
        // 参数一:起始地址
        // 参数二:映射内存区域的大小
        // 参数三:内存操作权限
        unicorn.mem_map(BASE,0x1000,UnicornConst.UC_PROT_WRITE | UnicornConst.UC_PROT_READ | UnicornConst.UC_PROT_EXEC);
 
        // mem_write 可以进行对映射出的内存进行写入操作
        // 参数一:写入的地址
        // 参数二:写入的内容
        unicorn.mem_write(BASE,code);
 
        // emu_start 开始执行CPU
        // 参数一:开始执行的地址(Thumb指令地址+1)
        // 参数二:结束地址(当结束地址命中时,结束执行)
        // 参数三:超时时间(ms),当该值为0时,Unicorn将在无限时间内模拟代码,直到模拟完成
        unicorn.emu_start(BASE+1,BASE+code.length,0,0);
 
        // 此时三条Thumb指令已经模拟完成
        // reg_read 可以读取寄存器,相应的reg_write可以写入寄存器的值
        // 参数:寄存器常量
        Long o = (Long) unicorn.reg_read(ArmConst.UC_ARM_REG_R0);
 
        System.out.println("the emulate finished result is ==> "+o.intValue());
 
    }
}
public class UnicornTest {
 
    long BASE = 0x1000;
 
    @Test
    public void test(){
        // 先来定义我们的机器码
        byte[] code = new byte[]{
            0x03,0x20,
            0x02,0x21,
            0x08,0x44};
 
        // 创建 Unicorn 对象, 它就像一个CPU, 我们可以使用它的接口来操作这个CPU
        // 参数一:架构
        // 参数二:运行模式(在ARM、Thumb中无关紧要,最终还是靠运行时判断)
        Unicorn unicorn = new Unicorn(UnicornConst.UC_ARCH_ARM,UnicornConst.UC_MODE_THUMB);
 
        // mem_map 进行内存映射,在使用Unicorn提供的内存时,必须先进行映射
        // 参数一:起始地址
        // 参数二:映射内存区域的大小
        // 参数三:内存操作权限
        unicorn.mem_map(BASE,0x1000,UnicornConst.UC_PROT_WRITE | UnicornConst.UC_PROT_READ | UnicornConst.UC_PROT_EXEC);
 
        // mem_write 可以进行对映射出的内存进行写入操作
        // 参数一:写入的地址
        // 参数二:写入的内容
        unicorn.mem_write(BASE,code);
 
        // emu_start 开始执行CPU
        // 参数一:开始执行的地址(Thumb指令地址+1)
        // 参数二:结束地址(当结束地址命中时,结束执行)
        // 参数三:超时时间(ms),当该值为0时,Unicorn将在无限时间内模拟代码,直到模拟完成
        unicorn.emu_start(BASE+1,BASE+code.length,0,0);
 
        // 此时三条Thumb指令已经模拟完成
        // reg_read 可以读取寄存器,相应的reg_write可以写入寄存器的值
        // 参数:寄存器常量
        Long o = (Long) unicorn.reg_read(ArmConst.UC_ARM_REG_R0);
 
        System.out.println("the emulate finished result is ==> "+o.intValue());
 
    }
}
the emulate finished result is ==> 5
the emulate finished result is ==> 5
public static final int UC_PROT_NONE = 0;
public static final int UC_PROT_READ = 1;
public static final int UC_PROT_WRITE = 2;
public static final int UC_PROT_EXEC = 4;
public static final int UC_PROT_ALL = 7;
public static final int UC_PROT_NONE = 0;
public static final int UC_PROT_READ = 1;
public static final int UC_PROT_WRITE = 2;
public static final int UC_PROT_EXEC = 4;
public static final int UC_PROT_ALL = 7;
// hook_add 添加一个Hook(后面的Hook参数通用,只有第一个不同)
// 参数一:Hook回调
// 参数二:Hook起始地址
// 参数三:Hook结束地址
// 参数四:自定义参数,可以在Hook的回调中拿到,也就是 Object user
unicorn.hook_add(new CodeHook() {
        /* CodeHook将注册UC_HOOK_CODE类型的Hook,它将在Unicorn对起始地址跟结束地址中间每一条指令的执行时进行调用,当结束地址 < 起始地址时,则将对每一条指令执行时进行调用,相当于Trace*/
        /* 回调函数有当前Unicorn对象,所以我们可以基于此对象做关于CPU的任何事情*/
        @Override
        public void hook(Unicorn u, long address, int size, Object user) {
            System.out.print(String.format(">>> Tracing instruction at 0x%x, instruction size = 0x%x\n", address, size));
        }
    },0,-1,null);
// hook_add 添加一个Hook(后面的Hook参数通用,只有第一个不同)
// 参数一:Hook回调
// 参数二:Hook起始地址
// 参数三:Hook结束地址
// 参数四:自定义参数,可以在Hook的回调中拿到,也就是 Object user
unicorn.hook_add(new CodeHook() {
        /* CodeHook将注册UC_HOOK_CODE类型的Hook,它将在Unicorn对起始地址跟结束地址中间每一条指令的执行时进行调用,当结束地址 < 起始地址时,则将对每一条指令执行时进行调用,相当于Trace*/
        /* 回调函数有当前Unicorn对象,所以我们可以基于此对象做关于CPU的任何事情*/
        @Override
        public void hook(Unicorn u, long address, int size, Object user) {
            System.out.print(String.format(">>> Tracing instruction at 0x%x, instruction size = 0x%x\n", address, size));
        }
    },0,-1,null);
>>> Tracing instruction at 0x1000, instruction size = 0x2
>>> Tracing instruction at 0x1002, instruction size = 0x2
>>> Tracing instruction at 0x1004, instruction size = 0x2
the emulate finished result is ==> 5
>>> Tracing instruction at 0x1000, instruction size = 0x2
>>> Tracing instruction at 0x1002, instruction size = 0x2
>>> Tracing instruction at 0x1004, instruction size = 0x2
the emulate finished result is ==> 5
/* BlockHook将注册UC_HOOK_BLOCK类型的Hook,当输入的基本块且基本块的地址(BB)在起始地址<=BB<=结束地址范围内时,将调用已注册的回调函数。当结束地址 < 起始地址时,输入任何基本块时,将调用回调*/
/* 基本块:在Unicorn中,基本块就相当于未发生跳转的所有指令为一基本块,发生跳转就是另一块*/
unicorn.hook_add(new BlockHook() {
            @Override
            public void hook(Unicorn u, long address, int size, Object user) {
                System.out.print(String.format(">>> Tracing basic block at 0x%x, block size = 0x%x\n", address, size));
            }
        }, 0, -1, null);
/* BlockHook将注册UC_HOOK_BLOCK类型的Hook,当输入的基本块且基本块的地址(BB)在起始地址<=BB<=结束地址范围内时,将调用已注册的回调函数。当结束地址 < 起始地址时,输入任何基本块时,将调用回调*/
/* 基本块:在Unicorn中,基本块就相当于未发生跳转的所有指令为一基本块,发生跳转就是另一块*/
unicorn.hook_add(new BlockHook() {
            @Override
            public void hook(Unicorn u, long address, int size, Object user) {
                System.out.print(String.format(">>> Tracing basic block at 0x%x, block size = 0x%x\n", address, size));
            }
        }, 0, -1, null);
>>> Tracing basic block at 0x1000, block size = 0x6
the emulate finished result is ==> 5
>>> Tracing basic block at 0x1000, block size = 0x6
the emulate finished result is ==> 5
0x1000: movs r0, #3
0x1002: movs r1, #2
0x1004: add r0, r1
0x1006: bl #0x1100
0x100a: add r0, r1
...
0x1100: add r0, r1
0x1102: movs.w r2, #0x1200
0x1106: str.w lr, [r2]
0x110a: ldr.w pc, [r2]
...
0x1000: movs r0, #3
0x1002: movs r1, #2
0x1004: add r0, r1
0x1006: bl #0x1100
0x100a: add r0, r1
...
0x1100: add r0, r1
0x1102: movs.w r2, #0x1200
0x1106: str.w lr, [r2]
0x110a: ldr.w pc, [r2]
...
>>> Tracing basic block at 0x1000, block size = 0xa
>>> Tracing basic block at 0x1100, block size = 0xe
>>> Tracing basic block at 0x100a, block size = 0x2
the emulate finished result is ==> 9
>>> Tracing basic block at 0x1000, block size = 0xa
>>> Tracing basic block at 0x1100, block size = 0xe
>>> Tracing basic block at 0x100a, block size = 0x2
the emulate finished result is ==> 9
/* ReadHook注册类型为UC_HOOK_MEM_READ的Hook,只要在地址范围起始地址<=read_addr<=结束地址内执行内存读取,就会调用已注册的回调函数。当结束地址 < 起始地址时,将为所有内存读取调用回调*/
/* 注意:这个内存读取必须由指令进行,如果使用Unicorn的api mem_read进行读取内存,是不会进行回调的*/
unicorn.hook_add(new ReadHook() {
    @Override
    public void hook(Unicorn u, long address, int size, Object user) {
        byte[] bytes = u.mem_read(address, size);
        System.out.print(String.format(">>> Memory read at 0x%x, block size = 0x%x, value is = 0x%s\n", address, size, Integer.toHexString(bytes[0] & 0xff)));
    }
}, 0, -1, null);
/* ReadHook注册类型为UC_HOOK_MEM_READ的Hook,只要在地址范围起始地址<=read_addr<=结束地址内执行内存读取,就会调用已注册的回调函数。当结束地址 < 起始地址时,将为所有内存读取调用回调*/
/* 注意:这个内存读取必须由指令进行,如果使用Unicorn的api mem_read进行读取内存,是不会进行回调的*/
unicorn.hook_add(new ReadHook() {
    @Override
    public void hook(Unicorn u, long address, int size, Object user) {
        byte[] bytes = u.mem_read(address, size);

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

最后于 2021-10-25 18:48 被r0ysue编辑 ,原因:
收藏
免费 8
支持
分享
最新回复 (7)
雪    币: 3277
活跃值: (1992)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
为什么人世间尽然会有如此美妙精华绝伦的好文章。
2021-10-26 00:23
0
雪    币: 852
活跃值: (9821)
能力值: ( LV13,RANK:385 )
在线值:
发帖
回帖
粉丝
3
感谢分享.学习了. 确实不错.
2021-10-26 09:11
0
雪    币: 180
活跃值: (1313)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
4
力挺,点赞,强烈支持,楼主威武
2021-10-26 10:34
0
雪    币: 351
活跃值: (80)
能力值: ( LV9,RANK:210 )
在线值:
发帖
回帖
粉丝
5
。。看不懂,但不影响收藏
2021-10-26 21:18
0
雪    币: 116
活跃值: (1012)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
收藏从未停止 学习从未开始
2021-10-27 11:38
0
雪    币: 669
活跃值: (1652)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
7
收藏=会了
2021-11-10 11:52
0
雪    币: 261
活跃值: (547)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
8
Keystone跟Capstone这对兄弟
2022-1-7 05:00
0
游客
登录 | 注册 方可回帖
返回
//