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参数保持一致,完美落幕!
安卓逆向的第一篇,写的详细一点,文中有什么问题欢迎指出!
1775





