SO逆向之小红书shield

抓包

安装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

image-20220224214105777

分析

jadx搜索shield,没什么实际结果,大概率不在java层,frida_hook_libart对libart进行hook,其中hook_RegisterNatives.js动态注册jni函数,hook_art.js对常用Native方法hook

image-20220224215953511

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) { }
});

}

image-20220224214326592

IDA打开libshield.so,G跳转到0x93fa8地址,看到该位置处于函数sub_939D8

image-20220224221418951

F5反汇编,X查看sub_939D8方法的调用处

image-20220224221553187

可知在Java层的intercept方法,参数**(Lokhttp3/Interceptor”,0x24,”Chain;J)Lokhttp3/Response;**

image-20220224221725263

搜索intercept(

image-20220224222142075

image-20220224222423752

开始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;
};

image-20220224222851531

这也就意味着http的请求和shield的生成都在so层完成。

Unidbg

打开libshield.so,搜索java,没有函数说明都是动态注册进入JNI_OnLoad

image-20220226104042957

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();
}
}

image-20220226110431367

initializeNative

对jni调用java方法的一些类进行初始化操作

进入sub_1027C函数

image-20220226104430970

发现函数有很多,在IDA View-A中alt+t搜索上文中的com.xingin.shield.http.XhsHttpInterceptorinitializeNative方法

image-20220226105749125

image-20220226105807738

.text 程序代码段内存区域
.data 已初始化的全局数据、全局常量段内存区域
.rodata 资源数据段,#define定义的常量
.bss 程序中未初始化的全局变量的内存区域

直接看.data数据块中的汇编,initializeNative地址0x94288+1,intercept地址sub_939D8+1,initialize地址sub_937B0+1

image-20220226111556828

g跳转到0x94288+1,查看initializeNative函数,n重命名

image-20220226150010437

在XhsHttpInterceptor类中函数执行顺序,initializeNative>initialize>intercept

image-20220226112824885

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();
}

image-20220226113553005

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);
}

image-20220226113820208

通过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

image-20220226115809498

image-20220226120639081

也可以通过jnitrace获取versionCode,jnitrace -l libshield.so -m spawn com.xingin.xhs --ignore-vm > xhs.log

jnitrace 出现app闪退或者黑屏解决方案

  1. 更新版本pip install jnitrace==v3.0.8 ,frida==12.8.0

  2. 修改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);
    }
    }
    }
    });

    image-20220228190245812

    python -m jnitrace.jnitrace -l libshield.so -b none com.xingin.xhs

image-20220226120720392

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);
}

image-20220226120944572

获取静态变量,frida一步到位,console.log(Java.use("com.xingin.shield.http.ContextHolder").sDeviceId.value)

image-20220226121315768

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);
}

image-20220226121408631

同理frida一步到位,console.log(Java.use("com.xingin.shield.http.ContextHolder").sAppId.value),或者jnitrace

image-20220226121616078

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);
}

image-20220226121711912

jnitrace或者frida一步到位

image-20220226121744091

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);
}

image-20220226121902850

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”

image-20220226135417525

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();
}

image-20220226161022449

image-20220226161050578

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);
}

image-20220226141612842

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)。

image-20220226155632734

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);
}

image-20220226195613330

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);
}

image-20220226202103259

intercept

该函数中完成了请求与响应,所以必然会出现shield的生成。

image-20220226140519884

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);
}

image-20220226200725718

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&note_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);
}

image-20220226201514098

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());
}

image-20220226202124581

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);
}

image-20220226202629238

1
2
3
4
case "okhttp3/HttpUrl->encodedPath()Ljava/lang/String;": {
HttpUrl httpUrl = (HttpUrl) dvmObject.getValue();
return new StringObject(vm, httpUrl.encodedPath());
}

image-20220226202740592

1
2
3
4
case "okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;": {
HttpUrl httpUrl = (HttpUrl) dvmObject.getValue();
return new StringObject(vm, httpUrl.encodedQuery());
}

image-20220226202824235

1
2
3
4
case "okhttp3/Request->body()Lokhttp3/RequestBody;": {
Request request = (Request) dvmObject.getValue();
return vm.resolveClass("okhttp3/RequestBody").newObject(request.body());
}

image-20220226202933455

1
2
3
4
case "okhttp3/Request->headers()Lokhttp3/Headers;": {
Request request = (Request) dvmObject.getValue();
return vm.resolveClass("okhttp3/Headers").newObject(request.headers());
}

image-20220226203025309

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);
}

image-20220226203137691

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;
}

image-20220226203345411

1
2
3
case "okhttp3/Headers->size()I":
Headers headers = (Headers) dvmObject.getValue();
return headers.size();

image-20220226203445936

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)));
}

image-20220226203601107

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)));
}

image-20220226203637038

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;
}

image-20220226203830675

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

image-20220226203908813

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

image-20220226204002902

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());
}

image-20220226204058455

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;
}

image-20220226204204774

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());
}

image-20220226204316786

1
2
3
case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;": {
return vm.resolveClass("okhttp3/Response").newObject(null);
}

image-20220226204342442

1
2
case "okhttp3/Response->code()I":
return 200;

image-20220226204517704

Unidbg-server

经过一番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&note_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);
}

image-20220226204843128

objection hook构造函数的方法

1.9.6版本以前/root/.pyenv/versions/3.8.0/lib/python3.8/site-packages/objection/agent.js的9211行

image-20220226173411982

1.9.6版本以后/root/.pyenv/versions/3.8.0/lib/python3.8/site-packages/objection/agent.js的20238行

image-20220226173319019

文章作者: J
文章链接: http://onejane.github.io/2022/02/24/SO逆向之小红书shield/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 万物皆可逆向
支付宝打赏
微信打赏