某红薯app shield参数逆向日记

该文章已生成可运行项目,

app版本:8.32.0.5
逆向时间:2025年8月6日23:30:18

1.1 抓包测试

打开Charles进行抓包,打开app刷新数据,发现抓包有点异常(能抓其他app的包),Charles里显示unknown,但是并没有ssl pinning检查的明显特征,还是使用ssl pinning hook脚本跑一下看能否过这个抓包检测。
在这里插入图片描述
使用spwan模式注入DroidSSLUnpinning脚本:
在这里插入图片描述

1.2 frida检测

发现frida明显检测特征Process terminated,那先过frida检测,准备hook一下加载了哪些so文件,并且注意在加载到某个so的时候退出frida的。hook 加载so的frida如下:

function hook_patch() {
    var patch = Module.findExportByName("libc.so", "pthread_create");
    console.log("[pth_create]", patch);
    Interceptor.attach(patch, {
        onEnter: function (args) {
            var module = Process.findModuleByAddress(args[2]);
            if (module != null) {
                console.log("开启线程-->", module.name, args[2].sub(module.base));
            }
        },
        onLeave: function (retval) {}
    });
}

在这里插入图片描述
发现在加载libmsaoaidsec.so 文件时frida退出,猜测可能是在这个so文件里做了对frida的检测,那么先尝试将这个so文件的这个几个线程置空处理,看能不能过frida的检测。(将上面出现的3个线程加进来置空)
在这里插入图片描述
在这里插入图片描述
发现过了frida检测并且还能正常抓包,神奇,过frida检测还顺带把抓包给解决了,直奔我们的主题头部请求参数shield。
在这里插入图片描述

1.3 shield参数逆向

使用MT管理器查看app并没有加固,直接将apk文件拖入jadx反编译分析,先直接搜shield关键字:

在这里插入图片描述
这2个方法点进去没什么有用的信息,那接着分析,由于参数是在headers头部里的,我们先尝试hook okhttp3库的Header(安卓逆向需积累自己的常用代码笔记,用的时候直接copy):
在这里插入图片描述

在这里插入图片描述

hook头部没有得到shield关键参数信息,继续猜测这个头部参数是在拦截器里加进去的,再次hook okhttp3拦截器:

//定位拦截器
function hookInter(){
    var Builder = Java.use('okhttp3.OkHttpClient$Builder');
    Builder.addInterceptor.implementation = function (a) {
        console.log("拦截器-->",JSON.stringify(a))
        var res = this.addInterceptor(a);
        return res;
    }
}

在这里插入图片描述
根据拦截器打印的信息,先在jadx里查找com.xingin.shield.http.XhsHttpInterceptor拦截器,写frida脚本,看看此方法的头部信息以及返回结果信息等:
在这里插入图片描述
我们发现此处hook拦截器的位置只打印的请求参数,并未出现头部shield关键信息,继续看后一个拦截器,看是否加密位置在下一个拦截器:
下一个拦截器在上上个截图里是拦截器i96.a1,继续hook:
在这里插入图片描述
仔细分析可知shield参数是入参的时候已经生成,这里需要想一下了,在第一次hook的拦截器里还没生产,第二次的入参里居然已经有了这个参数,回过看看看第一个拦截器有何特殊之处:
在这里插入图片描述
非常高明,不仔细分析看响应头都不知道shield参数在哪里生成的(这里还有一种猜测,尼玛,不会这里只是生成shield参数和生成响应数据的地方吧,因为没有看到结果返回,这里有点奇奇怪怪的,总之先把shield参数生成回头再来看这里了!!!)
整体来看下com.xingin.shield.http类里的拦截器XhsHttpInterceptor方法。
在这里插入图片描述
首先XhsHttpInterceptor有个构造方法,我们进去发现最终是private static native void initializeNative();
方法,
在这里插入图片描述
接下来是public native long initialize(String str);:
在这里插入图片描述
最后回到public Response intercept(Interceptor.Chain chain) throws IOException {xxx 方法里:
在这里插入图片描述
所以我们需要补齐这3个native方法。这个类没发现加载so的地方,所有我们需要hook 动态so文件的名称:

1.4 hook 动态so文件

hook动态so文件有2种方式,第一种,hook全部的so,在他们加载的时候打印导出方法,第二种,此处是加载com.xingin.shield.http类的XhsHttpInterceptor方法,所以只需要hook当加载到这个类的时候加载了哪些so文件以及导出方法,看他们是否2对得上,hook脚本:

function hook_RegisterNatives(addrRegisterNatives) {

    if (addrRegisterNatives != null) {
        Interceptor.attach(addrRegisterNatives, {
            onEnter: function (args) {
                console.log("[RegisterNatives] method_count:", args[3]);
                let java_class = args[1];
                let class_name = Java.vm.tryGetEnv().getClassName(java_class);
                //console.log(class_name);
                let taget_class ="com.xingin.shield.http.XhsHttpInterceptor";
                if (class_name === taget_class) {

                    console.log("java-类名:", class_name)
                    let methods_ptr = ptr(args[2]);
                    let method_count = parseInt(args[3]);
                    for (let i = 0; i < method_count; i++) {
                        let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
                        let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
                        let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));

                        let name = Memory.readCString(name_ptr);
                        let sig = Memory.readCString(sig_ptr);
                        let symbol = DebugSymbol.fromAddress(fnPtr_ptr)

                        console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, " fnOffset:", symbol, " callee:", DebugSymbol.fromAddress(this.returnAddress),"module_name:",symbol.name);
                    }
                }
            }
        });

    }
}

hook结果,native方法以及so文件的名称和对应方法的内存地址等都打印出来了,接着下一步开始构建unidbg环境了。
在这里插入图片描述

1.5 构建unidbg环境

package com.likeme.XHS;

import com.github.unidbg.AndroidEmulator;
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.jni.ProxyClassFactory;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class XHS extends AbstractJni {
    private final AndroidEmulator emulator;

    private final Module module;
    private final VM vm;

    public XHS() {
        // 创建模拟器实例,要模拟32位或者64位,在这里区分
        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xingin.xhs").build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机
        vm = emulator.createDalvikVM(new File("utils/XHS/小红书8.32.apk"));
        vm.setJni(this);
        // 加载SO文件
        DalvikModule dm = vm.loadLibrary("xyass", false);
        // 手动执行JNI_OnLoad函数  动态注册需要 静态不用
        dm.callJNI_OnLoad(emulator);
        // 基于module可以访问so中的成员
        module = dm.getModule();
    }

    //启动入口
    public static void main(String[] args) {
        XHS xhs = new XHS();
    }

}

因为我们需要补3个native方法,这3个方法的实现都在 libxyass.so文件中,开始在unidbg里构建这3个方法的实现:

2.1 补initializeNative方法及其相关环境

在java里这个是静态方法 private static native void initializeNative();,在unidbg里,我们这样补:

    public void initializeNative(){
        // 找到Java类
        DvmClass dvmClass = vm.resolveClass("com/xingin/shield/http/XhsHttpInterceptor");
        // 调用静态 JNI 方法
        String method = "initializeNative()V";
        dvmClass.callStaticJniMethod(emulator, method);
    }

在主方法里调用:

    public static void main(String[] args) {
        XHS xhs = new XHS();
        xhs.initializeNative();
    }

运行报错,提示缺少com/xingin/shield/http/ContextHolder->sLogger:Lcom/xingin/shield/http/ShieldLogger;
在这里插入图片描述
这个是记录日志相关的方法,我们这里直接使用 vm.resolveClass("com/xingin/shield/http/ShieldLogger") 解析 ShieldLogger 类,并创建一个空的 ShieldLogger 对象(newObject(null))作为返回值,因为这个字段看字面意思很明确是日志相关的,不是其他重要的字段,我们可以置空(当然如果app很高明,在这个字段的后续逻辑里做了一些判断,如果是空啥的,不是正常初始化的就走其他逻辑也有可能,这里的可能性极小,所以置空完全可以)。

    @Override
    public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) {
        switch (signature) {
            case "com/xingin/shield/http/ContextHolder->sLogger:Lcom/xingin/shield/http/ShieldLogger;": {
                // 构造 ShieldLogger 对象
                DvmClass loggerClass = vm.resolveClass("com/xingin/shield/http/ShieldLogger");
                return loggerClass.newObject(null); // 返回空的 ShieldLogger 对象
            }
        }
        return super.getStaticObjectField(vm, dvmClass, signature);
    }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->nativeInitializeStart()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007);
这种无返回值的最简单了,重写callVoidMethodV方法,补齐这个确实的方法即可:
继续运行报:java.lang.UnsupportedOperationException: java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset; at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
在java里Charset的一般用法是:

import java.nio.charset.Charset;
Charset charset = Charset.defaultCharset(); // 获取默认字符集
System.out.println(charset); // 可能打印 "UTF-8"

那我们在unidbg里模拟他的行为:

    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature){
            case "java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset;":{
                // 模拟 defaultCharset 返回 UTF-8
                DvmClass charsetClass = vm.resolveClass("java/nio/charset/Charset");
                return charsetClass.newObject(StandardCharsets.UTF_8);
            }
        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }

其中DvmClass charsetClass = vm.resolveClass("java/nio/charset/Charset");这行代码告诉 unidbg 找到 java.nio.charset.Charset 类,DvmClass 是 unidbg 中表示 Java 类的对象,vm.resolveClass 确保 unidbg 知道我们要处理 Charset 类。
针对charsetClass.newObject(StandardCharsets.UTF_8):

  • newObject 是 unidbg 的方法,用于创建一个模拟的 Java 对象(DvmObject 类型)
  • StandardCharsets.UTF_8 是 Java 的 Charset 对象,表示 UTF-8 字符集
  • newObject(StandardCharsets.UTF_8) 创建一个 DvmObject,表示 Charset 类的一个实例,并将其“值”设为 StandardCharsets.UTF_8

这行代码让 unidbg 返回一个模拟的 Charset 对象,假装是 Charset.defaultCharset() 的返回值(这里解释清楚一点,让初学者知其然知其所以然,后续比较重要的方法也尽量说明详细一点!)
继续运行,报:java.lang.UnsupportedOperationException: com/xingin/shield/http/ContextHolder->sDeviceId:Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
在这里插入图片描述
这里看是设备的id,我们需要hook一下这个id,写frida脚本:

function hookid(){
    let ContextHolder = Java.use("com.xingin.shield.http.ContextHolder");
    var sDeviceId = ContextHolder.sDeviceId.value;
    console.log("sDeviceId:",sDeviceId);
}

在这里插入图片描述
多次hook设备id是不变的,我们直接返回这个id即可:

            case "com/xingin/shield/http/ContextHolder->sDeviceId:Ljava/lang/String;":{
                return new StringObject(vm,"dcbc836b-fba2-302b-aadf-1c148374c46e");
            }

再次运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ContextHolder->sAppId:I at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticIntField(AbstractJni.java:136)
,静态字段的sAppId,hook一下这个值:

function hookid(){
    let ContextHolder = Java.use("com.xingin.shield.http.ContextHolder");
    var sDeviceId = ContextHolder.sDeviceId.value;
    console.log("sDeviceId:",sDeviceId);
    var sAppId = ContextHolder.sAppId.value;
    console.log("sAppId:",sAppId)
}

在这里插入图片描述
多次hook值没变。在unidbg里写死。

    @Override
    public int getStaticIntField(BaseVM vm, DvmClass dvmClass, String signature) {
        switch (signature){
            case "com/xingin/shield/http/ContextHolder->sAppId:I":{
                return -319115519;
            }
        }
        return super.getStaticIntField(vm, dvmClass, signature);
    }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->nativeInitializeEnd()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
这种无返回值的补齐方法就行:

            case "com/xingin/shield/http/ShieldLogger->nativeInitializeEnd()V":{
                return;
            }

再次运行,不报错了,我们第一个方法补环境正式完成,开始第二个native方法的补环境!

3.1 补initialize方法及其相关环境

在java环境里,此方法是public native long initialize(String str);,调用的时候会有一个入参,我们先hook一下这个方法的入参,而这里可以hook构造方法或者hook initialize方法本身都可以,hook脚本如下:

function hook_XhsHttpInterceptor(){
    let XhsHttpInterceptor = Java.use("com.xingin.shield.http.XhsHttpInterceptor");
    XhsHttpInterceptor["$init"].implementation = function (str, bVar) {
        console.log(`XhsHttpInterceptor.$init is called: str=${str}, bVar=${bVar}`);
        this["$init"](str, bVar);
    };
    console.log(888)
}
function hook_initialize(){
    let XhsHttpInterceptor = Java.use("com.xingin.shield.http.XhsHttpInterceptor");
    XhsHttpInterceptor["initialize"].implementation = function (str) {
        console.log(`XhsHttpInterceptor.initialize is called: str=${str}`);
        let result = this["initialize"](str);
        console.log(`XhsHttpInterceptor.initialize result=${result}`);
        return result;
    };
    console.log(8888)
}

