这是我把最近学习记录整理成一个帖子,如果阅读不方便可以看印象笔记链接
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 是一款基于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来操作内存:
指令执行类
内存访问类
异常处理类
调用hook_add函数可添加一个Hook。Unicorn的Hook是链式的,而不是传统Hook的覆盖式,也就是说,可以同时添加多个同类型的Hook,Unicorn会依次调用每一个handler。hook callback 是有作用范围的(见hook_add begin参数)。
python包中的hook_add函数原型如下
不同类型的hook,对应的callback的参数也不相同,这里只给出C语言定义。 Python 编写callback的时候参考C语言即可(看参数)。
user_data: hook_add 设置的user_data参数
user_data: hook_add 设置的user_data参数
返回值 返回真,继续模拟执行 返回假,停止模拟执行
Unicorn 支持多种编译和安装方式,本课程以Linux作为学习环境,Python3作为主要语言。Linux 下可以直接使用pip安装,方便快捷。
更多详细安装过程可以参考官方安装教程
Unicorn 支持多种不同的CPU指令集,每一种指令集都有自己独立的寄存器, Unicorn使用统一API管理多种不同的CPU指令集,并将寄存器名字映射成数字常量。
寄存器常量命名规则 UC_ + 指令集 + REG + 大写寄存器名 UC_ARMREG + 大写寄存器名 (UC_ARM_REG_R0) UC_X86REG + 大写寄存器名 (UC_X86_REG_EAX)
本课程以python3 + arm指令集为例子,导入arm的常量
为了简单起见,我们直接将要执行代码的数据硬编码。
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 函数映射一段内存。
这段代码在内存地址0x10000处映射了一段大小为2M的内存。mem_map函数特别娇气,要求 address 和 size 参数都与0x1000对齐,否则会报UC_ERR_ARG异常。
我们要执行代码,就需要将欲执行代码的字节数据写入到虚拟机内存中。
mem_write的第二个参数也很娇气,只支持python的byte数组,不能是String或者bytearray。
这个有点像单步调试的感觉。
在begin...end范围内的每一条指令被执行前都会调用callback。 让我们来看看hook_code 的实现吧
这段代码仅打印指令执行的地址和长度信息。 实际应用中可配合capstone反汇编引擎玩一些更骚的操作。
UC_HOOK_CODE的callback中可以修改PC或EIP等寄存器力来改变程序运行流程。实际上,unicorn调试器的单步调试就是以这个为基础实现的。
原谅我用开机这个词汇吧!我们已经映射内存并将数据写入到内存,并设置好执行Hook以监视指令是否正常执行,但是虚拟机还没有启动!
emu_start 可以通过timeout参数设置最长执行时长,防止线程死在虚拟机里面。原型如下
emu_start 执行完成后,可以通过读取内存或寄存器的方式来获取执行结果。
Unicorn 版的Hello world很好展示了Unicorn的使用过程。 美中不足的是,它过于简陋,没有涉及到栈操作、系统调用、API调用等,要知道,现在任何一段代码、一个函数都会至少涉及到其中的一项。
前一篇文章中,我们学习了如何使用Unicorn 来模拟执行一段二进制代码。然而目前Unicorn 对于我们来说,几乎就等同于黑盒子。我们只知输入输出,对于代码中间执行过程的失败与否全然不知,如果出现BUG将难以调试。 为了解决这种难以调试的状况,我决定写一个Unicorn 调试器, 用来调试Unicorn 模拟执行的二进制代码。
Unicorn 提供了强大的指令级Hook(UC_HOOK_CODE), 使得每一条指令执行前,我们都有机会处理。
UC_HOOK_CODE 的callback原型定义如下
调用hook_add 函数可以为指定的代码范围添加hook的callback。 python包中的hook_add函数原型如下
UC_HOOK_CODE 的功能是每条指令执行前调用callback。
callback中,我们可以通过参数得知指令执行地址、指令执行长度、虚拟机指针。 有了虚拟机指针,我们可以很方便的访问各种寄存器、内存等资源。在UC_HOOK_CODE的callback中,也可以直接修改PC寄存器来改变流程。
调试器要支持的调试指令如下
本文将编写一个UnicornDebugger 类,调试器的各种功能均在该类中实现, 调用该类的构造函数即可附加到一个Uc虚拟机对象上。 类的定义
附加调试器
可见,这段代码在0x1112233处添加了一个断点,当程序执行到0x1112233处的时候就会停下来,陷入调试器的处理,等待调试者的指令。
Unicorn 并没有反汇编功能,虽然它的内部一定有与反汇编相关的代码。我们只能自己想办法反汇编。Unicorn 有一个兄弟,它叫Capstone。Capstone是一款支持多种处理器和开发语言的反汇编框架。Capstone 官方地址 我将使用Capstone 作为调试模块的反汇编器。
Capstone 对python的支持特别好,我们的开发语言是python3,所以直接使用pip 安装capstone 即可。
Capstone 很强大,也可以很简单, 下面一段代码就是Capstone的入门例子。
上面这段代码的输出如下
可以看出,Capstone 十分简单, 而我们编写调试器的反汇编部分,也是如此简单。
Python的hexdump模块的偏移量不支持修改,所以我魔改了hexdump的代码实现了这个功能。
魔改的hexdump
Unicorn中的寄存器都是以常量来管理的,所以我们要把寄存器常量和文本映射起来。
使用list来记录所有断点的地址,当UC_HOOK_CODE的callback被调用的时候检查address是否在断点列表中,如果当前指令的执行地址没有断点,则直接返回,如果存在断点,则打印寄存器信息、最近的汇编代码、等待用户指令等操作。
单步分为两种:步入、步过。 步入:如果遇到call、bl指令会跟随进入(跟随每一条指令) 步过:遇到call、bl等指令不不进入(跟随每一条地址相邻的指令) 我这个思路可能有点牵强,因为按照地址相邻,会误判跳转指令,但是为了简单起见,我只好这么做。
下面是断点管理相关的代码。
callback 中判断断点。 _tmp_bpt 是临时断点, 用于支持步过。 _is_step 步入标记,每条指令都停下来。
处理调试指令
断点停下来后效果如下
本篇文章仅仅实现了一个ARM/Thumb调试器,能满足一些基本调试,很多地方仍需改进。其它架构的也可以按照类似方法设计!与传统的调试器断点相比,UnicornDebugger采用的是一种类似硬件断点的技术,Unicorn的指令级Hook 难以被模拟执行的代码检测到。UnicornDebugger从一个很低的level去调试程序,这有点像是硬件调试器了!基于时间的反调试无济于事,因为unicorn 可以读写任何一条指令或则API的执行结果。也不能通过检测status或其它系统特性检测调试器,因为UnicornDebugger不属于操作系统的调试体系。Unicorn 的虚拟机中没有运行任何操作系统,基于系统特性检测虚拟机的方法也是无效的!UnicornDebugger 借助Unicorn 接口,站在上帝视角调试程序,很无敌!
Android是基于Linux开发的,Android Native原生库是ELF文件格式。Unicorn 并不能加载ELF文件,所以我们要自己将ELF文件加载到Unicorn虚拟机的内存中去。 加载ELF 文件是一个很复杂的过程,涉及到ELF文件解析、重定位、符号解析、依赖库加载等。
python 可以使用elftools库解析ELF 文件。
elftools 安装
ELF 文件有两种视图,链接视图和执行视图。elftools 是基于链接视图解析ELF格式的,然而现在有一些ELF文件的section信息是被抹掉的,elftools就无法正常工作,我也没时间重写一个elf loader,就只能凑合用一下elftools。
我已经在前面一篇文章介绍了内存分配方面的东西,加载ELF文件第一步需要将ELF文件映射到内存。如何映射呢?只需要找到类型为PT_LOAD的segment,按照segment的信息映射即可。
代码如下:
ELF 的有一个列表,用于存储初始化函数地址,在动态链接的时候,linker会依次调用init_array中的每一个函数。init_array 中的函数一般用于初始化程序,偶尔也有ELF外壳程序在init_array中添加自解密代码,另外有一些字符串解密也是在init_array中完成的。想要模拟native程序,必然需要调用init_array 中的函数。
init_array 是一个数组, 一般情况下,每一项都是函数入口偏移, 然而也有为0的情况。因为init_array实际解析时机在重定位完成之后, init_array 也可能被重定位。 所以要解析init_array的时候还需要判断重定位表。
我的策略是,当读出init_array中为0的条目的时候就去重定位表中查找重定位值。
32位ELF文件 symbol table entry 的定义如下。
当st_shndx字段的值为SHN_UNDEF时,表明该符号在当前模块没有定义,是一个导入符号,要去其它模块查找。为了便于管理已经加载模块的符号地址,应该用一个map,将name和address映射起来。 其它情况,简单起见,均看成导出符号,将地址重定位后加入到管理符号map。
Unicorn 采用虚拟内存机制,使得虚拟CPU的内存与真实CPU的内存隔离。Unicorn 使用如下API来操作内存:
UC_MEM_ALIGN = 0x1000 Unicorn map的内存基地址和长度都需要对齐到0x1000,对齐函数如下
Memory类用于分配内存
为了对malloc和free等函数提供支持,需要实现heap。 可喜可贺,Afl-Unicorn 开源项目中已经有一个heap例子,可以直接使用。
unicorn heap
native程序可能直接通过syscall调用mmap2 映射内存。libc等库最底层也一定会调用mmap2分配内存。
mmap2 实际并不是一个纯粹的内存管理函数,它还能映射文件到内存,这就涉及到文件系统的支持。我在原作者的基础上进行了修改。
hook libc中的内存管理函数. 既然我们的框架支持加载多个lib库,直接加载libc就可以了,为何还需要hook libc中的这些符号呢? 实际上,目前的模拟框架并不完善,很多系统信息模拟不到位,libc初始化不完善,使用libc的malloc经常会出现异常。所以直接接替为内置的heap 管理器。
我们知道,常见的内存权限有可读可写可执行, 一般可以调用mprotect 函数修改指定内存页面的权限。为了方便,我们将所有内存设置为可读可写可执行。
很多情况下,有一些系统API无法直接调用,就需要手动实现它,比如以下应用场景: 1、在libc没有完全初始化的情况下,直接调用malloc 可能会崩溃, 为了使程序更加稳定,就需要自己实现malloc和free。 2、实现dlopen,就可以使用框架的模块管理器来加载模块,而不用linker。 3、打出dlopen、dlsym等函数的日志,还可以分析native程序运行过程中会调用的API。 4、360加固,在反调试完成后,它会调用dlopen加载libz.so,并调用uncompress函数解压数据。 hook uncompress 就可以拿到解压的数据。 5、目前,许多加固的整体加固会调用art中的api来加载dex文件,如果这些函数被Hook,那么就可以直接拿到dex文件,岂不美哉? 6、JNI Functions 实现,需要Hook 技术支撑。
不幸的是,Unicorn 内部并没有函数的概念,它只是一个单纯的CPU, 没有HOOK_FUNCTION的callback, Hook 函数看上去困难重重。AndroidNativeEmu 为我们提供了一个很好的Hook思路。
AndroidNativeEmu 中的函数级Hook 并不是真正意义上的Hook,它不仅能Hook存在的函数,还能Hook不存在的函数。AndroidNativeEmu 使用这种技术实现了JNI函数Hook、库函数Hook。 Jni函数是不存的,Hook它只是为了能够用Python 实现 Jni Functions。有一些库函数是存在的,Hook只是为了重新实现它。
用 Python 实现 Unicorn 虚拟机内部的函数,首先要解决 Unicorn 虚拟机内部如何与外部交互。AndroidNativeEmu 的实现类似于系统调用,它会为每一个Hook函数实现一个stub函数,stub函数中有一条“陷阱”指令,当虚拟CPU执行这一条”陷阱“指令的时候就会被HOOK_CODE 捕获,然后通过R4寄存器的值确定Python 的处理函数。
stub 函数代码如下:
HOOK_CODE的callback 直接检测PC寄存器指向内存的字节数据是否为"\xE8\xBF"来判断IT AL指令。如果是IT AL指令,则根据R4寄存器的值确定Python 回调的函数。
Native程序中可能会有许多IT AL指令,会误判吗? HOOK_CODE 会trace每一条指令,这种方式效率岂不是很低?
这两个问题都很好解决,因为HOOK_CODE是有作用范围的,如果开辟一段空间,完全用于存放stub,然后将该callback设置在这个空间范围内,就可以很好的避免了冲突和效率问题!
Keystone 是一款很牛逼的汇编框架,Unicorn的兄弟!
使用pip安装
keystone 使用方法十分简单,不需要额外学习即可理解接下来的代码。write_function函数将python的func函数映射到Unicorn虚拟机,并返回虚拟机中的函数地址, 如果在虚拟机中调用该地址就会被Python捕获并调用func函数。
上一个小结中讲到如何实现stub,那么如何hook 呢?AndroidNativeEmu实现了Symbol Hook。这种HOOK与平时常见的IAT HOOK、GOT Hook 原理是一样的。 调用add_symbol_hook函数,可以将符号和新地址关联起来, 模块加载的时候,查找符号优先从该表中获取地址。 这种实现方式有点bug,比如有一些Native 程序,自身会got hook,那么这种方式可能就会出问题。
大家可能对Python 装饰器的语法不太熟悉,可以参考廖雪峰老师博客:Python 装饰器
简单的讲,装饰器可以自定义修改函数,为目标函数套一层外壳。native_method主要功能是处理参数数据,这样就能很优雅地编写Native 函数,不用再函数中冗杂地调用寄存器读写函数。
这篇文章讲了如何实现 Unicorn 内部的函数级Hook,原理是在模块加载重定位阶段,填充stub函数地址到目标函数的 got表。 stub函数是Unicorn 内部环境与外部环境的一个桥梁,使用小巧的IT AL和R4寄存器实现交互,像极了系统调用。使用Python的装饰器,简化了Python 编写Hook 函数的难度。
系统调用是操作系统给应用程序提供的最底层的基础接口。应用程序读写文件、访问网络等操作都需要操作系统支持。
Unicorn 拦截系统调用只需要添加UC_HOOK_INTR的callback,该callback的参数定义如下:
我们只需要用hook_add 添加一个 UC_HOOK_INTR 的callback 就能够处理中断了。根据intno 中断号分发不同的中断处理函数。
我们将在文件系统章节详细讨论如何实现文件系统。
这些实现都非常的简单,就不过多展开了。
如果想尽可能完美的模拟Android Native程序,那就必须加入虚拟文件系统的支持。虚拟文件系统可以将Unicorn 虚拟机内部访问文件的操作映射到主机。
Hook 拦截文件操作相关的系统调用,转换成对主机的文件操作。
为了安全性,我们要在主机上划分一个用于专门虚拟文件系统的目录。 处理系统调用的时候,将路径转换成虚拟文件系统目录的路径。
需要处理的syscall已经在上一篇文章介绍过了
进一步分析_open_file 这个函数先对路径进行一些简单检验,判断是否为特殊文件,如果不是,则调用translate_path转换安全路径,并调用os.open打开本地文件,最后调用_store_fd转换文件句柄。
调用JNI是工程量最大的一部分,不仅需要实现JNI Functions,还要模拟JNI Env和Java类似的引用管理。 在前面的文章中已经学习了如何在虚拟机中调用主机的Python函数,接下来就学习如何实现JNI Functions里面的所有函数。
可以参考 Jni Functions Jni Function Table 是一个函数地址表,里面记录了Jni 函数地址。
write_function_table 函数实现了创建一个Function 地址表。 实现如下:
Jni Functions Table 中有 200 多个函数,全部实现的话工作量十分巨大。默认实现如下:
抛出异常可以保证当Native调用一个没有实现的JNI函数时能够及时发现,并实现它。
AndroidNativeEmu 支持使用Python类来代替Jvm中Java类,这是如何实现的呢?
jvm_super 指定父类 jvm_name 定义对应的类名 jvm_fields 定义字段,是一个列表,每一项都是由JavaFieldDef定义的字段,JavaFieldDef(name, signature, is_static, static_value=None) 如果不是static字段,则还需要在init 中创建这个这个私有的成员变量。
metaclass=JavaClassDef 是Python的元类机制,参考廖雪峰老师文章 元类可真的是魔法!它可以动态修改类的定义,比如给类增加成员变量,增加方法等。 JavaClassDef 将class 定义指定的jvm_name 和 jvm_fields保存到成员变量,并添加了find_method、find_method_by_id、find_field等函数,用于实现JNI。
注册方法
注册字段
查找字段的函数 支持类继承,Java是单继承,查找的时候先从基类开始递归查找。
metaclass 修饰了定义的java类,隐藏了类解析背后的细节,使得添加一个java类很方便。java类定义后,还需要添加模拟器的class管理。
保存的是类的定义而不是实例。
目前实现两种类型的引用,一种jobject和jclass。jobejct是用来引用实例对象,jclass用来引用类。 Python 实现java的类,如果返回一个String,那么就会自动创建一个String引用,然后把引用id返回给Native 函数。Native 函数再调用GetStringUtfChars获取引用的字符串。 GetStringUtfChars 的实现如下。
引用还分为局部引用和全局引用。局部引用的生命周期是进入native 函数到native 函数返回。 全局引用则作用于模拟器整个生命周期。
我们目前已经实现了Java 调用native 的过程,native再回调java是否可以实现呢?实际上,在class定义的时候就解析了所有带@java_method_def 装饰器的成员函数。native通过JNI调用java函数,我们可以模拟对应的JNI函数,使其在所有加载的类中查找是否有对应签名的函数,如果有则直接调用。
@java_method_def 装饰器不仅描述了函数的签名,还转换函数的参数数据和返回值信息。
这个修饰器的定义如下:
Java 调用 Native 的时候要把非数字类型转换为对象引用。 Native 调用 Java 或者 返回的时候要把引用转换成对象,转换方法实现如下:
附录: Unicorn 优秀项目:http://www.unicorn-engine.org/showcase/
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-8-15 01:40
被无名侠编辑
,原因: