anti-token算法分析
1. 概述
- 样本是pdd,来源见最后总结,目标算法是anti-token;
- 版本是v7.80.0,没有加固;
- 如果你要跟着分析算法,那么请划到最后获取样本,豌豆荚下载的只有32位的so;
- 其中unidbg补环境的部分我着重说了,基本上是手把手带着补了,不熟悉的可以学习一下;
2. 抓包&定位
- 我这里直接socksdroid转发就可以抓了,如果有问题网上的文章也挺多的,或者联系我;
- 这里找的是搜索接口,有我们的目标,其实好像很多个接口都有anti-token;

- 关于定位,这里选择hook hashmap的put方法,脚本如下:
function hook_map() {
Java.perform(function () {
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b) {
if (a == "anti-token") {
console.log(a, b)
showStacks()
}
return this.put(a, b)
}
})
}
function showStacks() {
Java.perform(function () {
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
));
})
}
setImmediate(hook_map);
- 这里我只给一组日志,并且我会去掉一部分无关的东西,因为太多了,找到正确的就好;
输出--> anti-token 2af5lghy4KzO2lcwVW6IEaYDwz6sVIqUW1+AK90ydjA7dmBnuo/zEr6TFTW3sz0qaLTz2uW963kvizoOrynVuciCjmqKOPmW9/r9x3Y4ceuOLzEfEsmq6+UPvzNau9i3EHIys5RoxMgiFamAkuRPLoal/2Of799pV3/+lUdhlg+sfCVlbD1SgYcmcGARbKkgAEOVjigIG8wljjUGqhjNUu+fMUGHjImahbFtjNZP/4e1Ut4/jtQRgZiHv2BmUUOMfrYF0oA9i+/psheOVod8/pgrJCkNsG33qrl+TVJRVmLNj7Kfw2SgKHIYtfRrumaTCuom9eJ5qYPPCxykYVadqj0WT1OZBV+PyiP6VV6f3k52JiC5peWzdYgqHciiYLs+N/Nzmiqx9/wGUSo7HmJsHAw8w==
java.lang.Throwable
at java.util.HashMap.put(Native Method)
at com.aimi.android.common.http.j.b(Pdd:198)
at com.aimi.android.common.http.i.g(Pdd:46)
at com.aimi.android.common.http.CquickCallBizLogicDelegate.wrapAntiToken(Pdd:0)
at com.xunmeng.pinduoduo.arch.quickcall.internal.QuickCallBizLogic.wrapAntiToken(Pdd:6)
at lo1.c.a(Pdd:105)
at qv2.g.g(Pdd:164)
at pv2.a.a(Pdd:27)
at qv2.g.g(Pdd:164)
···
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:920)
- 去找 com.aimi.android.common.http.j.b 这个位置;

- b方法这里是有关键点的,后续strY的生成就去找这个 y 方法,它是一个接口,自行寻找一下;
- 进入接口内部可以看见接口为 c,这种太多了,建议按N改个名字再搜索会有效率很多;



- 这个info2就是目标函数,它的参数是context和一个long类型的,懂得都知道应该是时间戳;
- 后续开始算法分析,定位到这里即算成功;
3. unidbg辅助算法分析
符号信息: 0x713d25c4e8 libpdd_secure.so!device_info2
so文件名: libpdd_secure.so
函数偏移: 0x104e8
- 你可以去hook一下这个函数拿一下入参,不过一个时间戳而已,无所谓;因为后面分析的时候会发现时间戳只是一小部分;
- 先把unidbg环境补好,中间涉及到的内容会比较多;
3.1 模拟执行
package com.Samples.pdd;
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.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
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.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.StringObject;
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.pointer.UnidbgPointer;
import keystone.Keystone;
import keystone.KeystoneArchitecture;
import keystone.KeystoneEncoded;
import keystone.KeystoneMode;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
public class anti_token extends AbstractJni implements IOResolver {
public static AndroidEmulator emulator;
public static Memory memory;
public static VM vm;
public static Module module;
@Override
public FileResult resolve(Emulator emulator, String pathname, int oflags) {
System.out.println("sana file open-->>" + pathname);
return null;
}
public anti_token() {
emulator = AndroidEmulatorBuilder
.for64Bit()
.setProcessName("com.xunmeng.pinduoduo")
.addBackendFactory(new Unicorn2Factory(false))
.build();
// 文件访问 注意这里的处理,加上这一句
emulator.getSyscallHandler().addIOResolver(this);
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("src/test/java/com/Samples/pdd/file/pinduoduo-7-80-0.apk"));
vm.setJni(this);
vm.setVerbose(true);
DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/Samples/pdd/file/libpdd_secure.so"), true);
module = dm.getModule();
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args) {
anti_token demo = new anti_token();
}
}
java.lang.UnsupportedOperationException: com/tencent/mars/xlog/PLog->i(Ljava/lang/String;Ljava/lang/String;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:710)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:705)
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature){
case "com/tencent/mars/xlog/PLog->i(Ljava/lang/String;Ljava/lang/String;)V":{
System.out.println("[+]PLog.i arg0-->>" + vaList.getObjectArg(0));
System.out.println("[+]PLog.i arg1-->>" + vaList.getObjectArg(1));
return;
}
}
super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}