在这里插入图片描述
每次hook的结果都是"main"字符串,那这里可以写死,unidbg补法如下:

    public Long initialize(String str){
        // 找到Java类
        DvmClass dvmClass = vm.resolveClass("com/xingin/shield/http/XhsHttpInterceptor");
        // 调用静态 JNI 方法
        String method = "initialize(Ljava/lang/String;)J";
        return dvmClass.newObject(null).callJniMethodLong(emulator,method,str);
    }

说明:initialize方法是实例方法,dvmClass.newObject(null)是创建 XhsHttpInterceptor 实例。
继续运行,报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->initializeStart()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
基本遇到void可以直接补(后续遇到void直接补上方法就行,就不写上来了):

            case "com/xingin/shield/http/ShieldLogger->initializeStart()V":{
                return;
            }

继续运行报java.lang.UnsupportedOperationException: android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
我们先在反编译apk文件里搜getSharedPreferences这个方法,看在代码里它是怎么使用的:
在这里插入图片描述
问下大模型这个方法的作用是什么:
在这里插入图片描述
大模型的解释很关键,这个是访问应用程序配置文件的,目录通常在/data/data/<package>/shared_prefs/<name>.xml里。
先在unidbg补齐这个方法,打印下传参,看访问了哪个文件:

    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature){
            case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;":{
                String name = (String) vaList.getObjectArg(0).getValue();
                int mode = vaList.getIntArg(1);
                System.out.println("getSharedPreferences called: name=" + name + ", mode=" + mode);
                DvmClass prefsClass = vm.resolveClass("android/content/SharedPreferences");
                return prefsClass.newObject(null); // 返回空的 SharedPreferences 对象
            }
        }
        return super.callObjectMethodV(vm, dvmObject, signature, vaList);
    }

在这里插入图片描述
从打印的信息来看,是读取了一个s.xml配置文件,补齐后报了android/content/SharedPreferences类里执行getString方法的操作,大模型的解释这个SharedPreferences的getString方法使用如下:
在这里插入图片描述
结合前面的读取配置文件以及这个方法的使用,我们推测这里是从/data/data/packgexxxxx目录里读取s.xml配置文件,再使用getString方法拿值,继续补齐getString方法,先看看能不能直接拿到值:
在这里插入图片描述
结合前面获取配置文件以及从日志打印信息得知这里,此处是访问了s.xml配置文件,并且从配置文件里取了2个key main和main_hmac的值,我们打开手机目录,看看这个配置文件里这2个key对应的value是什么:
在这里插入图片描述
在这里插入图片描述
从截图里得知,main是空,main_hmac是有值的,那么我们根据实际情况返回即可,在unidbg里补齐正确的返回值:

            case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
                String key = (String) vaList.getObjectArg(0).getValue();
                String defValue = (String) vaList.getObjectArg(1).getValue();
                System.out.println("getString called: key=" + key + ", defValue=" + defValue);
                String trueValue = "";
                if (key.equals("main_hmac")){
                    trueValue = "KgFBeCGEsu8ys8ZMZN换上兄弟们自己的值W17IhjTw1ESlYjyO7wr6NJNgswe7oWbrIZysM";
                }
                return new StringObject(vm,trueValue);
            }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/Base64Helper->decode(Ljava/lang/String;)[B at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
搜下Base64Helper在反编译代码里是怎么用的:
在这里插入图片描述
可以看到,decode是解密一个字符串,返回byte数组,我们使用大模型模拟这个方法即可:

    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature){
            case "com/xingin/shield/http/Base64Helper->decode(Ljava/lang/String;)[B":{
                String input = vaList.getObjectArg(0).getValue().toString();
                if (input == null) {
                    return new ByteArray(vm, new byte[0]);
                }
                try {
                    byte[] decodedBytes = Base64.getDecoder().decode(input);
                    return new ByteArray(vm, decodedBytes);
                } catch (IllegalArgumentException e) {
                    System.out.println("Invalid Base64 input: " + input);
                    return new ByteArray(vm, new byte[0]); // 返回空数组处理错误
                }
            }
        }
        return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
    }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->initializedEnd()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
,直接补空方法:

            case "com/xingin/shield/http/ShieldLogger->initializedEnd()V":{
                return;
            }

继续运行,不报错了,至此第二个initialize native方法完成!
在这里插入图片描述

4.1 补 intercept方法

在这里插入图片描述
首先,在java代码里,intercept第二个入参是initialize方法的返回值,同时他是一个实例方法,我们先补齐intercept方法的初始化:

    public DvmObject<?> intercept(DvmObject<?> chain, long j17) {
        DvmClass dvmClass = vm.resolveClass("com/xingin/shield/http/XhsHttpInterceptor");
        DvmObject<?> interceptor = dvmClass.newObject(null);
        String method = "intercept(Lokhttp3/Interceptor$Chain;J)Lokhttp3/Response;";
        return interceptor.callJniMethodObject(emulator, method, chain, j17);
    }

在主函数里:

    public static void main(String[] args) {
        XHS xhs = new XHS();
        xhs.initializeNative();
        Long initialize_value = xhs.initialize("main");
        System.out.println(initialize_value);
        //模拟intercept方法chain的传参
        DvmObject<?> chain = xhs.vm.resolveClass("okhttp3/Interceptor$Chain").newObject(null);
        DvmObject<?> response = xhs.intercept(chain, initialize_value);
        System.out.println("intercept result: " + response);
    }
}

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->buildSourceStart()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
直接补空方法返回即可。
继续运行报java.lang.UnsupportedOperationException: okhttp3/Interceptor$Chain->request()Lokhttp3/Request; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
在这里插入图片描述
结合java代码okhttp3/Interceptor$Chain->request()方法的使用,此处补齐方法返回即可。

            case "okhttp3/Interceptor$Chain->request()Lokhttp3/Request;":{
                DvmClass dvmClass = vm.resolveClass("okhttp3/Request");
                return dvmClass.newObject(null);
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Request->url()Lokhttp3/HttpUrl; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
此处是获取okhttp3/Request类的url()方法,返回是okhttp3/HttpUrl类型,我们可以问下大模型,让他给出okhttp3/HttpUrl的url方法是怎么使用的:
在这里插入图片描述
可以看到这里我们需要获取一个url,那么肯定不能随便填,需要填我们的目标接口的url,那么我们直接hookRequest方法的url方法,看他代码里传的标准url是什么,hook脚本如下:

function hook_okthttp_url(){
    let Builder = Java.use("okhttp3.Request$Builder");
    // 1. Hook url(String url) 方法(字符串类型 URL)
    Builder.url.overload("java.lang.String").implementation = function (urlStr) {
        console.log(`[URL 字符串] 设置的 URL: ${urlStr}`);
        // 调用原始方法并返回结果
        let result = this.url(urlStr);
        return result;
    };
    // 2. Hook url(HttpUrl url) 方法(HttpUrl 对象类型 URL)
    Builder.url.overload("okhttp3.HttpUrl").implementation = function (httpUrl) {
        // 从 HttpUrl 对象中获取完整 URL 字符串
        let urlStr = httpUrl.toString();
        console.log(`[HttpUrl 对象] 设置的 URL: ${urlStr}`);
        // 调用原始方法并返回结果
        let result = this.url(httpUrl);
        return result;
    };
}

在这里插入图片描述
现在我们知道他原始代码里要穿的url了,我们开始自己构建这个Request对象将这个url设置进去,因为要用到okhttp3这个类,我们需要在unidbg的pom文件里引入这个依赖:

        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>3.4.2</version>
        </dependency>

在unidbg的主方法里引入私有对象变量private Request request;,在构造方法里传入url,并构建Request对象:
在这里插入图片描述
主方法里传入这个url
在这里插入图片描述
最后补齐okhttp3/Request->url()Lokhttp3/HttpUrl;环境:

            case "okhttp3/Request->url()Lokhttp3/HttpUrl;":{
                DvmClass dvmClass = vm.resolveClass("okhttp3/HttpUrl");
                System.out.println(request.url());
                return dvmClass.newObject(request.url());
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/HttpUrl->encodedPath()Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
这个熟悉一点编程的其实能直接猜测出来,encodedPath是对请求路径做一个编码,不熟悉也没关系,有很多方式,首先我们可以在jadx里搜encodedPath,能直接搜到他的2个重载方法:
在这里插入图片描述
在这里插入图片描述
这里根据我们的报错信息,是第一个无参的返回值类型是String的方法,继按照这个条件搜代码在哪里引用的encodedPath:
在这里插入图片描述
可以看到在java代码里他是httpurl里直接调用的这个方法,我们前面已经设置了url,这里是对url的路径做编码,上面的截图里接着对query做编码,其实可以一起补了,先补encodedPath:

            case "okhttp3/HttpUrl->encodedPath()Ljava/lang/String;":{
                System.out.println("encodedPath called on HttpUrl");
                HttpUrl httpUrl = (HttpUrl) dvmObject.getValue();
                System.out.println("httpUrl:" + httpUrl);
                return new StringObject(vm, httpUrl.encodedPath());
            }

可能有人这里会有疑问,为啥这里用(HttpUrl) dvmObject.getValue()能去获取httpurl的值,在unidbg里,dvmObject.getValue()是获取上个步骤设置的值,前面我们构建Request请求对象的时候设置了url的值,他的返回类型是HttpUrl ,当代码走到这个分支的时候,可以直接用dvmObject.getValue()去获取,然后用自己的encodedPath方法编码,再直接返回即可。
继续运行报java.lang.UnsupportedOperationException: okhttp3/HttpUrl->encodedQuery()Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
果然是报encodedQuery的环境缺失,跟encodedPath方法一样补就行:

            case "okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;":{
                HttpUrl httpUrl = (HttpUrl) dvmObject.getValue();
                System.out.println(httpUrl.encodedQuery());
                return new StringObject(vm,httpUrl.encodedQuery());
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Request->body()Lokhttp3/RequestBody; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)

根据前面构建的request对象,直接返回即可

            case "okhttp3/Request->body()Lokhttp3/RequestBody;":{
                return vm.resolveClass("okhttp3/RequestBody").newObject(request.body());
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Request->headers()Lokhttp3/Headers; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)

获取请求头,直接补齐即可(这里留意一下,我们构建的request对象里并没有设置请求头):

            case "okhttp3/Request->headers()Lokhttp3/Headers;":{
                return vm.resolveClass("okhttp3/Headers").newObject(request.headers());
            }

继续运行报java.lang.UnsupportedOperationException: okio/Buffer-><init>()V at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:803)
我们需要问下大模型,okio/Buffer的基本使用:
在这里插入图片描述
这里的报错是没有okio/Buffer方法,也就是new Buffer(); 这一步补齐这个方法即可:

    @Override
    public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
        switch (signature){
            case "okio/Buffer-><init>()V":{
                return vm.resolveClass("okio/Buffer").newObject(new Buffer());
            }
        }
        return super.newObjectV(vm, dvmClass, signature, vaList);
    }

继续运行报java.lang.UnsupportedOperationException: okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
继续看下writeString方法的使用介绍:
在这里插入图片描述
writeString方法是返回他本身也就是Buffer自身,可以链式多次调用,一开始我的补法如下:

			case "okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;":{
                String inputString = (String) vaList.getObjectArg(0).getValue();
                Charset charset = (Charset) vaList.getObjectArg(1).getValue();
                System.out.println("writeString called on Buffer: string=" + inputString + ", charset=" + charset);
                vm.resolveClass("okio/Buffer").newObject(new Buffer().writeString(inputString,charset));
            }

相信刚开始写unidbg代码的同学会经常犯这种错:直接执行真实的java代码,创建 new Buffer(),这是 Okio 库的真实 Java 类(okio.Buffer),unidbg 是一个模拟器,不运行完整的 Android 环境或 Okio 的 Java 字节码。
那么这里我们比较合理的模拟writeString方法如下:

            case "okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;": {
//                String inputString = (String) vaList.getObjectArg(0).getValue();
//                DvmObject<?> charset = vaList.getObjectArg(1);
//                System.out.println("writeString called on Buffer: string=" + inputString + ", charset=" + charset);
//                // 模拟写入字符串,追加到 Buffer 的 value 中
//                HashMap<String, Object> bufferData = (HashMap<String, Object>) dvmObject.getValue();
//                if (bufferData == null) {
//                    bufferData = new HashMap<>();
//                    bufferData.put("content", new StringBuilder());
//                    dvmObject.setValue(bufferData);
//                }
//                StringBuilder content = (StringBuilder) bufferData.get("content");
//                content.append(inputString); // 追加字符串
//                bufferData.put("charset", charset.getValue());
//                return dvmObject; // 返回 Buffer 自身
                Buffer buffer = (Buffer)dvmObject.getValue();
                buffer.writeString(vaList.getObjectArg(0).getValue().toString(),(Charset)vaList.getObjectArg(1).getValue());
                return dvmObject;
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Headers->size()I at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
这里是检测请求的头部参数有几个,根据前面构建的request对象,直接返回即可;

    @Override
    public int callIntMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
        switch (signature){
            case "okhttp3/Headers->size()I":{
                System.out.println("headers size:" + request.headers().size());
                return request.headers().size();
            }
        }
        return super.callIntMethodV(vm, dvmObject, signature, vaList);
    }

