逆向调试时还是 IDA 的图形化界面更方便,一般首选 IDA 调试分析,后期要在生产线上生成 sign 字段,这时再用 unidbg 就更合适了!

1、调用 so 库中函数的一些方式

  • frida 的 rpc
  • xposed+andserver
  • unicorn+web 框架
  • unidbg ( 基于 Unicorn 引擎 )
  • qiling ( 基于 Unicorn 引擎 )
  • AndroidNativeEmu
  • 等等。

2、Unidbg 简介

Unidbg 是什么

unidbg 是一个标准的 java 项目,是一款基于 unicorn 和 dynarmic 的逆向工具。可以直接黑盒调用 Android 和 IOS 的 so 文件,无论是黑盒调用 so 层算法,还是白盒 trace 输出 so 层寄存器值变化都是一把利器~ 尤其是动态 trace 方面堪比 ida trace。

unidbg github 地址:https://github.com/zhkl0228/unidbg

做脱机协议,首先要找到关键的加密代码,然而这些代码一般都在so里面,因为逆向c/c++的难度远比java大多了!找到关键代码后,一般情况下是逐行分析,然后自己写代码复现整个加密过程。但是,有些非标准的加密算法是由一个团队实现的,整个过程非常复杂。逆向人员再去逐行分析和复现,有点“不划算”!怎么才能直接调用so里面的这些关键代码了?可以通过前面的介绍的frida hook,也可以通过今天介绍的这个so的模拟框架--unidbg!

Unidbg 特色

  • 模拟 JNI 调用 API,以便可以调用 JNI_OnLoad。
  • 支持 JavaVM,JNIEnv。
  • 模拟 syscalls 调用。
  • 支持 ARM32 和 ARM64。
  • 基于 HookZz 实现的 inline hook。
  • 基于 xHook 实现的 import hook。
  • 支持 iOS fishhook 、 substrate 、 whale hook。
  • 支持简单的控制台调试器,gdb,IDA android 调试器服务器,指令跟踪,内存读/写跟踪。
  • 支持 iOS objc 和 Swift 运行时。

unidbg 基于以下项目

  • unicorn
  • dynarmic
  • HookZz
  • xHook
  • AndroidNativeEmu
  • usercorn
  • keystone
  • capstone
  • idaemu
  • jelf
  • whale
  • kaitai_struct
  • fishhook
  • runtime_class-dump
  • mman-win32

看着很多,实际并不复杂。开发android app,在Android studio配置好各种环境和参数后是能直接在java层调用so层函数的。那么在unidbg,也能实现同样的功能:即调用so层的函数!这也是unidbg最核心的功能之一了!

keystone-engine 汇编框架

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

keystone-engine 是⼀个开源的轻量级多平台、多架构汇编框架,⽀持 Arm, Arm64 (AArch64/Armv8), Hexagon, Mips, PowerPC, Sparc, SystemZ、 X86 (16/32/64bit).  ⾮常强⼤!!

pip 安装:pip install keystone-engine

源码可以在Linux、Windows下顺利编译,自带一个kstool用于演示。

利用Keystone,很简单,大致这么几步:
ks_open()   指定CPU
ks_asm()    指定基址、汇编指令
printf()    输出机器码
ks_free()   释放动态分配的机器码空间
ks_close()  关闭

unidgb 有3 种调试模式,源码写得很清楚,分别是

  • CONSOLE
  • GDB_SERVER
  • ANDROID_SERVER_V7

Unidbg 有什么用

现在许多 app 把签名算法已经放到了 so 文件中,所以要想破解签名算法,必须能够破解 so 文件。但是 C++ 的逆向远比 Java 的逆向要难得多了,所以好多时候是没法破解的,那么这个时候还可以采用 hook 的方法,直接读取程序中算出来的签名,但是这样的话,需要实际运行这个应用,需要模拟器或者真机,效率又不是很高。unidbg 就是一个很巧妙地解决方案,他不需要直接运行 app,也无需逆向 so 文件,而是通过在 app 中找到对应的 JNI 接口,然后用 unicorn 引擎直接执行这个 so 文件,所以效率也比较高。

现在很多的 app 使用了so加密,可能你会直接破解 so 进行算法破解,但是也可以不用破解so,利用很多大佬写好的轮子即可 直接调用 so 中的函数方法

Frida + IDA 动静态 分析流程

使用 Frida + IDA 的动静态分析中,流程是这样的

  • 找线索 ---> Frida Hook验证 ---> 验证成功继续下一步 / 验证失败继续找线索

在以 Frida 为中心的逆向分析中,写 hook 代码是工作重心

Unidbg 模拟执行、算法还原 流程

以 Unidbg 为中心去做模拟执行/算法还原时,流程是这样的

  • Frida 主动调用获取一份正确结果
  • Unidbg写代码尝试运行→Unidbg给出报错→补环境
  • 循环往复,最后得到和Frida主动调用一致的结果

以 Unidbg 为中心的逆向分析里,补环境 是工作重心。

Unidbg 补环境

Unidbg 中的补环境,大体上可以分成两类,运行环境缺失和上下文缺失,

  • 上下文缺失则是由于样本在运行目标函数前对SO或目标函数做了一些初始化工作,Unidbg中对目标函数单独运行,自然就导致了上下文缺失。上下文缺失相较于运行环境缺失,是一种更加隐蔽的环境缺失。
  • 运行环境缺失。最简单的例子就是目标函数中通过 JNI 调用到了某个自己的 JAVA 方法,Unidbg 会及时报错,给出堆栈以及这个 JAVA 方法的签名,需要我们补上对应的 JAVA 方法,

补 JAVA 环境是补运行环境中主要的一部分,但是,补 JAVA 环境并不是工作的全部。还有哪些环境需要补呢?

  • 文件读写——对linux虚拟文件的读写,对ASSETS资源文件的读写、对app目录下文件的读取,对 Sharedpreference 的读取等等
  • 系统调用具体实现——比如popen函数所涉及的系统调用等
  • 系统库SO,Unidbg并没有,实际上也不可能模拟完整的 Android 系统 SO 环境,有的 SO 所依赖的 SO 比较多,很难调起来,所以 Unidbg 设计了 VirtualModule(虚拟SO模块),有时候我们需要和它打交道。
  • 补不了,打 patch

Unidbg 在大多数情况下是好的方案

在补环境的过程中,对算法细节有了一定的理解,而且用 Unidbg 跑出结果后,分析和还原算法会更快,因为 Unidbg 的 code trace / hook / console debugger 相比较 IDA trace,Frida hook 更快、更稳定、更方便复现。而且环境完全是我们的,有绝对的掌控。

Unidbg 并不是过往某个工具的替代品,而是 IDA、Frida 以及其工具套件的互补品。它和过去的工具结合在一起 1+1>2

Unidbg 环境配置、使用

Unidbg 环境配置

unidbg 项目用 Java 编写,并且官网下载的下来的代码使用的是标准的 maven 构建的,所以在使用 unidbg 之前需要修改先安装好 JDK 环境和 Maven 环境。将下载的 unidbg-master.zip 进行解压,然后使用 IDEA 导入项目。【File】 –> 【New】–> 【Project from Existing Sources】

​1、IntelliJ IDEA。官网:https://www.jetbrains.com.cn/idea/
2、Maven 环境。 官网:https://maven.apache.org/download.cgi
        解压文件:tar -zxvf apache-maven-3.8.7-bin.tar.gz
        配置环境变量:
        export MAVEN_HOME=/usr/local/apache-maven-3.8.7
        export PATH=$MAVEN_HOME/bin:$PATH
        测试是否配置成功:mvn -version

MAVEN_HOME : D:\Software\apache-maven-3.8.7
PATH : %MAVEN_HOME%\bin;
MAVEN_OPTS : -Xms128m -Xmx512m -Duser.language=zh -Dfile.encoding=UTF-8

idea 配置 maven

导入代码:git clone https://github.com/zhkl0228/unidbg.git

基本使用步骤

下载 unidbg 框架,​框架的目录 unidbg-android/src/test/java 放置了很多示例,足以支撑入门

简单罗列了一下基本的使用, 十分好上手~!

// 创建模拟器实例,建议使用实际进程名,可以规避进程名校验
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xxx.xxx").build();
// 创建模拟器内存接口
final Memory memory = emulator.getMemory();
// 设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));
// 创建 Android 虚拟机,传入 APK,unidbg 可以协助做一部分签名工作
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/xxx/xxx.apk"));
// 加载 so 到虚拟内存,第二个参数的意思表示是否执行动态库的初始化代码
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/xxx/libxxx.so"),true);
// 获取 so 模块的句柄
module = dm.getModule();
// 设置 JNI 
vm.setJni(this);
// 打印日志
vm.setVerbose(true);
// 调用 JNI_Onload
dm.callJNI_OnLoad(emulator);

// 构建函数参数格式
List<Object> args = new ArrayList<>(10);
// 各种基本参数格式兼容
// 参数1:JNIEnv *env
args.add(vm.getJNIEnv());
// 参数2:jobject或jclass 用不到直接填0即可
// 创建 jobject, 如果没用到的话可以不写
// cNative = vm.resolveClass("com/xxx/xxx");
// DvmObject<?> cnative = cNative.newObject(null);
// args.add(cnative.hashCode());
args.add(0);
// 参数3 字符串对象
String input = "abcdef";
args.add(vm.addLocalObject(new StringObject(vm, input)));
// 参数4 bytes 数组
String input = "abcdef";
byte[] input_bytes = input.getBytes(StandardCharsets.UTF_8);
ByteArray input_byte_array = new ByteArray(vm,input_bytes);
args.add(vm.addLocalObject(input_byte_array));
// 参数5 bool 
// false 填 0,true 填 1
args.add(1);
// unidbg 主动调用函数
// 第二个参数是函数偏移量(thumb 记得+1)
// 第三个参数是参数列表
Number number = module.callFunction(emulator,0x10618, args.toArray());

// unicorn trace(贼好用!!!堪比 ida trace!!!)
String traceFile = "trace.txt";
PrintStream traceStream = null;
try{
    traceStream = new PrintStream(new FileOutputStream(traceFile), true);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
// 核心 trace 开启代码,也可以自己指定函数地址和偏移量
emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream);
// 获取最终返回值,同时运行过程中的汇编代码和寄存器值会写入到文件中
return vm.getObject(number.intValue()).getValue().toString();

看看 trace log,建议搭配 010 editor 使用, 更香

示例:代码分析

入口点

第一步: 补环境 跟踪 TTEncrypt 函数,注释写的很清楚了,不做过多分析。基本套路都是这个样子。

第二步: HOOK 相关的函数 跟踪 ttEncrypt,可知代码 hook 了 ssencrypt 和 ssencrypted_size 两个函数。

第三步: 添加调试及主动调用

第四步: 销毁环境。跟踪 destroy

运行结果。如下图所示,运行成功就代表成功

这个时候按 c 继续,可以看到 hook 的结果以及JNI调用细节。

单步调试,ida_server 的 Debug方式相对简单,对于 unidbg 的强大之一在于它的单步调试-- Console Debugger,例子是以抖音作为例子的,还是很不错的。注释都写的比较清楚了。unidbg单步调试做的很棒,这个弥补了 frida 调试能力比较弱的缺点。

此示例就是对如下目录中的 so 进行操作。

unidbg-android/src/test/resources/example_binaries/libttEncrypt.so

查找 so 中的 sbox0、sbox1 导出符号,并打印其内存数据。

Hook一些函数,使用多个框架。

调用 so 中的 ttEncrypt 静态注册函数。

运行结果如下图所示:

打包成 jar 文件:https://www.jianshu.com/p/59e08e48ac20

打包成 jar 包

调用so 的有一个可执行程序呀,总不能让别人机器上也装个IDEA来陪你玩吧?

File ---> Project Structure …​ 然后选择 Artifacts,点加号 Add ---> jar ---> From modules with dependencies…​ 然后如下配置, 别忘了勾上 Include tests

OK了,Build → Build Artifacts 编译成功之后就在 unidbg-0.9.0/out/artifacts/unidbg_parent_jar 目录下生成了一堆依赖jar包和unidbg-parent.jar 我们把要载入的 fenfei/libnative-lib.so 放到和 unidbg-parent.jar同级目录

跑一下,成功输出,手工。 (努力的字都不会打了)

fenfeideMacBook-Pro:unidbg_parent_jar fenfei$ java -jar unidbg-parent.jar
call stringFromJNI rc = Hello from C++

unidbg 调用so层函数 ( 普通的so方法、jni_onload调用、jni函数调用 )

1、选择执行引擎:如果明确使用了以下代码,那么unidbg使用dynarmic引擎,否则默认使用unicorn引擎!

static {
        DynarmicLoader.useDynarmic();
    }

2、创建虚拟机/模拟器,并执行虚拟机的类型是art还是dailvik:

AndroidARMEmulator emulator= new AndroidARMEmulator("com.sun.jna",null,null);
final Memory memory = emulator.getMemory();
VM vm = emulator.createDalvikVM();
vm.setVerbose(true);//这里如果是true,后续调用jni_onload的时候就能打印日志

3、指定SDK的版本,这里用23版本:

LibraryResolver resolver = new AndroidResolver(23);
memory.setLibraryResolver(resolver);

