精华内容
下载资源
问答
  • unicorn.cr:Unicorn Engine的水晶绑定
  • 0x10 Unicorn Unicorn 是一个轻量级的多平台多架构的 CPU 仿真框架。作为一款著名的开源CPU 模拟框架,很多二进制逆向分析软件都用到了 Unicorn ,或者使用到了它的思想。比如 Radare2、Pwndbg、gdb-gef。Unicorn 在...

    0x10 Unicorn

    Unicorn 是一个轻量级的多平台多架构的 CPU 仿真框架。作为一款著名的开源CPU 模拟框架,很多二进制逆向分析软件都用到了 Unicorn ,或者使用到了它的思想。比如 Radare2、Pwndbg、gdb-gef。Unicorn 在 QEMU 的基础上,增加了许多新特性,以及对 CPU 仿真更好的支持 1

    • 多架构支持:Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64)
    • 轻量级的 API
    • 纯 C 语言实现,并支持 Pharo,Crystal,Clojure,Visual Basic,Perl,Rust,Haskell,Ruby,Python,Java,Go,.NET,Delphi / Pascal 和 MSVC 的编译
    • 原生支持 Windows 和类 Unix 系统,如 Mac OSX, Linux, *BSD & Solaris
    • 使用 JIT(Just-In-Time,即时编译技术)提高性能
    • 支持各种级别的细粒度分析
    • 线程安全
    • 根据免费软件许可 GPLv2 分发

    原作者在 2015 年的 BlackHat 上,发表了相关议题, BlackHat USA 2015 slides 提供更多的信息,有兴趣的读者可以观看一波。


    0x20 基于 C/C++ 使用 Unicorn 进行开发

    0x21 编译生成库文件

    项目地址:https://github.com/unicorn-engine/unicorn,以下是该项目的详细结构 2

       .                   <- 主要引擎core engine + README + 编译文档COMPILE.TXT 等
    ├── arch            <- 各语言反编译支持的代码实现
    │   ├── AArch64     <- ARM64 (aka ARMv8) 引擎
    │   ├── ARM         <- ARM 引擎
    │   ├── EVM         <- Ethereum 引擎
    │   ├── M680X       <- M680X 引擎
    │   ├── M68K        <- M68K 引擎
    │   ├── Mips        <- Mips 引擎
    │   ├── PowerPC     <- PowerPC 引擎
    │   ├── Sparc       <- Sparc 引擎
    │   ├── SystemZ     <- SystemZ 引擎
    │   ├── TMS320C64x  <- TMS320C64x 引擎
    │   ├── X86         <- X86 引擎
    │   └── XCore       <- XCore 引擎
    ├── bindings        <- 中间件
    │   ├── java        <- Java 中间件 + 测试代码
    │   ├── ocaml       <- Ocaml 中间件 + 测试代码
    │   └── python      <- Python 中间件 + 测试代码
    ├── contrib         <- 社区代码
    ├── cstool          <- Cstool 检测工具源码
    ├── docs            <- 文档,主要是capstone的实现思路
    ├── include         <- C头文件
    ├── msvc            <- Microsoft Visual Studio 支持(Windows)
    ├── packages        <- Linux/OSX/BSD包
    ├── windows         <- Windows 支持(Windows内核驱动编译)
    ├── suite           <- Capstone开发测试工具
    ├── tests           <- C语言测试用例
    └── xcode           <- Xcode 支持 (MacOSX 编译)
    

    由于项目原生支持 Windows,本次使用 Win10+Microsoft Visual Studio 2015 进行编译。使用 VS 2015 直接打开 msvc 目录下的 unicorn.sln,即可自动载入项目。右击 解决方案 -> 属性,可以弹出如下属性页,从而选择自己想编译的项目。
    在这里插入图片描述
    当然,你也可以直接在项目右下角的 属性 视图里面进行快捷设置
    在这里插入图片描述
    在项目的编译属性中,设置如下,其余设置默认即可

    • 不使用预编译头
    • 附加选项 /wd4018 /wd4244 /wd4267

    编译完成之后,会在项目文件夹的编译器目录下(如果你使用的是 Win32 编译器,那么就是 Win32)的 Debug 目录中生成相应的链接库(unicorn.lib 静态链接库 + unicorn.dll 动态链接库)
    在这里插入图片描述
    这两个库以及项目的头文件,就是我们开发需要的东西了。


    0x22 新建项目调用引擎

    新建一个VS工程(Win32 项目 或者 控制台项目)。将…\unicorn-master\include\unicorn中的头文件以及编译好的 lib 和 dll 文件全部拷贝到新建项目的主目录下

    #include<iostream>
    #include "include\unicorn\unicorn.h"
    
    #define X86_CODE "\x41\x4a"		// 要模拟的指令
    #define ADDRESS 0x2000000		// 起始地址
    
    using namespace std;
    
    int main()
    {
    	uc_engine * uc;
    	uc_err err;
    	int r_ecx = 0x1234;
    	int r_edx = 0x5678;
    
    	cout << "Emulate i386 code" << endl;
    
    	/*x86模式初始化*/
    	err = uc_open(UC_ARCH_X86, UC_MODE_32, &uc);
    	if (err != UC_ERR_OK)
    	{
    		cout << "Something error in uc_open() %u" << err << endl;
    		return -1;
    	}
    
    	/*申请模拟器内存大小*/
    	uc_mem_map(uc, ADDRESS, 4 * 1024 * 1024, UC_PROT_ALL);		// 4MB
    
    	/*要模拟的指令写入寄存器*/
    	if (uc_mem_write(uc, ADDRESS, X86_CODE, sizeof(X86_CODE) - 1))
    	{
    		cout << "Failed to write emulation code to memory, abort" << endl;
    		return -1;
    	}
    
    	/*初始化寄存器*/
    	uc_reg_write(uc, UC_X86_REG_ECX, &r_ecx);
    	uc_reg_write(uc, UC_X86_REG_EDX, &r_edx);
    	printf(">>> ecx = 0x%x\n", r_ecx);
    	printf(">>> edx = 0x%x\n", r_edx);
    
    	/*模拟代码*/
    	err = uc_emu_start(uc, ADDRESS, ADDRESS + sizeof(X86_CODE) - 1, 0, 0);
    	if (err)
    	{
    		printf("Something wrong in uc_emu_start(), error code: %u %s\n", err, uc_strerror(err));
    		return -1;
    	}
    
    	/*打印寄存器内容*/
    	printf("Emulation done. Blew is the CPU context\n");
    	uc_reg_read(uc, UC_X86_REG_ECX, &r_ecx);
    	uc_reg_read(uc, UC_X86_REG_EDX, &r_edx);
    	printf(">>> ecx = 0x%x\n", r_ecx);
    	printf(">>> edx = 0x%x\n", r_edx);
    
    	uc_close(uc);
    	
    	return 0;
    
    }
    

    解决方案资源管理器 头文件添加现有项 unicorn.h,资源文件中添加 unicorn.lib,重新生成解决方案。编译并运行。如下图所示,成功的执行指令 ecx + 1 edx - 1。如果报错,请参考 0x3 Q&A
    在这里插入图片描述

    0x23 关键代码解析

    • 第4行:我们要模拟的原始二进制代码。此示例中的代码处于十六进制模式,代表两个X86指令“ INC ecx ”和“ DEC edx ” 3
    • 第11行:声明一个指向uc_engine类型的句柄的指针。该句柄将在Unicorn的每个API中使用
    • 第12行:声明数据类型为uc_err的变量,以防 Unicor API返回错误。
    • 第53行:调用uc_close函数完成仿真

    其余代码请看下面的函数解析

    0x24 API 解析: unicorn.h 关键函数

    uc_engineuc_struct 的别名,这里第一行代码,是定义 uc 为指向 unicorn engine 的指针。
    uc_err 是错误类型,是 uc_errno() 的返回值

    typedef enum uc_err {
        UC_ERR_OK = 0,   // 无错误
        UC_ERR_NOMEM,      // 内存不足: uc_open(), uc_emulate()
        UC_ERR_ARCH,     // 不支持的架构: uc_open()
        UC_ERR_HANDLE,   // 不可用句柄
        UC_ERR_MODE,     // 不可用/不支持架构: uc_open()
        UC_ERR_VERSION,  // 不支持版本 (中间件)
        UC_ERR_READ_UNMAPPED, // 由于在未映射的内存上读取而退出模拟: uc_emu_start()
        UC_ERR_WRITE_UNMAPPED, // 由于在未映射的内存上写入而退出模拟: uc_emu_start()
        UC_ERR_FETCH_UNMAPPED, // 由于在未映射的内存中获取数据而退出模拟: uc_emu_start()
        UC_ERR_HOOK,    // 无效的hook类型: uc_hook_add()
        UC_ERR_INSN_INVALID, // 由于指令无效而退出模拟: uc_emu_start()
        UC_ERR_MAP, // 无效的内存映射: uc_mem_map()
        UC_ERR_WRITE_PROT, // 由于UC_MEM_WRITE_PROT冲突而停止模拟: uc_emu_start()
        UC_ERR_READ_PROT, // 由于UC_MEM_READ_PROT冲突而停止模拟: uc_emu_start()
        UC_ERR_FETCH_PROT, // 由于UC_MEM_FETCH_PROT冲突而停止模拟: uc_emu_start()
        UC_ERR_ARG,     // 提供给uc_xxx函数的无效参数
        UC_ERR_READ_UNALIGNED,  // 未对齐读取
        UC_ERR_WRITE_UNALIGNED,  // 未对齐写入
        UC_ERR_FETCH_UNALIGNED,  // 未对齐的提取
        UC_ERR_HOOK_EXIST,  // 此事件的钩子已经存在
        UC_ERR_RESOURCE,    // 资源不足: uc_emu_start()
        UC_ERR_EXCEPTION, // 未处理的CPU异常
        UC_ERR_TIMEOUT // 模拟超时
    } uc_err;
    

    uc_open() 用于创建 unicorn 实例

    uc_err uc_open(uc_arch arch, uc_mode mode, uc_engine **uc);
    
    @arch: 架构类型 (UC_ARCH_*)
    @mode: 硬件模式. 由 UC_MODE_* 组合
    @uc: 指向 uc_engine 的指针, 返回时更新
    @return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
    

    uc_mem_map() 为模拟器映射一块内存

    uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms);
    
    @uc: uc_open() 返回的句柄
    @address: 要映射到的新内存区域的起始地址。这个地址必须与4KB对齐,否则将返回UC_ERR_ARG错误。
    @size: 要映射到的新内存区域的大小。这个大小必须是4KB的倍数,否则将返回UC_ERR_ARG错误。
    @perms: 新映射区域的权限。参数必须是UC_PROT_READ | UC_PROT_WRITE | UC_PROT_EXEC或这些的组合,否则返回UC_ERR_ARG错误。
    @return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
    

    uc_mem_write() 向内存写入一段字节码

    uc_err uc_mem_write(uc_engine *uc, uint64_t address, const void *bytes, size_t size);
    
    @uc: uc_open() 返回的句柄
    @address: 写入字节的起始地址
    @bytes:   指向一个包含要写入内存的数据的指针
    @size:   要写入的内存大小。
    注意: @bytes 必须足够大以包含 @size 字节。
    @return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
    

    uc_reg_write() 向寄存器写入值

    uc_err uc_reg_write(uc_engine *uc, int regid, const void *value);
    
    @uc: uc_open()返回的句柄
    @regid:  将被修改的寄存器ID
    @value:  指向寄存器将被修改成的值的指针
    @return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
    

    uc_reg_read 读取寄存器的值

    uc_err uc_reg_read(uc_engine *uc, int regid, void *value);
    
    @uc: uc_open()返回的句柄
    @regid:  将被读取的寄存器ID
    @value:  指向保存寄存器值的指针
    @return 成功则返回UC_ERR_OK , 否则返回 uc_err 枚举的其他错误类型
    

    0x30 Q&A

    0x31 使用了不安全函数

    在这里插入图片描述
    在预处理器中,添加相应的定义即可
    在这里插入图片描述
    或者在程序中添加以下任意一行代码

    #define _CRT_SECURE_NO_DEPRECATE;
    #define _CRT_SECURE_NO_WARNINGS;
    #pragma warning(disable:4996);
    

    重新编译,并运行。

    0x32 error LNK2019: 无法解析的外部符号 _main

    在这里插入图片描述
    打开项目属性页,修改 调试器 >系统 > 子系统 ,切换成控制台(如果原来是控制台,则切换为窗口)
    在这里插入图片描述

    0x32 无法查找或打开 PDB 文件

    在这里插入图片描述
    【工具】->【选项】->【调试】->【常规]】勾选“启用源服务器支持” 4
    在这里插入图片描述
    【工具】->【选项】->【调试】->【符号】,勾选“Microsoft符号服务器”
    在这里插入图片描述

    0x40 基于 Python 调用 Unicorn Engine

    使用 Python 调用 Unicorn 相对来说,要简单许多。当然首先得安装相应的包

    pip install unicorn -i https://pypi.mirrors.ustc.edu.cn/simple/
    
    from __future__ import print_function
    from unicorn import *
    from unicorn.x86_const import *
    
    # code to be emulated
    X86_CODE32 = b"\x41\x4a" # INC ecx; DEC edx
    
    # memory address where emulation starts
    ADDRESS = 0x1000000
     
    print("Emulate i386 code")
    try:
        # Initialize emulator in X86-32bit mode
        mu = Uc(UC_ARCH_X86, UC_MODE_32)
    
        # map 2MB memory for this emulation
        mu.mem_map(ADDRESS, 2 * 1024 * 1024)
    
        # write machine code to be emulated to memory
        mu.mem_write(ADDRESS, X86_CODE32)
    
        # initialize machine registers
        mu.reg_write(UC_X86_REG_ECX, 0x1234)
        mu.reg_write(UC_X86_REG_EDX, 0x5678)
    
        # emulate code in infinite time & unlimited instructions
        mu.emu_start(ADDRESS, ADDRESS + len(X86_CODE32))
    
        # now print out some registers
        print("Emulation done. Below is the CPU context")
    
        r_ecx = mu.reg_read(UC_X86_REG_ECX)
        r_edx = mu.reg_read(UC_X86_REG_EDX)
        print(">>> ECX = 0x%x" %r_ecx)
        print(">>> EDX = 0x%x" %r_edx)
    
    except UcError as e:
        print("ERROR: %s" % e)
    

    输出结果
    在这里插入图片描述

    • 第2〜3行:在使用Unicorn之前,导入unicorn模块。此示例还使用了一些X86寄存器常量,因此也需要unicorn.x86_const
    • 第6行:要模拟的原始二进制代码。此示例中的代码处于十六进制模式,代表两个X86指令“ INC ecx ”和“ DEC edx ”
    • 第9行:将在其中模拟上面的代码的虚拟地址
    • 第14行:使用类Uc初始化Unicorn 。此类接受2个参数:硬件体系结构和硬件模式。在此示例中,我们要模拟X86体系结构的32位代码。mu接受返回值
    • 第17行:mem_map在第9行声明的地址处映射2MB的内存用于此仿真。在此过程中,所有CPU操作都只能访问该内存。该内存使用默认权限READ,WRITE和EXECUTE映射
    • 第20行:将要模拟的代码写入到我们上面刚刚映射的内存中。mem_write方法采用2个参数:要写入的地址和要写入内存的代码
    • 23〜24行:使用reg_write方法设置ECX和EDX寄存器的值
    • 第27行:使用方法emu_start启动仿真。该API包含4个参数:仿真代码的地址,仿真停止的地址(紧随X86_CODE32的最后一个字节之后),要仿真的时间以及要仿真的指令数。如果像本例一样忽略最后两个参数,Unicorn将在无限的时间和无限数量的指令中模拟代码
    • 第32〜35行:打印出寄存器ECX和EDX的值。我们使用reg_read函数读取寄存器的值

    0x50 总结

    Unicorn Engine 用于仿真各种架构的 CPU 指令集,在 QEMU 的基础上,扩展了很多新特性和新功能,本文初步介绍了其简单的用法,利用 C 和 Python 调用了其 API,模拟了 x86 指令集,如果用其实际开发一个项目,你将会体会到 Unicorn 的独特魅力。在这里,是希望通过简短的介绍,让大家能够从源码以及用法的角度上,对 Unicorn 有一个更加全面的了解,为后续 Afl-Unicorn 进行 Fuzz 测试,或者编写自己的模拟器,提供一种思想上的理念。附录中的参考网址,都是值得学习的平台,尤其感谢 kabeor制作的非官方 API 参考文档。


    1. http://www.unicorn-engine.org/ ↩︎

    2. https://github.com/kabeor/Micro-Unicorn-Engine-API-Documentation/blob/master/Micro%20Unicorn-Engine%20API%20Documentation.md ↩︎

    3. http://www.unicorn-engine.org/docs/tutorial.html ↩︎

    4. https://blog.csdn.net/qq_38410428/article/details/102720550 ↩︎

    展开全文
  • 使用unicorn-engine开发模拟器 什么是unicorn引擎 Unicorn是基于qemu开发的一个CPU模拟器,支持常见的各种指令集,能在各种系统上运行。 GITHUB项目地址: : 官网地址: : 一个中文API文档: : 它只是一个CPU...
  • 目 录 1.虚拟化软件的比较以及各自的特点. 2.unicorn-engine介绍. 3.Hack unicorn-engine. 4.wxemu的设计不实现. 5.wxemu 的功能 6.Demo. 7.目前存在缺点不计划.
  • python unicorn engine

    2021-08-13 10:06:36
    unicorn简介: Unicorn 是一个轻量级, 多平台, 多架构的 CPU 模拟器框架. 我们可以更好地关注 CPU 操作, 忽略机器设备的差异. 想象一下, 我们可以将其应用于这些情景: 比如我们单纯只是需要模拟代码的执行而非需要一...

    unicorn简介:

    Unicorn 是一个轻量级, 多平台, 多架构的 CPU 模拟器框架.
    我们可以更好地关注 CPU 操作, 忽略机器设备的差异. 想象一下, 我们可以将其应用于这些情景:
    比如我们单纯只是需要模拟代码的执行而非需要一个真的 CPU 去完成那些操作, 又或者想要更安全地
    分析恶意代码, 检测病毒特征, 或者想要在逆向过程中验证某些代码的含义. 使用 CPU 模拟器可以很好
    地帮助我们提供便捷.
    它的亮点 (这也归功于 Unicorn 是基于 qemu 而开发的) 包括:
    支持多种架构: Arm, Arm64 (Armv8), M68K, Mips, Sparc, & X86 (include X86_64).
    对 Windows 和 nix 系统 (已确认包含 Mac OSX, Linux, BSD & Solaris) 的原生支持
    具有平台独立且简洁易于使用的 API
    使用 JIT 编译技术, 性能表现优异
    
    from unicorn import *
    from unicorn.x86_const import *
    # 需要使用一些x86常量, import一下
    X86_CODE32 = b"\x41\x4a"    # 这是我们要模拟的代码,这里使用16进制表示,表示两个x86指令“INC ecx”和”DEC dex“
    Address = 0x1000000         # 指定虚拟地址,我们将在这儿模拟上述代码
    mu = Uc(UC_ARCH_X86, UC_MODE_32)
    # 使用Uc类初始化Unicorn, 该类接受2个参数:硬件架构和硬件模式。 在这里,我们希望模拟X86体系结构的32位代码
    mu.mem_map(Address, 2*1024*1024)
    # 使用mem_map方法在前面声明的地址处映射2MB内存。此过程中的所有CPU操作都只能访问此内存。 此内存使用默认权限READ,WRITE和EXECUTE进行映射
    mu.mem_write(Address, X86_CODE32)
    # 将要模拟的代码写入我们刚刚映射到的内存中。 mem_write方法有两个参数:要写入的地址和要写入内存的代码
    mu.reg_write(UC_X86_REG_ECX, 0X1234)
    mu.reg_write(UC_X86_REG_EDX, 0X7890)    # 使用reg_write方法设置ECX,EDX寄存器的值
    mu.emu_start(Address, Address+len(X86_CODE32))
    # 使用emu_start方法开始模拟。该API采用4个参数:需要模拟的代码的地址、模拟停止的地址(正好在X86_CODE32的最后一个字节之后)、要模拟的时间和要模拟的指令数量。如果我们忽略最后两个参数,比如这个例子,unicorn将在无限时间和无限数量的指令中模拟代码
    r_ecx = mu.reg_read(UC_X86_REG_ECX)
    r_edx = mu.reg_read(UC_X86_REG_EDX)
    # 通过reg_read函数读取
    print(">> ecx = 0x%x" % r_ecx)
    print(">> edx = 0x%x" % r_edx)        # 打印出寄存器ECX,EDX的值
    
    展开全文
  • 最近在研究用Unicorn Engine调用SO时,发现网上的资料很少,并且没有一个完整的调用实例。所以我把自己做的发出来跟大家分享,共同学习进步。 下面开始: 一、我们的目的 以上一串字符串中vf字段为标红部分的...

    最近在研究用Unicorn Engine调用SO时,发现网上的资料很少,并且没有一个完整的调用实例。所以我把自己做的发出来跟大家分享,共同学习进步。

    下面开始: 

    一、我们的目的

        

    以上一串字符串中vf字段为标红部分的signature。该算法在libmcto_media_player.so+0x249BC8处。如果是Android端调用的话很简单,我们编写一个loader调用该函数传入参数获取返回值即可轻易拿到。但如果你想在Windows或linux上获取该signature就会比较麻烦。一般都是通过逆向还原代码来进行移植。但是如果遇见混淆或VM的代码,那将是痛苦的。所以这就是我为什么要介绍Unicorn Engine的原因了。我们要用Unicorn Engine来完成跨平台的调用。 


    二、 用NDK编写loader用做验证用。

           

    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

    #include <stdio.h>

    #include <string.h>

    #include <dlfcn.h>

    #include <jni.h>

    #include <stdlib.h>

     

     

    int main(int argc,char** argv)

    {

     

        JavaVM* vm;

        JNIEnv* env;

        jint res;

         

        JavaVMInitArgs vm_args;

        JavaVMOption options[1];

        options[0].optionString = "-Djava.class.path=.";

        vm_args.version=0x00010002;

        vm_args.options=options;

        vm_args.nOptions =1;

        vm_args.ignoreUnrecognized=JNI_TRUE;

         

         

        printf("[+] dlopen libdvm.so\n");

        void *handle = dlopen("/system/lib/libdvm.so", RTLD_LAZY);//RTLD_LAZY RTLD_NOW

        if(!handle){

        printf("[-] dlopen libdvm.so failed!!\n");

        return 0;

        }

     

        typedef int (*JNI_CreateJavaVM_Type)(JavaVM**, JNIEnv**, void*);

        JNI_CreateJavaVM_Type JNI_CreateJavaVM_Func = (JNI_CreateJavaVM_Type)dlsym(handle, "JNI_CreateJavaVM");

        if(!JNI_CreateJavaVM_Func){

        printf("[-] dlsym failed\n");

        return 0;

        }

        res=JNI_CreateJavaVM_Func(&vm,&env,&vm_args);

            //libmctocurl.so   libcupid.so 为libmcto_media_player.so的依赖库

        dlopen("/data/local/tmp/libmctocurl.so",RTLD_LAZY);

        dlopen("/data/local/tmp/libcupid.so",RTLD_LAZY);

        void* si=dlopen("/data/local/tmp/libmcto_media_player.so",RTLD_LAZY);

        if(si == NULL)

        {

            printf("dlopen err!\n");

            return 0;

        }

     

        typedef char* (*FUN1)(char* plain);

        void *addr=(void*)(*(int*)((size_t)si+0x8c)+0x249BC9);

        FUN1 func=(FUN1)addr;

        if(func==NULL)

        {

            printf("can't find  func\n");

            return 0;

        }

        

        char *plain="/vps?tvid=11949478009&vid=7b23569cbed511dd58bcd6ce9ddd7b42&v=0&qypid=11949478009_unknown&src=02022001010000000000&tm=1519712402&k_tag=1&k_uid=359125052784388&bid=1&pt=0&d=1&s=0&rs=1&dfp=1413357b5efa4a4130b327995c377ebb38fbd916698ed95a28f56939e9d8825592&k_ver=9.0.0&k_ft1=859834543&k_err_retries=0&qd_v=1";

        char* ret=func(plain);

        printf("%s\n",ret);

        return 0;

    }

    我之前已经将那3个so(libmctocurl.so、libcupid.so、libmcto_media_player.so) 放到/data/local/tmp下。运行结果与上面的vf字段一致。

    三、 使用Unicorn Engine

    由于使用了混淆。分析起来比较麻烦,所以使用Unicorn进行调用

    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

    62

    63

    64

    65

    66

    67

    68

    69

    70

    71

    72

    73

    74

    75

    76

    77

    78

    79

    80

    81

    82

    83

    84

    85

    86

    87

    88

    89

    90

    91

    92

    93

    94

    95

    96

    97

    98

    99

    100

    101

    102

    103

    104

    105

    106

    107

    108

    109

    110

    111

    112

    113

    114

    115

    116

    117

    118

    119

    120

    121

    122

    123

    124

    125

    126

    127

    128

    129

    130

    131

    132

    133

    134

    135

    136

    137

    138

    139

    140

    141

    142

    143

    144

    145

    146

    147

    148

    149

    150

    151

    152

    153

    154

    155

    156

    157

    158

    159

    160

    161

    162

    163

    164

    165

    166

    167

    168

    169

    170

    171

    172

    173

    174

    175

    176

    177

    178

    179

    180

    181

    182

    183

    184

    185

    186

    187

    188

    189

    190

    191

    192

    193

    194

    195

    196

    197

    198

    199

    200

    201

    202

    203

    204

    205

    206

    207

    208

    209

    210

    211

    212

    213

    214

    215

    216

    217

    218

    219

    220

    #include "stdafx.h"

    #include <inttypes.h>

    #include <string.h>

    #include <math.h>

    #include <unicorn/unicorn.h>

    #pragma comment(lib,"unicorn.lib")

    //#define DEBUG

    #define _DWORD uint32_t

    #define LODWORD(x)  (*((_DWORD*)&(x)))

    #define HIDWORD(x)  (*((_DWORD*)&(x)+1))

    #define ADDRESS 0x249BC8

    #define BASE  0xaef52000

    #define CODE_SIZE  8*1024*1024

    #define STACK_ADDR  BASE+CODE_SIZE

    #define STACK_SIZE  1024 * 1024

    #define PARAM_ADDR  STACK_ADDR+STACK_SIZE

    #define PARAM_SIZE  1024 * 1024

    uint32_t offset=0;

    static uint32_t create_mem(uc_engine *uc,char* buffer,uint32_t len)

    {

        uint32_t addr = PARAM_ADDR + offset;

        uc_mem_write(uc, addr, buffer, len);

        offset += len + 1;

        return addr;

    }

     

    static void print_reg(uc_engine *uc, uint32_t address)

    {

    #ifdef DEBUG

        uint32_t pc = 0;

        uc_reg_read(uc, UC_ARM_REG_PC, &pc);

        if (pc == address)

        {

            printf("========================\n");        printf("Break on 0x%x\n", pc);

            uint32_t values = 0;

            uc_reg_read(uc, UC_ARM_REG_R0, &values);        printf("R0 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R1, &values);        printf("R1 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R2, &values);        printf("R2 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R3, &values);        printf("R3 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R4, &values);        printf("R4 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R5, &values);        printf("R5 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_R6, &values);        printf("R6 = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_PC, &values);        printf("PC = 0x%x \n", values);

            uc_reg_read(uc, UC_ARM_REG_SP, &values);        printf("SP = 0x%x \n", values);

            printf("========================\n");

        }

    #endif // DEBUG

    }

    static void hook_code(uc_engine *uc, uint64_t address, uint32_t size, void *user_data)

    {

    #ifdef DEBUG

        printf(">>> Tracing instruction at 0x%" PRIx64 ", instruction size = 0x%x\n", address, size);

    #endif // DEBUG

        switch (address)

        {

            //strlen

            case BASE + 0x249BEE:

            {

                uint32_t r0 = 0;

                char buffer[4096] = "";

                uc_reg_read(uc, UC_ARM_REG_R0, &r0);

                uc_mem_read(uc, r0, buffer, 4096);

                r0 = strlen(buffer);

                uc_reg_write(uc, UC_ARM_REG_R0, &r0);

                uint32_t pc = address;

                pc += 5;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //malloc

            case BASE+ 0x249f3c:

            case BASE+ 0x249f06:

            case BASE + 0x249c02:

            {

                uint32_t r0 = 0;

                uc_reg_read(uc, UC_ARM_REG_R0, &r0);

                char* buffer = (char*)malloc(r0);

                r0=create_mem(uc, buffer, r0);

                free(buffer);

                uc_reg_write(uc, UC_ARM_REG_R0, &r0);

                uint32_t pc = address;

                pc += 5;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //memcpy 后为THUMB指令

            case BASE+0x249c68:

            case BASE+0x249c0e:

            case BASE+0x24947A:

            case BASE+0x249456:

            {

                uint32_t r0 = 0;

                uint32_t r1 = 0;

                uint32_t r2 = 0;

                uc_reg_read(uc, UC_ARM_REG_R0, &r0);

                uc_reg_read(uc, UC_ARM_REG_R1, &r1);

                uc_reg_read(uc, UC_ARM_REG_R2, &r2);

                char *buffer =(char*)malloc(r2);

                uc_mem_read(uc, r1, buffer, r2);

                uc_mem_write(uc, r0, buffer, r2);

                free(buffer);

                uint32_t pc = address;

                //memcpy 后为ARM指令

                if (address == BASE + 0x249c68)

                    pc += 4;

                else

                    pc += 5;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //特殊处理4字ARM指令

            case BASE + 0x249C6C:

            {

                uint32_t pc = address;

                pc += 5;

                uint32_t r0 = 0x2c0;

                uc_reg_write(uc, UC_ARM_REG_R0, &r0);

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //跳过stack_guard错误的内存地址

            case BASE + 0x249BD8:

            {

                uint32_t pc = address;

                pc += 7;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //sin函数

            case BASE+0x249EE8:

            {

                uint32_t r0 = 0;

                uint32_t r1 = 0;

                uc_reg_read(uc, UC_ARM_REG_R0, &r0);

                uc_reg_read(uc, UC_ARM_REG_R1, &r1);

                double value = 0;

                memcpy(&value, &r0, 4);

                memcpy((char*)&value+4, &r1, 4);

                double ret=sin(value);

                r0 = LODWORD(ret);

                r1 = HIDWORD(ret);

                uc_reg_write(uc, UC_ARM_REG_R0, &r0);

                uc_reg_write(uc, UC_ARM_REG_R1, &r1);

                uint32_t pc = address;

                pc += 5;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

                break;

            }

            //free

            case BASE+ 0x24a68c:

            case BASE+0x249f24:

            {

                uint32_t pc = address;

                pc += 5;

                uc_reg_write(uc, UC_ARM_REG_PC, &pc);

            }  

            default:

            {

                print_reg(uc, address);

                break;

            }

        }

    }

    static unsigned char* read_file(char* path, uint32_t* len)

    {

        FILE* fp = fopen(path, "rb");

        if (fp == NULL)

            return nullptr;

        fseek(fp, 0, SEEK_END);

        *len = ftell(fp);

        fseek(fp, 0, SEEK_SET);

        unsigned char* code = (unsigned char*)malloc(*len);

        memset(code, 0, *len);

        fread(code, 1, *len, fp);

        fclose(fp);

        return code;

    }

    static void test_thumb(void)

    {

        uc_engine *uc;

        uc_err err;

        uc_hook trace1, trace2;

        uint32_t sp = STACK_ADDR; 

        offset = 0;

        err = uc_open(UC_ARCH_ARM, UC_MODE_THUMB, &uc);

        if (err) {

            printf("Failed on uc_open() with error returned: %u (%s)\n",

                err, uc_strerror(err));

            return;

        }

        char plain[] = "/vps?tvid=11949478009&vid=7b23569cbed511dd58bcd6ce9ddd7b42&v=0&qypid=11949478009_unknown&src=02022001010000000000&tm=1519712402&k_tag=1&k_uid=359125052784388&bid=1&pt=0&d=1&s=0&rs=1&dfp=1413357b5efa4a4130b327995c377ebb38fbd916698ed95a28f56939e9d8825592&k_ver=9.0.0&k_ft1=859834543&k_err_retries=0&qd_v=1";

        uc_mem_map(uc, PARAM_ADDR, PARAM_SIZE, UC_PROT_ALL);

        uc_mem_map(uc, BASE, CODE_SIZE, UC_PROT_ALL);

        uint32_t r0 = PARAM_ADDR;

        uint32_t sp_start = sp + STACK_SIZE;

        int ret=uc_mem_map(uc, sp, sp_start - sp, UC_PROT_ALL);

        uint32_t len = 0;

        unsigned char* code = read_file("./aef52000_36e000.so", &len);

        uc_mem_write(uc, BASE, code, len);

        free(code);

        create_mem(uc, plain, strlen(plain) + 1);

        uc_reg_write(uc, UC_ARM_REG_R0, &r0);

        uc_reg_write(uc, UC_ARM_REG_SP, &sp);

        uc_hook_add(uc, &trace2, UC_HOOK_CODE, hook_code, NULL, 1, 0);

        err = uc_emu_start(uc, BASE + 0x249BC8 + 1, BASE + 0x24a692, 0, 0);

        if (err) {

            printf("Failed on uc_emu_start() with error returned: %u\n", err);

        }

        char buffer[4096] = "";

        uc_reg_read(uc, UC_ARM_REG_R0, &r0);

        uc_mem_read(uc, r0, buffer, 4096);

        printf("result:%s\n", buffer);

        uc_close(uc);

    }

    int main()

    {

        test_thumb();

        system("pause");

        return 0;

    }

     

     

    代码已经给了,就不多说了,

    我没有直接使用libmcto_media_player.so因为data段需要重定位。所以我写了一个dump工具。

    将SO从内存中dump出来。直接调用这段已经重定位过的内存。

    修复内存报错的位置。实现该算法中涉及的几个API 包括 strlen memcpy malloc free  sin 函数。 

    主要就是注意BLX调用完API的时候下一条指令是THUMB模式还是ARM模式就好。

    最后运行,运行结果也与vf字段一致。

    dump通过命令

    1

    2

    3

    4

    5

    shell@hammerhead:/data/local/tmp $./dump ./libmcto_media_player.so ./libmctocurl.so ./libcupid.so

    [+] dlopen ./libmctocurl.so

    [+] dlopen ./libcupid.so

    [+] dlopen libdvm.so

    [+] save 0xaf009000_0x377000.so

    四、总结

           这只是一个简单的算法函数,涉及的API并不多,如果是复杂的算法涉及API数量庞大这种自己实现API的方式就并不可取。所以接下来有时间会继续研究SO的完整的调用。让他像loader一样方便。

    五、参考

    Android SO 高阶黑盒利用

    挑战4个任务:迅速上手Unicorn Engine


     

    [招聘]欢迎市场、营销人员加入看雪学院团队!

    最后于  2018-3-8 13:27 被scxc编辑 ,原因:

    上传的附件:

    •  unicorn.7z (4.42MB,305次下载)
    •  dumpso.7z (39.82kb,303次下载)
    •  loader.7z (2.03MB,280次下载)
    展开全文
  • http://eternal.red/2018/unicorn-engine-tutorial/

    如果你对 Unicorn Engine 还不是很了解的话,请点击我在之前发过的博客:Tutorial for Unicorn:Unicorn Engine 的开发和使用。有时候,我们可能不需要模拟整个系统或者整个二进制,这时候 Unicorn 作为一款优秀的模拟器,就发挥了它的作用。但是 Unicorn 并不支持系统调用,因此,需要手动映射内存,并将数据写入其中,从自定义的开始地址加载相应汇编代码。

    0x10 源码

    本次,先介绍一道简单的自定义题目。比如说,我们想要改写如下代码生成的二进制。这段代码意思很简单,a = 1 != 5,b = "spiderman" != “batman”,很显然,result 输出的结果一直是 0。最终的目标就是,在没有源码的情况下,让二进制输出 1。

    #include <stdio.h>
    
    int strcmp(char *a, char *b)
    {
        //get length
        int len = 0;
        char *ptr = a;
        while(*ptr)
        {
            ptr++;
            len++;
        }
        
        //comparestrings
        for(int i=0; i<=len; i++)
        {
            if (a[i]!=b[i])
                return 1;
        }
        
        return 0;
    }
    
    __attribute__((stdcall))
    int  super_function(int a, char *b)
    {
        if (a==5 && !strcmp(b, "batman"))
        {
            return 1;
        }
        return 0;
    }
    
    int main()
    {
        int result = super_function(1, "spiderman");
        printf("%d\n", result);
    } 
    

    编译生成二进制文件,这里 -m32 是说编译成 32 位程序,当然也可以不加该参数

    gcc test.c -m32 -o test
    

    运行程序,结果符合预期
    在这里插入图片描述

    0x20 Unicorn 模拟程序

    0x21 模拟原程序

    引入库文件,第一行代码是引入 unicorn 库,第二行则是根据要模拟的不同架构,选择不同的库,本次是要模拟 x86 架构,因此引入 unicorn.x86_const

    from unicorn import *
    from unicorn.x86_const import *
    

    初始化 unicorn engine 类,生成 unicorn 实例对象 mu

    mu = Uc(UC_ARCH_X86, UC_MODE_32)
    

    设计内存分布,调用 mem_map 方法来映射内存,这里假设我们要在将代码放在 0x000000 处执行,将栈的位置和大小也确定下来(tips:正常来说,对于 x86 4GB 内存空间,程序加载的起始地址是 0x08048000,这里为了让调试地址与 IDA 反汇编一一对应,直接写 0x00000,换成其他地址也是可以的

    BASE = 0x000000
    STACK_ADDR = 0X100000
    STACK_SIZE = 1024 * 1024 # 1MB
    
    mu.mem_map(BASE, 1024 * 1024)
    mu.mem_map(STACK_ADDR, STACK_SIZE)
    

    现在,我们需要像加载程序一样在我们的基地址加载二进制文件。然后我们需要设置 RSP 指向堆栈的末尾,接触过汇编的人应该都知道,这也是正常函数执行的开始过程,sub sp 用来设置栈的大小。

    mu.mem_write(BASE, read("./test"))
    mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 1)
    

    选择需要模拟执行的汇编代码,即要确定开始和结束的地址,将其加载进已经指定好的内存空间
    在这里插入图片描述

    mu.emu_start(0x1240, 0x127B)
    

    运行,可以追踪到详细的错误汇编代码
    在这里插入图片描述
    定位 0x1247,猜测可能是由于 ecx-4 处的内存地址并没有被映射,绕过即可

    .text:00001247                 push    dword ptr [ecx-4]
    

    怎么绕过呢?绕过其实就是将 eip 寄存器存放的地址指向另外一条指令(eip 存放下一条即将执行指令的地址)

    instructions_skip_list = [0x1247]
    
    def hook_code(mu, address, size, user_data):
    	print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' % (address, size))
    
    	if address in instructions_skip_list:
    		mu.reg_write(UC_X86_REG_EIP, address + size)
    

    到此,我们完整的运行了指定的代码片段。但是这并不足够,因为程序还没有输出,而 printf 作为 libc 的库函数,我们并没有加载到内存中去,所以没办法模拟,那么就需要使用 unicorn 将结果显示出来。

    0x22 修改运行逻辑

    对于本例来说,很好解决,因为在调用 printf 之前,要打印的值也就是参数值,已经存放在相关寄存器或者变量 a 中。

    再次查看汇编代码,发现,0x00001266 处调用 super_function,那么调用这个函数之后,返回值就会存放在 eax 寄存中,只要该代码运行之后,将 eax 的值打印出来就行了。

    def hook_code(mu, address, size, user_data):
    	# print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' % (address, size))
    
    	if address == 0x0000126B:
    		print("result = %x" % mu.reg_read(UC_X86_REG_EAX))
    

    运行会打印出:result = 0,现在真正的问题就在于怎么修改程序让结果为 1 了(为了加大难度,不要再上面的代码基础上,直接加一条指令 mu.reg_write(UC_X86_REG_EAX, 1))。我们是要更改一下传入的参数,让判断逻辑成立。
    在这里插入图片描述
    要修改的汇编代码如下
    在这里插入图片描述
    可以直接使用 IDA 修改汇编指令,但是这不符合我们的初衷,我们是要使用 unicorn 来修改运行过程中,寄存器的值,从而达到修改程序逻辑的目的。

    在栈中,写入一个字符串,即 batman, 替换 0x125D 处的代码。而 push 1 没有用到寄存器,怎么修改呢?这个时候就需要对栈比较了解了。 在本例中,函数是从栈中取参数,而栈又是靠 esp 寄存器存放的栈指针进行定位

    mu.mem_write(STACK_ADDR, "batman\x00")
    reg_esp = STACK_ADDR + STACK_SIZE / 2
    mu.reg_write(UC_X86_REG_ESP, reg_esp)
    mu.mem_write(reg_esp + 4, p32(5))
    mu.mem_write(reg_esp + 8, p32(STACK_ADDR))
    

    0x30 总结

    使用 unicorn 模拟执行二进制代码的过程就像上面所述,使用其提供的 api,就可以完成很多操作。具体代码的执行,可能因为不同的环境,所有差别,但是总体的思想是一致的,先初始化 unicorn 引擎,然后分配内存,将目标程序加载进我们设置的内存,接着 hook 函数,排错;最后调用 emu_start 即可开始。期间,可以使用相应的函数,修改寄存器的值,进而改变程序执行的逻辑。

    0x40 附录

    0x21 节完整代码

    from unicorn import *
    from unicorn.x86_const import *
    import struct
    
    def read(name):
    	with open(name) as fp:
    		return fp.read()
    
    instructions_skip_list = [0x1247]
    
    def hook_code(mu, address, size, user_data):
    	# print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' % (address, size))
    
    	if address in instructions_skip_list:
    		mu.reg_write(UC_X86_REG_EIP, address + size)
    
    	if address == 0x0000126B:
    		print("result = %x" % mu.reg_read(UC_X86_REG_EAX))
    		
    
    def main():
    	# 初始化 unicorn
    	mu = Uc(UC_ARCH_X86, UC_MODE_32)
    	
    	# 内存布局
    	BASE = 0x000000
    	STACK_ADDR = 0X100000
    	STACK_SIZE = 1024 * 1024 # 1MB
    	
    	mu.mem_map(BASE, 1024 * 1024)
    	mu.mem_map(STACK_ADDR, STACK_SIZE)
    	
    	# 加载程序
    	mu.mem_write(BASE, read("./test"))
    	mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 1)
    
    	mu.hook_add(UC_HOOK_CODE, hook_code)
    	mu.emu_start(0x1240, 0x127B)
    
    if __name__ == '__main__':
    	main()
    
    

    0x22 节完整代码

    from unicorn import *
    from unicorn.x86_const import *
    import struct
    
    BASE = 0x08048000
    STACK_ADDR = 0X100000
    STACK_SIZE = 1024 * 1024 # 1MB
    instructions_skip_list = [BASE + 0x1247]
    
    def read(name):
    	with open(name) as fp:
    		return fp.read()
    
    def p32(num):
    	return struct.pack("I", num)
    
    def hook_code(mu, address, size, user_data):
    	print('>>> Tracing instruction at 0x%x, instruction size = 0x%x' % (address, size))
    
    	if address in instructions_skip_list:
    		mu.reg_write(UC_X86_REG_EIP, address + size)
    
    	if address == BASE + 0x0000126B:
    		print("result = %x" % mu.reg_read(UC_X86_REG_EAX))
    		
    
    def main():
    	mu = Uc(UC_ARCH_X86, UC_MODE_32)
    
    	mu.mem_map(BASE, 1024 * 1024)
    	mu.mem_map(STACK_ADDR, STACK_SIZE)
    
    	mu.mem_write(BASE, read("./test"))
    	#mu.reg_write(UC_X86_REG_ESP, STACK_ADDR + STACK_SIZE - 1)
    	
    	mu.mem_write(STACK_ADDR, "batman\x00")
    	reg_esp = STACK_ADDR + STACK_SIZE / 2
    	mu.reg_write(UC_X86_REG_ESP, reg_esp)
    	mu.mem_write(reg_esp + 4, p32(5))
    	mu.mem_write(reg_esp + 8, p32(STACK_ADDR))
    
    	mu.hook_add(UC_HOOK_CODE, hook_code)
    	mu.emu_start(BASE + 0x1240, BASE + 0x127B)
    
    if __name__ == '__main__':
    	main()
    
    

    参考文章
    https://blog.csdn.net/song_lee/article/details/104439329
    http://eternal.red/2018/unicorn-engine-tutorial/

    展开全文
  • Unicorn是基于的轻量级多平台,多体系结构的CPU仿真器框架。 执照 GPLv2 编译器兼容性 免费Pascal> = v3 Mac OS Windows Linux 德尔斐 Windows 特征 与C核心相同的API 一些针对Pascals不区分大小写的变通办法...
  • Cmulator是(x86-x64) 用于Shellcode和PE二进制文件的可编写脚本的逆向工程沙盒模拟器基于Unicorn&Capstone Engine和javascript。 :speech_balloon: 这是最后一个受支持的Pascal版本,新的代码库(C / C ++)将在...
  • ServerEngine是一个框架,用于实现强大的多进程服务器,例如Unicorn。 主要特点: Heartbeat via pipe & auto-restart / \ ---+ +------------+ / +----------+ \ +--------+ | | Supervisor |------| Server |-...
  • WxSpectre由两大部分组成:Unicorn-engine+仿真WINDOWS系统环境.WxSpectre实现了大部分的windows执行应用程序的环境,包括文件系统,对象管理,注册表,线程调度,内存管理,异常/中断处理,Windows API(ring3).pe ...
  • unicorn教程二

    千次阅读 2020-01-30 12:14:52
    在优化之前,我们首先模拟正常的程序,一旦程序成功运行后,我们再在Unicorn Engine中对其进行优化。 新建一个python脚本,注意与fibonacci同样路径 导入unicorn,以及x86体系结构的unicorn constant read...
  • 使用Unicorn-engine 续1

    2016-05-24 16:39:00
     在windows上,我主要是用python进行分析程序,因此我最初安装的是官网上的unicorn-0.9-python2.7-win32.exe。至于我为什么要用32位的版本,因为64位的版本不能在IDAPython里使用,就像下面:  后来,我干脆...
  • 1.第一步添加包含目录 2.添加资源文件 3.就可以愉快的写代码了 转载于:https://www.cnblogs.com/Reserved/p/8526510.html
  • unicorn教程一

    千次阅读 2020-01-30 12:06:45
    http://www.unicorn-engine.org/docs/tutorial.html 先来了解下什么是unicorn引擎。。 Unicorn 是一个轻量级, 多平台, 多架构的 CPU 模拟器框架. 我们可以更好地关注 CPU 操作, 忽略机器设备的差异. 想象一下, 我们...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 609
精华内容 243
关键字:

engineunicorn