继续运行报java.lang.UnsupportedOperationException: okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)

 case "okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V":{
     BufferedSink buffers =  (BufferedSink) vaList.getObjectArg(0).getValue();
     RequestBody reqbody = (RequestBody) dvmObject.getValue();
     System.out.println("buffers" + reqbody);
     if(reqbody !=null){
         try {
             reqbody.writeTo(buffers);
         }catch (IOException e){
             e.printStackTrace();
         }

     }else {
         return;
     }

 }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->buildSourceEnd()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
日志相关的直接补空:

            case "com/xingin/shield/http/ShieldLogger->buildSourceEnd()V":{
                return;
            }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->calculateStart()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
补齐方法即可。
继续运行报java.lang.UnsupportedOperationException: okio/Buffer->clone()Lokio/Buffer; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
没有什么特殊的,获取原本的Buffer对象返回即可:

            case "okio/Buffer->clone()Lokio/Buffer;":{
                Buffer buffer = (Buffer) dvmObject.getValue();
                return vm.resolveClass("okio/Buffer").newObject(buffer.clone());
            }

继续运行报java.lang.UnsupportedOperationException: okio/Buffer->read([B)I at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
同上一样补就行:

            case "okio/Buffer->read([B)I":{
                Buffer buffer = (Buffer) dvmObject.getValue();
                return buffer.read((byte[]) vaList.getObjectArg(0).getValue());
            }

继续运行报java.lang.UnsupportedOperationException: com/xingin/shield/http/ShieldLogger->calculateEnd()V at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
日志相关的直接补空方法。
继续运行报java.lang.UnsupportedOperationException: okhttp3/Request->newBuilder()Lokhttp3/Request$Builder; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
前面初始化了request对象,常规补法即可:

            case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;":{
                //Request request = (Request) dvmObject.getValue();
                return vm.resolveClass("okhttp3/Request$Builder").newObject(request.newBuilder());
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
常规补法:

            case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;":{
                Request.Builder builder = (Request.Builder) dvmObject.getValue();
                String value1 = vaList.getObjectArg(0).getValue().toString();
                String value2 = vaList.getObjectArg(1).getValue().toString();
                System.out.println("请求头:" + value1 + "---->" + value2 );
                return vm.resolveClass("okhttp3/Request$Builder").newObject(builder.header(value1,value2));
            }

