抓包 安装charles证书
安卓8
1 2 3 4 5 6 7 cd /data/misc/user/0/cacerts-added/ mount -o remount,rw / mount -o remount,rw /system chmod 777 * cp * /etc/security/cacerts/ mount -o remount,ro / mount -o remount,ro /system
避免SSL Pinning报错网络错误,也可以通过frida过掉SSLPinning
分析 jadx搜索shield,没什么实际结果,大概率不在java层,frida_hook_libart 对libart进行hook,其中hook_RegisterNatives.js 动态注册jni函数,hook_art.js 对常用Native方法hook
1 2 3 4 5 ./fs14216arm64 pyenv local 3.8.2 frida --version 14.2.16 frida -UF -l hook_art.js -o xhs.log
由于在 NewStringUTF 函数中处理了这个shield签名,添加定位地址,修改hook_art.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 if (addrNewStringUTF != null) { Interceptor.attach(addrNewStringUTF, { onEnter: function (args) { if (args[1] != null) { var module = Process.findModuleByAddress(this.returnAddress); if (module != null && module.name.indexOf(so_name) == 0) { var string = Memory.readCString(args[1]); // console.log("[NewStringUTF] bytes:" + string, DebugSymbol.fromAddress(this.returnAddress)); //////xhs//////// if (string.length > 50) { console.log("[NewStringUTF] bytes:" + string); console.log("xhs---------", Thread.backtrace(this.context, Backtracer.FUZZY) .map(DebugSymbol.fromAddress).join("\n")) } //////xhs//////// } } }, onLeave: function (retval) { } }); }
IDA打开libshield.so,G跳转到0x93fa8地址,看到该位置处于函数sub_939D8
F5反汇编,X查看sub_939D8 方法的调用处
可知在Java层的intercept 方法,参数**(Lokhttp3/Interceptor”,0x24,”Chain;J)Lokhttp3/Response;**
搜索intercept(
开始hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 干掉 Android SSL Pinning var array_list = Java.use("java.util.ArrayList"); var ApiClient = Java.use('com.android.org.conscrypt.TrustManagerImpl'); ApiClient.checkTrustedRecursive.implementation = function(a1, a2, a3, a4, a5, a6) { var k = array_list.$new(); return k; } // shield var shieldCls = Java.use("com.xingin.shield.http.XhsHttpInterceptor"); shieldCls.intercept.overload('okhttp3.Interceptor$Chain', 'long').implementation = function(chain,j){ var result = this.intercept(chain,j); var request = chain.request(); console.log(request.toString()); console.log(result.toString()); return result; } // okhttp var OkHttpClient = Java.use("okhttp3.OkHttpClient"); OkHttpClient.newCall.implementation = function (request) { var result = this.newCall(request); console.log(request.toString()); var stack = threadinstance.currentThread().getStackTrace(); console.log("http >>> Full call stack:" + Where(stack)); return result; };
这也就意味着http的请求和shield的生成都在so层完成。
Unidbg 打开libshield.so,搜索java,没有函数说明都是动态注册进入JNI_OnLoad
1 2 3 4 jint JNI_OnLoad(JavaVM *vm, void *reserved) { return sub_1027C(vm); }
搭建框架 调用callJNI_OnLoad
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class XhsShield extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; private Headers headers; private Request request; public static String shield; private String commonParams; public XhsShield(){ emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xhs").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分 final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口 memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析 vm = emulator.createDalvikVM(new File("小红书v6.73.0.apk")); vm.setVerbose(true); vm.setJni(this); DalvikModule dm = vm.loadLibrary(new File("libshield.so"), true); module = dm.getModule(); System.out.println("call JNIOnLoad"); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { XhsShield test = new XhsShield(); } }
initializeNative 对jni调用java方法的一些类进行初始化操作
进入sub_1027C函数
发现函数有很多,在IDA View-A中alt+t搜索上文中的com.xingin.shield.http.XhsHttpInterceptor
的initializeNative
方法
.text 程序代码段内存区域 .data 已初始化的全局数据、全局常量段内存区域 .rodata 资源数据段,#define定义的常量 .bss 程序中未初始化的全局变量的内存区域
直接看.data数据块中的汇编,initializeNative地址0x94288+1,intercept地址sub_939D8+1,initialize地址sub_937B0+1
g跳转到0x94288+1,查看initializeNative函数,n重命名
在XhsHttpInterceptor类中函数执行顺序,initializeNative>initialize>intercept
1 2 3 4 5 6 7 8 9 10 public void callinitializeNative(){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。 module.callFunction(emulator, 0x94288+1, list.toArray()); } public static void main(String[] args) { XhsShield test = new XhsShield(); test.callinitializeNative(); }
1 2 3 4 5 6 7 8 9 @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset;":{ return vm.resolveClass("java/nio/charset/Charset").newObject(Charset.defaultCharset()); } } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); }
通过objection内存搜索,是PackageInfo的一个实例变量,该类获取AndroidManifest.xml文件的信息
1 2 3 frida-ps -Ua objection -g com.xingin.xhs explore -P ~/.objection/plugins plugin wallbreaker classdump --fullname android.content.pm.PackageInfo
也可以通过jnitrace获取versionCode,jnitrace -l libshield.so -m spawn com.xingin.xhs --ignore-vm > xhs.log
jnitrace 出现app闪退或者黑屏解决方案
更新版本pip install jnitrace==v3.0.8 ,frida==12.8.0
修改jnitrace-engine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Interceptor.attach(dlopenRef, { onEnter:function(args){ this.path = args[0].readCString(); },onLeave:function(retval){ if (this.path !== null) { if (checkLibrary(this.path)) { trackedLibs.set(retval.toString(), true); } else { libBlacklist.set(retval.toString(), true); } } } });
python -m jnitrace.jnitrace -l libshield.so -b none com.xingin.xhs
1 2 3 4 5 6 7 8 9 @Override public int getIntField(BaseVM vm, DvmObject<?> dvmObject, String signature) { switch (signature){ case "android/content/pm/PackageInfo->versionCode:I":{ return 6730157; } } return super.getIntField(vm, dvmObject, signature); }
获取静态变量,frida一步到位,console.log(Java.use("com.xingin.shield.http.ContextHolder").sDeviceId.value)
1 2 3 4 5 6 7 8 9 @Override public DvmObject<?> getStaticObjectField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature){ case "com/xingin/shield/http/ContextHolder->sDeviceId:Ljava/lang/String;":{ return new StringObject(vm, "145b5374-b973-38d1-8299-eb98f9d950ce"); } } return super.getStaticObjectField(vm, dvmClass, signature); }
同理frida一步到位,console.log(Java.use("com.xingin.shield.http.ContextHolder").sAppId.value)
,或者jnitrace
1 2 3 4 5 6 7 8 9 @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); }
jnitrace或者frida一步到位
1 2 3 4 5 6 7 8 @Override public boolean getStaticBooleanField(BaseVM vm, DvmClass dvmClass, String signature) { switch (signature) { case "com/xingin/shield/http/ContextHolder->sExperiment:Z": return true; } return super.getStaticBooleanField(vm, dvmClass, signature); }
initialize 1 2 3 4 5 6 7 public XhsHttpInterceptor(String str, a<Request> aVar) { this.cPtr = initialize(str); this.predicate = aVar; } public static XhsHttpInterceptor newInstance(String str, a<Request> aVar) { return new XhsHttpInterceptor(str, aVar); }
在XhsHttpInterceptor的newInstance查找用例找到b函数,直接传入的字符串为”main”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 第二个初始化函数 public native long initialize(String str); public long callinitialize(){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。 list.add(vm.addLocalObject(new StringObject(vm, "main"))); Number number = module.callFunction(emulator, 0x937B0+1, list.toArray())[0]; return number.longValue(); } public static void main(String[] args) { XhsShield test = new XhsShield(); test.callinitializeNative(); long ptr = test.callinitialize(); }
1 2 3 4 5 6 7 8 9 @Override public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case "java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset;": { return vm.resolveClass("java/nio/charset/Charset").newObject(Charset.defaultCharset()); } } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); }
Context.getSharedPreferences(String name,int mode)**获取一个SharedPreferences实例,name文件名称,不需要加后缀.xml,一般这个文件存储在 /data/data//shared_prefs**下,mode指读写权限,从jnitrace中查看jstring为0x11,jint为0,断点调试jstring就是”s”,DvmObject对象是Unidbg抽象出来的一个JNI交互的对象,给他一个这样的对象都不会报错,这个值后面要用到所以不能newObject(null)。
1 2 3 4 5 6 7 8 9 @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;": return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObjectArg(0)); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // ???为什么main返回空,main_hmac返回s.xml的内容 @Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature) { // android.content.SharedPreferences android.content.Context.getSharedPreferences(java.lang.String, int) case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;": return vm.resolveClass("android/content/SharedPreferences").newObject(vaList.getObjectArg(0)); case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": { if(((StringObject) dvmObject.getValue()).getValue().equals("s")){ System.out.println("getString :"+vaList.getObjectArg(0).getValue()); if (vaList.getObjectArg(0).getValue().equals("main")) { return new StringObject(vm, ""); } if(vaList.getObjectArg(0).getValue().equals("main_hmac")){ return new StringObject(vm, "hmLDH4NVbddu/qNZWZj80kqBVKWrexc+1w3zCF0FCNQ03x3a9o9RsHcP2e1LwpK0gPbC4nHeU9dU2d0hyOhPElbIBZlNMjj9HRCCNMmb0uRWywu1tq3IvjogrlLosRl5"); } } } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }
intercept 该函数中完成了请求与响应,所以必然会出现shield的生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 目标函数 public Response intercept(Interceptor.Chain chain) public void callintercept(long ptr){ List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数,实例方法是jobject,静态方法是jclazz,直接填0,一般用不到。 DvmObject<?> chain = vm.resolveClass("okhttp3/Interceptor$Chain").newObject(null); list.add(vm.addLocalObject(chain)); list.add(ptr); module.callFunction(emulator, 0x939D8 + 1, list.toArray()); } // jadx中分析调用initialize得到的结果作为参数传入intercept函数中 public static void main(String[] args) { XhsShield test = new XhsShield(); test.callinitializeNative(); long ptr = test.callinitialize(); test.callintercept(ptr); }
1 2 3 4 5 6 7 8 // ??? request哪里来 request = new Request.Builder() .url("https://edith.xiaohongshu.com/api/sns/v6/homefeed?oid=homefeed_recommend&cursor_score=&geo=eyJsYXRpdHVkZSI6MC4wMDAwMDAsImxvbmdpdHVkZSI6MC4wMDAwMDB9%0A&trace_id=3eb910d5-a037-3076-92d3-739e2d02e3e0¬e_index=0&refresh_type=1&client_volume=0.00&preview_ad=&loaded_ad=%7B%22ads_id_list%22%3A%5B%5D%7D&personalization=1&pin_note_id=&pin_note_source=&unread_begin_note_id=620cc5700000000021036137&unread_end_note_id=6203bff30000000021039a28&unread_note_count=6") .addHeader("xy-common-params", "fid=164554128510caadf802049b8d38a1ca58cbf294b8d2&device_fingerprint=20220222224814dd7db8705f0befea0b24b13732c811a70164bde096a4c7f6&device_fingerprint1=20220222224814dd7db8705f0befea0b24b13732c811a70164bde096a4c7f6&launch_id=1645664833&tz=Asia%2FShanghai&channel=YingYongBao&versionName=6.73.0&deviceId=145b5374-b973-38d1-8299-eb98f9d950ce&platform=android&sid=session.1645766627567507884448&identifier_flag=0&t=1645784087&project_id=ECFAAF&build=6730157&x_trace_page_current=explore_feed&lang=zh-Hans&app_id=ECFAAF01&uis=light") .build(); case "okhttp3/Interceptor$Chain->request()Lokhttp3/Request;": { return vm.resolveClass("okhttp3/Request").newObject(request); }
1 2 3 4 5 // ??? 为什么url为request的url case "okhttp3/Request->url()Lokhttp3/HttpUrl;": { Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/HttpUrl").newObject(request.url()); }
1 2 3 4 5 case "com/xingin/shield/http/Base64Helper->decode(Ljava/lang/String;)[B":{ String input = (String) vaList.getObjectArg(0).getValue(); byte[] result = Base64.decodeBase64(input); return new ByteArray(vm, result); }
1 2 3 4 case "okhttp3/HttpUrl->encodedPath()Ljava/lang/String;": { HttpUrl httpUrl = (HttpUrl) dvmObject.getValue(); return new StringObject(vm, httpUrl.encodedPath()); }
1 2 3 4 case "okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;": { HttpUrl httpUrl = (HttpUrl) dvmObject.getValue(); return new StringObject(vm, httpUrl.encodedQuery()); }
1 2 3 4 case "okhttp3/Request->body()Lokhttp3/RequestBody;": { Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/RequestBody").newObject(request.body()); }
1 2 3 4 case "okhttp3/Request->headers()Lokhttp3/Headers;": { Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Headers").newObject(request.headers()); }
1 2 3 4 5 6 7 8 @Override public DvmObject<?> newObjectV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature){ case "okio/Buffer-><init>()V": return dvmClass.newObject(new Buffer()); } return super.newObjectV(vm, dvmClass, signature, vaList); }
1 2 3 4 5 6 case "okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;": { System.out.println("write to my buffer:"+vaList.getObjectArg(0).getValue()); Buffer buffer = (Buffer) dvmObject.getValue(); buffer.writeString(vaList.getObjectArg(0).getValue().toString(), (Charset) vaList.getObjectArg(1).getValue()); return dvmObject; }
1 2 3 case "okhttp3/Headers->size()I": Headers headers = (Headers) dvmObject.getValue(); return headers.size();
1 2 3 4 case "okhttp3/Headers->name(I)Ljava/lang/String;": { Headers headers = (Headers) dvmObject.getValue(); return new StringObject(vm, headers.name(vaList.getIntArg(0))); }
1 2 3 4 case "okhttp3/Headers->value(I)Ljava/lang/String;": { Headers headers = (Headers) dvmObject.getValue(); return new StringObject(vm, headers.value(vaList.getIntArg(0))); }
1 2 3 4 5 6 7 8 9 10 11 12 case "okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V": { BufferedSink bufferedSink = (BufferedSink) vaList.getObjectArg(0).getValue(); RequestBody requestBody = (RequestBody) dvmObject.getValue(); if(requestBody != null){ try { requestBody.writeTo(bufferedSink); } catch (IOException e) { e.printStackTrace(); } } return; }
1 2 3 4 case "okio/Buffer->clone()Lokio/Buffer;": { Buffer buffer = (Buffer) dvmObject.getValue(); return vm.resolveClass("okio/Buffer").newObject(buffer.clone()); }
1 2 3 case "okio/Buffer->read([B)I": Buffer buffer = (Buffer) dvmObject.getValue(); return buffer.read((byte[]) vaList.getObjectArg(0).getValue());
1 2 3 4 case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;": { Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request$Builder").newObject(request.newBuilder()); }
1 2 3 4 5 6 7 8 case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;": { Request.Builder builder = (Request.Builder) dvmObject.getValue(); builder.header(vaList.getObjectArg(0).getValue().toString(), vaList.getObjectArg(1).getValue().toString()); if("shield".equals(vaList.getObjectArg(0).getValue().toString())){ shield = vaList.getObjectArg(1).getValue().toString(); } return dvmObject; }
1 2 3 4 case "okhttp3/Request$Builder->build()Lokhttp3/Request;": { Request.Builder builder = (Request.Builder) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request").newObject(builder.build()); }
1 2 3 case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;": { return vm.resolveClass("okhttp3/Response").newObject(null); }
1 2 case "okhttp3/Response->code()I": return 200;
经过一番getStaticObjectField补环境,终于拿到了shield,保存在静态变量中,引入unidbg-server
1 2 3 4 5 6 7 8 9 10 11 12 13 @RequestMapping(value = "xhs", method = {RequestMethod.GET, RequestMethod.POST}) public String xhsShield() { String url = "https://edith.xiaohongshu.com/api/sns/v6/homefeed?oid=homefeed_recommend&cursor_score=&geo=eyJsYXRpdHVkZSI6MC4wMDAwMDAsImxvbmdpdHVkZSI6MC4wMDAwMDB9%0A&trace_id=3eb910d5-a037-3076-92d3-739e2d02e3e0¬e_index=0&refresh_type=1&client_volume=0.00&preview_ad=&loaded_ad=%7B%22ads_id_list%22%3A%5B%5D%7D&personalization=1&pin_note_id=&pin_note_source=&unread_begin_note_id=620cc5700000000021036137&unread_end_note_id=6203bff30000000021039a28&unread_note_count=6"; String commonParams = "fid=164554128510caadf802049b8d38a1ca58cbf294b8d2&device_fingerprint=20220222224814dd7db8705f0befea0b24b13732c811a70164bde096a4c7f6&device_fingerprint1=20220222224814dd7db8705f0befea0b24b13732c811a70164bde096a4c7f6&launch_id=1645664833&tz=Asia%2FShanghai&channel=YingYongBao&versionName=6.73.0&deviceId=145b5374-b973-38d1-8299-eb98f9d950ce&platform=android&sid=session.1645766627567507884448&identifier_flag=0&t=1645784087&project_id=ECFAAF&build=6730157&x_trace_page_current=explore_feed&lang=zh-Hans&app_id=ECFAAF01&uis=light"; String platformInfo = "platform=android&build=6730157&deviceId=145b5374-b973-38d1-8299-eb98f9d950ce"; XhsShield xhs = new XhsShield(); String shield = xhs.getShield(); Map<String, String> headMap = new HashMap<>(); headMap.put("xy-common-params",commonParams); headMap.put("shield",shield); headMap.put("xy-platform-info",platformInfo); return httpGet(url,null, headMap); }
objection hook构造函数的方法
1.9.6版本以前/root/.pyenv/versions/3.8.0/lib/python3.8/site-packages/objection/agent.js的9211行
1.9.6版本以后/root/.pyenv/versions/3.8.0/lib/python3.8/site-packages/objection/agent.js的20238行