吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1596|回复: 9
上一主题 下一主题
收起左侧

[Android 原创] 某应用AntiToken算法分析

  [复制链接]
跳转到指定楼层
楼主
xiayutianz 发表于 2025-12-6 16:24 回帖奖励
本帖最后由 xiayutianz 于 2025-12-6 16:27 编辑

anti-token算法分析

1. 概述

  • 样本是pdd,来源见最后总结,目标算法是anti-token;
  • 版本是v7.80.0,没有加固;
  • 如果你要跟着分析算法,那么请划到最后获取样本,豌豆荚下载的只有32位的so;
  • 其中unidbg补环境的部分我着重说了,基本上是手把手带着补了,不熟悉的可以学习一下;

2. 抓包&定位

  • 我这里直接socksdroid转发就可以抓了,如果有问题网上的文章也挺多的,或者联系我;
  • 这里找的是搜索接口,有我们的目标,其实好像很多个接口都有anti-token;

image-20251203143428069

  • 关于定位,这里选择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 这个位置;

image-20251203144926629

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

image-20251203145351115

  • 具体看哪个很明显了,进到这个位置;

image-20251203145925755

  • 再往里稍微跟一下就好;

image-20251203150013953

  • 这个info2就是目标函数,它的参数是context和一个long类型的,懂得都知道应该是时间戳;
  • 后续开始算法分析,定位到这里即算成功;

3. unidbg辅助算法分析

  • 既然是native,先把基本的信息找出来,动态注册直接hook就好;

  • 输出是这样的:

符号信息: 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);
}
  • 那两个输出是多余的,直接返回就行;

image-20251203154742135

  • 第一步完成了,下面就开始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)
  • 这个方法我也不知道是什么意思,去查一下;

image-20251203173359753

  • 返回值是int,那返回1吧;
@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;
}
  • 返回1应该也没事,继续看下一个;
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遇到类似的情况怎么做的;

image-20251204104617716

  • 这里稍微提一下,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);
}
  • 这么做是有隐患的,看看输出就知道了;

image-20251204110635568

  • 我找到了一篇文章,这位大佬的方式是这样的:
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());
}
  • 以后遇到这种环境都是这样来,某个对象的某个方法,没有好的方式就取出对象然后调方法,或者你直接返回空字符串也行,只要能够跑通,毕竟我们的目的是算法还原,继续执行;
  • 在看报错之前我们先看看取到了什么;

image-20251204111844521

  • 可以看出来不是一个正常的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);
}
  • 继续执行能看到一个gzip压缩相关的内容;
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);
}
  • 补的时候注意点,这里位置换了;补完这个方法过后成功的出值了;

image-20251204175713580

  • 接下来看一看文件访问,借用龙哥的话:补文件访问在 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;
}
  • 看看输出;

image-20251204181707857

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

image-20251205100951787

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

image-20251205150332655

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

image-20251205150440722

  • 这里是随机点1,我改成一个固定字符串了;
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");
}
  • 这里提一个小技巧,我们在修改这种源码性质的代码时,最好在旁边加个标记,如我这里的 // add,后期想要改回来直接全局搜索就好,非常容易定位到;

  • 再一个比较容易出现的点就是时间戳;UnixSyscallHandler这个文件下有一些系统调用的实现,准确说应该是库函数;

  • 我们改 gettimeofday64 这个函数;

image-20251205151022468

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;

image-20251205151727846

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

image-20251205155424659

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

image-20251206103545707

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

image-20251206103701118

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

image-20251206103932391

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

image-20251206104729357

image-20251206104756158

  • 去ida看看这个地址;

image-20251206104817952

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

image-20251206105115843

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

image-20251206110727639

emulator.attach().addBreakPoint(module.base + 0x11378);
  • a1不用看是jnienv,其余的参数是这样的:

image-20251206110824595

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

image-20251206110854281

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

image-20251206111125261

  • 参数3看起来是前缀,现在看返回值;

