查看原文
其他

Unicorn 在 Android 的应用之Hello World

无名侠 看雪学院 2019-09-17

本文为看雪论坛精华文章
看雪论坛作者ID:无名侠


Unicorn 是一款非常优秀的跨平台模拟执行框架,该框架可以跨平台执行Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集的原生程序。

Unicorn 不仅仅是模拟器,更是一种“硬件级”调试器,使用Unicorn的API可以轻松控制CPU寄存器、内存等资源,调试或调用目标二进制代码,现有的反调试手段对Unicorn 几乎是无效的。

目前国内的Unicorn 学习资料尚少,防御手段也稀缺,官方入门教程虽短小精悍缺无法让你快速驾驭强大的Unicorn,故写这一些列文章。

这几篇文章将带你学习Unicorn 框架并开发一款支持JNI的原生程序调用框架、o-llvm 还原脚本、静态脱壳机等。
 
分析基于https://github.com/AeonLucid/AndroidNativeEmu 开源项目做分析。本人能力微薄,冒昧对此项目进行完善,目前已经实现更多的JNI 函数和syscall,完善mmap映射文件等。

参考我的项目:
https://github.com/Chenyuxin/AndroidNativeEmu。
 
我由衷地感谢AndroidNativeEmu 原作者提供函数hook及模拟JNI的思路,我曾日思夜想如何优雅地模拟JNI,没想到该项目的实现方式竟然十分优雅。
 
我希望通过这一系列的文章,让更多的人学习Unicorn 框架,学习如何模拟调用,也希望厂商重视对Unicorn的检测!实践中,这都9102年了,我发现目前仍有加固产品能用Unicorn 跑出dex文件!

这一些列的文章,不仅会学习Unicorn,还会学习到优秀的反汇编框架Capstone、汇编框架Keystone。
 

应用场景


Windows & Linux 跨平台调用Android Native 程序、Api监控、病毒分析、获取Code Coverage、加固方案分析、反混淆等。安全防御方面,简化Unicorn,魔改Unicorn,甚至可以打造一款让逆向工作者感觉云里雾里代码保护器。这里列出的应用场景只是冰山一角!
 
分析360加固的时候使用Unicorn , 反调试清晰可见


检查status和tcp 文件,只需模拟文件系统,就可以绕过。
 
360加固寻找解压缩函数地址的操作

 
Native 动态注册

 
JNI操作一览无余

 
dump 某加固dex



Unicorn 快速入门


多架构

Unicorn 是一款基于qemu模拟器的模拟执行框架,支持Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)等指令集。

多语言

Unicorn 为多种语言提供编程接口比如C/C++、Python、Java 等语言。Unicorn的DLL 可以被更多的语言调用,比如易语言、Delphi,前途无量。

多线程安全

Unicorn 设计之初就考虑到线程安全问题,能够同时并发模拟执行代码,极大的提高了实用性。


虚拟内存

Unicorn 采用虚拟内存机制,使得虚拟CPU的内存与真实CPU的内存隔离。Unicorn 使用如下API来操作内存:

uc_mem_map
uc_mem_read
uc_mem_write


使用uc_mem_map映射内存的时候,address 与 size 都需要与0x1000对齐,也就是0x1000的整数倍,否则会报UC_ERR_ARG 异常。如何动态分配管理内存并实现libc中的malloc功能将在后面的课程中讲解。



Hook 机制



Unicorn的Hook机制为编程控制虚拟CPU提供了便利。


Unicorn 支持多种不同类型的Hook。


大致可以分为(hook_add第一参数,Unicorn常量):


指令执行类

UC_HOOK_INTR
UC_HOOK_INSN
UC_HOOK_CODE
UC_HOOK_BLOCK


内存访问类

UC_HOOK_MEM_READ
UC_HOOK_MEM_WRITE
UC_HOOK_MEM_FETCH
UC_HOOK_MEM_READ_AFTER
UC_HOOK_MEM_PROT
UC_HOOK_MEM_FETCH_INVALID
UC_HOOK_MEM_INVALID
UC_HOOK_MEM_VALID


异常处理类

UC_HOOK_MEM_READ_UNMAPPED
UC_HOOK_MEM_WRITE_UNMAPPED

UC_HOOK_MEM_FETCH_UNMAPPED


调用hook_add函数可添加一个Hook。Unicorn的Hook是链式的,而不是传统Hook的覆盖式,也就是说,可以同时添加多个同类型的Hook,Unicorn会依次调用每一个handler。hook callback 是有作用范围的(见hook_add begin参数)。