- 第一步完成了,下面就开始call info2函数了;
private void call_info2_byApi() {
DvmClass DeviceNative = vm.resolveClass("com/xunmeng/pinduoduo/secure/DeviceNative");
DvmObject<?> result = DeviceNative.callStaticJniMethodObject(
emulator,
"info2(Landroid/content/Context;J)Ljava/lang/String;",
vm.resolveClass("android/content/Context").newObject(null),
1764743489547L
);
System.out.println("主动调用result-->>" + result.toString());
}
- 我这里使用api的方式来call,因为参数不多也挺方便的;这里需要注意,细心的读者可以发现我执行的是64位的so,但是豌豆荚下载的apk只有32位,如果需要和我的一致,我会放上样本;当然,32位环境也是差不多的;总体只有极个别差异;
java.lang.UnsupportedOperationException: android/content/Context->checkSelfPermission(Ljava/lang/String;)I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:967)
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:940)

@Override
public int callIntMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "android/content/Context->checkSelfPermission(Ljava/lang/String;)I": {
System.out.println("[+]Context.checkSelfPermission arg0-->>" + varArg.getObjectArg(0));
return 1;
}
}
return super.callIntMethod(vm, dvmObject, signature, varArg);
}
java.lang.UnsupportedOperationException: android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:869)
- 这个api是获取系统服务相关,应用程序通过 Context 对象的 getSystemService 方法,传入一个表示所需服务的字符串常量
系统返回对应服务的代理对象,应用可以通过这个对象与系统服务交互;
@Override
public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;": {
String serviceName = varArg.getObjectArg(0).getValue().toString();
System.out.println("getSystemService->" + serviceName);
}
}
return super.callObjectMethod(vm, dvmObject, signature, varArg);
}
getSystemService->phone
java.lang.UnsupportedOperationException: android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:179)
- 这里需要结合起来看,他要返回的就是phone相关的服务,后续再对这个服务做一些操作;
case "android/content/Context->getSystemService(Ljava/lang/String;)Ljava/lang/Object;": {
// 获取服务名称
String serviceName = varArg.getObjectArg(0).getValue().toString();
System.out.println("getSystemService->" + serviceName);
switch (serviceName) {
// 根据服务名称返回不同的对象
case "phone": {
// 返回TelephonyManager对象
return vm.resolveClass("android/telephony/TelephonyManager").newObject(null);
}
}
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimState()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:967)
at com.Samples.pdd.anti_token_64.callIntMethod(anti_token_64.java:136)
- 这个方法是为了获取当前sim卡的状态的,返回5吧,表示就绪状态,注意位置不要补错了;
case "android/telephony/TelephonyManager->getSimState()I": {
return 5;
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimOperatorName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:186)
case "android/telephony/TelephonyManager->getSimOperatorName()Ljava/lang/String;":{
return new StringObject(vm, "中国移动");
}
- 这里给空字符串也行吧,不过和前面一致比较好,继续看;
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getSimCountryIso()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:182)
case "android/telephony/TelephonyManager->getSimCountryIso()Ljava/lang/String;":{
return new StringObject(vm, "cn");
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkType()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:967)
at com.Samples.pdd.anti_token_64.callIntMethod(anti_token_64.java:136)
- 这部分几乎都是这种类型的,后续的我不再进行解释了,因为我遇到不会的也是google,但不要百度就好;
case "android/telephony/TelephonyManager->getNetworkType()I": {
return 2;
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkOperator()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:183)
case "android/telephony/TelephonyManager->getNetworkOperator()Ljava/lang/String;":{
return new StringObject(vm, "46000");
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkOperatorName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:184)
case "android/telephony/TelephonyManager->getNetworkOperatorName()Ljava/lang/String;":{
return new StringObject(vm, "中国移动");
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getNetworkCountryIso()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:185)
case "android/telephony/TelephonyManager->getNetworkCountryIso()Ljava/lang/String;":{
return new StringObject(vm, "cn");
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getDataState()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:967)
at com.Samples.pdd.anti_token_64.callIntMethod(anti_token_64.java:136)
case "android/telephony/TelephonyManager->getDataState()I": {
return 0;
}
java.lang.UnsupportedOperationException: android/telephony/TelephonyManager->getDataActivity()I
at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethod(AbstractJni.java:967)
at com.Samples.pdd.anti_token_64.callIntMethod(anti_token_64.java:136)
- 这个api的含义是用于获取当前移动数据连接的活动状态,表示数据正在如何传输;都是一个系列的;
case "android/telephony/TelephonyManager->getDataActivity()I": {
return 0;
}
java.lang.UnsupportedOperationException: com/xunmeng/pinduoduo/secure/EU->gad()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:506)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:440)
- 这个很明显是样本自己的方法,去hook看看返回值,这里触发时机随意找,最好先别登录账号;
@Override
public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
switch (signature) {
case "com/xunmeng/pinduoduo/secure/EU->gad()Ljava/lang/String;":
return new StringObject(vm, "3512d332db67860a");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}
java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethod(AbstractJni.java:181)
at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethod(AbstractJni.java:176)
- 这个是判断是否被debugger,肯定返回false嘛;
@Override
public boolean callStaticBooleanMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "android/os/Debug->isDebuggerConnected()Z":
return false;
}
return super.callStaticBooleanMethod(vm, dvmClass, signature, varArg);
}
java.lang.UnsupportedOperationException: java/lang/Throwable-><init>()V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:755)
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:735)
- 关于Throwable相关的都不是特别好补,点进第一行日志,去看看unidbg遇到类似的情况怎么做的;

- 这里稍微提一下,AbstractJni里面是unidbg作者为我们实现的一些环境,绝大多数的环境都可以参考着来;
@Override
public DvmObject<?> newObject(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) {
switch (signature) {
case "java/lang/Throwable-><init>()V":{
return vm.resolveClass("java/lang/Throwable").newObject(new Throwable());
}
}
return super.newObject(vm, dvmClass, signature, varArg);
}
- 这里参考着来的,总体就是new一个Throwable对象,和它的方式类似,但是unidbg是没有内置Throwable的对象所以手动new一个出来,继续执行;
java.lang.UnsupportedOperationException: java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:186)
- 这个是获取调用栈信息的,他返回的是一个数组,我们尝试这么返回;
case "java/lang/Throwable->getStackTrace()[Ljava/lang/StackTraceElement;":{
StackTraceElement[] elements = Thread.currentThread().getStackTrace();
DvmObject[] objs = new DvmObject[elements.length];
for (int i = 0; i < elements.length; i++) {
objs[i] = vm.resolveClass("java/lang/StackTraceElement").newObject(elements[i]);
System.out.println("[+]StackTraceElement-->>" + elements[i]);
}
return new ArrayObject(objs);
}

return new ArrayObject(
vm.resolveClass("java/lang/StackTraceElement").newObject(null),
vm.resolveClass("java/lang/StackTraceElement").newObject(null),
vm.resolveClass("java/lang/StackTraceElement").newObject(null)
);
- 感觉比上面的好一些,但是最保险的还是自己模拟一下调用栈情况,这里一切从简了;继续执行吧;
java.lang.UnsupportedOperationException: java/lang/StackTraceElement->getClassName()Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:194)
case "java/lang/StackTraceElement->getClassName()Ljava/lang/String;":{
StackTraceElement obj = (StackTraceElement) dvmObject.getValue();
return new StringObject(vm, obj.getClassName());
}
- 以后遇到这种环境都是这样来,某个对象的某个方法,没有好的方式就取出对象然后调方法,或者你直接返回空字符串也行,只要能够跑通,毕竟我们的目的是算法还原,继续执行;
- 在看报错之前我们先看看取到了什么;