image-20251206111212890

  • 这是最开始看NewStringUTF定位的时候的位置,也就是说返回值应该看x0,除非ida有特别提示你返回值在哪个寄存器,否则就是x0,这属于调用约定;

  • 在控制台下blr断点,这么久了这个应该不用多说了;

image-20251206111343779

  • 断下来之后发现它并不是一个地址,不信的可以去读一下;到这里属于陷入了一点僵局;
  • 其实这种情况在前面某篇文章也遇到了,我不记得是哪篇了,总之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;
    }
});
  • 参数这里不看了,直接看目标;

image-20251206112350788

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

image-20251206112719385

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

image-20251206112913495

  • x1则是压缩后的数据,可以看作是明文;

image-20251206113021051

  • x2是长度,x3则是缓冲区;

image-20251206113040336

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

image-20251206113108924

  • 这串数据是我们没见过的,再看看伪代码;

image-20251206113549364

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

image-20251206140823335

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

image-20251206141456892

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

image-20251206141821019

  • 原so:

image-20251206141837145

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

image-20251206142037064

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

image-20251206142345116

  • 再选中这些数据按A或者右键选A转成字符串;

image-20251206142427238

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

image-20251206142757492

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

image-20251206144512923

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

image-20251206144542821

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

image-20251206145708304

  • x1:

image-20251206145729404

  • x2:

image-20251206145844910

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

image-20251206150125303

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

image-20251206150638885

  • 第一次出参(blr断点读缓冲区):

image-20251206150706452

  • 这是最终结果的前16字节,再看第二次入参:

image-20251206150827780

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

image-20251206151133907

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

image-20251206151613625

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

image-20251206151642295

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

image-20251206151755557

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

image-20251206152626479

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

image-20251206152848740

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

image-20251206155328072

  • 再一个,由于样本有很多字符串加密,可以进行解密再分析,这样大部分key可以看的更清楚;

4. 总结

  • 算法不是很困难,总体难度应该在指纹的获取上,我也不熟悉这块,后续会学习的;
  • 资料:aHR0cHM6Ly9wYW4uYmFpZHUuY29tL3MvMUVxZ0Q2QUd3YmZUSmY4TlFLV2xOUVE/cHdkPXNhbmE=
  • 致谢:文章参考了Fashion哥移动安全的文章,以及xiaofeng的帮助,在此致谢;
  • by:2025-12-06;

免费评分

参与人数 13吾爱币 +14 热心值 +10 收起 理由
llfly + 1 谢谢@Thanks!
vLove0 + 1 + 1 谢谢@Thanks!
hexiaomo + 1 + 1 热心回复!
fengbolee + 1 + 1 热心回复!
Jokerboxs + 1 谢谢@Thanks!
InfiniteBoy + 1 + 1 用心讨论,共获提升!
hjw01 + 1 我很赞同!
WinterIsC0ming + 1 热心回复!
mengxinb + 3 + 1 大佬 佩服!!!
helian147 + 1 + 1 热心回复!
唐小样儿 + 1 + 1 我很赞同!
buluo533 + 1 + 1 用心讨论,共获提升!
ForGot_227 + 1 + 1 虽然后面好多裂图。用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

沙发
 楼主| xiayutianz 发表于 2025-12-6 23:52 |楼主
我说一句 图都是在我自己服务器的 一般裂了可以刷新 基本上都是加载的问题 实在加载不出来可以移步我博客 或者点击裂图也可以看
3#
fryant 发表于 2025-12-7 11:53
4#
chitose0173 发表于 2025-12-7 19:21
5#
imxz 发表于 2025-12-9 10:59
收藏加保存本地了, 感谢大佬分享教程
6#
fhs123 发表于 2025-12-9 15:59
不错啊,,,,,,大佬啊
7#
hjw01 发表于 2025-12-10 09:17
火钳刘明,学习了
8#
不苦小和尚 发表于 2025-12-10 10:25
学习一下,技术贴
9#
xiayea 发表于 2025-12-10 11:18
感谢楼主的分享,受益良多
10#
vLove0 发表于 2025-12-10 23:28
牛x的大佬,学习一下
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-12-12 09:14

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表