4、开始加载so库了:

Module unicorn08module=emulator.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\unicorn08.so"));

5、调用so层的导出函数:这两个都是导出函数,直接用符号名就行了;

Number result=unicorn08module.callFunction(emulator,"_Z3addii",1,2)[0];//导出函数直接用符号名就行了
System.out.println("_Z3addii result:"+result.intValue());
//_Z7add_sixiiiiii
result=unicorn08module.callFunction(emulator,"_Z7add_sixiiiiii",1,2,3,4,5,6)[0];
System.out.println("_Z7add_sixiiiiii result:"+result.intValue());

这个是打印的结果:

_Z3addii result:3
_Z7add_sixiiiiii result:21

6、这两个都是对字符串做操作的,so层仅仅求了传入字符串的长度:

MemoryBlock block1=memory.malloc(10,true);
UnidbgPointer str1_ptr=block1.getPointer();
str1_ptr.write("hello".getBytes());
String content=ARM.readCString(emulator.getBackend(),str1_ptr.peer);
System.out.println("_Z15getstringlengthPKc:"+str1_ptr.toString()+"---"+content);
result=unicorn08module.callFunction(emulator,"_Z15getstringlengthPKc",new PointerNumber(str1_ptr))[0];
Number r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0);
System.out.println("_Z15getstringlengthPKc result:"+result.intValue()+"----"+r0value);

MemoryBlock block2=memory.malloc(10,true);
UnidbgPointer str2_ptr=block2.getPointer();
str2_ptr.write("666".getBytes());
String content2=ARM.readCString(emulator.getBackend(),str2_ptr.peer);
System.out.println("_Z16getstringlength2PKcS0_:"+str2_ptr.toString()+"---"+content2);
result=unicorn08module.callFunction(emulator,"_Z16getstringlength2PKcS0_",new PointerNumber(str1_ptr),new PointerNumber(str2_ptr))[0];
r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0);
System.out.println("_Z16getstringlength2PKcS0_ result:"+result.intValue()+"----"+r0value);

打印结果:

_Z15getstringlengthPKc:RW@0x4016b000---hello
_Z15getstringlengthPKc result:5----5
_Z16getstringlength2PKcS0_:RW@0x4016c000---666
_Z16getstringlength2PKcS0_ result:8----8

7、这核心的核心:直接调用jni_onload

vm.callJNI_OnLoad(emulator,unicorn08module);

打印结果:这里可以看到分别在so的0x8c7、0xccb调用了FindClass和RegisterNative方法,然后注册MainActivity这个类的stringFromJNI2方法,该方法和so层中0xb35的方法是映射的!

JNIEnv->FindClass(com/example/unicorncourse08/MainActivity) was called from RX@0x40000c87[libnative-lib.so]0xc87
JNIEnv->RegisterNatives(com/example/unicorncourse08/MainActivity, unidbg@0xbffff778, 1) was called from RX@0x40000ccb[libnative-lib.so]0xccb
RegisterNative(com/example/unicorncourse08/MainActivity, stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;, RX@0x40000b35[libnative-lib.so]0xb35)

去so的0xc87和0xccb查看,果然是FindClass和RegisterNative方法,unidbg 诚不我欺! 作为逆向,其实最重要的还是最后那个打印结果:java层的stringFromJNI2方法就是和so层的这个方法是映射的:

进入里面调用的各个函数仔细分析,发现这个函数还是比较简单:先接受传入的string,再打印出来!由于这个函数并未导出,但是和java层的函数做了映射,所以这里也可以直接通过java层的名字来直接调用,代码如下:

//调用jni函数,对于动态注册的jni函数必须在完成地址的绑定才能调用
System.out.println("stringFromJNI1-------------------------");
DvmClass MainActivity_dvmclass=vm.resolveClass("com/example/unicorncourse08/MainActivity");//先把类找到,这里的原理很像反射
DvmObject resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;","helloworld");//再通过类去调用里面的函数
System.out.println("resultobj:"+resultobj);
resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellokanxue"));
System.out.println("resultobj:"+resultobj);
System.out.println("stringFromJNI1-------------------------");

//动态注册的jni函数stringFromJNI2
resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2"));
System.out.println("resultobj:"+resultobj);
System.out.println("stringFromJNI2-------------------------");

DvmObject mainactivity=MainActivity_dvmclass.newObject(null);
mainactivity.callJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2"));
System.out.println("resultobj:"+resultobj);
System.out.println("stringFromJNI2-------------------------");

打印的结果如下:这里面除了我们显式调用println打印的日志,还有unidbg这个框架自身打印的日志(主要是JNIenv这个类的函数调用):