- 可以看出来不是一个正常的name,这里我们可以直接返回一个正确的字符串,但还是那句话,算法还原才是目的;
java.lang.UnsupportedOperationException: java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:419)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
@Override
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
switch (signature) {
case "java/lang/String->replaceAll(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
// 获取原始字符串
String original = (String) dvmObject.getValue();
// 获取参数
DvmObject<?> regexObj = vaList.getObjectArg(0);
DvmObject<?> replacementObj = vaList.getObjectArg(1);
if (regexObj == null || replacementObj == null) {
return new StringObject(vm, original);
}
String regex = (String) regexObj.getValue();
String replacement = (String) replacementObj.getValue();
// 执行替换
String result = original.replaceAll(regex, replacement);
// 返回结果
return new StringObject(vm, result);
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
java.lang.UnsupportedOperationException: java/io/ByteArrayOutputStream-><init>()V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:755)
at com.Samples.pdd.anti_token_64.newObject(anti_token_64.java:234)
case "java/io/ByteArrayOutputStream-><init>()V":{
ByteArrayOutputStream OutputMemoryBuffer = new ByteArrayOutputStream();
return vm.resolveClass("java/io/ByteArrayOutputStream").newObject(OutputMemoryBuffer);
}
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream-><init>(Ljava/io/OutputStream;)V
at com.github.unidbg.linux.android.dvm.AbstractJni.newObject(AbstractJni.java:755)
at com.Samples.pdd.anti_token_64.newObject(anti_token_64.java:235)
case "java/util/zip/GZIPOutputStream-><init>(Ljava/io/OutputStream;)V":{
DvmObject obj = varArg.getObjectArg(0);
OutputStream outputStream = (OutputStream) obj.getValue();
try {
return dvmClass.newObject(new GZIPOutputStream(outputStream));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream->write([B)V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:987)
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:982)
@Override
public void callVoidMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) {
switch (signature) {
case "java/util/zip/GZIPOutputStream->write([B)V":{
OutputStream outputStream = (OutputStream) dvmObject.getValue();
ByteArray array = varArg.getObjectArg(0);
try {
outputStream.write(array.getValue());
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
}
}
super.callVoidMethod(vm, dvmObject, signature, varArg);
}
java.lang.UnsupportedOperationException: java/util/zip/GZIPOutputStream->finish()V
at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethod(AbstractJni.java:987)
at com.Samples.pdd.anti_token_64.callVoidMethod(anti_token_64.java:278)
case "java/util/zip/GZIPOutputStream->finish()V":{
GZIPOutputStream gzipOutputStream = (GZIPOutputStream) dvmObject.getValue();
try {
gzipOutputStream.finish();
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
}
java.lang.UnsupportedOperationException: java/io/ByteArrayOutputStream->toByteArray()[B
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethod(AbstractJni.java:935)
at com.Samples.pdd.anti_token_64.callObjectMethod(anti_token_64.java:196)
case "java/io/ByteArrayOutputStream->toByteArray()[B": {
ByteArrayOutputStream ByteArrayArg = (ByteArrayOutputStream) dvmObject.getValue();
byte[] data = ByteArrayArg.toByteArray();
// Inspector.inspect(data, "java/io/ByteArrayOutputStream->toByteArray()");
return new ByteArray(vm, data);
}
- 补的时候注意点,这里位置换了;补完这个方法过后成功的出值了;

- 接下来看一看文件访问,借用龙哥的话:补文件访问在 Unidbg 补环境这项工作里的重要性仅次于补 JNI 环境,它有两个主要特征:
- 我的习惯是不管什么样本我都会写上文件访问的代码,在我们补jni环境的过程中其实就有一些输出,大致如下:
sana file open-->>stdin
sana file open-->>stdout
sana file open-->>stderr
sana file open-->>/dev/__properties__
sana file open-->>/proc/stat
sana file open-->>/system/build.prop
sana file open-->>/proc/version
sana file open-->>/proc/13352/status
- 这些文件比较普遍的是后面的三个,build.prop是一个属性文件,version可能就是什么版本,status主要就是看调试状态,这些其实我们好像都有进行手动返回,一般来说这里不补其实也没什么;但是这个算法入参其实就是指纹数据,所以需要注意一下,我们先不补文件访问,还记得gzip有一个write方法嘛?我们打印一下写入了什么;
case "java/util/zip/GZIPOutputStream->write([B)V": {
OutputStream outputStream = (OutputStream) dvmObject.getValue();
ByteArray array = varArg.getObjectArg(0);
Inspector.inspect(array.getValue(), "java/util/zip/GZIPOutputStream->write outputStream");
try {
outputStream.write(array.getValue());
} catch (IOException e) {
throw new IllegalStateException(e);
}
return;
}

- 勉强能看出一些东西,实际上他们大多数是来自于我们打开的/dev/properties这个文件里,我们没补但是为什么会有数据呢?

- 这是因为unidbg有内置的,做算法分析的话就用他这个就好,后续开始算法分析;
3.2 固定随机数
- 首先还是把memcpy hook好,这是我的习惯,代码不给出了,前面很多次了;
- 其次,整个运行结果还是随机的,这对于算法还原是极不友好的,一般来说,随机的来源有以下几种:
- 这里我只简单提一下,如果有兴趣我可以出一篇文章,另外,此理论来源于龙哥;
- 看一下输出日志:

- 如果你足够细心你是可以发现这个调用的,之所以我们没补是因为AbstractJni里已经为我们内置好了;

case "java/util/UUID->toString()Ljava/lang/String;": {
UUID uuid = (UUID) dvmObject.getValue();
// return new StringObject(vm, uuid.toString());
// add
return new StringObject(vm, "32DAB5DB-A036-4B83-8884-1E95A552C4B2");
}

long currentTimeMillis = 1724570121338L;
第一次:
2afh0N9kGamECtqV8zoLP+OT0XPMBL0QemNaiXKm1tJrViD1j34DN9P0Gr9vZ1BZKU3v2qNnoT0qC6eDhRy1Giyz6aRhbJ3MwdwlV2UhsS9MiQU05Fz78OPf0FPQSL8b4QA3C3+S6WDE3se+lBAoOKK2QG4/N56/VQ+ovb1sgOPrcAuroeXlcic4GoeRxtOBjxWYeHWxF7qRp4hVQbYFMnwMuD/TfAM1rjz3kP/7kcB3RiWKh6DIbQUTxXcMt6ib0o08SWt45VVCHluWIRRNcKXRFsz/E7k936mA944GxTmmYePEWBvtfdaR3pjUPJMi4v7mG8mFWM+wC3ZC8NXCDa2OTBfKReV3ECtDIk5HkDJziHoibt83DKuKgqnr0jvDFSG7DGyAPsZMV7rpGKh/rnl5A==
第二次:
2afh0N9kGamECtqV8zoLP+OT0XPMBL0QemNaiXKm1tJrViD1j34DN9P0Gr9vZ1BZKU3v2qNnoT0qC6eDhRy1Giyz6aRhbJ3MwdwlV2UhsS9MiQU05Fz78OPf0FPQSL8b4QA3C3+S6WDE3se+lBAoOKK2QG4/N56/VQ+ovb1sgOPrcAuroeXlcic4GoeRxtOBjxWYeHWxF7qRp4hVQbYFMnwMuD/TfAM1rjz3kP/7kcB3RiWKh6DIbQUTxXcMt6ib0o08SWt45VVCHluWIRRNcKXRFsz/E7k936mA944GxTmmYePEWBvtfdaR3pjUPJMi4v7mG8mFWM+wC3ZC8NXCDa2OTBfKReV3ECtDIk5HkDJziHoibt83DKuKgqnr0jvDFSG7DGyAPsZMV7rpGKh/rnl5A==
3.3 算法分析
- 到这里,我们开始首次打开ida,事实上由于工作繁忙,从开始写到现在已经三天了···
- 去看目标函数,偏移是0x104e8;

- 有轻微的混淆很正常,毕竟都是大厂了,实际上这个样本混淆没构成什么分析阻碍;
- 先分析一下密文,前面说了,打开memcpy的hook;

- 得到一个浅显的信息,2af是独立的,其实这个多次抓包也可以发现,后续分析只看后面的部分就好;
- 然后再看看其他部分;

- 这个需要读者有一些魔数相关的储备,并且前面补环境就已经聊到gzip压缩,不往这方面想实在是有点不应该;我们去搜一下gzip的魔数;

- 这一部分就应当是一个压缩过后的数据,那我们当然可以看看压缩前的数据;

- 如果你对这组数据没印象了,请往前看,他就是我们补环境的时候gzip写入的那部分数据,前面说过了,实际上是指纹相关的数据,这组数据的来源我暂时没有分析,只看算法,有后续我也会写;
- 剩下的日志大概就是明文相关的了,可以用在后续指纹的获取分析上,这里我们开始分析这个算法;
- 首先看一下返回结果的位置,就是下面这个NewStringUTF结尾的地址;



- 它在sub_11378函数里,代码量不多,并且有字符串解密的存在;

- 这种我建议dump一下内存里的so,有些会呈现解密状态或更容易被我们分析,当然你也可以不管;我这里是分析的dump后的so,具体的步骤也不提了;后续我会给到文件链接;
- 先看看这个函数出参入参吧,注意,后续所有图片均是dump下来的so分析的,参数情况是这样的:

emulator.attach().addBreakPoint(module.base + 0x11378);

- 这是参数1,它是压缩后的数据,所以它前面应该还有个压缩函数;

- 参数2是压缩数据的长度,0x12b也及就是299,对得上;



- 断下来之后发现它并不是一个地址,不信的可以去读一下;到这里属于陷入了一点僵局;
- 其实这种情况在前面某篇文章也遇到了,我不记得是哪篇了,总之unidbg在处理java对象的时候,如map等就会这样,返回的这个数值是类似于hashcode的东西,这是我的好友XiaoFeng去阅读源码后的结论,总之他需要使用unidbg的api去阅读,请看以下代码;
/**
* 打印Java对象内容
*/
protected void printJavaObject(long jobjectOffset, String label) {
DvmObject<?> javaObject = vm.getObject((int) jobjectOffset);
if (javaObject != null) {
System.out.println("[" + label + "] " + javaObject.getValue());
} else {
System.out.println("[" + label + "] null");
}
}
emulator.attach().addBreakPoint(module.base + 0x11378, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
RegisterContext context = emulator.getContext();
UnidbgPointer pointer0 = context.getPointerArg(1);
UnidbgPointer pointer3 = context.getPointerArg(3);
Inspector.inspect(pointer0.getByteArray(0, 299), "0x11378 入参1");
Inspector.inspect(pointer3.getByteArray(0, 100), "0x11378 入参2");
emulator.attach().addBreakPoint(context.getLRPointer().peer, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
UnidbgPointer pointer0 = context.getPointerArg(0);
printJavaObject(pointer0.peer, "0x11378");
return true;
}
});
return true;
}
});

- 可以发现,正确的打印出了目标,说明这确实是目标函数没有任何问题,这个方法大家可以记住,后续又出现类似情况可以往这方面想想;
- 到现在,入参出参明确,继续分析;sub_11378这个函数代码里极少,非常容易找到关键位置;

- sub_53A08的参数2 v21实际上就是传进来的明文,不用多讲了,再去hook;
emulator.attach().addBreakPoint(module.base + 0x53A08);
- 看看参数情况,x0是一串字符串,有aes的字样,可能是key;



- 那就下断点读返回值了;依旧是blr再按c,读x3的地址的值,切记不要去按mx3;此时x3早就不知道是什么东西了;


- 还有一个sub_53FE8函数,经过了它计算可能就是最终的结果,进去看看;

- 重点可能也就在这一大段字符串解密上,如果你仔细数一下一共是64位,有可能是base64的码表,并且从结果来看是有充足的理由怀疑是经过base64的;
- 我们尝试把密文解一下,或者把那段数据编码一下;

- 结果完美契合,证明它是一个标准的base64函数,这里引申一下,我为何要看dump下来的so,请看对比;


- dump的so是已经解密的状态,而原so你也可以自行解密测试,因为它是原地解密的形式;看这一条测试;

- 手动解密出来也是可以的,我们去看dump后的so;
- 选中9D110-9D148按U把他们转成未定义状态,随后这段数据会呈现为金色的状态;


