Unidbg 简介
Unidbg 是一个基于 Unicorn 引擎的 Android 模拟执行框架,能够让我们在 无需真实 Android 设备 或 不启动 APK 的情况下执行 SO 库中的 JNI 方法。它主要用于:
- 逆向分析:解密、协议分析、破解加密算法。
- 自动化测试:在 PC 端模拟 JNI 调用,提高效率。
- 安全研究:分析恶意 APK 代码,检测安全漏洞。
核心概念
AndroidEmulator
AndroidEmulator
是 Unidbg 提供的 Android 进程模拟器,它负责创建 ARM 架构的运行环境,支持 32/64 位。
AndroidEmulator emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.example.test")
.build();
Memory(虚拟内存管理)
Unidbg 通过 Memory
模块管理 so 库的加载和 Android 进程的内存映射。
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本
DalvikVM(虚拟机)
DalvikVM
是 Unidbg 提供的 Java 层虚拟机,用于 JNI 交互。
VM vm = emulator.createDalvikVM();
vm.setJni(new AbstractJni() {});
vm.setVerbose(true); // 打开日志输出
DalvikModule Module(SO模块)
DalvikModule
负责加载 Java 层 DEX 代码,同时支持解析 JNI 调用。Module
代表一个已加载的so
文件,支持解析导出函数及调用。
DalvikModule dm = vm.loadLibrary(new File("path\\libxyz.so"), false);
Module module = dm.getModule();
##
如何编译
使用Maven编译,下载解压到本地目录后,进入到unidbg的源码目录,执行编译命令:
D:\maven\apache-maven-3.9.9\bin\mvn.cmd clean install -DskipTests
编译出错:
--- gpg:1.5:sign (default) @ unidbg ---
'gpg.exe' 不是内部或外部命令,也不是可运行的程序或批处理文件。
修改 unidbg根目录下的 pom.xml
文件,删除如下内容后重新编译即可。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
首次使用
AndroidStudio先随便创建一个安卓应用的工程,然后为工程添加模块,选择 Java or Kotlin Library
,根据向导填写基本信息,即可创建一个Java应用程序。
添加依赖库:
dependencies {
implementation 'com.github.zhkl0228:unidbg:0.9.8'
}
模拟调用
例如有一个so文件libxyz.so,它有一些注册函数,其中函数targetFunc是我们想要模拟调用的。
首先使用IDA查看该函数的偏移地址,如下,该偏移地址是:0x6A60。
.data:0001A6B8 DCD atargetFunc ; "targetFunc"
.data:0001A6BC DCD aLandroidConten_6 ; "(Landroid/content/Context;I[B[B)I"
.data:0001A6C0 DCD sub_6A60
编写代码:
package com.bigsing.unidbgdemo;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
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.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
public class MyClass extends AbstractJni {
final AndroidEmulator emulator;
final VM vm;
final Module module;
final DalvikModule dalvikModule;
MyClass() {
// 创建 Android ARM 32位模拟器
emulator = AndroidEmulatorBuilder.for32Bit()
.setProcessName("com.example.test")
.addBackendFactory(new Unicorn2Factory(true))
.build();
// 获取模拟内存并设置 Android 版本解析器
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23)); // 23 代表 Android 版本
// 创建 Android 虚拟机
vm = emulator.createDalvikVM();
vm.setJni(this);
vm.setVerbose(true);
// 注册 Android 模块,确保 Unidbg 能正确解析系统库
new AndroidModule(emulator, vm).register(memory);
// 加载目标 so 文件
dalvikModule = vm.loadLibrary(new File("E:\\APPs\\libxyz.so"), false);
module = dalvikModule.getModule(); // 获取加载后的模块对象
}
public static void main(String[] args) {
String input = "这里是加密字符串...";
MyClass test = new MyClass();
test.callFuncByOffset(input);
test.callFuncByRegister(input);
}
// 通过 JNI 注册的方法调用
public void callFuncByRegister(String input) {
byte[] inData = input.getBytes(StandardCharsets.UTF_8);
final byte[] outData = new byte[4096];
ByteArray inByteArray = new ByteArray(vm, inData);
ByteArray outByteArray = new ByteArray(vm, outData);
// 执行 JNI_OnLoad 以初始化 JNI 相关的符号
dalvikModule.callJNI_OnLoad(emulator);
DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");
try {
// 调用 targetFunc 方法
int len = nativeClass.callStaticJniMethodInt(emulator,
"targetFunc(Landroid/content/Context;I[B[B)I",
null,
1,
vm.addLocalObject(inByteArray),
vm.addLocalObject(outByteArray));
if (len > 0) {
byte[] out = outByteArray.getValue();
System.out.println("[Register Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 通过 native 层函数偏移调用
public void callFuncByOffset(String input) {
Pointer env = vm.getJNIEnv(); // 获取 JNI 环境指针
byte[] inData = input.getBytes(StandardCharsets.UTF_8);
final byte[] outData = new byte[4096];
ByteArray inByteArray = new ByteArray(vm, inData);
ByteArray outByteArray = new ByteArray(vm, outData);
// 获取目标 Java 类对象的引用
DvmClass nativeClass = vm.resolveClass("com/xyz/app/abcd");
DvmObject<?> nativeClassObject = nativeClass.newObject(null);
int jclassRef = vm.addLocalObject(nativeClassObject); // 获取 Java 层的 jclass 句柄,也可以填 0
// 构造参数列表
ArrayList<Object> list_arg = new ArrayList<>();
list_arg.add(env); // JNIEnv*
list_arg.add(jclassRef); // jclass (静态方法调用)
list_arg.add(null); // Context (可选参数)
list_arg.add(1); // int 参数
list_arg.add(vm.addLocalObject(inByteArray)); // byte[] 输入数据
list_arg.add(vm.addLocalObject(outByteArray)); // byte[] 输出数据
// 调用目标 native 层函数,偏移地址 0x6A60
Number result = module.callFunction(emulator, 0x6A60, list_arg.toArray());
int len = result.intValue();
if (len > 0) {
byte[] out = outByteArray.getValue();
System.out.println("[Offset Call] Decoded Text: " + new String(out, StandardCharsets.UTF_8));
}
}
}
代码使用了两种方法来调用目标函数:按照函数偏移地址调用(callFuncByOffset
)、按照注册的jni函数调用(callFuncByRegister
)。
因为我们的目标函数恰巧被它的JNI_OnLoad
进行了注册,所以可以按照调用jni函数的方式进行调用,但是这个方法不够通用,特别是在目标函数没有被注册的情况下,这个时候就要通过函数偏移进行调用了。
函数的偏移地址已经要分析正确,否则调用肯定是不会成功的,这需要在IDA里进行分析。因为这个函数是被注册成jni函数了,通过运行日志也可以看出它的偏移是0x6a60
,也可以佐证在IDA里获取的地址是否正确。
JNIEnv->FindClass(com/xyz/app/abcd) was called from RX@0x40004e60[libxyz.so]0x4e60
JNIEnv->NewGlobalRef(class com/xyz/app/abcd) was called from RX@0x40004e80[libxyz.so]0x4e80
JNIEnv->FindClass(java/lang/String) was called from RX@0x40004ec0[libxyz.so]0x4ec0
JNIEnv->NewGlobalRef(class java/lang/String) was called from RX@0x40004ee0[libxyz.so]0x4ee0
JNIEnv->RegisterNatives(com/xyz/app/abcd, RW@0x4001a6a0[libxyz.so]0x1a6a0, 3) was called from RX@0x40004f2c[libxyz.so]0x4f2c
RegisterNative(com/xyz/app/abcd, targetFunc1(Landroid/content/Context;II)I, RX@0x40005cbc[libxyz.so]0x5cbc)
RegisterNative(com/xyz/app/abcd, targetFunc2(Landroid/content/Context;ILjava/lang/String;Ljava/lang/String;[BII)J, RX@0x4000646c[libxyz.so]0x646c)
RegisterNative(com/xyz/app/abcd, targetFunc(Landroid/content/Context;I[B[B)I, RX@0x40006a60[libxyz.so]0x6a60)
Find native function Java_com_xyz_app_abcd_targetFunc => RX@0x40006a60[libxyz.so]0x6a60
JNIEnv->GetArrayLength([B@496bc455 => 231) was called from RX@0x40006aa0[libxyz.so]0x6aa0
日志分析
有时候日志较多,直接在AndroidStudio的控制台里会显示不下导致较早的日志丢失。可以将输出重定向到文件中,运行后分析日志文件即可。
public static void main(String[] args) {
try {
// 创建日志文件
File logFile = new File("trace_log.txt");
PrintStream fileOut = new PrintStream(logFile);
System.out.println(logFile.getAbsolutePath());
// 重定向 System.out 到文件
System.setOut(fileOut);
// 根据情况选择要不要把错误输出也重定向到文件
System.setErr(fileOut);
} catch (Exception e) {
}
// ...
}
这样后面在代码中凡是使用System.out.println
类似函数做的输出日志就会被写入到文件中。
指令跟踪
可以对全部指令的执行调用做跟踪记录,但是指令级别的跟踪记录未免日志太多,不容易分析。可以简单一些,做一下函数调用跟踪,观察函数的调用情况就比较容易找到关键CALL。如下这段代码实现了:
- 对BLX和BL指令进行跟踪,输出当前指令地址、当前指令、被调用地址(如果操作数是寄存器的话会读取寄存器的数值);
- 汇总被调用地址以及被调用的次数和首次调用时间;
// 函数调用记录
private final Map<Long, CallRecord> callRecords = new LinkedHashMap<>();
private void traceCode() {
// 获取 SO 模块的基地址
long base = module.base;
long size = module.size;
// 开启代码跟踪
emulator.traceCode(base, base + size, new TraceCodeListener() {
@Override
public void onInstruction(Emulator<?> emulator, long address, Instruction insn) {
if (insn.getMnemonic().equals("blx") || insn.getMnemonic().equals("bl")) {
RegisterContext context = emulator.getContext();
long timestamp = System.nanoTime(); // 记录当前时间
long targetAddress;
Capstone.OpInfo operands = insn.getOperands();
Arm.Operand operand = ((Arm.OpInfo) operands).operands[0];
if (operand.type == Arm_const.ARM_OP_REG) {
targetAddress = Integer.toUnsignedLong(context.getIntByReg(operand.value.reg));
} else {
targetAddress = operand.value.imm;
}
// 记录调用地址
callRecords.compute(targetAddress, (key, record) -> {
if (record == null) {
return new CallRecord(targetAddress, timestamp);
} else {
record.increment();
return record;
}
});
System.out.printf("%08X %s // %08X\n", address, insn.toString(), targetAddress);
}
}
});
}
public class CallRecord {
long address;
long firstTimestamp;
int count;
CallRecord(long address, long timestamp) {
this.address = address;
this.firstTimestamp = timestamp;
this.count = 1;
}
void increment() {
this.count++;
}
}
public void printCallRecords() {
System.out.println("\n==== Call Records ====");
// 按 firstTimestamp 进行排序
List<CallRecord> sortedRecords = new ArrayList<>(callRecords.values());
sortedRecords.sort(Comparator.comparingLong(record -> record.firstTimestamp));
// 输出排序后的调用记录
for (CallRecord record : sortedRecords) {
System.out.printf("First Call Time: %d, 0x%08X, Call Count: %d\n",
record.firstTimestamp, record.address, record.count);
}
}
调用printCallRecords()之后,函数调用跟踪情况记录大概如下:
……
40006B18 blx r3 // FFFE0BB0
40006B3C bl #0x4001381c // 4001381C
40013F64 blx ip // 40015E2D
40006B50 blx #0x40015e44 // 40015E44
40015E66 blx #0x40017e60 // 40017E60
40006BB0 bl #0x40017890 // 40017890
……
==== Call Records ====
First Call Time: 429011673421200, 0xFFFE0AB0, Call Count: 2
First Call Time: 429011693048600, 0xFFFE0BB0, Call Count: 2
First Call Time: 429011730031600, 0x4001381C, Call Count: 1
First Call Time: 429011750418300, 0x40015E2D, Call Count: 4
First Call Time: 429011798832600, 0x40015E44, Call Count: 3
First Call Time: 429011810345500, 0x40017E60, Call Count: 7
First Call Time: 429011865146400, 0x40017890, Call Count: 1
First Call Time: 429011885883400, 0x40017E80, Call Count: 3
First Call Time: 429011901284200, 0x40007020, Call Count: 2
First Call Time: 429011918953900, 0x4000BD84, Call Count: 1
First Call Time: 429012282955600, 0x40007134, Call Count: 2
First Call Time: 429012317379200, 0x4001417C, Call Count: 2
First Call Time: 429012356067200, 0x40017DF0, Call Count: 2
First Call Time: 429012363470400, 0x40014284, Call Count: 5
First Call Time: 429012465077200, 0x40013EA0, Call Count: 1
First Call Time: 429012492479400, 0x40017E10, Call Count: 10
First Call Time: 429012502974200, 0x400157E4, Call Count: 7
First Call Time: 429012566738000, 0x40012190, Call Count: 1
First Call Time: 429012585442100, 0x40012A50, Call Count: 2
First Call Time: 429012788305500, 0x400124E0, Call Count: 4
First Call Time: 429012821809600, 0x400127B8, Call Count: 4
First Call Time: 429012910144600, 0x40012634, Call Count: 1
First Call Time: 429013039641100, 0x40017298, Call Count: 1
First Call Time: 429013060267600, 0x40017E30, Call Count: 1
First Call Time: 429013140221300, 0x40013834, Call Count: 1
First Call Time: 429013236234100, 0x40014092, Call Count: 1
First Call Time: 429013267792300, 0x40015E80, Call Count: 1
First Call Time: 429013290610900, 0x40013820, Call Count: 2
First Call Time: 429013303239500, 0x40007254, Call Count: 1
First Call Time: 429013317807000, 0x40014014, Call Count: 1
First Call Time: 429013326587400, 0x400178E0, Call Count: 2
First Call Time: 429013333701400, 0xFFFE0C50, Call Count: 2
这样记录的好处是:一般来说被调用一次的函数是切入点(当然这个也是经验之谈,如果被调用一次的函数没有分析出什么,那就可以继续分析被调用两次、三次的函数,以此类推)。
例如上述的跟踪记录,随便看下调用次数比较多的,例如0x40017E10
和 0x40017E60
,在IDA里跳转到偏移地址查看,分别调用的其实是函数:qmemcpy
、memset
。其他类似,因此我们重点去分析被调用一次的函数即可,可以对这些函数进行HOOK分析,请看下节。
HOOK函数找关键CALL
unidbg对函数做HOOK是通过断点来实现的,对单个函数做HOOK的方式如下(仅作参考不推荐,推荐后面的批量HOOK的做法):
private void hook_sub12190() {
Map<String, Object> paramCache = new HashMap<>();
long targetAddress = module.base + 0x12190; // 计算函数真实地址
// 1. 进入 sub_12190 时打印参数
emulator.attach().addBreakPoint(targetAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
// 获取入参
Pointer a1 = context.getPointerArg(0); // 获取第一个参数,a1
Pointer a2 = context.getPointerArg(1); // 获取第2个参数,a2
int a3 = context.getIntArg(2); // 获取第三个参数,a3
int a4 = context.getIntArg(3); // 获取第四个参数,a4
int a5 = context.getIntArg(4); // 获取第五个参数,a5
Pointer a6 = context.getPointerArg(5); // a6 是 int* 类型的指针
Pointer a7 = context.getPointerArg(6); // 获取第七个参数,a7(指针)
// 读取 a6 指向的整数值
int a6_value = (a6 != null) ? a6.getInt(0) : 0;
// 打印进入时的参数
System.out.println(">>> Entering sub_12190");
// 保存参数:以参数名作为键
paramCache.put("a1", a1);
paramCache.put("a2", a2);
paramCache.put("a3", a3);
paramCache.put("a4", a4);
paramCache.put("a5", a5);
paramCache.put("a6", a6);
paramCache.put("a7", a7);
// 解析并打印参数
System.out.println("a1: " + readStringOrHex(a1, 200));
System.out.println("a2: " + readStringOrHex(a2, 200));
System.out.println("a3: " + a3);
System.out.println("a4: " + a4);
System.out.println("a5: " + a5);
System.out.println("a6: " + a6_value);
System.out.println("a7: " + readStringOrHex(a7, 200));
// 获取返回地址并在此处设置断点
long returnAddress = context.getLRPointer().peer;
emulator.attach().addBreakPoint(returnAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
// 从缓存中获取保存的参数值
Object a1 = paramCache.get("a1");
Object a2 = paramCache.get("a2");
Object a3 = paramCache.get("a3");
Object a4 = paramCache.get("a4");
Object a5 = paramCache.get("a5");
Object a6 = paramCache.get("a6");
Object a7 = paramCache.get("a7");
// 读取 a6 指向的整数值
int a6_value = (a6 != null) ? ((Pointer) a6).getInt(0) : 0;
System.out.println("<<< Returning from sub_12190");
// 打印返回时的参数(从缓存中获取)
System.out.println("a1: " + readStringOrHex((Pointer) a1, 200));
System.out.println("a2: " + readStringOrHex((Pointer) a2, 200));
System.out.println("a3: " + a3);
System.out.println("a4: " + a4);
System.out.println("a5: " + a5);
System.out.println("a6: " + a6_value);
System.out.println("a7: " + readStringOrHex((Pointer) a7, 200));
return true; // 继续执行
}
});
return true; // 继续执行
}
});
}
当需要分析的函数较多时,逐个编写hook代码就不推荐了,这个时候可以进行批量HOOK,推荐做法如下。
private static final int PARAM_COUNT = 8; // 假设所有函数都有 8 个参数
private static final int HEXDUMP_SIZE = 200; // HexDump 大小
// 定义一个成员变量来缓存每个函数调用的参数
private final Map<Long, Map<Integer, Object>> funcParamCache = new HashMap<>();
private void hook_funcs(long[] offsets) {
for (long offset : offsets) {
long targetAddress = module.base + offset;
emulator.attach().addBreakPoint(targetAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
boolean is64Bit = emulator.is64Bit();
System.out.println("\n===>>> Entering function at offset: 0x" + Long.toHexString(offset));
// 创建一个新的 paramCache,与当前函数调用关联
Map<Integer, Object> paramCache = new HashMap<>();
funcParamCache.put(address, paramCache);
for (int i = 0; i < PARAM_COUNT; i++) {
Pointer ptr = context.getPointerArg(i);
long value = is64Bit ? context.getLongArg(i) : context.getIntArg(i);
boolean isMemoryReadable = isReadableMemory(ptr);
ArgItem argItem = new ArgItem(ptr, value, isMemoryReadable);
paramCache.put(i, argItem); // 存入缓存
printParamInfo("Arg" + i, argItem);
}
// **监听返回地址**
long returnAddress = context.getLRPointer().peer;
emulator.attach().addBreakPoint(returnAddress, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
boolean is64Bit = emulator.is64Bit();
System.out.println("\n<<<=== Returning from function at offset: 0x" + Long.toHexString(offset));
// 获取当前函数的 paramCache
Map<Integer, Object> paramCache = funcParamCache.remove(targetAddress);
if (paramCache == null) {
System.err.println("No param cache found for function at offset: 0x" + Long.toHexString(offset));
return true;
}
// 解析参数
for (int j = 0; j < PARAM_COUNT; j++) {
System.out.println("Cached Arg " + j);
ArgItem argItem = (ArgItem) paramCache.getOrDefault(j, null);
printParamInfo("Cached Arg" + j, argItem);
}
// **解析返回值**
Pointer retPtr = context.getLRPointer();
long retValue = context.getLR();
boolean isMemoryReadable = isReadableMemory(retPtr);
ArgItem argItem = new ArgItem(retPtr, retValue, isMemoryReadable);
printParamInfo("Return Value", argItem );
return true;
}
});
return true;
}
});
}
}
public class ArgItem {
public Pointer ptr;
long value;
boolean isMemoryReadable;
ArgItem(Pointer ptr, long value, boolean isMemoryReadable) {
this.ptr = ptr;
this.value = value;
this.isMemoryReadable = isMemoryReadable;
}
}
// **📌 输出参数/返回值信息**
private void printParamInfo(String label, ArgItem argItem) {
System.out.printf("%s: Decimal=%d, Hex=0x%x\n", label, argItem.value, argItem.value);
// **如果是指针,并且可读**
if (argItem.isMemoryReadable) {
System.out.printf("%s (HexDump):\n%s\n", label, hexdump(argItem.ptr, HEXDUMP_SIZE));
}
}
// **📌 判断是否是可读内存**
private boolean isReadableMemory(Pointer ptr) {
if (ptr == null) {
return false;
}
try {
byte[] data = ptr.getByteArray(0, 1); // 尝试读取 1 字节,判断是否可访问
return true;
} catch (Exception e) {
return false;
}
}
// **📌 HexDump 实现**
private String hexdump(Pointer ptr, int length) {
byte[] data = ptr.getByteArray(0, length);
StringBuilder hexDump = new StringBuilder();
for (int i = 0; i < data.length; i += 16) {
StringBuilder hexPart = new StringBuilder();
StringBuilder asciiPart = new StringBuilder();
for (int j = 0; j < 16 && i + j < data.length; j++) {
byte b = data[i + j];
hexPart.append(String.format("%02X ", b));
asciiPart.append((b >= 32 && b <= 126) ? (char) b : '.');
}
hexDump.append(String.format("%08X %-48s %s%n", i, hexPart, asciiPart));
}
return hexDump.toString();
}
使用时就可以这样批量对函数HOOK了:
hook_funcs(new long[]{0xBD84, 0x13EA00, x12190, 0x12634, 0x17298});
这里简单介绍下代码:
funcParamCache
是一个全局变量,用来缓存函数的各个参数,在函数返回时直接读取缓存的参数数值。避免:当内部的返回地址断点回调被触发时访问错误的内部的变量。ArgItem
用来缓存参数的信息。因为在函数返回时触发断点的函数内部,做内存地址访问的判断会导致异常,但是在函数进入时却不会,这个可能是unidbg的一个BUG,我们缓存下内存可访问的标志来规避下这个BUG。- 一律打印8个参数,这个数值可以通过
PARAM_COUNT
修改。输出参数的十进制、十六进制数值,然后再通过isReadableMemory
判断是否可以作为内存地址进行读取,如果可以则会调用hexdump对该地址dump默认200个字节的二进制数据,这个大小也可以通过HEXDUMP_SIZE
来修改。
一个个手动输入offsets
也挺麻烦的,其实可以对printCallRecords
做下改造,让它接着按顺序输出只调用了一次的函数偏移地址,改造后的代码:
public void printCallRecords() {
System.out.println("\n==== Call Records ====");
// 按 firstTimestamp 进行排序
List<CallRecord> sortedRecords = new ArrayList<>(callRecords.values());
sortedRecords.sort(Comparator.comparingLong(record -> record.firstTimestamp));
// 输出排序后的调用记录
for (CallRecord record : sortedRecords) {
System.out.printf("First Call Time: %d, 0x%08X, Call Count: %d\n",
record.firstTimestamp, record.address, record.count);
}
// 获取列表中count为1的元素
List<CallRecord> countOneRecords = sortedRecords.stream()
.filter(record -> record.count == 1)
.collect(Collectors.toList());
// 格式化生成一个字符串:对元素的地址address进行join
StringBuilder sb = new StringBuilder();
for (CallRecord record : countOneRecords) {
sb.append(String.format("0x%X", record.address - module.base)).append(", ");
}
// 去掉最后多余的 ", "
if (sb.length() > 0) {
sb.setLength(sb.length() - 2);
}
String addressString = sb.toString();
System.out.println("\nAddresses with count 1: " + addressString);
}
这样在跟踪调用后,会额外输出如下内容:
Addresses with count 1: 0x1381C, 0x17890, 0xBD84, 0x12190, 0x12634, 0x17298, 0x17E30, 0x14092, 0x15E80, 0x7254, 0x14014
我们把地址序列整体复制使用即可,作为hook_funcs
的参数列表。
最终输出的日志形式(考虑篇幅问题,数据有一些删减):
===>>> Entering function at offset: 0xbd84
Arg0: Decimal=-1073748240, Hex=0xffffffffbfffe6f0
Arg0 (HexDump):
00000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Arg1: Decimal=223, Hex=0xdf
Arg2: Decimal=1075920902, Hex=0x40214006
Arg2 (HexDump):
00000000 43 53 66 62 4D 41 55 63 6D 46 30 43 45 66 72 56 CSfbMAUcmF0CEfrV
00000010 63 32 59 41 41 41 41 41 47 52 44 59 39 34 64 2D c2YAAAAAGRDY94d-
Arg3: Decimal=223, Hex=0xdf
Arg4: Decimal=-1073753264, Hex=0xffffffffbfffd350
Arg4 (HexDump):
00000000 DF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Arg5: Decimal=-1073753280, Hex=0xffffffffbfffd340
Arg5 (HexDump):
00000000 CE E7 FF BF 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 DF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Arg6: Decimal=0, Hex=0x0
Arg7: Decimal=0, Hex=0x0
<<<=== Returning from function at offset: 0xbd84
Cached Arg 0
Cached Arg0: Decimal=-1073748240, Hex=0xffffffffbfffe6f0
Cached Arg0 (HexDump):
00000000 09 27 DB 30 05 1C 98 5D 02 11 FA D5 73 66 00 00 .'.0...]....sf..
00000010 00 00 19 10 D8 F7 87 7E D5 B0 12 22 6E 2B BA 97 .......~..."n+..
Cached Arg 1
Cached Arg1: Decimal=223, Hex=0xdf
Cached Arg 2
Cached Arg2: Decimal=1075920902, Hex=0x40214006
Cached Arg2 (HexDump):
00000000 43 53 66 62 4D 41 55 63 6D 46 30 43 45 66 72 56 CSfbMAUcmF0CEfrV
00000010 63 32 59 41 41 41 41 41 47 52 44 59 39 34 64 2D c2YAAAAAGRDY94d-
Cached Arg 3
Cached Arg3: Decimal=223, Hex=0xdf
Cached Arg 4
Cached Arg4: Decimal=-1073753264, Hex=0xffffffffbfffd350
Cached Arg4 (HexDump):
00000000 A7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Cached Arg 5
Cached Arg5: Decimal=-1073753280, Hex=0xffffffffbfffd340
Cached Arg5 (HexDump):
00000000 E5 40 21 40 00 00 00 00 00 00 00 00 00 00 00 00 .@!@............
00000010 A7 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Cached Arg 6
Cached Arg6: Decimal=0, Hex=0x0
Cached Arg 7
Cached Arg7: Decimal=0, Hex=0x0
Return Value: Decimal=1073769560, Hex=0x40006c58
Return Value (HexDump):
00000000 50 00 9D E5 00 50 C6 E7 07 00 A0 E1 50 10 9D E5 P....P......P...
00000010 31 01 00 EB 50 70 9D E5 08 00 57 E3 01 00 00 2A 1...Pp....W....*
===>>> Entering function at offset: 0x12190
Arg0: Decimal=1076457488, Hex=0x40297010
Arg0 (HexDump):
00000000 82 92 29 F8 6D 61 BE 1D 71 7D A3 67 F0 66 22 E3 ..).ma..q}.g.f".
00000010 3E 8C F0 D4 69 20 E3 93 DE 2B B4 BB F1 C7 ED 62 >...i ...+.....b
Arg1: Decimal=1076457488, Hex=0x40297010
Arg1 (HexDump):
00000000 82 92 29 F8 6D 61 BE 1D 71 7D A3 67 F0 66 22 E3 ..).ma..q}.g.f".
00000010 3E 8C F0 D4 69 20 E3 93 DE 2B B4 BB F1 C7 ED 62 >...i ...+.....b
Arg2: Decimal=141, Hex=0x8d
Arg3: Decimal=0, Hex=0x0
Arg4: Decimal=1076457632, Hex=0x402970a0
Arg4 (HexDump):
00000000 94 C7 F6 92 7A 9D 6D C3 29 8C CF 87 0C 97 28 A4 ....z.m.).....(.
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Arg5: Decimal=-1073753288, Hex=0xffffffffbfffd338
Arg5 (HexDump):
00000000 C7 FF A1 BF A1 D3 94 B4 50 D3 FF BF 00 00 00 00 ........P.......
00000010 88 13 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Arg6: Decimal=-1073748264, Hex=0xffffffffbfffe6d8
Arg6 (HexDump):
00000000 32 B7 2F 21 56 3C A1 F2 F0 E6 FF BF C8 00 00 00 2./!V<..........
00000010 00 10 00 00 00 00 00 00 09 17 6F 88 07 C4 78 3C ..........o...x<
Arg7: Decimal=0, Hex=0x0
<<<=== Returning from function at offset: 0x12190
Cached Arg 0
Cached Arg0: Decimal=1076457488, Hex=0x40297010
Cached Arg0 (HexDump):
00000000 87 00 00 00 F0 78 0A 21 08 B1 BC BC 02 12 06 E5 .....x.!........
00000010 B0 8F E9 98 B3 1A 12 38 37 37 30 39 34 34 39 35 .......877094495
Cached Arg 1
Cached Arg1: Decimal=1076457488, Hex=0x40297010
Cached Arg1 (HexDump):
00000000 87 00 00 00 F0 78 0A 21 08 B1 BC BC 02 12 06 E5 .....x.!........
00000010 B0 8F E9 98 B3 1A 12 38 37 37 30 39 34 34 39 35 .......877094495
Cached Arg 2
Cached Arg2: Decimal=141, Hex=0x8d
Cached Arg 3
Cached Arg3: Decimal=0, Hex=0x0
Cached Arg 4
Cached Arg4: Decimal=1076457632, Hex=0x402970a0
Cached Arg4 (HexDump):
00000000 94 C7 F6 92 7A 9D 6D C3 29 8C CF 87 0C 97 28 A4 ....z.m.).....(.
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Cached Arg 5
Cached Arg5: Decimal=-1073753288, Hex=0xffffffffbfffd338
Cached Arg5 (HexDump):
00000000 C7 FF A1 BF A1 D3 94 B4 50 D3 FF BF 00 00 00 00 ........P.......
00000010 88 13 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Cached Arg 6
Cached Arg6: Decimal=-1073748264, Hex=0xffffffffbfffe6d8
Cached Arg6 (HexDump):
00000000 32 B7 2F 21 56 3C A1 F2 F0 E6 FF BF C8 00 00 00 2./!V<..........
00000010 00 10 00 00 00 00 00 00 09 17 6F 88 07 C4 78 3C ..........o...x<
Cached Arg 7
Cached Arg7: Decimal=0, Hex=0x0
Return Value: Decimal=857760878, Hex=0x3320646e
然后通过函数进出时候的数据变化,就可以判断出哪个函数是关键CALL了,这个方法适合做数据加密解密算法的分析。
例如上日志,可以看出0xbd84
函数是做了base64解密,0x12190
函数是做了另一个解密。
其实这个功能效果类似frida
的Stalker
功能,可以参考:使用Frida的stalker功能跟踪分析Native函数调用 — 朱皮特的烂笔头
文档信息
- 本文作者:zhupite
- 本文链接:https://zhupite.com/sec/unidbg.html
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)