stringFromJNI1-------------------------
Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI1(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000a71[libnative-lib.so]0xa71
JNIEnv->GetStringUtfChars("helloworld") was called from RX@0x40000b03[libnative-lib.so]0xb03
JNIEnv->NewStringUTF("helloworld") was called from RX@0x40000b2f[libnative-lib.so]0xb2f
resultobj:"helloworld"
Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI1(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000a71[libnative-lib.so]0xa71
JNIEnv->GetStringUtfChars("hellokanxue") was called from RX@0x40000b03[libnative-lib.so]0xb03
[main]I/stringFromJNI1: content:helloworld,length:10
[main]I/stringFromJNI1: content:hellokanxue,length:11
[main]I/stringFromJNI2: content:hellostringFromJNI2,length:19
[main]I/stringFromJNI2: content:hellostringFromJNI2,length:19
JNIEnv->NewStringUTF("hellokanxue") was called from RX@0x40000b2f[libnative-lib.so]0xb2f
resultobj:"hellokanxue"
stringFromJNI1-------------------------
Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI2(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000b35[libnative-lib.so]0xb35
JNIEnv->GetStringUtfChars("hellostringFromJNI2") was called from RX@0x40000b03[libnative-lib.so]0xb03
JNIEnv->NewStringUTF("hellostringFromJNI2") was called from RX@0x40000b2f[libnative-lib.so]0xb2f
resultobj:"hellostringFromJNI2"
stringFromJNI2-------------------------
Find native function Java_com_example_unicorncourse08_MainActivity_stringFromJNI2(Ljava/lang/String;)Ljava/lang/String; => RX@0x40000b35[libnative-lib.so]0xb35
JNIEnv->GetStringUtfChars("hellostringFromJNI2") was called from RX@0x40000b03[libnative-lib.so]0xb03
JNIEnv->NewStringUTF("hellostringFromJNI2") was called from RX@0x40000b2f[libnative-lib.so]0xb2f
resultobj:"hellostringFromJNI2"
stringFromJNI2-------------------------

完整的代码:

package com.unicorncourse08;

import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.PointerNumber;
import com.github.unidbg.arm.ARM;
import com.github.unidbg.arm.backend.dynarmic.DynarmicLoader;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.StringObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import unicorn.ArmConst;
import java.io.File;

public class MainActivity {
    static {
        DynarmicLoader.useDynarmic();
    }
    public static void main(String[] args) {
        AndroidARMEmulator emulator= new AndroidARMEmulator("com.sun.jna",null,null);
        final Memory memory = emulator.getMemory();

VM vm = emulator.createDalvikVM();
        vm.setVerbose(true);

LibraryResolver resolver = new AndroidResolver(23);
        memory.setLibraryResolver(resolver);
        Module unicorn08module=emulator.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\unicorn08.so"));
        Number result=unicorn08module.callFunction(emulator,"_Z3addii",1,2)[0];//导出函数直接用符号名就行了
        System.out.println("_Z3addii result:"+result.intValue());

//_Z7add_sixiiiiii
        result=unicorn08module.callFunction(emulator,"_Z7add_sixiiiiii",1,2,3,4,5,6)[0];
        System.out.println("_Z7add_sixiiiiii result:"+result.intValue());

//_Z15getstringlengthPKc
        MemoryBlock block1=memory.malloc(10,true);
        UnidbgPointer str1_ptr=block1.getPointer();
        str1_ptr.write("hello".getBytes());
        String content=ARM.readCString(emulator.getBackend(),str1_ptr.peer);
        System.out.println("_Z15getstringlengthPKc:"+str1_ptr.toString()+"---"+content);
        result=unicorn08module.callFunction(emulator,"_Z15getstringlengthPKc",new PointerNumber(str1_ptr))[0];
        Number r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0);
        System.out.println("_Z15getstringlengthPKc result:"+result.intValue()+"----"+r0value);

MemoryBlock block2=memory.malloc(10,true);
        UnidbgPointer str2_ptr=block2.getPointer();
        str2_ptr.write("666".getBytes());
        String content2=ARM.readCString(emulator.getBackend(),str2_ptr.peer);
        System.out.println("_Z16getstringlength2PKcS0_:"+str2_ptr.toString()+"---"+content2);
        result=unicorn08module.callFunction(emulator,"_Z16getstringlength2PKcS0_",new PointerNumber(str1_ptr),new PointerNumber(str2_ptr))[0];
        r0value=emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0);
        System.out.println("_Z16getstringlength2PKcS0_ result:"+result.intValue()+"----"+r0value);

//调用jni_OnLoad函数
        vm.callJNI_OnLoad(emulator,unicorn08module);

//调用jni函数,对于动态注册的jni函数必须在完成地址的绑定才能调用
        System.out.println("stringFromJNI1-------------------------");
        DvmClass MainActivity_dvmclass=vm.resolveClass("com/example/unicorncourse08/MainActivity");//先把类找到,这里的原理很像反射
        DvmObject resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;","helloworld");//再通过类去调用里面的函数
        System.out.println("resultobj:"+resultobj);
        resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI1(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellokanxue"));
        System.out.println("resultobj:"+resultobj);
        System.out.println("stringFromJNI1-------------------------");

//动态注册的jni函数stringFromJNI2
        resultobj=MainActivity_dvmclass.callStaticJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2"));
        System.out.println("resultobj:"+resultobj);
        System.out.println("stringFromJNI2-------------------------");

DvmObject mainactivity=MainActivity_dvmclass.newObject(null);
        mainactivity.callJniMethodObject(emulator,"stringFromJNI2(Ljava/lang/String;)Ljava/lang/String;",new StringObject(vm, "hellostringFromJNI2"));
        System.out.println("resultobj:"+resultobj);
        System.out.println("stringFromJNI2-------------------------");

}
}

总结一下,上述API包括了3种so函数的调用方法:

  • 普通的so方法
  • jni_onload调用
  • jni 函数调用

上面举例的这些内容相对简单,并不涉及到so层调用java层的函数。如果遇到so层函数调用java层函数怎么办么?我们如果自己在apk写java代码调用so层函数,遇到so通过反射调用java层函数时,需要自己补上java层对应的类、方法和变量,因为这些需要执行的代码是绕不过去的!unidbg是这么样的么? 答案是肯定的!

示例:Unidbg模拟执行某段子so实操教程 ( 跑 native_init、执行sign )

:http://91fans.com.cn/post/unidbgzyone/

:http://91fans.com.cn/post/unidbgzytwo/

so 通过反射调用 java 层 ( 补环境 )

比如下面的这个so层的方法,会在jni_onload中被调用;这里需要获取java层普通变量、static变量后打印出来;也会获取java层的普通方法然后调用,这该怎么办了?

上面说了:so层调用java层的代码肯定是要补齐的(如果直接简单粗暴改so层代码,可能导致别人原来的逻辑错误)! 这里该怎么实操了? 大概的思路是:

  • 自己补上缺失的方法(当然java层的方法可以用jadx、jeb等反编译得到,不用自己反编译smali),这里缺失了base64方法,需要补上!
  • 自己补上缺失的变量,方法同上!
  • 重写getStaticObjectField、getObjectField、callObjectMethodV等方法,然后检测传入的参数。一旦发现使用/调用的是java层变量、方法,用自己补上的哪些代码替换(原理像不像平时常用的hook?)

说了那么多,完整的demo代码如下:

package com.unicorncourse08;

import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.github.unidbg.virtualmodule.android.JniGraphics;
import org.apache.commons.codec.binary.Base64;
import sun.applet.Main;

import java.io.File;
import java.lang.reflect.Field;

public class MainActivitymethod1 extends AbstractJni {

private static DvmClass MainActivityClass;

@Override
    /*
    * staticcontent是java层的静态变量;getStaticObjectField,一旦检测到so层引用这个变量,那么自己返回这个变量的值
    * */
    public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        System.out.println("getStaticObjectField->"+signature);
        if(signature.equals("com/example/testjni/MainActivity->staticcontent:Ljava/lang/String;")){
            return new StringObject(vm,"staticcontent");//源码 public static string staticcontent = "staticcontent"
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }

@Override
    /*
    * objcontent是java层的变量;这里重写getObjectField方法,一旦检测到so层引用这个变量,那么自己返回这个变量的值
    * */
    public DvmObject<?> getObjectField(BaseVM vm, DvmObject<?> dvmObject, String signature) {
        System.out.println("getObjectField->"+signature);
        if(signature.equals("com/example/testjni/MainActivity->objcontent:Ljava/lang/String;")){
            return new StringObject(vm,"objcontent");//public string objcontent
        }
        return super.getObjectField(vm, dvmObject, signature);
    }
    /*
    * java层的方法,这里需要复现,否则不知道怎么执行
    * */
    public String base64(String arg3) {
        String result=Base64.encodeBase64String(arg3.getBytes());
        return result;
    }

@Override
    /*
    * base64是java层的方法,这里重写callObjectMethodV方法:一旦发现调用的是java层的base64方法,这里就用自己复现的base64方法替换
    * */
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        System.out.println("callObjectMethodV->"+signature);
        if(signature.equals("com/example/testjni/MainActivity->base64(Ljava/lang/String;)Ljava/lang/String;")){
            DvmObject dvmobj=vaList.getObjectArg(0);
            String arg= (String) dvmobj.getValue();
            String result=base64(arg);
            return new StringObject(vm,result);
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

public static void main(String[] args) {
        MainActivitymethod1 mainActivitymethod1=new MainActivitymethod1();
        AndroidARMEmulator emulator = new AndroidARMEmulator("org.telegram.messenger",null,null);
        final Memory memory = emulator.getMemory();
        memory.setLibraryResolver(new AndroidResolver(23));
        VM vm = emulator.createDalvikVM();
        vm.setVerbose(true);
        vm.setJni(mainActivitymethod1);
        DalvikModule dm = vm.loadLibrary(new File("D:\\xxxx\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\unicorncourse08\\calljava.so"), true);
        dm.callJNI_OnLoad(emulator);
        MainActivityClass=vm.resolveClass("com/example/testjni/MainActivity");
        DvmObject obj=MainActivityClass.newObject(null);
        obj.callJniMethodObject(emulator,"base64byjni(Ljava/lang/String;)Ljava/lang/String;","callbase64byjni");
  }
}

整个逻辑其实并不复杂,从main函数开始:创建模拟器、创建虚拟机、加载so、调用so层函数!打印的结果如下:

JNIEnv->FindClass(com/example/testjni/MainActivity) was called from RX@0x400009df[libnative-lib.so]0x9df
JNIEnv->RegisterNatives(com/example/testjni/MainActivity, unidbg@0xbffff768, 1) was called from RX@0x40000f19[libnative-lib.so]0xf19
RegisterNative(com/example/testjni/MainActivity, stringFromJNI(Ljava/lang/String;)Ljava/lang/String;, RX@0x40000cb1[libnative-lib.so]0xcb1)
Find native function Java_com_example_testjni_MainActivity_base64byjni(Ljava/lang/String;)Ljava/lang/String; => RX@0x4000088d[libnative-lib.so]0x88d
JNIEnv->FindClass(com/example/testjni/MainActivity) was called from RX@0x400009df[libnative-lib.so]0x9df
getStaticObjectField->com/example/testjni/MainActivity->staticcontent:Ljava/lang/String;
JNIEnv->GetStaticObjectField(class com/example/testjni/MainActivity, staticcontent Ljava/lang/String; => "staticcontent") was called from RX@0x40000aa5[libnative-lib.so]0xaa5
JNIEnv->GetStringUtfChars("staticcontent") was called from RX@0x40000adb[libnative-lib.so]0xadb
[main]I/stringFromJNI: staticcontent:staticcontent
[main]I/stringFromJNI: objcontent:objcontent
getObjectField->com/example/testjni/MainActivity->objcontent:Ljava/lang/String;
JNIEnv->GetObjectField(com.example.testjni.MainActivity@7b3300e5, objcontent Ljava/lang/String; => "objcontent") was called from RX@0x40000b11[libnative-lib.so]0xb11
JNIEnv->GetStringUtfChars("objcontent") was called from RX@0x40000adb[libnative-lib.so]0xadb
JNIEnv->GetMethodID(com/example/testjni/MainActivity.base64(Ljava/lang/String;)Ljava/lang/String;) was called from RX@0x40000b55[libnative-lib.so]0xb55
callObjectMethodV->com/example/testjni/MainActivity->base64(Ljava/lang/String;)Ljava/lang/String;
JNIEnv->CallObjectMethodV(com.example.testjni.MainActivity@7b3300e5, base64("callbase64byjni") => "Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000bb1[libnative-lib.so]0xbb1
JNIEnv->GetStringUtfChars("Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000adb[libnative-lib.so]0xadb
JNIEnv->NewStringUTF("Y2FsbGJhc2U2NGJ5am5p") was called from RX@0x40000c05[libnative-lib.so]0xc05
[main]I/stringFromJNI: base64result:Y2FsbGJhc2U2NGJ5am5p

大杀器Unidbg真正的威力

:大杀器Unidbg真正的威力 - 知乎

大杀器内置的HOOK框架

当然Unidbg还内置了多种HOOK框架,今天讲一个分析So比较实用的一款HookZz

// 1. 获取HookZz对象
IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
// 2. enable hook
hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
index = 0;
 
hookZz.replace(module.findSymbolByName("lrand48"), new ReplaceCallback() {
    @Override
    public void postCall(Emulator<?> emulator, HookContext context) {
 
            ((EditableArm32RegisterContext)context).setR0(0x12345678);
 
        int ptrace_args0 = context.getIntArg(0);
        System.out.println("lrand48=" + ptrace_args0);
 
    }
 
 
},true);
 
 
//aesdecode hook
hookZz.wrap((module.base)+0x39634+1, new WrapCallback<RegisterContext>() { // inline wrap导出函数
    UnidbgPointer addr = null;
 
    @Override
    // 4. 方法执行前
    public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
        addr= ctx.getPointerArg(0);
        UnidbgPointer pointerArg = ctx.getPointerArg(1);
        UnidbgPointer pointer = pointerArg.getPointer(12);
        int anInt = pointerArg.getInt(8);
        byte[] byteArray = pointer.getByteArray(0, anInt);
        String s =xuzi1(byteArray);
        System.out.println("aes aesdecode= " + s);
 
    }
 
    @Override
    // 5. 方法执行后
    public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
 
        byte[] aaaa = addr.getPointer(0).getPointer(12).getByteArray(0,0x30);
        String s =xuzi1(aaaa);
        System.out.println("aes aesdecode1= " + s);
    }
 
});

同理,此框架也支持导出函数HOOK以及InlineHOOK 有了这个方法,在你分析一些函数的时候,可以充当Log的效果或者强行改变一些函数的返回值让你更容易的分析,比如本例中笔者改变了Lrand48的返回值,让函数每次都强行返回0x12345678,这样在逆向分析的时候能让一些不确定性变成可控性。

拟执行某段子so实操教程

:Unidbg模拟执行某段子so实操教程(一) 先把框架搭起来_奋飞安全的博客-CSDN博客

运行自己的 so 文件

下面我们执行 libnative-lib.so 中的 stringFromJNI 函数。

在 unidbg-android/src/test/java/com 下新建 test 文件夹,然后新建个 java 类 MainActivity。

代码如下:

package com.test;public class MainActivity {public static void main(String[] args) {MainActivity mainActivity = new MainActivity();mainActivity.stringFromJNI();}private final AndroidEmulator emulator;private final VM vm;private DvmClass cNative;private MainActivity() {emulator = new AndroidARMEmulator();Memory memory = emulator.getMemory();// 设置 sdk版本 23LibraryResolver resolver = new AndroidResolver(23);memory.setLibraryResolver(resolver);//创建DalvikVM,可以载入apk,也可以为nullvm = emulator.createDalvikVM(null);// 是否打印日志vm.setVerbose(false);// System.out.println(getPath());// 载入要执行的 soDalvikModule dm = vm.loadLibrary(new File(getPath() + "/fenfei/libnative-lib.so"), false);dm.callJNI_OnLoad(emulator);}private void stringFromJNI() {// Jni调用的类cNative = vm.resolveClass("com/fenfei/myndk/MainActivity");DvmObject<?> strRc = cNative.callStaticJniMethodObject(emulator, "stringFromJNI()Ljava/lang/String;");System.out.println("call stringFromJNI rc = " + strRc.getValue());}public String getPath() {String path = this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath();if (System.getProperty("os.name").contains("dows")) {path = path.substring(1, path.length());}if (path.contains("jar")) {// System.out.println("jar = " + path);path = path.substring(0, path.lastIndexOf("."));return path.substring(0, path.lastIndexOf("/"));}// System.out.println(path);// path.replace("target/classes/", "");return path.replace("/target/test-classes/", "");}
}

输出如下:

call stringFromJNI rc = Hello from C++

示例 2

调用 libencryptLib.so 文件中的 getGameKey 函数。编写 EncryptUtilsJni 类

package cn.hestyle;import com.github.unidbg.Module;
import com.github.unidbg.arm.ARMEmulator;
import com.github.unidbg.linux.android.AndroidARMEmulator;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;import java.io.File;
import java.io.IOException;/*** description: EncryptUtils调用so** @author hestyle* @version 1.0* @className unidbg->EncryptUtilsJni* @date 2020-05-20 22:01**/
public class EncryptUtilsJni extends AbstractJni {// ARM模拟器private final ARMEmulator emulator;// vmprivate final VM vm;// 载入的模块private final Module module;private final DvmClass TTEncryptUtils;/**** @param soFilePath   需要执行的so文件路径* @param classPath    需要执行的函数所在的Java类路径* @throws IOException*/public EncryptUtilsJni(String soFilePath, String classPath) throws IOException {// 创建app进程,包名可任意写emulator = new AndroidARMEmulator("cn.hestyle");Memory memory = emulator.getMemory();// 作者支持19和23两个sdkmemory.setLibraryResolver(new AndroidResolver(23));// 创建DalvikVM,利用apk本身,可以为nullvm = ((AndroidARMEmulator) emulator).createDalvikVM(null);// (关键处1)加载so,填写so的文件路径DalvikModule dm = vm.loadLibrary(new File(soFilePath), false);// 调用jnidm.callJNI_OnLoad(emulator);module = dm.getModule();// (关键处2)加载so文件中的哪个类,填写完整的类路径TTEncryptUtils = vm.resolveClass(classPath);}/*** 调用so文件中的指定函数* @param methodSign 传入你要执行的函数信息,需要完整的smali语法格式的函数签名* @param args       是即将调用的函数需要的参数* @return 函数调用结果*/private String myJni(String methodSign, Object ...args) {// 使用jni调用传入的函数签名对应的方法()Number ret = TTEncryptUtils.callStaticJniMethod(emulator, methodSign, args);// ret存放返回调用结果存放的地址,获得函数执行后返回值StringObject str = vm.getObject(ret.intValue() & 0xffffffffL);return str.getValue();}/*** 关闭模拟器* @throws IOException*/private void destroy() throws IOException {emulator.close();System.out.println("emulator destroy...");}public static void main(String[] args) throws IOException {// 1、需要调用的so文件所在路径String soFilePath = "src/test/resources/myso/libencryptLib.so";// 2、需要调用函数所在的Java类完整路径,比如a/b/c/d等等,注意需要用/代替.String classPath = "com/.../EncryptUtils";// 3、需要调用函数的函数签名,我这里调用EncryptUtils中的getGameKey方法,由于此方法没有参数列表,所以不需要传入String methodSign = "getGameKey()Ljava/lang/String;";EncryptUtilsJni encryptUtilsJni = new EncryptUtilsJni(soFilePath, classPath);// 输出getGameKey方法调用结果System.err.println(encryptUtilsJni.myJni(methodSign));encryptUtilsJni.destroy();}
}

参数说明
EncryptUtilsJni类 中最重要的设置为 main 方法中的 soFilePath、classPath、methodSign 三个参数。main方法中已经注释过了,这里再次解释一下。

  • soFilePath,填写你需要调用的so文件路径
  • classPath,填写你需要调用的函数所在Java类的完整类路径。
  • methodSign,填写你要调用的函数签名,语法为smali。(在jadx中,直接可以看smali代码)

备注:如果你要调用的函数还需要传入参数,直接传入 myJni 方法中即可,myJni 方法中省略 args 参数就是供你传入参数。

3、Unidbg  一些基本使用

在 unidbg-android 的 src/test 包含了几个 Demo,可以尝试将 Demo 运行起来,即可看到 Unidbg 的效果:https://github.com/zhkl0228/unidbg/tree/master/unidbg-android/src/test/java

快速使用步骤

//1.创建Android模拟器实例
AndroidEmulator emulator = AndroidEmulatorBuilder
                .for32Bit()
                .addBackendFactory(new DynarmicFactory(true))
                .build();

// 创建模拟器实例,建议使用实际进程名,可以规避进程名校验
// emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xxx.xxx").build();

//2.创建模拟器内存接口            
Memory memory = emulator.getMemory();
// final Memory memory = emulator.getMemory();

//3.设置Android SDK 版本,设置系统类库解析
memory.setLibraryResolver(new AndroidResolver(23));

//4.创建虚拟机
VM vm = emulator.createDalvikVM();
// 创建 Android 虚拟机,传入 APK,unidbg 可以协助做一部分签名工作
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/xxx/xxx.apk"));

//5.加载ELF文件
// 创建 jobject, 如果没用到的话可以不写
cNative = vm.resolveClass("com/xxx/xxx");
// 加载 so 到虚拟内存,第二个参数的意思表示是否执行动态库的初始化代码
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/xxx/libxxx.so"),true);
DalvikModule dm = vm.loadLibrary(new File("你的ELF文件路径名"), false);
// 获取 so 模块的句柄
module = dm.getModule();
// 设置 JNI 
vm.setJni(this);
// 打印日志
vm.setVerbose(true);

//6.调用 JNI_OnLoad
dm.callJNI_OnLoad(emulator);

//此时ELF将加载到内存,可以对其做任意操作
//7.执行JNI方法
DvmObject<?> obj = ProxyDvmObject.createObject(vm, this);
boolean result = obj.callJniMethodBoolean(emulator, "jnitest(Ljava/lang/String;)Z", str);

构建 函数参数 格式

// 构建函数参数格式
List<Object> args = new ArrayList<>(10);

// 各种基本参数格式兼容

// 参数1:JNIEnv *env
args.add(vm.getJNIEnv());

// 参数2:jobject 或 jclass 用不到直接填0即可
// DvmObject<?> cnative = cNative.newObject(null);
// args.add(cnative.hashCode());
args.add(0);

// 参数3 字符串对象
String input = "abcdef";
args.add(vm.addLocalObject(new StringObject(vm, input)));

// 参数4 bytes 数组
String input = "abcdef";
byte[] input_bytes = input.getBytes(StandardCharsets.UTF_8);
ByteArray input_byte_array = new ByteArray(vm,input_bytes);
args.add(vm.addLocalObject(input_byte_array));

// 参数5 bool 
// false 填 0,true 填 1
args.add(1);

unidbg 主动调用 函数

// 第二个参数是函数偏移量(thumb 记得+1),第三个参数是参数列表
Number number = module.callFunction(emulator,0x10618, args.toArray());

// unicorn trace(贼好用!!!堪比 ida trace!!!)
String traceFile = "trace.txt";
PrintStream traceStream = null;
try{
    traceStream = new PrintStream(new FileOutputStream(traceFile), true);
} catch (FileNotFoundException e) {
    e.printStackTrace();
}
// 核心 trace 开启代码,也可以自己指定函数地址和偏移量
emulator.traceCode(module.base,module.base+module.size).setRedirect(traceStream);
// 获取最终返回值,同时运行过程中的汇编代码和寄存器值会写入到文件中
return vm.getObject(number.intValue()).getValue().toString();

输出结果:

看看 trace log,建议搭配 010 editor 使用, 更好用

创建 AndroidEmulator 实例

使用 AndroidEmulatorBuilder 可以来帮助你快速创建一个 AndroidEmulator 的实例。

AndroidEmulator emulator = AndroidEmulatorBuilder
                //指定32位CPU
                .for32Bit() 
                //添加后端,推荐使用Dynarmic,运行速度快,但并不支持某些新特性
                .addBackendFactory(new DynarmicFactory(true)) 
                //指定进程名,推荐以安卓包名做进程名
                .setProcessName("com.github.unidbg")
                //设置根路径
                .setRootDir(new File("target/rootfs/default"))
                //生成AndroidEmulator实例
                .build();

使用 AndroidEmulator

当使用 AndroidEmulatorBuilder 构造了一个 AndroidEmulator 实例之后,就可以直接来操作这个实例

//获取内存操作接口
Memory memory = emulator.getMemory();
//获取进程pid
int pid = emulator.getPid();
//创建虚拟机
VM dalvikVM = emulator.createDalvikVM();
//创建虚拟机并指定APK文件
VM dalvikVM = emulator.createDalvikVM(new File("apk file path"));
//获取已创建的虚拟机
VM dalvikVM = emulator.getDalvikVM();
//显示当前寄存器状态 可指定寄存器
emulator.showRegs();
//获取后端CPU
Backend backend = emulator.getBackend();
//获取进程名
String processName = emulator.getProcessName();
//获取寄存器
RegisterContext context = emulator.getContext();
//Trace读内存
emulator.traceRead(1,0);
//Trace写内润
emulator.traceWrite(1,0);
//Trace汇编
emulator.traceCode(1,0);
//是否正在运行
boolean running = emulator.isRunning();

Memory 操作

Memory memory = emulator.getMemory();
//指定Android SDK 版本,目前支持19和23两个版本
memory.setLibraryResolver(new AndroidResolver(23));

//拿到一个指针,指向内存地址,通过该指针可操作内存
UnidbgPointer pointer = memory.pointer(0x4000000);

//获取当前内存映射情况
Collection<MemoryMap> memoryMap = memory.getMemoryMap();

//根据模块名来拿到某个模块
Module module = memory.findModule("module name");

//根据地址拿到某个模块
Module module = memory.findModuleByAddress(0x40000000);

VM 操作

//推荐指定APK文件,Unidbg会自动做许多固定的操作
VM vm = emulator.createDalvikVM(new File("apk file path"));

//是否输出JNI运行日志
vm.setVerbose(true);

//加载SO模块 参数二设置是否自动调用init函数
DalvikModule dalvikModule = vm.loadLibrary(new File("so file path"), true);

//设置JNI交互接口 参数需实现Jni接口,推荐使用this继承AbstractJni
vm.setJni(this);

//获取JNIEnv指针,可作为参数传递
Pointer jniEnv = vm.getJNIEnv();

//获取JavaVM指针,可作为参数传递
Pointer javaVM = vm.getJavaVM();

//调用JNI_OnLoad函数
vm.callJNI_OnLoad(emulator,dalvikModule.getModule());

//向VM添加全局对象,返回该对象的hash值
int hash = vm.addGlobalObject(dvmObj);

//获取虚拟机中的对象,参数为该对象的hash值
DvmObject<?> object = vm.getObject(hash);

unidbg 之 hook

hook 代码是逆向最基本的功能之一,frida 的 hook 代码都不陌生,Unidbg 还内置了多种 HOOK 框架,unidbg 底层用的是分析So比较实用的 HookZz 框架,所以 hook 的代码长这样的:

public void hook(){
        //unidbg集成了HookZz框架
        HookZz hook = HookZz.getInstance(emulator);
        //直接hook add函数的地址,比通过符号hook更具有“普适性”
        hook.replace(module.base + 0x3DC + 1, new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
                //R2和R3才是参数,R0是env,R1是object
                System.out.println(String.format("R2: %d, R3: %d",context.getIntArg(2),context.getIntArg(3)));
                //把第二个参数R3改成5
                emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R3,5);
                return super.onCall(emulator, context, originFunction);
            }

@Override
            public void postCall(Emulator<?> emulator, HookContext context) {
                emulator.getBackend().reg_write(Unicorn.UC_ARM_REG_R0,10);
                //返回值放R0,这里直接修改返回值
                super.postCall(emulator, context);
            }
        }, true);
    }

代码整体的结构和 frinda 的 hook 类似,onCall就是刚进入函数时候的回调(本质就是在函数入口处hook),onPost就是在函数ret前的hook回调!

unidbg  之 打patch

打patch方法:hook本质也是patch,还有很多关键的跳转代码(android下的B、BL等)可能也要NOP掉才能按照我们自己的逻辑执行!最原始打patch的办法就是在IDA或010editor更改,为了更好地逆向so,unidbg也提供了打patch的方法,如下:

public void patch(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8);
        byte[] code = new byte[]{(byte) 0xd0, 0x1a};//直接用硬编码改原so的代码:subs r0,r2,r3
        pointer.write(code);
    }
    public void patch2(){
        UnidbgPointer pointer = UnidbgPointer.pointer(emulator,module.base + 0x3E8);
        Keystone keystone = new Keystone(KeystoneArchitecture.Arm, KeystoneMode.ArmThumb);
        String s = "subs r0, r2, r3";
        byte[] machineCode = keystone.assemble(s).getMachineCode();
        //byte[] code = ;
        pointer.write(machineCode);
    }