- 就可以看到我们熟悉的码表了,这个函数即是base64编码函数;
- 再次返回这个函数按F5重新分析;

- 现在就是一种很好看的形式了,就说到这里,去分析前一个函数的算法;
- 前面分析到了sub_53A08这个函数;

- 实际走的是sub_53A60这个函数,继续进去看;

- sub_508AC可能是关键函数,我们前面觉得是aes,伪代码也有点那个意思,我们往这个方向猜测一下,先hook一下看看入参是不是正确的;
- x0:



- 信息量非常大,可以说我们已经解决了这个算法的90%,x0自然是明文,但是只有第一排是对的,并且后续执行多次,证明它是一个分组加密,且分组长度为16字节;x1是缓冲区,x2是编排过后的秘钥,不信的可以本地跑一下秘钥编排算法;

- 那么到这里可以宣布,这个算法是一个AES128,模式暂且未知,下面看看模式怎么分析;
- 在某对app算法分析这篇文章中我曾经提到过怎样分辨cbc和ecb,这里我们不采用修改入参的方式来验证,从原理入手,因为这里非常适合;
- cbc和ecb的区别只有一个,有iv以及没有iv,当加密模式是cbc的时候,每轮加密前当前的明文都会与iv进行异或;且iv除了首次是算法使用者提供的以外,其余的加密均是上一次分组加密的结果;从这里就可以引申出我们的验证法则了,请继续往下看;
- 我们来看第一次入参(这里指的是sub_53A60函数):