python包中的hook_add函数原型如下:

def hook_add(self, htype, callback, user_data=None, begin=1, end=0, arg1=0):
    pass


> htype 就是Hook的类型,callbackhook回调用;
callback 是Hook的处理handler指针。请注意!不同类型的hook,handler的参数定义也是不同的;
user_data 附加参数,所有的handler都有一个user_data参数,由这里传值;
begin hook 作用范围起始地址;
end hook 作用范围结束地址,默认则作用于所有代码。


Hook callback

不同类型的hook,对应的callback的参数也不相同,这里只给出C语言定义。


Python 编写callback的时候参考C语言即可(看参数)。


UC_HOOK_CODE & UC_HOOK_BLOCK 的 callback 定义:

typedef void (*uc_cb_hookcode_t)(uc_engine *uc, uint64_t address, uint32_t size, void *user_data);

address:当前执行的指令地址
size:当前指令的长度,如果长度未知,则为0
user_data:hook_add 设置的user_data参数


READ, WRITE & FETCH 的 callback 定义:

typedef void (*uc_cb_hookmem_t)(uc_engine *uc, uc_mem_type type,
        uint64_t address, int size, int64_t value, void *user_data)
;


type:内存操作类型 READ, or WRITE
address:当前指令地址
size:读或写的长度
value:写入的值(type = read时无视)
user_data:hook_add 设置的user_data参数


invalid memory access events (UNMAPPED and PROT events) 的 callback 定义

typedef bool (*uc_cb_eventmem_t)(uc_engine *uc, uc_mem_type type,
    uint64_t address, int size, int64_t value, void *user_data)
;


ype:内存操作类型 READ, or WRITE
address:当前指令地址
size:读或写的长度
value:写入的值(type = read时无视)
user_data:hook_add 设置的user_data参数
返回值
返回真,继续模拟执行
返回假,停止模拟执行



编译与安装 Unicorn


Unicorn 支持多种编译和安装方式,本课程以Linux作为学习环境,Python3作为主要语言。Linux 下可以直接使用pip安装,方便快捷。


更多详细安装过程可以参考官方安装教程



Unicorn 的 Hello World


在python中导入Unicorn库

from unicorn import *


导入处理器相关的常量


Unicorn 支持多种不同的CPU指令集,每一种指令集都有自己独立的寄存器, Unicorn使用统一API管理多种不同的CPU指令集,并将寄存器名字映射成数字常量。


from unicorn.arm_const import *
from unicorn.arm64_const import *
from unicorn.m68k_const import *
from unicorn.mips_const import *
from unicorn.sparc_const import *
from unicorn.x86_const import *


寄存器常量命名规则:

  • UC_ + 指令集 + REG + 大写寄存器名
  • UC_ARMREG + 大写寄存器名(UC_ARM_REG_R0)
  • UC_X86REG + 大写寄存器名(UC_X86_REG_EAX)
 

本课程以python3 + arm指令集为例子,导入arm的常量


from unicorn import *
from unicorn.arm_const import *


模拟执行的代码


为了简单起见,我们直接将要执行代码的数据硬编码。


ARM_CODE   = b"\x37\x00\xa0\xe3\x03\x10\x42\xe0"
# mov r0, #0x37;
# sub r1, r2, r3


创建一个UC对象并设置异常处理


# Test ARM
def test_arm():
    print("Emulate ARM code")
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM, UC_MODE_ARM)
        # 其它代码添加到此处
    except UcError as e:
        print("ERROR: %s" % e)


Uc 是unicorn的主类,Uc对象则代表了一个独立的虚拟机实例,它有独立的寄存器和内存等资源,不同Uc对象之间的数据是独立的。Uc的构造函数有两个参数 arch 和 mode,用来指定模拟执行的指令集和对应的位数或模式。


arch常量参数一般以 UCARCH 开头,MODE常量以UCMODE 开头。

 

同一种指令集可以有多种模式,比如x86可以同时运行32位和16位的汇编,arm也有arm模式和Thumb模式,它们是向下兼容的,并可以通过特殊指令来切换CPU运行模式。调用构造函数时的模式(mode)以第一条执行指令的模式为准。


映射内存


想用Unicorn模拟执行代码,是不能将代码字节流直接以参数形式传递给Unicorn,而是将要执行的代码写入到Unicorn 的虚拟内存中。Uc 虚拟机实例初始内存是没有任何映射的,在读写内存之前使用uc_mem_map函数映射一段内存。