代码很简单,可以直接在目标位置写硬编码,也可以借助 keystone 写汇编代码!

hook 的时候需要知道so的基址和代码偏移,unidbg 提供的方法如下:

// 加载so到虚拟内存
DalvikModule dm = vm.loadLibrary("libnative-lib.so", true);
// 得到模块对象,然后根据导出的函数名找到函数入口偏移,比直接在代码写死地址灵活一些
module = dm.getModule();
int address = (int) module.findSymbolByName("funcNmae").getAddress();

unidbg 的单步调试

unidbg 的单步调试也叫 console debug,就是在 console 下输入各种命令调试!操作也简单:

(1)先下个断点:当然这里也能制定特定的偏移地址

emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress());

(2)代码运行到断点后正常情况下会停下,然后逆向人员就可以在console下输入各种命令操作了,原理和 hyperpwn、gbd 等类似,如下:

比如 r 是删除断点,b 是增加断点,n 是步过等!其他写方面的操作命令如下:

wr0-wr7, wfp, wip, wsp <value>: write specified register
wb(address), ws(address), wi(address) <value>: write (byte, short, integer) memory of specified address, address must start with 0x
wx(address) <hex>: write bytes to memory at specified address, address must start with 0x

如果命中断点后想做一个个性化的操作,但是又觉得在 console 上挨个敲命令麻烦,也可以写代码固化下来,比如这样:

public void ReplaceArgByConsoleDebugger(){emulator.attach().addBreakPoint(module.findSymbolByName("funName").getAddress(), new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {RegisterContext context = emulator.getContext();String fakeInput = "hello world";int length = fakeInput.length();// 修改r1值为新长度emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R1, length);MemoryBlock fakeInputBlock = emulator.getMemory().malloc(length, true);fakeInputBlock.getPointer().write(fakeInput.getBytes(StandardCharsets.UTF_8));// 修改r0为指向新字符串的新指针emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, fakeInputBlock.getPointer().peer);Pointer buffer = context.getPointerArg(2);// OnLeaveemulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {String result = buffer.getString(0);System.out.println("base64 result:"+result);return true;}});return true;}});
}

条件断点

为了避免被过多信息干扰,很多时候的断点或hook是需要设置条件的,符合了条件才需要进一步打印出来查看结果,unidbg也不例外,也是这个思路。举个例子:比如strcat、strstr、strcmp这种函数,每时每刻都在被大量的模块调用,直接hook打印会产生大量无用日志,严重影响排查。同时大量日志得打印也会严重拖慢运行速度,所以需要自己写条件判断是否需要打印日志!比如这种:

(1)只打印某个特定so调用的strcat函数:

public void hookstrcmp(){long address = module.findSymbolByName("strcat").getAddress();emulator.attach().addBreakPoint(address, new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {RegisterContext registerContext = emulator.getContext();String arg1 = registerContext.getPointerArg(0).getString(0);String moduleName = emulator.getMemory().findModuleByAddress(registerContext.getLRPointer().peer).name;if(moduleName.equals("libxxx.so")){System.out.println("strcat arg1:"+arg1);}return true;}});
}

(2)只打印某个特定函数中调用的strcat函数:

// 早先声明全局变量 public boolean show = false;public void hookstrcat(){emulator.attach().addBreakPoint(module.findSymbolByName("targetfunName").getAddress(), new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {RegisterContext registerContext = emulator.getContext();show = true;//进入目标函数就把show设置为true,下面才好打印日志emulator.attach().addBreakPoint(registerContext.getLRPointer().peer, new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {show = false;//离开目标函数就把show设置为false,下面才知道不打印日志return true;}});return true;}});emulator.attach().addBreakPoint(module.findSymbolByName("strcat").getAddress(), new BreakPointCallback() {@Overridepublic boolean onHit(Emulator<?> emulator, long address) {RegisterContext registerContext = emulator.getContext();String arg1 = registerContext.getPointerArg(0).getString(0);if(show){System.out.println("strcmp arg1:"+arg1);}return true;}});
}

内存检索

搜索某些 sign 字段、字符串的时候特别重要,如下: 

private Collection<Pointer> searchMemory(long start, long end, byte[] data) {List<Pointer> pointers = new ArrayList<>();for (long i = start, m = end - data.length; i < m; i++) {byte[] oneByte = emulator.getBackend().mem_read(i, 1);if (data[0] != oneByte[0]) {continue;}if (Arrays.equals(data, emulator.getBackend().mem_read(i, data.length))) {pointers.add(UnidbgPointer.pointer(emulator, i));i += (data.length - 1);}}return pointers;
}

4、Unidbg 调用so文件

示例:Unidbg 调用so文件生成京东 sign 参数

​案例学习,模拟调用so文件生成京东sign参数:https://blog.csdn.net/qq_44628911/article/details/127322805

抓包 "商品详情页",要模拟的是sign参数

搭建基础框架代码:

package com.kdd.test;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;import java.io.*;public class jd_main extends AbstractJni {private static final Log log = LogFactory.getLog(AbstractJni.class);public static void main (String[] args) throws IOException {jd_main RunLDQ =new jd_main();RunLDQ.runJni();RunLDQ.destroy();}private void destroy() throws IOException{emulator.close();System.out.println("destroy");}private static LibraryResolver createLibraryResolver() {return new AndroidResolver(23);}private static AndroidEmulator createARMEmulator() {return AndroidEmulatorBuilder.for32Bit().build();}private final AndroidEmulator emulator;private final VM vm;private Module module;private DvmClass aBitmapkitUtils;//初始化public jd_main(){emulator = createARMEmulator();final Memory memory = emulator.getMemory();// 设置 sdk版本 23memory.setLibraryResolver(createLibraryResolver());//使用apk文件加载so的话,会自动处理签名方面的jni,具体可看AbstractJni,利用apk加载的好处,vm = emulator.createDalvikVM(new File("F:\\frida_learn_app\\jd\\jd-9.2.2.apk"));vm.setJni(this);// 是否打印日志vm.setVerbose(true);}public String runJni(){//加载apk的soDalvikModule dm = vm.loadLibrary("jdbitmapkit", false);//调用jnidm.callJNI_OnLoad(emulator);module = dm.getModule();return null;}

运行有报错补代码

    @Overridepublic DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {switch (signature) {case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);}}return super.getStaticObjectField(vm, dvmClass, signature);}