- 这里开始,入参不可控了,这是因为它是cbc模式,如果是ecb他应该是明文的部分不是吗;按照前面所说的,我们去计算一下;

- 非常正确,这里b1是明文第二排,到这里就可以证明,模式是cbc;
- 总结一下:明文是压缩后的指纹数据,算法为AES-128-CBC,key暂时不知道是否固定、iv未知;
- 实际上有经验的读者肯定已经猜出来了,iv是全0;至于理由很简单,第一轮明文没变化,而第一轮明文也要和原始iv异或,只有全0可以做到;
- 我不再去分析了,接下来看看key是否固定;退回到这个位置;

- key是xmmword_99830这一块内存的数据,那大概是固定的;

- 也是原地解密,跟前面一样的手法即可转成字符串,说句题外话,之所以要手动转是因为ida识别能力是有限的,需要我们进行一些协助;

- 至此算法分析结束,严格来说明文的构成是没有分析的,总体看一下加密流程吧;
- base64(aes(gzip(data))),整体流程是这样的;
- 回头看的时候才发现,iv在这里也是能看出来的,我分析的时候命名没注意写错了,但不影响分析;

- iv就是v4,也就是上面声明的0;至于gzip函数,在这个位置;

- 如果你想要分析指纹都是哪些字段,这里有两种方式,unidbg模拟执行的时候hook __system_property_get这个api,因为样本是使用这个api获取指纹数据的;

- 再一个,由于样本有很多字符串加密,可以进行解密再分析,这样大部分key可以看的更清楚;
4. 总结
- 算法不是很困难,总体难度应该在指纹的获取上,我也不熟悉这块,后续会学习的;
- 资料:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUVxZ0Q2QUd3YmZUSmY4TlFLV2xOUVE/cHdkPXNhbmE=
- 致谢:文章参考了Fashion哥移动安全的文章,以及xiaofeng的帮助,在此致谢;
- by:2025-12-06;