# map 2MB memory for this emulation
ADDRESS = 0x10000
mu.mem_map(ADDRESS, 2 * 0x10000)


这段代码在内存地址0x10000处映射了一段大小为2M的内存。mem_map函数特别娇气,要求 address 和 size 参数都与0x1000对齐,否则会报UC_ERR_ARG异常。


写入代码


我们要执行代码,就需要将欲执行代码的字节数据写入到虚拟机内存中。


mu.mem_write(ADDRESS, ARM_CODE)


mem_write的第二个参数也很娇气,只支持python的byte数组,不能是String或者bytearray。


给寄存器赋值


mu.reg_write(UC_ARM_REG_R0, 0x1234)
mu.reg_write(UC_ARM_REG_R2, 0x6789)
mu.reg_write(UC_ARM_REG_R3, 0x3333)


添加指令级的Hook


这个有点像单步调试的感觉。


mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)


在begin...end范围内的每一条指令被执行前都会调用callback。


让我们来看看hook_code 的实现吧:


# callback for tracing instructions
def hook_code(uc, address, size, user_data):
    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))


这段代码仅打印指令执行的地址和长度信息。实际应用中可配合capstone反汇编引擎玩一些更骚的操作。

 

UC_HOOK_CODE的callback中可以修改PC或EIP等寄存器力来改变程序运行流程。实际上,unicorn调试器的单步调试就是以这个为基础实现的。


开机


原谅我用开机这个词汇吧!我们已经映射内存并将数据写入到内存,并设置好执行Hook以监视指令是否正常执行,但是虚拟机还没有启动!


# emulate machine code in infinite time
mu.emu_start(ADDRESS, ADDRESS + len(ARM_CODE))


emu_start 可以通过timeout参数设置最长执行时长,防止线程死在虚拟机里面。原型如下

def emu_start(self, begin, until, timeout=0, count=0):
    pass


emu_start 执行完成后,可以通过读取内存或寄存器的方式来获取执行结果。


获取结果


r0 = mu.reg_read(UC_ARM_REG_R0)
r1 = mu.reg_read(UC_ARM_REG_R1)
print(">>> R0 = 0x%x" % r0)
print(">>> R1 = 0x%x" % r1)


完整代码


from unicorn import *
from unicorn.arm_const import *
ARM_CODE = b"\x37\x00\xa0\xe3\x03\x10\x42\xe0"
# mov r0, #0x37;
# sub r1, r2, r3
# Test ARM
 
# callback for tracing instructions
def hook_code(uc, address, size, user_data):
    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
 
def test_arm():
    print("Emulate ARM code")
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
 
        # map 2MB memory for this emulation
        ADDRESS = 0x10000
        mu.mem_map(ADDRESS, 2 * 0x10000)
        mu.mem_write(ADDRESS, ARM_CODE)
 
        mu.reg_write(UC_ARM_REG_R0, 0x1234)
        mu.reg_write(UC_ARM_REG_R2, 0x6789)
        mu.reg_write(UC_ARM_REG_R3, 0x3333)
 
        mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)
        # emulate machine code in infinite time
        mu.emu_start(ADDRESS, ADDRESS + len(ARM_CODE))
        r0 = mu.reg_read(UC_ARM_REG_R0)
        r1 = mu.reg_read(UC_ARM_REG_R1)
        print(">>> R0 = 0x%x" % r0)
        print(">>> R1 = 0x%x" % r1)
    except UcError as e:
        print("ERROR: %s" % e)



小结


Unicorn 版的Hello world很好展示了Unicorn的使用过程。美中不足的是,它过于简陋,没有涉及到栈操作、系统调用、API调用等,要知道,现在任何一段代码、一个函数都会至少涉及到其中的一项。


注意:本文为系列推广文章,接下来将是:

  • Unicorn 调试器模块编写

  • Unicorn 调用SO之加载模块

  • 敬请期待!



- End -



看雪ID: 无名侠

https://bbs.pediy.com/user-617255.htm  


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





进阶安全圈,不得不读的一本书





推荐文章++++

彩蛋解密之物理内存读写到****的转变

Win10_64 默认应用的UserChoice Hash算法学习

VMP学习笔记之壳基础流程、X86指令Opcode快速入门

网络游戏安全之实战FPS游戏CRC检测的对抗与防护

密码学基础:AES加密算法






官方微博:看雪安全

商务合作请发邮件至:wsc@kanxue.com





“阅读原文”一起来充电吧!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存