报错,补代码

    @Overridepublic DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {switch (signature) {case "android/app/Application->getPackageName()Ljava/lang/String;": {String packageName = vm.getPackageName();if (packageName != null) {return new StringObject(vm, packageName);}}}throw new UnsupportedOperationException(signature);}

报错,补代码

    @Overridepublic DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {switch (signature) {case "sun/security/pkcs/PKCS7-><init>([B)V": {ByteArray array = varArg.getObjectArg(0);return new StringObject(vm, new String(array.getValue()));}}return super.newObject(vm, dvmClass, signature, varArg);}

基础环境没报错后,调用签名函数

        //加载so的哪个类aBitmapkitUtils = vm.resolveClass("com/jingdong/common/utils/BitmapkitUtils");//调用方法DvmObject<?> strRc = aBitmapkitUtils.callStaticJniMethodObject(emulator,"getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",vm.addLocalObject(null),vm.addLocalObject(new StringObject(vm,"wareBusiness")),vm.addLocalObject(new StringObject(vm,"{\"abTest800\":true,\"avoidLive\":false,\"brand\":\"360\",\"cityId\":2144,\"darkModelEnum\":3,\"districtId\":24463,\"eventId\":\"Searchlist_Productid\",\"fromType\":0,\"isDesCbc\":true,\"latitude\":\"26.618816\",\"lego\":true,\"longitude\":\"106.644705\",\"model\":\"1605-A01\",\"ocrFlag\":false,\"pluginVersion\":90220,\"plusClickCount\":0,\"plusLandedFatigue\":0,\"provinceId\":\"24\",\"skuId\":\"10024083045618\",\"source_type\":\"search\",\"source_value\":\"鼠标垫小号\",\"townId\":51707,\"uAddrId\":\"0\"}")),vm.addLocalObject(new StringObject(vm,"uuid")),vm.addLocalObject(new StringObject(vm,"android")),vm.addLocalObject(new StringObject(vm,"9.2.2")));System.out.println(strRc.getValue());//获取返回值return (String) strRc.getValue();

后面有报错也是跟着报错补环境。最后成功运行出结果:

全部代码如下:

package com.kdd.test;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.LibraryResolver;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.linux.android.dvm.Enumeration;
import com.github.unidbg.linux.android.dvm.api.*;
import com.github.unidbg.linux.android.dvm.api.ClassLoader;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;import java.io.*;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.*;public class jd_main extends AbstractJni {private static final Log log = LogFactory.getLog(AbstractJni.class);public static void main (String[] args) throws IOException {jd_main RunLDQ =new jd_main();RunLDQ.runJni(args);RunLDQ.destroy();}private void destroy() throws IOException{emulator.close();System.out.println("destroy");}private static LibraryResolver createLibraryResolver() {return new AndroidResolver(23);}private static AndroidEmulator createARMEmulator() {return AndroidEmulatorBuilder.for32Bit().build();}private final AndroidEmulator emulator;private final VM vm;private Module module;private DvmClass aBitmapkitUtils;//初始化public jd_main(){emulator = createARMEmulator();final Memory memory = emulator.getMemory();// 设置 sdk版本 23memory.setLibraryResolver(createLibraryResolver());//使用apk文件加载so的话,会自动处理签名方面的jni,具体可看AbstractJni,利用apk加载的好处,vm = emulator.createDalvikVM(new File("F:\\frida_learn_app\\jd\\jd-9.2.2.apk"));vm.setJni(this);// 是否打印日志
//        vm.setVerbose(true);}public String runJni(String[] args){//加载apk的soDalvikModule dm = vm.loadLibrary("jdbitmapkit", false);//调用jnidm.callJNI_OnLoad(emulator);module = dm.getModule();//加载so的哪个类aBitmapkitUtils = vm.resolveClass("com/jingdong/common/utils/BitmapkitUtils");//调用方法DvmObject<?> strRc = aBitmapkitUtils.callStaticJniMethodObject(emulator,"getSignFromJni()(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",vm.addLocalObject(null),vm.addLocalObject(new StringObject(vm,"wareBusiness")),vm.addLocalObject(new StringObject(vm,"{\"abTest800\":true,\"avoidLive\":false,\"brand\":\"360\",\"cityId\":2144,\"darkModelEnum\":3,\"districtId\":24463,\"eventId\":\"Searchlist_Productid\",\"fromType\":0,\"isDesCbc\":true,\"latitude\":\"26.618816\",\"lego\":true,\"longitude\":\"106.644705\",\"model\":\"1605-A01\",\"ocrFlag\":false,\"pluginVersion\":90220,\"plusClickCount\":0,\"plusLandedFatigue\":0,\"provinceId\":\"24\",\"skuId\":\"10024083045618\",\"source_type\":\"search\",\"source_value\":\"鼠标垫小号\",\"townId\":51707,\"uAddrId\":\"0\"}")),vm.addLocalObject(new StringObject(vm,"uuid")),vm.addLocalObject(new StringObject(vm,"android")),vm.addLocalObject(new StringObject(vm,"9.2.2")));System.out.println(strRc.getValue());//获取返回值return (String) strRc.getValue();}@Overridepublic DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {switch (signature) {case "com/jingdong/common/utils/BitmapkitUtils->a:Landroid/app/Application;": {return vm.resolveClass("android/app/Activity", vm.resolveClass("android/content/ContextWrapper", vm.resolveClass("android/content/Context"))).newObject(null);}}return super.getStaticObjectField(vm, dvmClass, signature);}@Overridepublic DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {switch (signature) {case "sun/security/pkcs/PKCS7-><init>([B)V": {ByteArray array = varArg.getObjectArg(0);return new StringObject(vm, new String(array.getValue()));}}return super.newObject(vm, dvmClass, signature, varArg);}@Overridepublic DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {switch (signature) {case "android/app/Application->getPackageName()Ljava/lang/String;": {String packageName = vm.getPackageName();if (packageName != null) {return new StringObject(vm, packageName);}}}throw new UnsupportedOperationException(signature);}@Overridepublic DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {switch (signature) {case "java/lang/StringBuffer-><init>()V":{return vm.resolveClass("java/lang/StringBuffer").newObject(new StringBuffer());}case "java/lang/Integer-><init>(I)V" :{return vm.resolveClass("java/lang/Integer").newObject(new Integer(vaList.getIntArg(0)));}}throw new UnsupportedOperationException(signature);}@Overridepublic DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {switch (signature) {case "android/app/Application->getAssets()Landroid/content/res/AssetManager;":return new AssetManager(vm, signature);case "android/app/Application->getClassLoader()Ljava/lang/ClassLoader;":return new ClassLoader(vm, signature);case "android/app/Application->getContentResolver()Landroid/content/ContentResolver;":return vm.resolveClass("android/content/ContentResolver").newObject(signature);case "java/util/ArrayList->get(I)Ljava/lang/Object;": {int index = vaList.getIntArg(0);ArrayListObject arrayList = (ArrayListObject) dvmObject;return arrayList.getValue().get(index);}case "android/app/Application->getSystemService(Ljava/lang/String;)Ljava/lang/Object;": {StringObject serviceName = vaList.getObjectArg(0);assert serviceName != null;return new SystemService(vm, serviceName.getValue());}case "java/lang/String->toString()Ljava/lang/String;":return dvmObject;case "java/lang/Class->getName()Ljava/lang/String;":return new StringObject(vm, ((DvmClass) dvmObject).getName());case "android/view/accessibility/AccessibilityManager->getEnabledAccessibilityServiceList(I)Ljava/util/List;":return new ArrayListObject(vm, Collections.<DvmObject<?>>emptyList());case "java/util/Enumeration->nextElement()Ljava/lang/Object;":return ((Enumeration) dvmObject).nextElement();case "java/util/Locale->getLanguage()Ljava/lang/String;":Locale locale = (Locale) dvmObject.getValue();return new StringObject(vm, locale.getLanguage());case "java/util/Locale->getCountry()Ljava/lang/String;":locale = (Locale) dvmObject.getValue();return new StringObject(vm, locale.getCountry());case "android/os/IServiceManager->getService(Ljava/lang/String;)Landroid/os/IBinder;": {ServiceManager serviceManager = (ServiceManager) dvmObject;StringObject serviceName = vaList.getObjectArg(0);assert serviceName != null;return serviceManager.getService(vm, serviceName.getValue());}case "java/io/File->getAbsolutePath()Ljava/lang/String;":File file = (File) dvmObject.getValue();return new StringObject(vm, file.getAbsolutePath());case "android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;":case "android/content/ContextWrapper->getPackageManager()Landroid/content/pm/PackageManager;":case "android/content/Context->getPackageManager()Landroid/content/pm/PackageManager;":DvmClass clazz = vm.resolveClass("android/content/pm/PackageManager");return clazz.newObject(signature);case "android/content/pm/PackageManager->getPackageInfo(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;": {StringObject packageName = vaList.getObjectArg(0);assert packageName != null;int flags = vaList.getIntArg(1);if (log.isDebugEnabled()) {log.debug("getPackageInfo packageName=" + packageName.getValue() + ", flags=0x" + Integer.toHexString(flags));}return new PackageInfo(vm, packageName.getValue(), flags);}case "android/app/Application->getPackageName()Ljava/lang/String;":case "android/content/ContextWrapper->getPackageName()Ljava/lang/String;":case "android/content/Context->getPackageName()Ljava/lang/String;": {String packageName = vm.getPackageName();if (packageName != null) {return new StringObject(vm, packageName);}break;}case "android/content/pm/Signature->toByteArray()[B":if (dvmObject instanceof Signature) {Signature sig = (Signature) dvmObject;return new ByteArray(vm, sig.toByteArray());}break;case "android/content/pm/Signature->toCharsString()Ljava/lang/String;":if (dvmObject instanceof Signature) {Signature sig = (Signature) dvmObject;return new StringObject(vm, sig.toCharsString());}break;case "java/lang/String->getBytes()[B": {String str = (String) dvmObject.getValue();return new ByteArray(vm, str.getBytes());}case "java/lang/String->getBytes(Ljava/lang/String;)[B":String str = (String) dvmObject.getValue();StringObject charsetName = vaList.getObjectArg(0);assert charsetName != null;try {return new ByteArray(vm, str.getBytes(charsetName.getValue()));} catch (UnsupportedEncodingException e) {throw new IllegalStateException(e);}case "java/lang/Integer->toString()Ljava/lang/String;":{Integer iUse =  (Integer)dvmObject.getValue();return new StringObject(vm, Integer.toString(iUse));}case "java/lang/StringBuffer->toString()Ljava/lang/String;":{StringBuffer str1 = (StringBuffer) dvmObject.getValue();return new StringObject(vm,str1.toString());}case "java/lang/StringBuffer->append(Ljava/lang/String;)Ljava/lang/StringBuffer;": {StringBuffer str1 = (StringBuffer) dvmObject.getValue();StringObject serviceName = vaList.getObjectArg(0);assert serviceName != null;return vm.resolveClass("java/lang/StringBuffer").newObject(str1.append(serviceName.getValue()));}case "java/security/cert/CertificateFactory->generateCertificate(Ljava/io/InputStream;)Ljava/security/cert/Certificate;":CertificateFactory factory = (CertificateFactory) dvmObject.getValue();DvmObject<?> stream = vaList.getObjectArg(0);assert stream != null;InputStream inputStream = (InputStream) stream.getValue();try {return vm.resolveClass("java/security/cert/Certificate").newObject(factory.generateCertificate(inputStream));} catch (CertificateException e) {throw new IllegalStateException(e);}case "java/security/cert/Certificate->getEncoded()[B": {Certificate certificate = (Certificate) dvmObject.getValue();try {return new ByteArray(vm, certificate.getEncoded());} catch (CertificateEncodingException e) {throw new IllegalStateException(e);}}case "java/security/MessageDigest->digest([B)[B": {MessageDigest messageDigest = (MessageDigest) dvmObject.getValue();ByteArray array = vaList.getObjectArg(0);assert array != null;return new ByteArray(vm, messageDigest.digest(array.getValue()));}case "java/util/ArrayList->remove(I)Ljava/lang/Object;": {int index = vaList.getIntArg(0);ArrayListObject list = (ArrayListObject) dvmObject;return list.getValue().remove(index);}case "java/util/List->get(I)Ljava/lang/Object;":List<?> list = (List<?>) dvmObject.getValue();return (DvmObject<?>) list.get(vaList.getIntArg(0));case "java/util/Map->entrySet()Ljava/util/Set;":Map<?, ?> map = (Map<?, ?>) dvmObject.getValue();return vm.resolveClass("java/util/Set").newObject(map.entrySet());case "java/util/Set->iterator()Ljava/util/Iterator;":Set<?> set = (Set<?>) dvmObject.getValue();return vm.resolveClass("java/util/Iterator").newObject(set.iterator());}throw new UnsupportedOperationException(signature);}
}