再次运行我们惊奇的发现 shield参数居然已经出来了。
在这里插入图片描述
先继续把报错的环境补齐:

            case "okhttp3/Request$Builder->build()Lokhttp3/Request;":{
                Request.Builder builder = (Request.Builder)dvmObject.getValue();
                return vm.resolveClass("okhttp3/Request").newObject(builder.build());
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
常规补法:

            case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;":{
                Interceptor.Chain chain = (Interceptor.Chain)dvmObject.getValue();
                Request request1 = (Request) vaList.getObjectArg(0).getValue();
                try {
                    if (chain != null){
                        return vm.resolveClass("okhttp3/Response").newObject(chain.proceed(request1));
                    }
                    else {
                        return vm.resolveClass("okhttp3/Response").newObject(null);
                    }
                    

                }catch (IOException e){
                    e.printStackTrace();
                    // 返回一个空的 Response,避免 NPE
                    Response fakeResp = new Response.Builder()
                            .request(request1)
                            .protocol(Protocol.HTTP_1_1)
                            .code(500)
                            .message("unidbg simulated error")
                            .build();
                    return vm.resolveClass("okhttp3/Response").newObject(fakeResp);
                }
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Response->code()I at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
这里是取响应状态码,我们直接返回200(返回原始的也行):

            case "okhttp3/Response->code()I":{
                Response response = (Response) dvmObject.getValue();
                //return response.code();
                return 200;
            }

再次运行没有报错了,回过头来处理一下前面暂时可能还有问题的地方,首先构建request请求的时候,我们只传了url,没有hook addHeader,我们使用hook脚本hook一下本身他自己加的请求头有哪些:

function hook_okthttp_url(){
    let Builder = Java.use("okhttp3.Request$Builder");
    // 1. Hook url(String url) 方法(字符串类型 URL)
    Builder.url.overload("java.lang.String").implementation = function (urlStr) {
        console.log(`[URL 字符串] 设置的 URL: ${urlStr}`);
        // 调用原始方法并返回结果
        let result = this.url(urlStr);
        return result;
    };
    // 2. Hook url(HttpUrl url) 方法(HttpUrl 对象类型 URL)
    Builder.url.overload("okhttp3.HttpUrl").implementation = function (httpUrl) {
        // 从 HttpUrl 对象中获取完整 URL 字符串
        let urlStr = httpUrl.toString();
        console.log(`[HttpUrl 对象] 设置的 URL: ${urlStr}`);
        // 调用原始方法并返回结果
        let result = this.url(httpUrl);
        return result;
    };
    // Hook addHeader(String, String)
    Builder.addHeader.implementation = function (name, value) {
        console.log("[Request.Builder.addHeader] called, name =", name, ", value =", value);
        return this.addHeader(name, value); // 调原方法
    };
}

在这里插入图片描述
从目标url开始,出现的请求头我们都补上:

  • xy-common-params:没啥特殊的,多次对比只有一个时间戳会变,另外x_trace_page_current会变,都暂时写死
  • User-Agent:写死
  • X-XHS-Ext-Failover:实际接口没有这个参数,可不写
  • X-XHS-Ext-DNSIsolateTag :实际接口没有这个参数,可不写
  • X-XHS-Ext-CustomIPList:显示信息是ip相关的,实际接口没有这个参数,可不写

构造方法改造如下:

    public XHS(String url,String xy_common_params) {
        // 创建模拟器实例,要模拟32位或者64位,在这里区分
        emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.xingin.xhs").build();
        // 模拟器的内存操作接口
        final Memory memory = emulator.getMemory();
        // 设置系统类库解析
        memory.setLibraryResolver(new AndroidResolver(23));
        // 创建Android虚拟机
        vm = emulator.createDalvikVM(new File("utils/XHS/小红书8.32.apk"));
        vm.setJni(this);
        // 加载SO文件
        DalvikModule dm = vm.loadLibrary("xyass", false);
        // 手动执行JNI_OnLoad函数  动态注册需要 静态不用
        dm.callJNI_OnLoad(emulator);
        // 基于module可以访问so中的成员
        module = dm.getModule();
        String userAgent = "Dalvik/2.1.0 (Linux; U; Android 10; Pixel 4 Build/QQ3A.200805.001) Resolution/1080*2280 Version/8.32.0 Build/8320689 Device/(Google;Pixel 4) discover/8.32.0 NetType/WiFi";
        request = new Request.Builder()
                .url(url) // 请求 URL
                .addHeader("xy-common-params", xy_common_params)
                .addHeader("User-Agent", userAgent)
                .get() // GET 请求(默认,可省略)
                .build();
    }

在主方法传参:
在这里插入图片描述
继续运行报java.lang.UnsupportedOperationException: okhttp3/Headers->name(I)Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
这是因为补了请求头,现在在取请求头的name,常规补上缺失的环境:

            case "okhttp3/Headers->name(I)Ljava/lang/String;":{
                Headers headers = (Headers)dvmObject.getValue();
                int  value = vaList.getIntArg(0);
                System.out.println("headers name is :" + headers.name(value));
                return new StringObject(vm,headers.name(value));
            }

继续运行报java.lang.UnsupportedOperationException: okhttp3/Headers->value(I)Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
这个是取请求头的value,补法跟请求那么一样:

            case "okhttp3/Headers->value(I)Ljava/lang/String;":{
                Headers headers = (Headers)dvmObject.getValue();
                int  value = vaList.getIntArg(0);
                System.out.println("headers value is :" + headers.value(value));
                return new StringObject(vm,headers.value(value));
            }

再次运行,环境完成。终于大功告成,中间2次写的差不多了忘了保存关机了,这该死的记性!!!
最后Charles抓包测试生成的shield参数是否和页面一致:
在这里插入图片描述
在这里插入图片描述
经过Python测试后,请求头部的这几个参数是必带的,我们在我们unidbg环境的请求头里需要加上xy-direction参数,即如下:
在这里插入图片描述
后续根据情况再改是否需要动态传参。其他参数保持一致,运行unidbg代码:
在这里插入图片描述
最后和抓包的shield参数保持一致,完美落幕!

安卓逆向的第一篇,写的详细一点,文中有什么问题欢迎指出!

本文章已经生成可运行项目
评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值