打包成 jar,方便其它程序调用

IDEA 找到 File ---> Project Structure …​ 然后选择 Artifacts, 点加号 Add 
如图配置,勾上 Include tests

点击 ok 后,Build ---> Build Artifacts 进行编译,编译成功后会生成很多jar文件

在控制台测试运行下:java -jar unidbg-master.jar

运行出了结果,证明打包的没问题

python 调用 打包的 jar 包

# coding:utf-8
import requests, urllib, subprocess
import chardet, jpype,osheaders = {"Host": "api.m.jd.com","charset": "UTF-8","cache-control": "no-cache","content-type": "application/x-www-form-urlencoded; charset=UTF-8","user-agent": "okhttp/3.12.1"
}
cookies = {
}
url = "https://api.m.jd.com/client.action"
params = {"functionId": "wareBusiness","clientVersion": "9.2.2","build": "85371","client": "android","d_brand": "360","d_model": "1605-A01","osVersion": "6.0.1","screen": "1920*1080","partner": "ks012","aid": "xxx","oaid": "","eid": "xxx","sdkVersion": "23","lang": "zh_CN","uuid": "xxx","area": "24_2144_2149_21104","networkType": "wifi","wifiBssid": "xxx",# "st": "1665562015795",# "sign": "45a7dc3f547be113a6a4dfa942e190c6",# "sv": "111"
}
body = '''{"abTest800":true,"avoidLive":false,"brand":"360","cityId":2144,"darkModelEnum":3,"districtId":24463,"eventId":"Searchlist_Productid","fromType":0,"isDesCbc":true,"latitude":"","lego":true,"longitude":"","model":"1605-A01","ocrFlag":false,"pluginVersion":90220,"plusClickCount":0,"plusLandedFatigue":0,"provinceId":"24","skuId":"10024083045618","source_type":"search","source_value":"鼠标垫小号","townId":51707,"uAddrId":"0"}'''
data = {"lmt": "0","body": body,"": ""
}
jvmPath=jpype.getDefaultJVMPath()
d='unidbg_master_jar2/unidbg-master.jar'#对应jar地址
jpype.startJVM(jvmPath,"-ea","-Djava.class.path="+d+"")JDClass=jpype.JClass("com.kdd.test.runliudq")  //类目
jd=JDClass()
signature=jd.runJni(["wareBusiness", body, "uuid", "android", "9.2.2"])url = url + "?" + urllib.parse.urlencode(params) + "&" + str(signature)
print(url)
response = requests.post(url, headers=headers, cookies=cookies, data=data)print(response.text)
print(response)
jpype.shutdownJVM()

成功跑出结果

示例:com.du.du

From:https://zhuanlan.zhihu.com/p/111793677

案例来自 JXU2QkQyYXBwJTIwdjQuMTYuMA== 对于该 app 而言,是非常适合入门的一个app,未加固、算法简单、很容易找到 so 的 jni。​

"毒unidbg" 文件放在:https://github.com/zhaoboy9692/dailyanalysis

这里重要的是目前利用 unidbg + springboot 做成 web 服务。

先去 github 下载 https://github.com/zhkl0228/unidbg,下载完毕用 idea 打开,等待 maven 下载完毕。创建好du的文件。

代码解析

package com.du;import cn.banny.unidbg.Module;
import cn.banny.unidbg.arm.ARMEmulator;
import cn.banny.unidbg.linux.android.AndroidARMEmulator;
import cn.banny.unidbg.linux.android.AndroidResolver;
import cn.banny.unidbg.linux.android.dvm.*;
import cn.banny.unidbg.memory.Memory;
import java.io.File;
import java.io.IOException;//毒app 4.16.0
public class du extends AbstractJni {//ARM模拟器private final ARMEmulator emulator;//vmprivate final VM vm;//载入的模块private final Module module;private final DvmClass TTEncryptUtils;//初始化public du() throws IOException {//创建毒进程,这里其实可以不用写的,我这里是随便写的,使用app本身的进程就可以绕过进程检测emulator = new AndroidARMEmulator("com.du.du");Memory memory = emulator.getMemory();//作者支持19和23两个sdkmemory.setLibraryResolver(new AndroidResolver(23));memory.setCallInitFunction();//创建DalvikVM,利用apk本身,可以为null//如果用apk文件加载so的话,会自动处理签名方面的jni,具体可看AbstractJni,这就是利用apk加载的好处
//        vm = emulator.createDalvikVM(new File("src/test/resources/du/du4160.apk"));vm = emulator.createDalvikVM(null);//加载so,使用armv8-64速度会快很多DalvikModule dm = vm.loadLibrary(new File("src/test/resources/du/libJNIEncrypt.so"), false);//调用jnidm.callJNI_OnLoad(emulator);module = dm.getModule();//Jni调用的类,加载soTTEncryptUtils = vm.resolveClass("com/duapp/aesjni/AESEncrypt");}//关闭模拟器private void destroy() throws IOException {emulator.close();System.out.println("destroy");}public static void main(String[] args) throws IOException {du t = new du();t.encodeByte("lastIdloginTokenplatformandroidsellTime201912timestamp1577459413370uuid7337c8189240625v4.16.0");t.destroy();}private String encodeByte(String strs) {//调试// 这里还支持gdb调试,//emulator.attach(DebuggerType.GDB_SERVER);//附加调试器
//        emulator.attach(DebuggerType.SIMPLE);
//        emulator.traceCode();//这里是打断点,原地址0x00005028->新地址0x40005028 新地址需要改成0x4
//        emulator.attach().addBreakPoint(null, 0x40001188);//encode地址
//        emulator.attach().addBreakPoint(null, 0x40000D10);Number ret = TTEncryptUtils.callStaticJniMethod(emulator, "getByteValues()Ljava/lang/String;");long hash = ret.intValue() & 0xffffffffL;StringObject st1 = vm.getObject(hash);//毒这里要处理下字符串String byteString = st1.getValue();StringBuilder builder = new StringBuilder(byteString.length());for (int i = 0; i < byteString.length(); i++) {if (byteString.charAt(i) == '0') {builder.append('1');} else {builder.append('0');}}//获取encodeByte地址ret = TTEncryptUtils.callStaticJniMethod(emulator, "encodeByte(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;",//传参,这里需要两个字符串,所以就传入两个参数vm.addLocalObject(new StringObject(vm, strs)),vm.addLocalObject(new StringObject(vm, builder.toString())));//ret 返回的是地址,hash = ret.intValue() & 0xffffffffL;//或得其值StringObject str = vm.getObject(hash);System.out.println(str.getValue());return str.getValue();}
}

上边代码有 jni 的类是哪一个需要知道,就是下面这个类,这个其实是和加载so有关系的。
TTEncryptUtils=vm.resolveClass("com/*/aesjni/AESEncrypt");

我们需要逆向app,这里不细说如何在app中寻找加载so的类。如下图,encodeByte是该app调用native层加密的入口,loadLibrary是java加载so的方法,这个类就是上述代码中填写的。

然后再看 "encodeByte(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"这里,这是smali写法,不补基础,后面跟上需要传的参数, getByteValues 这个方法是毒获取的一个01字符串,并且在java层进行了处理,然后再传进 encodeByte里面, encodeByte这个方法最后获取的其实并不是最终需要的,需要md5才是最后的newSign。可以验证一下下。

测试结果通过。

最后,启动 java 文件时候注意这个改成自己的平台!!!
VM options:-Djava.library.path=prebuilt/os -Djna.library.path=prebuilt/os
Where os may: linux64, win32, win64, osx64

示例:unidbg 使用姿势

:https://blog.csdn.net/chl191623691/article/details/118415796

演示unidbg项目的导入、封装自己的调用so文件的API,其实这只是入门了,unidbg还支持断点调试so文件,也能导入到IDA中进行动态调试,自己去研究

使用 unidbg 调用 libbaseEncryptLib.so、libencryptLib.so 中的方法,这样就不用去逆向so文件了。

示例:unidbg 过混淆过的 arm64 程序初体验

:https://bbs.pediy.com/thread-268376.htm

示例:调用 native 方法、传不同的参数

:https://blog.csdn.net/xubaoyong/article/details/121750645

代码如下

package com.example.jnitest4.jni;import android.content.Context;public class JniManager {// Used to load the 'native-lib' library on application startup.static {System.loadLibrary("native-lib");}public native String str2str(String org,String append);/*** 获取uuid字符串* @param type* @return*/public native String uuid(int type);/*** 加密** @param msg  原始字符串* @param type 加密的方式* @return 加密后的数据*/public native String encode(String msg, int type);/*** 解密** @param msg  解密前的 字符串* @param type 解密的方式* @return 解密后的数据*/public native String decode(String msg, int type);/*** A native method that is implemented by the 'native-lib' native library,* which is packaged with this application.*/public native String stringFromJNI();/*** SHA1签名    --失败** @param src* @return*/public native String encodeBySHA1(byte[] src);/*** SHA224签名** @param src* @return*/public native String encodeBySHA224(byte[] src);/*** SHA384签名** @param src* @return*/public native String encodeBySHA256(byte[] src);/*** SHA256签名** @param src* @return*/public native String encodeBySHA384(byte[] src);/*** SHA512签名** @param src* @return*/public native String encodeBySHA512(byte[] src);/*** AES加密** @param keys* @param src* @return*/public native byte[] encodeByAES(byte[] keys, byte[] src);/*** AES解密** @param keys* @param src* @return*/public native byte[] decodeByAES(byte[] keys, byte[] src);/*** RSA公钥加密** @param keys* @param src* @return*/public native byte[] encodeByRSAPubKey(byte[] keys, byte[] src);/*** RSA私钥解密** @param keys* @param src* @return*/public native byte[] decodeByRSAPrivateKey(byte[] keys, byte[] src);/*** RSA私钥加密** @param keys* @param src* @return*/public native byte[] encodeByRSAPrivateKey(byte[] keys, byte[] src);/*** RSA公钥解密** @param keys* @param src* @return*/public native byte[] decodeByRSAPubKey(byte[] keys, byte[] src);/*** RSA私钥签名** @param keys* @param src* @return*/public native byte[] signByRSAPrivateKey(byte[] keys, byte[] src);/*** RSA公钥验证签名 (未测试)** @param keys* @param src* @param sign* @return 1:验证成功*/public native int verifyByRSAPubKey(byte[] keys, byte[] src, byte[] sign);/*** 异或加解密** @param src* @return*/public native byte[] xOr(byte[] src);/*** MD5编码** @param src* @return 默认小写*/public native String md5(byte[] src);/*** HmacSHA1签名** @param context* @param src* @return*/public native byte[] encodeByHmacSHA1(Context context, byte[] src);/*** 获取apk-sha1** @param context* @return*/public native String sha1OfApk(Context context);/*** 校验apk签名是否有效(未验证)** @param context* @return*/public native boolean verifySha1OfApk(Context context);/*** 字节测试用例* @param bytes* @return*/public native byte[] byteTestFn(byte[] bytes) ;}

unidbg 实现代码

package com.jni4;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyClassFactory;
import com.github.unidbg.memory.Memory;import java.io.File;
import java.io.IOException;public class JniManagerUtil {private final AndroidEmulator emulator;private final DvmClass jniManagerUtil;private final VM vm;public JniManagerUtil() {emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.example.jnitest4").build();Memory memory = emulator.getMemory();memory.setLibraryResolver(new AndroidResolver(23));vm = emulator.createDalvikVM();vm.setDvmClassFactory(new ProxyClassFactory());vm.setVerbose(false);DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/jnitest4/libnative-lib.so"), false);jniManagerUtil = vm.resolveClass("com/example/jnitest4/jni/JniManager");dm.callJNI_OnLoad(emulator);}public void destroy() throws IOException {emulator.close();}/*** 这里有个问题是使用callStaticJniMethodObject还是callJniMethodObject*/public void stringFromJNI(){String methodStringFromJNI = "stringFromJNI()Ljava/lang/String;";DvmObject<?> strRc =  jniManagerUtil.callStaticJniMethodObject(emulator, methodStringFromJNI);System.out.println("stringFromJNI返回值:"+strRc.getValue());}/*** 这个例子的重点是参数是int类型,对应的参数类型标识为I*/public void UUIDTest(){String methodStringFromJNI = "uuid(I)Ljava/lang/String;";int paramInt = 15;DvmObject<?> strRc =  jniManagerUtil.callStaticJniMethodObject(emulator, methodStringFromJNI,paramInt);System.out.println("UUIDTest返回值:"+strRc.getValue());}//    public native String str2str(String org,String append);public void str2strTest(){String methodStringFromJNI = "str2str(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;";String paramString0 = "hello ";String paramString1 = "mr wang";DvmObject<?> strRc =  jniManagerUtil.callStaticJniMethodObject(emulator, methodStringFromJNI,paramString0,paramString1);System.out.println("str2strTest返回值:"+strRc.getValue());}public void encode(){String methodStringFromJNI = "encode(Ljava/lang/String;I;)Ljava/lang/String;";String paramString0 = "hello ";int paramString1 = 20;DvmObject<?> strRc =  jniManagerUtil.callStaticJniMethodObject(emulator, methodStringFromJNI,paramString0,paramString1);System.out.println("encode返回值:"+strRc.getValue());}/*** 学习重点:*  1:这里重点是参数是byte数组,学习byte数组如何传参。*  2:调用方法采用先new一个对象,然后再调用非静态方法来调用。*/public void encodeBySHA1(){String methodStringFromJNI = "encodeBySHA1(B[;)Ljava/lang/String;";String paramString0 = "99999 ";
//  方法1
//        DvmObject<?> strRc =  jniManagerUtil.callStaticJniMethodObject(emulator, methodStringFromJNI,paramString0.getBytes());
//   方法2:DvmObject<?> strRc =  jniManagerUtil.newObject(null).callJniMethodObject(emulator, methodStringFromJNI,paramString0.getBytes());System.out.println("encodeBySHA1返回值:"+strRc.getValue());}public static void main(String[] args) throws Exception {JniManagerUtil jniManagerUtil = new JniManagerUtil();jniManagerUtil.stringFromJNI();jniManagerUtil.UUIDTest();jniManagerUtil.str2strTest();jniManagerUtil.encode();jniManagerUtil.encodeBySHA1();jniManagerUtil.destroy();}}

frida、unidbg 写 hook 方法时的基本数据类型

参考:

  • Dalvik 可执行文件格式:https://source.android.google.cn/docs/core/dalvik/dex-format
  • frida-java-bridge:https://github.com/frida/frida-java-bridge/blob/main/lib/types.js
  • com.github.unidbg.linux.android.dvm.Shorty.java

调用 native 方法

  • 使用方法:callStaticJniMethodObject
  • 先 new 出对象,再调用方法 callJniMethodObject

传不同的参数:

  • 调用无参函数
  • int类型参数
  • btye数组类型参数
  • 多个String类型的参数

TypeDescriptor 各个变体的含义

语法    含义
V        void;仅对返回类型有效
Z        boolean
B        byte
S        short
C        char
I        int
J        long
F        float
D        double
Lfully/qualified/Name;    类 fully.qualified.Name
[descriptor                      descriptor的数组,可递归地用于“数组的数组”,但维数不能超过 255。

示例:完整流程分析

:https://blog.csdn.net/qq_39736559/article/details/126205037

​一、Jadx 分析

首先用jadx打开apk文件,查看MainActivity可以发现,页面判断了MyApp.m这个类变量的值,并调用类work()这个函数,且当类变量m的值为0时会跳转到RegActivity注册页面

RegActivity界面比较简单,就是把输入的sn传入MyApp.saveSN()函数,然后退出,可以看出关键都在MyApp这个类。

所以我们继续查看MyApp这个类,发现类有三个native函数,所以需要进一步分析so文件

二、IDA PRO分析

(1)找关键函数

将对应的so文件拖到ida pro后通过Export栏可以发现有JNi_OnLoad函数,说明函数为动态注册,所以进入JNi_OnLoad函数查看注册的函数。

Tips: ida pro直接反编译的格式可能会很乱,这个时候可以把变量右键设置set item type设置成为JNIEnv*,然后许多函数都能解析出来类,就好看了很多(如果不知道设置哪一个变量,就把能试试的都试一便,总有一个能行_)

通过分析Onload函数可以发现注册函数在off_5044这个位置上,点击跳转后发现注册的函数名字符串找到了,但是函数名却是n1,n2,n3,可以对这些函数重命名,这样好看一点儿。

由此三个关键函数,work(), initSN(), saveSN()就找到了。

(2)分析关键函数
- work()函数分析
进入work函数,将传入的变量类型右键设置set lvar type设置为JNIEnv*后,F5反编译如下,可以看到该函数用getValue的方法获取了MyApp.m的值(getValue 函数用同样的方法进行反编译),然后将unk_2EFB或unk_2F25处的值赋给了V3,其中unk_2F25处的值啥也看不到,unk_2EFB处的值能看到有flag字样,应该和flag有关。最后该函数调用类callWork函数(反编译了一下,暂时没看懂,不过不重要)

因此work函数的主要逻辑就是判断MyApp.m的值是否为1,如果为1则赋值对应地址的值给V3,然后调用callWork。

- initSN()函数分析
进入initSN()函数分析其逻辑为:读取reg.dat的内容,如果内容为"EoPAoY62@ElRD",则MyApp.m的值设置为1,否则为0

- saveSN()函数分析

进入saveSN()函数分析,首先修改变量类型,使得反编译更加人性化,一般变量第一个为JEIEnv*, 第二个参数jobject或者jclass, 后面的参数就是传入的native 函数中传入的参数,依次修改尝试就行。

通过分析代码,v10为数组的索引,从0-sn的长度,依次增长,然后将v10的值会在字符串的指定位置取一个值来与sn对应索引位置的字符串作异或运算。所以可以看出逻辑应该为,输入的sn的每一位和字符串"W3_arE_whO_we_ARE"的固定位置的字符进行了异或运算,然后输出到V8上,最后使用f_puts函数保存到文件中。(至于在"W3_arE_whO_we_ARE"取了那几位,不重要,反正异或运算可逆)

(3)整体流程思路
通过分析三个函数,可以看出该程序的整体调用思路为,work->initSN->saveSN,逻辑思路为:

  • (1) work函数:判断MyApp.m的值是否为1,然后赋值对应地址的值给v3
  • (2) initSN函数:判断reg.dat的内容是否为 “EoPAoY62@ElRD”,若是则MyApp.m赋值为1
  • (3) saveSN函数: 将输入的sn与"W3_arE_whO_we_ARE"做异或运算后保存到reg.dat中

通过jadx可以看出只有当MyApp.m的值为1时才算已注册,所以reg.dat的内容应该为"EoPAoY62@ElRD",而reg.dat的内容是根据输入sn与字符串"W3_arE_whO_we_ARE"通过异或的算法得出的,因此只要将"EoPAoY62@ElRD"与字符串"W3_arE_whO_we_ARE"做异或运算的算法,也能得出我们应该输入的sn,及输入"EoPAoY62@ElRD"进行注册就能得到应该输入的sn。

unidbg 脚本编写

通过编写unidbg脚本,需要实现的函数有

  • saveSN: 主动调用saveSN函数,传入sn参数
  • f_puts: saveSN函数中的子函数,hook该函数可以得到运算后的字符串
  • work: 打印work中的地址unk_2EFB查看是否有提示

整体代码如下

package com.hack;import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;
import com.sun.jna.JNIEnv;
import com.sun.jna.Pointer;
import unicorn.ArmConst;import java.io.File;
import java.util.ArrayList;
import java.util.List;public class hack extends AbstractJni {private final AndroidEmulator emulator;private final VM vm;private final Module module;private DvmClass cNative;private hack () {emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.test").build();final Memory memory = emulator.getMemory();memory.setLibraryResolver(new AndroidResolver(23));vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/hack/hack.apk"));DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/hack/libmyjni.so"), true);vm.setJni(this);vm.setVerbose(true);dm.callJNI_OnLoad(emulator);module = dm.getModule();}@Overridepublic void setStaticIntField(BaseVM vm, DvmClass dvmClass, String signature, int value) {switch (signature) {case "com/gdufs/xman/MyApp->m:I":System.out.println("> Patched: com/gdufs/xman/MyApp->m:I");return;}super.setStaticIntField(vm, dvmClass, signature, value);}@Overridepublic int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {switch (signature) {case "com/gdufs/xman/MyApp->m:I":System.out.println("> Patched: com/gdufs/xman/MyApp->m:I");return 0;}return super.getStaticIntField(vm, dvmClass, signature);}@Overridepublic DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {switch (signature) {case "com/gdufs/xman/MainActivity-><init>()V":System.out.println("> Patched: com/gdufs/xman/MainActivity-><init>()V");return vm.resolveClass("com/gdufs/xman/MainActivity").newObject(null);}return super.newObject(vm, dvmClass, signature, varArg);}@Overridepublic void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {switch (signature) {case "com/gdufs/xman/MainActivity->work(Ljava/lang/String;)V":System.out.println("> Patched: com/gdufs/xman/MainActivity->work(Ljava/lang/String;)V");return;}super.callVoidMethod(vm, dvmObject, signature, varArg);}public static void main(String[] args) {hack test = new hack();test.hookPuts();test.hookWork();test.saveSN();test.work();}private void saveSN() {List<Object> list = new ArrayList<>(10);list.add(vm.getJNIEnv());list.add(0);list.add(vm.addLocalObject(new StringObject(vm, "201608Am!2333")));   // arg 3Number number =  module.callFunction(emulator, 0x000011F8+1, list.toArray());}private void work() {DvmClass dvmClass = vm.resolveClass("com/gdufs/xman/MyApp");String methodSign = "work()V";DvmObject<?> dvmObject = dvmClass.newObject(null);DvmObject ret = dvmObject.callJniMethodObject(emulator, methodSign);Pointer pointer = emulator.getMemory().pointer(module.base + 0x00002EEB);System.out.println("> Pointer:"+pointer.getString(0x10));}private void hookPuts() {// hook saveSN中的f_puts函数HookZz hook = HookZz.getInstance(emulator);hook.replace(module.base + 0x00002C3C+1, new ReplaceCallback() {@Overridepublic HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {System.out.println("> onCall:f_puts()");System.out.println("> arg0:"+context.getPointerArg(0).getString(0));  // 入参1 R0寄存器return super.onCall(emulator, context,originFunction);}}, true);}private void hookWork() {HookZz hook = HookZz.getInstance(emulator);hook.replace(module.base + 0x000014AC, new ReplaceCallback() {@Overridepublic HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {System.out.println("onCall work");System.out.println(context.getPointerArg(0).getString(0));  // 入参1 R0寄存器return super.onCall(emulator, context,originFunction);}@Overridepublic void postCall(Emulator<?> emulator, HookContext context) {System.out.println("postCall work");System.out.println(context.getPointerArg(0).getString(0));  // 入参1 R0寄存器super.postCall(emulator, context);}}, true);}
}

运行结果如下

根据结果可以发现 work 中的函数是提示 flag 格式的,格式为 xman{……},而且输入的 sn 即是flag,然后我们本应该输入的sn由异或运算可以得出为。

所以最终 flag 为:xman{201608Am!2333}

Unidbg + Web = Unidbg-server

:http://91fans.com.cn/post/unidbgweb/

手把手教你搭个签名服务器

把so用unidbg跑起来后,就可以通过 web 服务对外提供 API 接口

  • unidbg
  • Unidbg-server

集成 springboot 运行 unidbg 的方案::https://github.com/cxapython/unidbg-server

git clone 下来,然后导入到 idea,然后、编译、运行…​…​。

跑一下作者提供的例子:python3 send.py

一套行云流水,顺利跑通。 简直开箱即用。

只下载了Unidbg-server的代码,并没有下载Unidbg的代码?为啥直接就能跑起来?

奥秘在pom.xml里面,加载了线上的unidbg模块,所以可以直接跑起来。

不加载线上的Unidbg的代码,而加载我们本机修改过的版本?

  • 首先把我们定制版的unidbg编译成jar包,参考 http://91fans.com.cn/post/unidbgone/
  • 在 Unidbg-server工程的根目录下(和pom.xml同级目录)新建 libs 目录

把定制版的unidbg编译生成的一堆jar包拷进去

  • 修改 pom.xml

// 删除这两个段, 不使用线上的unidbg
<dependency>
        <groupId>com.github.zhkl0228</groupId>
    <artifactId>unidbg-api</artifactId>
    <version>0.9.0</version>
</dependency>
<dependency>
        <groupId>com.github.zhkl0228</groupId>
    <artifactId>unidbg-android</artifactId>
    <version>0.9.0</version>
</dependency>

// 增加这一个段,使用本地的unidbg
<dependency>
        <groupId>unidbg</groupId>
        <artifactId>unidbg</artifactId>
    <version>0.9.5</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/libs/unidbg-parent.jar</systemPath>
</dependency>

  • 然后在左侧工程窗口点右键 Maven → Reload project
  • 重新编译下。 报错了,我就知道不会这么顺利。

有点慢?再优化一把

生产环境下的性能瓶颈可能在unidbg的模拟器初始化上,我们可以只初始化一个模拟器,然后每次做签名的时候只需要调用指定的函数就行。

在controller目录下创建一个 FenfeiController.java

public class FenfeiController {
    public static DouyinSign instance;

static {
        instance = new DouyinSign();
    }

@RequestMapping(value="dySignEx",method =  {RequestMethod.GET,RequestMethod.POST})
    @ResponseBody
    public String dySign(@RequestParam("url") String url) {
        Map<String,String> result= instance.crack(url);
        String jsonString = JSON.toJSONString(result);

return jsonString;
    }
}

这样模拟器只初始化了一次,感觉能快一些了。

不过又引入了一个新问题,模拟器是被共享了,并发的时候是会出问题的,这也难不倒我们,加个锁就行了

public String dySign(@RequestParam("url") String url) {
        synchronized (this) {
            Map<String, String> result = instance.crack(url);
            String jsonString = JSON.toJSONString(result);

return jsonString;
        }
    }

可以通过application.properties自行修改服务的地址和端口, 目前我使用的结果是只改端口就行,ip地址就保持 0.0.0.0就可以了。

开源程序的版本搭配也很重要,发现和最新代码不兼容的时候,可以研究下回退一两个版本。

github 上 unidbg 项目

:https://github.com/search?q=unidbg

unidbgweb

:https://github.com/zhaoboy9692/unidbgweb

unidbg的服务化,毒、酷安、快手、小红书、马蜂窝、抖音、今日头条、美团、拼多多、启信宝、天眼查、封面新闻的相关so调用

unidbg_api

脱离安卓手机调用第三方.so文件,集成了spring boot框架提供web服务 可打成jar包一键部署

unidbg-local-server

:https://github.com/gl953236368/uls

  • 小红书
  • 最右
  • 拼多多
  • 携程
  • bilibili
  • 轻小说
  • 美团
  • ......
  • 懂车帝

Unidbg 调试 浮点数 运算

Unidbg 代码同步到官方最新版,最新版已经支持浮点寄存器的显示了。

:http://91fans.com.cn/post/unidbgreturnone/

在做代码还原的时候,经常能看到一些奇怪的寄存器和奇怪的指令:

vldr s15, [r1]
vadd.f32 s15, s14, s15

很像某些流量明星,看上去很眼熟,仔细看看又不认识。

它们就是传说中的浮点数运算,今天我们来点亮一个很有用的技能树: Unidbg调试浮点数运算

先写个floatdemo

有这么一个祖传的算法函数。

extern "C" JNIEXPORT jstring JNICALL
Java_com_fenfei_app_floatdemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject Obj, jdouble value) {
    std::string hello = "Hello from C++";

double p=3.14159;
    double s,v,rc;

v = 2*p*value;
    s = p*value*value;

rc = v+s;

hello = std::to_string(rc);

return env->NewStringUTF(hello.c_str());
}

算出圆的周长和面积,然后再把它们相加。

高级语言就是好,一目了然。

IDA 打开 so

可以看出两个区别, 一个是寄存器不一样,普通运算使用的寄存器是R0-Rx,浮点数运算使用的是D0-Dx (其实还有 S0-Sx),另一个是指令不一样,普通运算是MOV、MUL,而浮点数运算使用的是VMOV,VMUL,感觉就是普通运算的VIP版。

第一个知识点就出来了,V 开头的指令就是浮点数运算指令,Dx Sx Qx 就是浮点数寄存器。

使用 unidbg 把编译的 floatdemo.apk 跑起来,然后增加一个 stringFromJNI 函数的调用。

private String callfun(String methodSign, Object ...args) {
        DvmObject mainactivity = MainActivity_dvmclass.newObject(null);
        Object value = mainactivity.callJniMethodObject(emulator,methodSign,args).getValue();
        return value.toString();
}

由于 stringFromJNI 不是静态(static)的类函数,所以我们需要先创建个一个 MainActivity 对象,才可以调用它的方法。

先跑一下看看结果

Find native function Java_com_fenfei_app_floatdemo_MainActivity_stringFromJNI(D)Ljava/lang/String; => RX@0x4000c6c9[libnative-lib.so]0xc6c9
JNIEnv->NewStringUTF("150.796320") was called from RX@0x4000c73d[libnative-lib.so]0xc73d
ret:150.796320
emulator destroy...

我们传了个参数6,半径是6的圆, 周长是 37.699, 面积是113.097 ,它们之和是 150.796。 结果没毛病,那我们开始调试了。

Unidbg 调试

从刚才运行的结果里我们知道 stringFromJNI 函数的地址在 0xc6c9, 那么我们现在需要在这个地址下个断点,让调试器停在这个地址。

Unidbg的调试功能依然很强大,它支持三种调试模式 CONSOLE、GDB和IDA,目前我用的顺手的是 CONSOLE 模式,今天先介绍这个。

开启调试炒鸡简单,加上这两行代码就行

Debugger MyDbg = emulator.attach(DebuggerType.CONSOLE); MyDbg.addBreakPoint(module.base + 0xc6c9);

运行一下,就顺利的进入到调试器命令行了,直接回车,会显示目前支持的调试命令。

新手嘛,先掌握一个n和s两个命令就行了,n是单步步过,就是执行一条指令,步过函数调用;s是单步步入,就是执行一条指令,进入函数调用。

n命令跑几下来到我们要分析的浮点数运算的位置,发现尴尬了……

Unidbg调试器只显示了Rx寄存器,没有显示Dx系列的寄存器,这下怎么分析,不能盲摸呀?

打开 Unidbg 浮点数寄存器显示

Unidbg是支持浮点数运算模拟的,那么一定是有地方去读取浮点数寄存器的,只是没有显示出来而已。

我们先分析下Unidbg调试时寄存器显示部分的代码。

先搜索 r0= 在哪里处理的?

showRegs 就是显示寄存器, 当参数为null的时候,通过 ARM.getAllRegisters 来显示所有的寄存器。但是为啥没有显示浮点寄存器呢?奇怪。

我们再往下翻,发现在ARM64的模拟下显示了Q0-Q31寄存器,通过查阅资料,我们知道了原来它们都是一伙的。

那ARM32先放一下,我们把模拟环境切换到ARM64

emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.fenfei.runfloatdemo").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分

再跑一下,调试器没有激活?

这是为什么? 原来我们把模拟器从Arm32切换到了Arm64,那么载入的so就是64位的了,所以 stringFromJNI 函数的地址也变了,需要把断点下在新的地址 0x12738 上面。

这下不一样了,浮点寄存器都显示出来了。

优化 浮点寄存器的显示

这个0x400921f9f01b866e是啥意思呀,你是不是搞错了,浮点数寄存器显示的咋不是 3.14159 ,而是这个乱七八糟的数据?

程序员的母语就是16进制,没有一眼把 0x400921f9f01b866e 认出是 3.14159的,晚饭是不配加鸡腿的,也不配变秃的。

有理想的同学请自行搜索 IEEE754 二进制浮点数算术标准

其他的同学请和我一起优化下浮点寄存器的显示。

由于飞哥目前为止还没有变秃,确实也看不出来这玩意就是 3.14159, 只好另辟蹊径,给大家传授一个神奇的函数:

public static double bytes2Double(byte[] arr) {
    long value = 0;
    for (int i = 0; i < 8; i++) {
        value |= ((long) (arr[i] & 0xff)) << (8 * i);
    }
    return Double.longBitsToDouble(value);
}

// 在showRegs64函数里面加个显示
case Arm64Const.UC_ARM64_REG_Q0:
    byte[] data = backend.reg_read_vector(reg);

double bOut = bytes2Double(data);

if (data != null) {
        builder.append("\n>>>");
        builder.append(String.format(Locale.US, " q0=0x%s(%.3f)", newBigInteger(data).toString(16),bOut));
    }
    break;

其实C/C++ 有个神奇的玩意叫指针,这种显示一把梭就行

BYTE dPiByte[8] = {0x6e, 0x86, 0x1b, 0xf0, 0xf9, 0x21, 0x09, 0x40 };
double dPi = *(double*)dPiByte;

好了,后面的几步运算就是乘乘加加了,同学你自己n几下就ok了。

总结

为什么要去调试,直接F5大法不香吗?

现在Ollvm肆虐,掌握一些手撕汇编的良方可保你无忧。

为什么要用Unidbg去调试,IDA不香吗?

悟空,等你遇上内存防修改,无法下软件断点和一些BT的反调试的时候,你自会回来和为师唱这首歌的: Only You …​…​

条件断点

:http://91fans.com.cn/post/unidbgreturntwo/

进阶学习

某电商App sign算法升级验证

Mac 10.14.x 下Android 10源码(QP1A.190711.020)编译和刷机 (Pixel 2 XL)

AndroidNativeEmu中Hook gettimeofday和lrand48来验证签名值

算法还原的助手(一) 先让时间停下来

AndroidNativeEmu模拟执行计算出某电商App sign

某电商App反反调试

Unicorn反混淆:恢复被OLLVM保护的程序(一)

Unicorn用法示例(二)

Unicorn用法示例(一)

如何保护你的代码 - Ollvm(一)

小程序逆向分析 (二) 跑起来

小程序逆向分析 (一)

IDA F5 增强插件: I Have a Dream (二)

IDA F5 增强插件,还我源代码(一)

借你一双慧眼, Frida Native Trace

frida调试不了怎么办?着急,在线等!

Frida-syscall-interceptor

一通操作猛如虎 合并Unidbg的更新,继续跑sign

Sekiro + Xposed 签名解决方案

unidbg 简介、基本使用、调用so中方法、unidbg-web相关推荐

  1. 动态调用类 java_Java动态调用类中方法

    在Java中,调用类的方法有两种方式:对于静态方法可以直接使用类名调用,对于非静态方法必须使用类的对象调用.反射机制提供了比较另类的调用方式,可以根据需要指定要调用的方法,而不必在编程时确定.调用的方 ...

  2. 多态在android中(利用接口调用服务中方法)的应用

    首先我们已经了解了java中多态的基本概念 1方法重载. 2向上转型,向下转型 今天我们主要讲向上转型在android中的应用范例. 在java中向上向下转型的图解: 重点:子类对象向上转型为父类对象 ...

  3. 关于Html中jsp调用Android中方法无效的一点建议

    2019独角兽企业重金招聘Python工程师标准>>> 其实,后来我认真查找资料后,得知Android API 17及以上的版本,就会出现js调用不了android的方法.解决方案: ...

  4. 支付宝小程序组件传参,父组件调用子组件方法ref

    组件传参 子组件中 //js中声明 Component({props: {title: "标题", // 支付宝小程序props变量传参不能传对象,设置类型,只能传字符串onCon ...

  5. 【Unity3D】Android Studio 工程中使用 Java 代码调用 Unity 的 C# 脚本 ( Java 中调用 UnityPlayer#UnitySendMessage 方法 )

    文章目录 一. Java 调用 C# 依赖库准备 1.依赖库位置 2.unityLibrary 依赖库位置 二. Java 调用 C# 的 UnityPlayer#UnitySendMessage 方 ...

  6. 继承实现的原理、子类中调用父类的方法、封装

    一.继承实现的原来 1.继承顺序 Python的类可以继承多个类.继承多个类的时候,其属性的寻找的方法有两种,分别是深度优先和广度优先. 如下的结构,新式类和经典类的属性查找顺序都一致.顺序为D--- ...

  7. android中方法调用super(..)的相关知识

    java中的多态有重写 方法被子类重写后 父类的原方法就会被隐藏 当你又需要调用父类所定义的原方法  这个时候就可以用super来调用 super调用指向了父类,在一些调用里可以很巧妙的利用,比如监听 ...

  8. JavaScript文件中调用AngularJS内部方法或改变$scope变量

    需要在其他JavaScript文件中调用AngularJS内部方法或改变$scope变量,同时还要保持双向数据绑定: 首先获取AngularJS application: 方法一:通过controll ...

  9. @Transactional-同一个类中方法自调,调用方法事物失效

    问题分析 一个类中的方法调用另一个事物传播性为创建事物的方法,调用的方法事物失效? SpringAOP 代理的Service对象调用了其方法,这个方法再去调用这个Service中的其他方法是没有使用A ...

最新文章

  1. Python中if语句练习题
  2. 著名投资人Chris Dixon:计算的下一波浪潮是什么?
  3. python加密程序_Python加密程序
  4. php的mcypt,php 7.3 在Centos6.x下的安装过程
  5. 动点四边形周长最短_初中几何--线段之和最小值 Part 1:通过点关于直线对称点得到两定点之间直线段长度最短。...
  6. 2012-01-09_1
  7. 自动化专业学python如何_如何系统有效学习 Python 自动化测试?
  8. ZOJ 1242 Carbon Dating
  9. System V信号量
  10. 详细介绍Access数据库注入
  11. Spring学习手札(二)面向切面编程AOP
  12. 明基5560 win7 64驱动_这个Win7系统,稳定又纯净!
  13. 2021年度最全“Java面试宝典+Java核心知识集”,一箭双雕杠春招
  14. 基于MATLAB的有源三相滤波器的设计,基于MATLAB的电力系统有源滤波器设计
  15. 推荐一款免费开源的GIF动图软件(录制,编辑,压缩)
  16. jQuery fadeIn淡入的使用
  17. 罗格斯大学计算机信息工程专业排名,罗格斯大学硕士统计学专业排名?这一些重点迟早得掌握...
  18. 我们一起追逐过的大肥猫——tomcat部署
  19. adb install 安装错误常见列表
  20. 华为ensp搭建习题

热门文章

  1. 编码UTF8,Unicode,GBK
  2. 【重磅!】《2021隐私机密计算蓝皮书》正式发布
  3. Day01 python基础1
  4. 旧城镇文兴小学2008年校长述职报告
  5. 电脑开不了机是什么原因?
  6. Docker镜像地址
  7. 上海2022年平均工资为12184,涨幅只有6.9%
  8. JQuery 添加动态元素JS或CSS无效
  9. 获取《王者荣耀》全英雄高清无码图
  10. 游戏模型师生存手册丨角色布线秘诀