某红薯shield参数逆向分析

样本地址:aHR0cHM6Ly93d3cud2FuZG91amlhLmNvbS9hcHBzLzYyMzM3MzkvaGlzdG9yeV92OTI0MDgxMQ==

初步分析

是否加固

先看一下有没有壳,大厂一般是不会进行加壳的,但还是看一下
image.png
也确实发现没加固

抓包分析

直接抓的登录 login 的包,发现参数还挺多的
image.png
其中 x-mini-gid, x-mini-s1, x-mini-sig, x-mini-mua, shield 等应该就是加密参数了
本篇文章主要分析的也就是 shield 这个加密参数的生成

加密位置定位

直接 hook NewStringUTF 就能够定位到相关的位置,还是需要把 libmsaoaidsec.so 的检测给过掉

 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
28
29
30
31
32
33
34
var symbols = Module.enumerateSymbolsSync("libart.so");
var addrNewStringUTF = null;
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols[i];
    if (symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) {
        addrNewStringUTF = symbol.address;
        console.log("NewStringUTF is at ", symbol.address, symbol.name);
        break
    }
}
if (addrNewStringUTF != null) {
    Interceptor.attach(addrNewStringUTF, {
        onEnter: function (args) {
            var c_string = args[1];
            if (c_string.isNull()) {
                return;
            }
            var dataString = null;
            try {
                dataString = c_string.readCString();
            } catch (e) {
                return;
            }
            if (dataString == null || dataString == "") {
                return;
            }
            if (dataString.includes("XYAA")) {
                console.log("[XYAA] " + dataString);
                console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
                console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            }
        }
    });
}

然后就能够得到下面的堆栈信息了
image.png
到 jadx 中跳到这个堆栈中进行分析
网上有很多这样的分析了,直接说结论吧:
他会按照这样的顺序执行 Native.initializeNative()=>Native.initialize(this.token)=>Native.intercept(chain, this.cPtr)

unidbg 模拟执行

还是先搭个架子

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
package com.xhs;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.file.FileResult;
import com.github.unidbg.file.IOResolver;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.BaseVM;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VaList;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.hook.hookzz.HookZz;
import com.github.unidbg.hook.hookzz.IHookZz;
import com.github.unidbg.hook.hookzz.HookEntryInfo;
import com.github.unidbg.arm.context.RegisterContext;

import java.io.File;

public class Shield extends AbstractJni implements IOResolver {

    // ========== 每次新项目只改这里 ==========

    // 模拟器位数:so在armeabi-v7a目录下填false,arm64-v8a目录下填true
    private static final boolean IS_64BIT         = false;

    // app包名(随意,一般填真实包名)
    private static final String  PROCESS_NAME     = "com.xingin.xhs";

    // apk路径,不需要apk时填null(填null时LOAD_BY_NAME必须为false)
    private static final String  APK_PATH         = "apks/xhs/xhs.apk";

    // so加载方式:
    //   false = 传文件路径(用SO_PATH),不依赖apk,最常用
    //   true  = 传库名(用SO_NAME),unidbg自动去apk内查找,必须提供APK_PATH
    private static final boolean LOAD_BY_NAME     = true;
    private static final String  SO_PATH          = ""; // LOAD_BY_NAME=false时生效
    private static final String  SO_NAME          = "xyass";                // LOAD_BY_NAME=true时生效,不带lib前缀和.so后缀


    // 是否打印JNI调用细节,调试时改true
    private static final boolean VERBOSE          = true;


    // ========================================

    public static AndroidEmulator emulator;
    public static Memory memory;
    public static VM vm;
    public static Module module;

    // 1. 构造方法 —— 初始化模拟器
    public Shield() {

        // 1. 创建模拟器(32/64位由IS_64BIT控制)
        emulator = IS_64BIT
                ? AndroidEmulatorBuilder.for64Bit().setProcessName(PROCESS_NAME).build()
                : AndroidEmulatorBuilder.for32Bit().setProcessName(PROCESS_NAME).build();

        // 2. 获取内存对象
        memory = emulator.getMemory();

        // 3. 设置安卓SDK版本(只支持19、23)
        memory.setLibraryResolver(new AndroidResolver(23));

        // 4. 创建虚拟机
        vm = APK_PATH != null
                ? emulator.createDalvikVM(new File(APK_PATH))
                : emulator.createDalvikVM();
        vm.setJni(this);
        vm.setVerbose(VERBOSE);

        // 5. 加载so文件(两种方式由LOAD_BY_NAME控制)
        DalvikModule dm = LOAD_BY_NAME
                ? vm.loadLibrary(SO_NAME, false)
                : vm.loadLibrary(new File(SO_PATH), false);

        // 6. 动态注册才需要执行JNI_OnLoad
        dm.callJNI_OnLoad(emulator);

        // 7. 获取module对象(后续拿基址、偏移等)
        module = dm.getModule();

    }





    // 5. main方法 —— 右键直接运行
    public static void main(String[] args) {
        Shield shield = new Shield();
    }

    @Override
    public FileResult resolve(Emulator emulator, String pathname, int oflags) {
        System.out.println("pathname:" + pathname);
        return null;
    }
}

接着开始调用吧,首先调用 initializeNative 这个方法

1
2
3
public void initializeNative(){
	Native.callStaticJniMethodObject(emulator,"initializeNative()V");
}

然后报错,开始补环境

1
2
3
4
java.lang.UnsupportedOperationException: com/xingin/shield/http/ContextHolder->writeLog(I)V
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:708)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticVoidMethodV(AbstractJni.java:703)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticVoidMethodV(DvmMethod.java:204)

这个看起来跟日志相关,直接返回即可

1
2
3
4
5
6
7
8
9
@Override
public void callStaticVoidMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) {
	switch (signature){
		case "com/xingin/shield/http/ContextHolder->writeLog(I)V":{
			return;
		}
	}
	super.callStaticVoidMethodV(vm, dvmClass, signature, vaList);
}

接着,报错

1
2
3
4
5
java.lang.UnsupportedOperationException: java/nio/charset/Charset->defaultCharset()Ljava/nio/charset/Charset;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:504)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
	at com.github.unidbg.linux.android.dvm.DalvikVM$113.handle(DalvikVM.java:1816)

直接实例化这个类,将方法的调用传进去

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 dvmClass.newObject(Charset.defaultCharset());
	   }
   }
   return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
}

接着报下面的这个错误

1
2
3
4
java.lang.UnsupportedOperationException: com/xingin/shield/http/ContextHolder->sDeviceId:Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:103)
	at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticObjectField(AbstractJni.java:53)
	at com.github.unidbg.linux.android.dvm.DvmField.getStaticObjectField(DvmField.java:106)

这个可以去 jadx 中找到这个类下面的这个属性,直接 frida hook 即可

1
2
3
4
5
Java.perform(function () {
    var ContextHolder = Java.use("com.xingin.shield.http.ContextHolder");
    var sDeviceId = ContextHolder.sDeviceId.value;
    console.log("sDeviceId: " + sDeviceId);
});

得到的结果如下: sDeviceId: e1dcca27-a4a5-39da-a2b2-a8329886cb4a,直接去补上
接着,报错

1
2
3
4
java.lang.UnsupportedOperationException: com/xingin/shield/http/ContextHolder->sAppId:I
	at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticIntField(AbstractJni.java:136)
	at com.github.unidbg.linux.android.dvm.AbstractJni.getStaticIntField(AbstractJni.java:128)
	at com.github.unidbg.linux.android.dvm.DvmField.getStaticIntField(DvmField.java:121)

这个,同理进行 hook 可以得到 -319115519,直接补上
接着,就没有报错了,那就接着调用下一个函数,第二个函数中间会传一个字符串进去,不知道传的是什么,还是可以进行 hook 一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java.perform(function () {
    var Native = Java.use("com.xingin.shield.http.Native");

    Native["initializeNative"].implementation = function () {
        console.log(`Native.initializeNative is called`);
        this["initializeNative"]();
    };
    Native["initialize"].implementation = function (str) {
        console.log(`Native.initialize is called: str=${str}`);
        let result = this["initialize"](str);
        console.log(`Native.initialize result=${result}`);
        return result;
    };
});

hook 结果如下:

1
2
3
4
[Pixel 3 XL::com.xingin.xhs ]-> Native.initialize is called: str=main
Native.initialize result=3624499008
Native.initialize is called: str=main
Native.initialize result=3624496928

可以看到,传进去的是"main"这个字符串,直接调用

1
2
3
4
public void initialize(){
	cPtr = Native.callStaticJniMethodLong(emulator,"initialize(Ljava/lang/String;)J","main");
	System.out.println(cPtr);
}

发现环境缺失了,继续

1
2
3
4
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)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

这儿是在获取 SharedPreferences 的持久化存储的数据

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).getValue().toString());
		}
	}
	return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

接着报错

1
2
3
4
5
java.lang.UnsupportedOperationException: android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:136)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

这里是在获取,读取了哪一个 xml 文件,通过打印知道读取的是 s 这个 xml 文件下的 main_hmac 这个键的值
直接使用 adb 进行获取
image.png
直接补吧,值都出来了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{
	String xml_val = dvmObject.getValue().toString();
	System.out.println(xml_val);
	String key = vaList.getObjectArg(0).getValue().toString();
	System.out.println(key);
	if (key.equals("main_hmac")){
		return new StringObject(vm,"2VQ6BnkUxADf9jksuKER+rbqJJY6PIBjsMZqYUjXbrMHFF4BaKzHFHGyEWwuBlQW3j8klUI5ZctIAokjwtQlsOih+FWRnUQ17kd6C8oar90n8PQ+Bg+iWsShB88kb9+K");
	}

}

接着,又报错了

1
2
3
4
5
6
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)
	at com.xhs.Shield.callStaticObjectMethodV(Shield.java:106)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callStaticObjectMethodV(AbstractJni.java:438)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callStaticObjectMethodV(DvmMethod.java:59)
	at com.github.unidbg.linux.android.dvm.DalvikVM$113.handle(DalvikVM.java:1816)

这个直接补就行了

1
2
3
4
case "com/xingin/shield/http/Base64Helper->decode(Ljava/lang/String;)[B":{
	String input = vaList.getObjectArg(0).getValue().toString();
	return new ByteArray(vm, Base64.decodeBase64(input));
}

现在就已经能够得到 cPtr 的值了,继续调用

1
2
3
4
public void intercept(){
	DvmObject<?> chain = vm.resolveClass("okhttp3/Interceptor$Chain").newObject(null);
Native.callStaticJniMethodObject(emulator,"intercept(Lokhttp3/Interceptor$Chain;J)Lokhttp3/Response;",chain,cPtr).getValue().toString();
}

发现报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Interceptor$Chain->request()Lokhttp3/Request;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:152)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

在这里首先需要构造一个请求,request
image.png
接着,直接补就行了

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

接着,报错,补环境

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Request->url()Lokhttp3/HttpUrl;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:175)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

这儿,直接补就行了

1
2
3
case "okhttp3/Request->url()Lokhttp3/HttpUrl;":{
	return vm.resolveClass("okhttp3/HttpUrl").newObject(request.url());
}

然后,继续报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/HttpUrl->encodedPath()Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:183)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

直接补环境

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
5
java.lang.UnsupportedOperationException: okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:183)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

直接补环境不行,还需要在 url 后面添加一个 ?

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
java.lang.UnsupportedOperationException: okhttp3/Request->body()Lokhttp3/RequestBody;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:187)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

直接补上

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

接着又报错了

1
2
3
4
java.lang.UnsupportedOperationException: okhttp3/Request->headers()Lokhttp3/Headers;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:193)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

直接补上

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

继续报错

1
2
3
4
java.lang.UnsupportedOperationException: okio/Buffer-><init>()V
	at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:803)
	at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:758)
	at com.github.unidbg.linux.android.dvm.DvmMethod.newObjectV(DvmMethod.java:214)

直接补上

1
2
3
4
5
6
7
8
9
@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
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)
	at com.xhs.Shield.callObjectMethodV(Shield.java:194)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

直接补就行

1
2
3
4
5
case "okio/Buffer->writeString(Ljava/lang/String;Ljava/nio/charset/Charset;)Lokio/Buffer;":{
	Buffer buffer = (Buffer)dvmObject.getValue();
	buffer.writeString(vaList.getObjectArg(0).getValue().toString(),(Charset) vaList.getObjectArg(1).getValue());
	return vm.resolveClass("okio/Buffer").newObject(buffer);
}

接着,他还有报错

1
2
3
4
java.lang.UnsupportedOperationException: okhttp3/Headers->size()I
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:529)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethodV(DvmMethod.java:109)

那我接着补

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
public int callIntMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
	switch (signature){
		case "okhttp3/Headers->size()I":{
			Headers headers = (Headers)dvmObject.getValue();
			return headers.size();
		}
	}
	return super.callIntMethodV(vm, dvmObject, signature, vaList);
}

但是还是在报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Headers->name(I)Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:200)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

那就继续接着补

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
java.lang.UnsupportedOperationException: okhttp3/Headers->value(I)Ljava/lang/String;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:204)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)

继续补

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
java.lang.UnsupportedOperationException: okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V
	at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:1007)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callVoidMethodV(AbstractJni.java:990)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callVoidMethodV(DvmMethod.java:229)

继续补

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Override
public void callVoidMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) {
	switch (signature){
		case "okhttp3/RequestBody->writeTo(Lokio/BufferedSink;)V":{
			RequestBody requestBody = (RequestBody)dvmObject.getValue();
			BufferedSink bufferedSink = (BufferedSink)vaList.getObjectArg(0).getValue();
			if (requestBody!=null){
				try {
					requestBody.writeTo(bufferedSink);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			return;
		}
	}
	super.callVoidMethodV(vm, dvmObject, signature, vaList);
}

报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okio/Buffer->clone()Lokio/Buffer;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:211)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

继续补

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
4
5
java.lang.UnsupportedOperationException: okio/Buffer->read([B)I
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
	at com.xhs.Shield.callIntMethodV(Shield.java:236)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:529)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethodV(DvmMethod.java:109)

继续补

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

报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:215)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

继续补

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
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)
	at com.xhs.Shield.callObjectMethodV(Shield.java:219)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

还是要继续补啊

1
2
3
4
5
6
case "okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder;":{
	Request.Builder builder = (Request.Builder)dvmObject.getValue();
	String key = vaList.getObjectArg(0).getValue().toString();
	String value = vaList.getObjectArg(1).getValue().toString();
	return vm.resolveClass("okhttp3/Request$Builder").newObject(builder.header(key,value));
}

仍在报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Request$Builder->build()Lokhttp3/Request;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:225)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

接着补

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

还报错啊

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
	at com.xhs.Shield.callObjectMethodV(Shield.java:229)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

直接补

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

继续报错

1
2
3
4
5
java.lang.UnsupportedOperationException: okhttp3/Response->code()I
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563)
	at com.xhs.Shield.callIntMethodV(Shield.java:254)
	at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:529)
	at com.github.unidbg.linux.android.dvm.DvmMethod.callIntMethodV(DvmMethod.java:109)

继续补就是

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

然后就能够得到结果了
image.png

算法分析

base64 编码

看一下 XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438MeOFeRawXxNDjzedlS52p+7Faz8MjiJx+gKBgEgwfGWaKbbP82ngygrWkkUvuxqUQQShXUDT1nUFV 这段密文是如何生成的
首先还是需要 trace 一份日志吧,这样如果后面分析不出来的时候,可以参照这个日志进行分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public void traceCode() {
	String traceFile = "unidbg-android/src/test/java/com/xhs/traceCode.log";
	PrintStream traceStream = null; // 打印流
	try {
		traceStream = new PrintStream(new FileOutputStream(traceFile), true);
	} catch (FileNotFoundException e) {
		throw new RuntimeException(e);
	}
	// traceCode 对代码进行监控
	emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
}

还需要 hook 一下 memcpy,因为很多时候都会用到这个 C 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public void hookMemcpy() {
	final RegisterContext registerContext = emulator.getContext();
	Symbol memcpySymbol = module.findSymbolByName("memcpy");
	emulator.attach().addBreakPoint(memcpySymbol.getAddress(), new BreakPointCallback() {
		@Override
		public boolean onHit(Emulator<?> emulator, long address) {
			System.out.print("call memcpy");
			UnidbgPointer srcAddress = registerContext.getPointerArg(1);
			int length = registerContext.getIntArg(2);
			Inspector.inspect(srcAddress.getByteArray(0, length), "source:" + srcAddress);
			return true;
		}
	});
}

hook 之后,发现将 shield 的这个值分成了两个部分
前面是 XY,接着是剩余的部分的值
image.png
前面的这个 XY 是固定的,不用分析,主要还是看后面的这个部分,后面这段值的源地址是 0x123d8140,在 unidbg 中提供了一个 api,traceWrite 可以找到这个地址值的写入

1
emulator.traceWrite(0x123d8140, 0x123d8140 + 132);

接着发现在 libxyass.so 的 0x4bd60 偏移的这些地方在不断的进行写入
image.png

跳转到 0x4bd60,跳转过去之后发现有点像是在做 base64 编码的样子
image.png
每次读取 v136 的三个值,然后写入到 v138 四个值
image.png
从汇编也能看出,每次读取 X10 寄存器的三个字节数据,然后吸入到 X15 寄存器,写 4 个字节的数据,所以也就是说读取 x10 寄存器,能够得到原始的数据,原始的数据也需要考虑是否是标准的 base64 编码还是魔改的 base64 码表的编码
hook 过后得到的数据如下,这个好像跟刚才我 hook memcpy 的时候好像已经 hook 到这段数据了
image.png

尝试去 cyberChef 中看一下这个是否是标准的 base64 编码
发现并没有进行魔改,就是标准的 base64 编码
image.png
那接着对这 99 个字节进行分析,溯源

1
emulator.traceWrite(0x123e3070, 0x123e3070 + 99);

发现在控制台,将这段数据分成了两个部分
image.png
前 0x10 个字节和后 0x53 个字节,那先来分析前 0x10 个字节吧

前 0x10 个字节分析

继续 trace,看一下写入

1
emulator.traceWrite(0xe4ffefb9L, 0xe4ffefb9L + 0x10);

它的结果如下:
image.png

第 1~4 字节

跳转到 0x4bc0c,好吧,就是下面这一行 *(_DWORD *)&v157[1] = bswap32((v155 << 16) | 4);
这里就需要看 v155 是什么值了,看一下汇编
image.png
这里是在将 w16 的低 16 位写入到 w9 的高 16 位
image.png
直接 hook 发现这个值是 0x4
image.png

他的计算流程也就是下面这样的

1
2
3
4
5
6
v155 << 16        = 0x00040000
0x00040000 | 4    = 0x00040004
bswap32(0x00040004):
  字节: 00 04 00 04
  翻转: 04 00 04 00
结果  = 0x04000400

这也就需要看 x16 这个值是从哪儿来的了
看一下 v155 的交叉引用
image.png
发现来自于 a4 这个参数,a4 就是当前这个子函数的参数啊
image.png

继续找他的交叉引用,发现只有一个,跳转过去
image.png
发现参数是 v5,又往上溯源
image.png
怕跟错了,继续 hook 一下这个子函数

1
emulator.attach().addBreakPoint(module.base+0x467DC);

发现没跟错,继续
image.png
然后对 x2 寄存器的数据进行溯源

1
emulator.traceWrite(0xe4fff630L,0xe4fff630L+0x4);

继续跳转
image.png
跳转到 0x4ae64,看不太懂
image.png
直接看汇编吧
image.png
将 w8 寄存器的值存到 x21 寄存器,但是 w8 寄存器的值又是从 x29 偏移 0xa8 这个地址处进行取的,hook 过去,发现 fp 的地址是 0xe4fff500,偏移 0xa8,地址就变成了 0xe4fff458
image.png
从 0xe4fff458 地址处取 32 位的值到 x8 寄存器,也就刚好是 0x00000004 即 0x4,继续 trace
image.png
跳转到 0x4a5dc,发现在这儿正好是在将 w9 寄存器的值放进 x29 寄存器偏移 0xa8 的位置啊,而 w9 寄存器的值来源于 x22 寄存器取值
image.png
继续 hook,得到
image.png
trace 一下 0x123e00c0 地址的来源,发现又来源于 memcpy
image.png
继续 trace
image.png

跳转到 0x890a0,看看怎么个事儿
image.png
跳转到这儿,q0 已经算完了,q0 是上面的 v1 和 v0 的16 字节逐字节异或得到的结果,在 0x8908C 下个断点,看一下 x22 寄存器和 x19 寄存器,x10 寄存器的值是 0x0,那就不管
image.png

q1 和 q0 的值只有 0x31 和 0x35 不一样,继续对 0xe4fff130 进行 traceWrite
image.png
发现是在 0x80a58 进行写入的,跳转过去,发现 q0=[x8]
image.png
直接 hook 一下

1
emulator.attach().addBreakPoint(module.base+0x80A54);

得到 x8 的地址为 0x12015c50,对应着在 ida 中的偏移就是 0x15c50,跳转过去
image.png
发现 q1就是固定的, .rodata 段的值是只读的
接下来看一下 q0 这个值,他跟 q1 不同的地方也就是 0x35,继续 trace 一下这段数据的生成

1
emulator.traceWrite(0xe4fff088L, 0xe4fff088L + 0x10);

trace 结果如下:
image.png
跳转到 0x88714
image.png
这一堆看着头有点大,直接看 trace 日志吧
image.png
0x35 是从 w22 寄存器来的,接着需要找 w22 寄存器的值是在哪儿生成的,w22 寄存器的值是 w24 逻辑右移 18 位得到的
w24 又是 w22 和 w25 异或得到的结果,接着就需要看 w22 和 w25 寄存器的值是从哪儿来的了
首先看 w22 寄存器的值
image.png
w22 是 orr(按位或) w22 寄存器和 w24 寄存器的值得到的
首先看这个 0x66 吧,他是 w24 寄存器得到的值,往上看,是取的 "ldrb w24, [x24, x26]" x24=0x7a41ab7 x26=0xa5d5f84 => w24=0x66 这一行,取的地址也就是 0x12017a3b,即偏移 0x17a3b
image.png
又是 .rodata 段固定的值,有点怀疑这个 0x35 也是固定的值了,现在 w24 的值得到了,继续看 w22 寄存器的值,看 0xa1b97700 是怎么来的,继续看
image.png

0xa1b97700 这个是是从 w22 寄存器与 w23 寄存器按位或的结果,那还是需要看一下 w22 寄存器和 w23 寄存器的值,首先看 0x7700 吧,这个值是从 0xe4ffef9c 读的,trace 一下他的写入
image.png
发现偏移是在 0x88164,跳转过去
image.png
0x7700 来自于上一次运算的 w23 寄存器的 0x77 逻辑左移 8 位,得到 0x7700,所以需要看一下 0x77 是从哪儿来的,发现是从 0x12017afd 取的,偏移为 0x17afd,跳转看一下
image.png
发现又是 .rodata 段只读的,固定的,那接着看 0xa1b90000 这个是从哪儿来的,发现是从 0xe4ffef98 这个地址进行取的,继续 trace 这个地方的写入,我怀疑也是只读的
image.png
继续跳转到 0x8813c
image.png

0xa1b90000 是将 w24 的取低 8 位,插入到 w22 的 16到23 的位置,即

1
2
3
4
5
6
W24 低8位:  0xb9 = 1011 1001

W22 原始:   0xa1 00 00 00
插入位置:         ^^        <- bits[23:16]
插入后:     0xa1 b9 00 00
          = 0xa1b90000

所以需要找 0xb9 是哪儿来的,0xa1000000 是哪儿来的,继续往上面看,能够发现 0xa1000000 是 0xa1«0x18 位得到的,0xa1 是从 0x12017a3a 取的,是个只读的数据,固定的
image.png
继续看 0xb9,他是 0x12017a5e 取的,也是固定的,那所有的都是只读固定的,那 0x35 也是固定的了

第 5 ~8 字节

跳转到 0x4bbfc
image.png
这个值就是固定的,不需要分析了

第 9 字节

继续回到之前的位置,跳转到 0x4bc2c 的位置
image.png
这里可能是 ida 识别的问题,索引下标数字不对,这儿也是固定的

第 10~13 字节

跳转到 0x4bc34
image.png

来自于v126,v126 是反转的 v124 的值,v124 发现来自于 v160,总之跟 v160 有关系,寻找交叉引用
image.png
发现 v160 是 v25 的值,v25 又是由调用了 sub_1DD30(&v158, v22, v23) 这个函数的结果值,进去看一下这个函数干了什么
发现这个函数会进行 memcpy,好像是在进行拼接什么东西
image.png
去问一下 AI
image.png
也就是说 0x53 是拼接数据的长度,刚好后需要拼接一个长度为 0x53 的数据

第 14 ~16 字节

跳转到 0x4bc30,发现跟刚才第 10 到 13 字节数据挨着的
image.png
还是需要去寻找 v162 的交叉引用,发现到最后还是会调用 sub_1DD30 这个字符串拼接函数,说明跟上面的 0x53 是一样的,那就继续分析下面的东西,前 0x10 个字节的数据可以归结为都是固定的

后 0x53 个字节分析

RC4 算法

继续回到之前 memcpy 的地方
image.png
发现源地址是在 0x123e0060,trace 一下写入
image.png
跳转到 0x4b82c 看一下
image.png
0x35 是后面这一堆 *v66 ^ *((_DWORD *)&v164 + (unsigned __int8)(v76 + v73) + 2); 算出来的,还是异或,他在这个循环中每次会写入 8 个值,循环条件是 v70
image.png
在上面,看日志可以得到循环有 10 轮,每次写入 8 个字节,但是也才 80 字节啊,还有 3 个字节呢?当然往下面看还在写
看这个模式,逐字节异或有点像流密码的方式,或许可能是 rc4 算法
如果是 rc4 算法的话,会有 s 盒的初始化,密钥一些内容
用了一下小风大佬的魔改 unidbg,确实要省事许多,重新 trace 一份日志进行分析
从第二个字节分析一下吧,数据是如何生成的 0x16
image.png
image.png
最后就跳转到这儿了
image.png
而且看到很多从 1 开始进行一直排序的序列,很有可能这儿是在进行 S 盒初始化
跳转到 0x4b694,发现这一块应该就在做 S 盒初始化了
image.png

看一下对应的汇编
image.png
现在已经是 ai时代了,我只需要判断就行,害,手动看汇编对应伪码的时代貌似已经过去了,扣汇编交给 ai 吧
image.png

S 盒就是 X10 寄存器,在这块代码结束的地方断点,这个时候 S 盒应该初始化已经完成了
直接 hook 看一下

1
emulator.attach().addBreakPoint(module.base+0x4B6BC);

看一下 x10 寄存器
image.png
这个时候已经初始化完成了,当 S 盒初始化完成之后,就应该密钥参数进行打乱 S 盒的顺序了
继续往下面看,S 盒参与到运算中了
image.png
让 ai 同志翻一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
4B6BC  ADD   X13, X10, X8, LSL#2  ; X13 = S盒基址(X10) + i*4,当前S[i]地址
4B6C0  LDR   W14, [X13, #8]       ; W14 = S[i](读取当前元素)
4B6C4  LDRB  W15, [X9, W12, UXTW] ; W15 = key[key_idx],X9=key数组,W12=key_idx
4B6C8  ADD   X16, X10, #8         ; X16 = S盒数据区基址(对齐偏移)
4B6CC  ADD   W0,  W12, #1         ; W0 = key_idx + 1(预备下次索引)
4B6D0  ADD   W11, W14, W11        ; W11 = j + S[i]
4B6D4  ADD   W11, W11, W15        ; W11 = j + S[i] + key[key_idx]
4B6D8  AND   W15, W11, #0xFF      ; j = (j + S[i] + key[key_idx]) & 0xFF(取低8位)
4B6DC  LSL   X15, X15, #2         ; j * 4(int数组偏移)
4B6E0  LDR   W17, [X16, X15]      ; W17 = S[j](读取交换目标)
4B6E4  CMP   W0,  #0xD            ; key_idx+1 == 13?(key长度为13)
4B6E8  CSINC W12, WZR, W12, EQ   ; key_idx = (key_idx+1==13) ? 0 : key_idx(mod 12)
4B6EC  ADD   W0,  W12, #1         ; 继续推进索引
4B6F0  STR   W17, [X13, #8]       ; S[i] = S[j](交换前半步)
4B6F4  STR   W14, [X16, X15]      ; S[j] = tmp(交换后半步,完成 swap(S[i], S[j])

ai 同志好样的,直接能看到 X9 寄存器的值就是 key,key 的长度 13
image.png

可以知道 key 就是 std::abort() 了,那么明文呢?有没有魔改呢?有没有魔改看一下就行了,在最开始跳转到 0x4b82c 的地方应该就是 rc4 加密的核心逻辑了,rc4 加密的核心逻辑一般都是明文与密钥流进行异或操作
image.png
这里的这个 v66 就是明文了,hook 对应的汇编地址
image.png
哟西,明文也找到了,那去 cyberchef 中试一下呢
image.png
标准的,不用我操心其他的了,直接还原吧

 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
28
29
30
31
def rc4(key: bytes, data: bytes) -> bytes:
    # KSA - Key Scheduling Algorithm
    S = list(range(256))
    j = 0
    key_len = len(key) 
    
    for i in range(256):
        j = (j + S[i] + key[i % key_len]) & 0xFF
        S[i], S[j] = S[j], S[i]
    
    # PRGA - Pseudo-Random Generation Algorithm
    i = 0
    j = 0
    result = []
    for byte in data:
        i = (i + 1) & 0xFF
        j = (j + S[i]) & 0xFF
        S[i], S[j] = S[j], S[i]
        k = S[(S[i] + S[j]) & 0xFF]
        result.append(byte ^ k)
    
    return bytes(result)


key = b"std::abort();" 

data = bytes.fromhex("00000001ECFAAF01000000020000000700000024000000103932343038313165316463636132372D613461352D333964612D613262322D61383332393838366362346131403731641E81A8BC2C6B7CA3BEB6EE")


output = rc4(key, data)
print(output.hex())

那接着就需要找这个明文是在哪儿生成的了啊

溯源 RC4 算法明文

明文的前 67 个字节数据

首先还是先看一下这段明文的数据

1
2
3
4
5
6
7
8
x10=RW@0x123d80a0, md5=10d9b81db7889a6314a27620973ebda3, hex=00000001ecfaaf01000000020000000700000024000000103932343038313165316463636132372d613461352d333964612d613262322d61383332393838366362346131403731641e81a8bc2c6b7ca3beb6ee
size: 83
0000: 00 00 00 01 EC FA AF 01 00 00 00 02 00 00 00 07    ................
0010: 00 00 00 24 00 00 00 10 39 32 34 30 38 31 31 65    ...$....9240811e
0020: 31 64 63 63 61 32 37 2D 61 34 61 35 2D 33 39 64    1dcca27-a4a5-39d
0030: 61 2D 61 32 62 32 2D 61 38 33 32 39 38 38 36 63    a-a2b2-a8329886c
0040: 62 34 61 31 40 37 31 64 1E 81 A8 BC 2C 6B 7C A3    b4a1@71d....,k|.
0050: BE B6 EE                                           ...

先 trace 以下,看是在哪儿写入的

1
emulator.traceWrite(0x123d80a0, 0x123d80a0 + 0x53);

发现是通过 memcpy 过来的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
source:RW@0x12411000, md5=657a70ce87e9ce48458f506290ee6138, hex=00000001ecfaaf01000000020000000700000024000000103932343038313165316463636132372d613461352d333964612d613262322d613833323938383663623461
size: 67          
0000: 00 00 00 01 EC FA AF 01 00 00 00 02 00 00 00 07    ................
0010: 00 00 00 24 00 00 00 10 39 32 34 30 38 31 31 65    ...$....9240811e
0020: 31 64 63 63 61 32 37 2D 61 34 61 35 2D 33 39 64    1dcca27-a4a5-39d
0030: 61 2D 61 32 62 32 2D 61 38 33 32 39 38 38 36 63    a-a2b2-a8329886c
0040: 62 34 61                                           b4a
^-----------------------------------------------------------------------------^
[08:24:56 905] Memory WRITE at 0x123d80a0, data size = 8, data value = 0x01affaec01000000, PC=RX@0x121fc220[libc.so]0x1c220, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8
[08:24:56 905] Memory WRITE at 0x123d80a8, data size = 8, data value = 0x0700000002000000, PC=RX@0x121fc220[libc.so]0x1c220, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8
[08:24:56 905] Memory WRITE at 0x123d80b0, data size = 8, data value = 0x1000000024000000, PC=RX@0x121fc224[libc.so]0x1c224, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8

继续 trace,发现源地址 0x123dd580

1
2
3
4
5
6
7
8
source:RW@0x123dd580, md5=95c44e390e50ca9292230fa9ae784d38, hex=00000001ecfaaf010000000200000007000000240000001039323430383131
size: 31
0000: 00 00 00 01 EC FA AF 01 00 00 00 02 00 00 00 07    ................
0010: 00 00 00 24 00 00 00 10 39 32 34 30 38 31 31       ...$....9240811
^-----------------------------------------------------------------------------^
[08:32:46 156] Memory WRITE at 0x12411000, data size = 8, data value = 0x01affaec01000000, PC=RX@0x121fc18c[libc.so]0x1c18c, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8
[08:32:46 156] Memory WRITE at 0x12411008, data size = 8, data value = 0x0700000002000000, PC=RX@0x121fc18c[libc.so]0x1c18c, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8
[08:32:46 156] Memory WRITE at 0x1241100f, data size = 8, data value = 0x0000002400000007, PC=RX@0x121fc1a4[libc.so]0x1c1a4, LR=RX@0x1201dbf8[libxyass.so]0x1dbf8

接着 trace, 发现偏移在 0x4b474
image.png
去 trace 日志看一下
image.png
后面的 9240811 不用管,这个是版本号,固定的,而且版本号后面跟着的类似于 uuid 的东西,就是之前的 sDeviceId,主要看的也就是前面这一堆,跳转到 0x4b474
image.png
这发现也就是需要分析 v15 到 v20 的值,看看是在哪儿生成的

首先看 v15 吧
image.png
a1 是当前函数的参数,在他调用的时候直接传的 1 进来,是写死的,固定的
image.png

v16 这个值就是 sAppId 这个值补码的 16 进制表现形式

1
2
0xECFAAF01 = 3975851777 (unsigned)
3975851777 - 2^32 = -319115519(sAppId)

正好可以对上
v17 是根据前面的那个 a4 的值(就是前面分析的那个 0x35 和 0x31 异或的那个值)来判断的,如果 a4 有值,赋值 v17 为 2
image.png
v18 是 0x7,这个是代表后面要拼接数据的长度,也就是那个版本号的长度
接着的 24,代表这的是 sDeviceId 的长度
最后的 0x10 代表最后拼接的那个不知道的 16 字节长度
所以可以归纳一下了

  • 0x01000000 传参是固定的值
  • 0x01affaec sAppId 补码的 16 进制表现形式
  • 0x02000000 根据 0x35 和 0x31 异或之后的值,直接固定的赋值数据
  • 0x07000000 代表后面要拼接版本号的长度
  • 0x24000000 代表后面要拼接的sDeviceId 的长度
  • 0x10000000 代表后面要拼接的未知的 16 字节的长度
  • 39 32 34 30 38 31 31(9240811)代表版本号,可以直接写死
  • 65 31 64 63 63 61 32 37 2D 61 34 61 35 2D 33 39 64 61 2D 61 32 62 32 2D 61 38 33 32 39 38 38 36 63 62 34 61(e1dcca27-a4a5-39da-a2b2-a8329886cb4a)代表sDeviceId,也可以直接写死,因为本来传参进来就是写死的嘛

那么,剩下不知道的,也就是最后的这 16 个字节的数据了

明文的后 16 个字节数据

通过 trace 可以发现这 16 个字节的位置
image.png
接着就需要找 q0 是在哪儿生成的
image.png
q0 是从 0xe4fff438 读出来的,继续找
image.png
发现这里是在 4 个字节 4 个字节写写入的,一共写 16 个字节,4 个字节,abcd,有可能是 hash 算法
然后一直向上找
image.png
发现是 add 加出来的,更怀疑是 md5 加密了,但是很有可能会进行魔改,先用一下小风的插件去看一下,发现一共有两次 md5
image.png
但是明文应该不是这个,看着不像,或许只能得到一部分明文的数据,也就是 sDeviceId+xy-scene,但是可以看到当前的 PC 寄存器保存的地址是 0x8189c,跳转过去
image.png
这里好像是在填充 0x80,这儿是在对明文进行处理吗?之前我做过关于 md5 的笔记的,看其他地方好像也是将 md5 的运算过程给拆散了,在 ida 中,他是将 md5 的 64 轮运算拆散了,直接看 ida 的话,感觉不是特别好还原,还是主要依靠 trace 日志进行分析吧
先拿一份标准的 md5 来进行对照吧,看改了哪些地方

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import struct

class CustomMD5:

    @staticmethod
    def left_rotate(x, c):
        return ((x << c) | (x >> (32 - c))) & 0xFFFFFFFF

    @staticmethod
    def FF(i, g, a, b, c, d, x, s, ac):
        f = ((c ^ d) & b) ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def GG(i, g, a, b, c, d, x, s, ac):
        f = ((b ^ c) & d) ^ c
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def HH(i, g, a, b, c, d, x, s, ac):
        f = b ^ c ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def II(i, g, a, b, c, d, x, s, ac):
        f = (c ^ (b | ~d)) & 0xFFFFFFFF
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:08x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    def __init__(self, message: bytes):
        self.message = message
        # 标准 MD5 初始向量
        self.A = 0x67452301
        self.B = 0xefcdab89
        self.C = 0x98badcfe
        self.D = 0x10325476

    def hexdigest(self):
        original_bit_len = len(self.message) * 8
        message = bytearray(self.message)
        message.append(0x80)
        while len(message) % 64 != 56:
            message.append(0x00)
        message += struct.pack("<Q", original_bit_len)

        for offset in range(0, len(message), 64):
            chunk = message[offset: offset + 64]
            M = struct.unpack("<16I", chunk)
            A, B, C, D = self.A, self.B, self.C, self.D

            # ================= 第一轮 (0-15) =================
            # 标准 shifts: 7,12,17,22 循环; 标准 T 常量; M 索引 0-15 顺序
            A = self.FF(0,  0,  A, B, C, D, M[0],   7, 0xd76aa478); 
            D = self.FF(1,  1,  D, A, B, C, M[1],  12, 0xe8c7b756)
            C = self.FF(2,  2,  C, D, A, B, M[2],  17, 0x242070db); 
            B = self.FF(3,  3,  B, C, D, A, M[3],  22, 0xc1bdceee)
            A = self.FF(4,  4,  A, B, C, D, M[4],   7, 0xf57c0faf);
            D = self.FF(5,  5,  D, A, B, C, M[5],  12, 0x4787c62a)
            C = self.FF(6,  6,  C, D, A, B, M[6],  17, 0xa8304613);
            B = self.FF(7,  7,  B, C, D, A, M[7],  22, 0xfd469501)
            A = self.FF(8,  8,  A, B, C, D, M[8],   7, 0x698098d8); 
            D = self.FF(9,  9,  D, A, B, C, M[9],  12, 0x8b44f7af)
            C = self.FF(10, 10, C, D, A, B, M[10], 17, 0xffff5bb1); 
            B = self.FF(11, 11, B, C, D, A, M[11], 22, 0x895cd7be)
            A = self.FF(12, 12, A, B, C, D, M[12],  7, 0x6b901122); 
            D = self.FF(13, 13, D, A, B, C, M[13], 12, 0xfd987193)
            C = self.FF(14, 14, C, D, A, B, M[14], 17, 0xa679438e); 
            B = self.FF(15, 15, B, C, D, A, M[15], 22, 0x49b40821)

            # ================= 第二轮 (16-31) =================
            # 标准 shifts: 5,9,14,20 循环; g = (5i+1) mod 16
            A = self.GG(16,  1, A, B, C, D, M[1],   5, 0xf61e2562); 
            D = self.GG(17,  6, D, A, B, C, M[6],   9, 0xc040b340)
            C = self.GG(18, 11, C, D, A, B, M[11], 14, 0x265e5a51); 
            B = self.GG(19,  0, B, C, D, A, M[0],  20, 0xe9b6c7aa)
            A = self.GG(20,  5, A, B, C, D, M[5],   5, 0xd62f105d); 
            D = self.GG(21, 10, D, A, B, C, M[10],  9, 0x02441453)
            C = self.GG(22, 15, C, D, A, B, M[15], 14, 0xd8a1e681); 
            B = self.GG(23,  4, B, C, D, A, M[4],  20, 0xe7d3fbc8)
            A = self.GG(24,  9, A, B, C, D, M[9],   5, 0x21e1cde6); 
            D = self.GG(25, 14, D, A, B, C, M[14],  9, 0xc33707d6)
            C = self.GG(26,  3, C, D, A, B, M[3],  14, 0xf4d50d87); 
            B = self.GG(27,  8, B, C, D, A, M[8],  20, 0x455a14ed)
            A = self.GG(28, 13, A, B, C, D, M[13],  5, 0xa9e3e905); 
            D = self.GG(29,  2, D, A, B, C, M[2],   9, 0xfcefa3f8)
            C = self.GG(30,  7, C, D, A, B, M[7],  14, 0x676f02d9); 
            B = self.GG(31, 12, B, C, D, A, M[12], 20, 0x8d2a4c8a)

            # ================= 第三轮 (32-47) =================
            # 标准 shifts: 4,11,16,23 循环; g = (3i+5) mod 16; 恢复正常顺序(去掉39/40、41/42互换)
            A = self.HH(32,  5, A, B, C, D, M[5],   4, 0xfffa3942); 
            D = self.HH(33,  8, D, A, B, C, M[8],  11, 0x8771f681)
            C = self.HH(34, 11, C, D, A, B, M[11], 16, 0x6d9d6122); 
            B = self.HH(35, 14, B, C, D, A, M[14], 23, 0xfde5380c)
            A = self.HH(36,  1, A, B, C, D, M[1],   4, 0xa4beea44); 
            D = self.HH(37,  4, D, A, B, C, M[4],  11, 0x4bdecfa9)
            C = self.HH(38,  7, C, D, A, B, M[7],  16, 0xf6bb4b60); 
            B = self.HH(39, 10, B, C, D, A, M[10], 23, 0xbebfbc70)
            A = self.HH(40, 13, A, B, C, D, M[13],  4, 0x289b7ec6); 
            D = self.HH(41,  0, D, A, B, C, M[0],  11, 0xeaa127fa)
            C = self.HH(42,  3, C, D, A, B, M[3],  16, 0xd4ef3085); 
            B = self.HH(43,  6, B, C, D, A, M[6],  23, 0x04881d05)
            A = self.HH(44,  9, A, B, C, D, M[9],   4, 0xd9d4d039); 
            D = self.HH(45, 12, D, A, B, C, M[12], 11, 0xe6db99e5)
            C = self.HH(46, 15, C, D, A, B, M[15], 16, 0x1fa27cf8); 
            B = self.HH(47,  2, B, C, D, A, M[2],  23, 0xc4ac5665)

            # ================= 第四轮 (48-63) =================
            # 标准 shifts: 6,10,15,21 循环; g = (7i) mod 16
            A = self.II(48,  0, A, B, C, D, M[0],   6, 0xf4292244); 
            D = self.II(49,  7, D, A, B, C, M[7],  10, 0x432aff97)
            C = self.II(50, 14, C, D, A, B, M[14], 15, 0xab9423a7); 
            B = self.II(51,  5, B, C, D, A, M[5],  21, 0xfc93a039)
            A = self.II(52, 12, A, B, C, D, M[12],  6, 0x655b59c3); 
            D = self.II(53,  3, D, A, B, C, M[3],  10, 0x8f0ccc92)
            C = self.II(54, 10, C, D, A, B, M[10], 15, 0xffeff47d); 
            B = self.II(55,  1, B, C, D, A, M[1],  21, 0x85845dd1)
            A = self.II(56,  8, A, B, C, D, M[8],   6, 0x6fa87e4f); 
            D = self.II(57, 15, D, A, B, C, M[15], 10, 0xfe2ce6e0)
            C = self.II(58,  6, C, D, A, B, M[6],  15, 0xa3014314); 
            B = self.II(59, 13, B, C, D, A, M[13], 21, 0x4e0811a1)
            A = self.II(60,  4, A, B, C, D, M[4],   6, 0xf7537e82); 
            D = self.II(61, 11, D, A, B, C, M[11], 10, 0xbd3af235)
            C = self.II(62,  2, C, D, A, B, M[2],  15, 0x2ad7d2bb); 
            B = self.II(63,  9, B, C, D, A, M[9],  21, 0xeb86d391)

            self.A = (self.A + A) & 0xFFFFFFFF
            self.B = (self.B + B) & 0xFFFFFFFF
            self.C = (self.C + C) & 0xFFFFFFFF
            self.D = (self.D + D) & 0xFFFFFFFF

            print("last: ", hex(self.A), hex(self.B), hex(self.C), hex(self.D))

        res = bytearray()
        res.extend(struct.pack("<I", self.A))
        res.extend(struct.pack("<I", self.B))
        res.extend(struct.pack("<I", self.C))
        res.extend(struct.pack("<I", self.D))
        return res.hex()


if __name__ == "__main__":
    test = bytes.fromhex("71DBB96E5C6CD9A710C189EFC86E6085")
    result = CustomMD5(test).hexdigest()
    print("Result:  ", result)
第一大轮魔改

先用第一次 md5 的结果再去加密 md5 得到 31403731641e81a8bc2c6b7ca3beb6ee,只不过中间是进行魔改过的 md5
先运行一下,看下状态
image.png
这个代码是标准 md5 的原因,可能这个样本的 md5 已经被深度魔改了,T 表,左移位数都搜索不到,那么他的明文一定会在 trace 日志中出现,所以也就是说一定能够搜索到,直接搜索的话效率有点低啊,可以使用正则的方式进行搜索 add.*0x6eb9db71.*=>.*,这样就只会搜索到 4 个,也就对应着在 md5 运算中的 4 大轮运算
image.png
直接找第一个,也就能够确定,这里是 4 轮运算中的第一大轮,但是不能确定是否是 16 小轮运算中的第 1 小轮
跳转过去对应着下面的日志

1
[libxyass.so 0x12000000+0x82a08] 0x12082a08: "add w8, w8, w4" ; w8=0xf12c7987 w4=0x6eb9db71 => w8=0x5fe654f8

直接跳转到 0x82a08
image.png
这一块就很像非线性函数中的 FF 函数了,首先先来看一下 FF 函数,挨个来对应一下吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 @staticmethod
def FF(i, g, a, b, c, d, x, s, ac):
	# x = M[g]  s = S[i]  ac = T[i]
	f = ((c ^ d) & b) ^ d
	res = (f + a + ac + x) & 0xFFFFFFFF
	new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
	print(f"i: {i:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
	return new_val
 
#  *(v1 + 476); 可以确定的是对应着M[g],明文部分
# ((*(v1 + 428) ^ *(v1 + 424)) & *(v1 + 432) ^ *(v1 + 424)) 这一块对应着 f
# *(v1 + 436) 不确定是a还是T
# 但是这儿好像少加了一个数,也不确定是少加的a还是T

观察汇编,发现后面会 br 跳转,发现在另一个地方还会加一个数
image.png

(v1 + 436) =>对应着 0x2abdd689
另外加的一个值对应着 0xe9c9b756,谁是 T,谁是 a 呢?
因为 T 一直是固定的,或者说可以在内存中被 dump 出来,它不能够被推断出来,也就是算出来
a 就不一样了,a 在 64 轮运算中既可以参与运算,也可以被算出来,那这样就可以使用正则在日志中进行匹配,从而推断谁是 a,谁是 T 了 add.*=>.*0x2abdd689 发现 0x2abdd689 能够被算出来,所以也就能推出它是 a
image.png

所以,就能够推出相关信息了

1
2
3
4
5
6
7
8
*(v1 + 492) = ((*(v1 + 428) ^ *(v1 + 424)) & *(v1 + 432) ^ *(v1 + 424)) + *(v1 + 436) + *(v1 + 476);
					c				d				b			d				a		+	M[g]
				0xd41eaea4		0x266582ff		0xe08fa1a5	0x266582ff	+	0x2abdd689  +   0x6eb9db71
				
self.A = 0x2abdd689
self.B = 0xe08fa1a5
self.C = 0xd41eaea4
self.D = 0x266582ff

进行替换之后,发现第一小轮算出来的 b,还是不对,说明有可能 T 常量表给改了,或者常量转换的地方也改了
这一堆算出来的值是 0x5fe654f8,接着搜索,看看后面加的那个 T 表是什么, add.*0x5fe654f8,发现
image.png
第一个 T 常量表的值是 0xe9c9b756,ror 右移,在 md5 中是左移,右移 0x1a(26) 位,即左移 6 位,常量转换也改了,更换之后得到的新 b=>0x4c92b537 能够搜到了,说明这一小轮正确了
接着看第 2 小轮,发现 f_old 能够搜到,说明是正确的,f 不能搜到,那么就是在非线性函数运算过后的 f 运算中出了错误,即 f = (f_old + a + self.T[i] + M[g]) & 0xFFFFFFFF 这一块出了问题
在 trace 文件中进行搜索,可以发现新得到的 f 的值是 0x75e93f79
image.png

看一下前面哪个流程错误了,0x75e93f79 = 0xd08eaba5(f_old) + 0xa55a93d4,对应到新 f 的运算中,0xa55a93d4 应该就是 a+T[i]+M[g],也就是说前面应该还会对应运算
找到 0xa55a93d4 = 0xa7d96c5c + 0x266582ff +0xd71ba479,其中 0xa7d96c5c 是 M[g]明文,0x266582ff 是上一轮的 a,0xd71ba479 就应该是 T 表里面的数据了,说明第二小轮的 t 表也改了
同时在下面看到 ror 右移 0x13(19)位,说明左移 13 位,常量转换也改了
结果继续搜索,发现新的 b 又搜索不到,很可能 T 表所有的值都被魔改了,直接 trace 能够发现在偏移为 0x94910 的地方是固定的
image.png

直接让 ai 写了个 idapython 的脚本

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
"""
IDAPython script to dump modified MD5 T-table from address 0x94910
The table contains 64 elements, each 4 bytes (DWORD)
First element should be 0xe9c9b756
"""

import ida_bytes
import struct

def dump_md5_t_table():
    # T表的起始地址
    start_addr = 0x94910
    # 数组长度
    array_length = 64
    # 每个元素的大小(字节)
    element_size = 4
    
    print("=" * 60)
    print("Dumping Modified MD5 T-Table")
    print("=" * 60)
    print(f"Start Address: 0x{start_addr:X}")
    print(f"Array Length: {array_length}")
    print(f"Element Size: {element_size} bytes")
    print("=" * 60)
    
    # 存储T表的列表
    t_table = []
    
    # 读取64个DWORD值
    for i in range(array_length):
        addr = start_addr + (i * element_size)
        # 读取4字节作为小端序DWORD
        dword_value = ida_bytes.get_dword(addr)
        t_table.append(dword_value)
        
        # 打印每个元素(每行8个)
        if i % 8 == 0:
            print(f"\n[{i:2d}-{min(i+7, array_length-1):2d}] ", end="")
        print(f"0x{dword_value:08X}", end=" ")
    
    print("\n" + "=" * 60)
    
    # 验证第一个元素
    expected_first = 0xe9c9b756
    actual_first = t_table[0]
    print(f"\nVerification:")
    print(f"Expected first element: 0x{expected_first:08X}")
    print(f"Actual first element:   0x{actual_first:08X}")
    print(f"Match: {expected_first == actual_first}")
    
    # 生成Python数组格式
    print("\n" + "=" * 60)
    print("Python Array Format:")
    print("=" * 60)
    print("T_TABLE = [")
    for i in range(0, array_length, 8):
        values = ", ".join([f"0x{t_table[j]:08X}" for j in range(i, min(i+8, array_length))])
        print(f"    {values},")
    print("]")
    
    # 生成C数组格式
    print("\n" + "=" * 60)
    print("C Array Format:")
    print("=" * 60)
    print("uint32_t T_TABLE[64] = {")
    for i in range(0, array_length, 8):
        values = ", ".join([f"0x{t_table[j]:08X}" for j in range(i, min(i+8, array_length))])
        print(f"    {values},")
    print("};")
    
    print("\n" + "=" * 60)
    print("Dump completed successfully!")
    print("=" * 60)
    
    return t_table

# 执行dump
if __name__ == "__main__":
    t_table = dump_md5_t_table()

接着就能够得到魔改过后的 T 表了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 魔改MD5的T表 (64个元素)
T_TABLE = [
    0xE9C9B756, 0xD71BA479, 0x241081DB, 0x681088D9, 0x9B14F7AF, 0xFF1F5BB1, 0x881CD7BE, 0x66666122,
    0xF6666193, 0xA619639E, 0x49140921, 0xC11DCEEE, 0xF51C0FAF, 0x4717C62A, 0xA9104613, 0xFD169501,
    0xF61E2562, 0x02741453, 0xD221E691, 0xE213FBC9, 0x2261CDE6, 0xF2D50D97, 0x425A14ED, 0xA277E905,
    0xF277A3F9, 0x626F12D9, 0x922A4C9A, 0xC040B340, 0x265E5A51, 0xE9F6C7AA, 0xD63F105D, 0xC35707D6,
    0xFFFC3942, 0x977CD691, 0xA4BCEA44, 0x4BDCCFA9, 0xBEBCBC70, 0x288C7EC6, 0xF6CC4B60, 0xEAAC27FA,
    0xD4EC1095, 0xD9DCD039, 0xE6DC88E5, 0x048C1D05, 0x1FA27CF9, 0x6D9D6122, 0xC4AC5665, 0xFDE5391C,
    0xF4292244, 0xAB9423A7, 0xF593A039, 0x655B59C3, 0x452AFF97, 0xF5EF247D, 0x85845DD1, 0x850CCC92,
    0xF99926E0, 0xF9997E4F, 0xA9994314, 0xC5537E82, 0x450811A1, 0x450811A6, 0xBD3AF235, 0xEB86D391,
]

又让 ai 写了脚本来提取左移的位数

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import re

# 读取trace日志文件
log_file = r"逆向案例/小红书/trace_asm.log"

# 正则表达式匹配 ror wX, wX, #0xXX (任意寄存器)
pattern = r'ror w\d+, w\d+, #(0x[0-9a-fA-F]+|[0-9]+)'

shift_values = []

print("正在提取左移位数...")

with open(log_file, 'r', encoding='utf-8') as f:
    for line in f:
        match = re.search(pattern, line)
        if match:
            shift_hex = match.group(1)
            # 转换为十进制
            if shift_hex.startswith('0x'):
                shift_val = int(shift_hex, 16)
            else:
                shift_val = int(shift_hex)
            
            # ror是右旋转,需要转换为左旋转
            # 32位数据:左移 = 32 - 右移
            left_shift = 32 - shift_val
            
            shift_values.append(left_shift)
            
            # 只提取前64个
            if len(shift_values) >= 64:
                break

print(f"\n提取到 {len(shift_values)} 个左移位数:")
print("\n# S表 - 左移位数 (64个值)")
print("S = [")

# 每行16个值,分4轮
for i in range(0, 64, 16):
    round_num = i // 16 + 1
    values = shift_values[i:i+16]  # 修正:应该是i+16,不是i+15
    
    if len(values) == 16:
        values_str = ', '.join(f'{v:2d}' for v in values)
        print(f"    # 第{round_num}轮 ({i+1}-{i+16})")
        print(f"    {values_str},")
    else:
        print(f"    # 警告:第{round_num}轮只有 {len(values)} 个值")
        values_str = ', '.join(f'{v:2d}' for v in values)
        print(f"    {values_str}")

print("]")

# 验证:检查是否符合MD5的S表模式
print("\n验证S表模式:")
print("第一轮:", shift_values[0:16])
print("第二轮:", shift_values[16:32])
print("第三轮:", shift_values[32:48])
print("第四轮:", shift_values[48:64])

又让 ai 写了一份提取左移位数表的脚本,提取出来的左移位数如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
S = [
	# 第1轮 (1-16)
	6, 13, 17, 21,  7, 12, 17, 20,  7, 12, 16, 22,  7, 13, 17, 22,
	# 第2轮 (17-32)
	5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,  5,  9, 14, 20,
	# 第3轮 (33-48)
	4, 11, 16, 23,  4, 11, 16,  4, 23, 16, 11, 23,  4, 11, 16, 23,
	# 第4轮 (49-64)
	6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,  6, 10, 15, 21,
]

但是发现第 15 轮出现错误了,那就需要去日志中进行定位了
f_old 能搜到说明这一轮运算的经过前面的非线性函数的运算是对的,f 不对,说明后面的这一堆更新 f 出现错误了

1
2
f = (f_old + a + self.T_TABLE[i] + M[g]) & 0xFFFFFFFF
f_old正确,a正确,T表能搜到,正确,那只能出现在明文的填充上面出问题了

去日志中看一下
image.png
它最后填充的明文是 0x280,这也能理解,在这里只是取的中间状态来进行 md5 加密,而这个样本中 md5 是有多轮的,所以填充出了问题

第二大轮魔改

接着到后面没啥问题,但是到了第 17 轮的时候,又不对了,又来继续看吧
f_old 是对的,能运算出来,f 不对,说明更新 f 的时候出错了,那就要看是 T 表的问题,还是 a 的问题,还是明文的问题,或者说是左移的位数不对,看一下这段汇编

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[libxyass.so 0x12000000+0x83720] 0x12083720: "eor w4, w6, w4" ; w6=0xc6e6d15 w4=0xa3f4ea5b => w4=0xaf9a874e
[libxyass.so 0x12000000+0x83724] 0x12083724: "add w8, w8, w1" ; w8=0xf6002500 w1=0xa7d96c5c => w8=0x9dd9915c
[libxyass.so 0x12000000+0x83728] 0x12083728: "ldr w1, [x19, #0x374]" ; x19=0xe4ffee20 => w1=0xc6e6d15
(r 4) 0xe4fff194  15 6d 6e 0c                                      |.mn.| [0x0c6e6d15]
[libxyass.so 0x12000000+0x8372c] 0x1208372c: "and w4, w4, w21" ; w4=0xaf9a874e w21=0x6006f4f6 => w4=0x20028446
[libxyass.so 0x12000000+0x83730] 0x12083730: "add w8, w8, w30" ; w8=0x9dd9915c w30=0xa14b640 => w8=0xa7ee479c
[libxyass.so 0x12000000+0x83734] 0x12083734: "eor w4, w4, w28" ; w4=0x20028446 w28=0xa3f4ea5b => w4=0x83f66e1d
[libxyass.so 0x12000000+0x83738] 0x12083738: "add w8, w8, w4" ; w8=0xa7ee479c w4=0x83f66e1d => w8=0x2be4b5b9
[libxyass.so 0x12000000+0x8373c] 0x1208373c: "ror w8, w8, #0x1b" ; w8=0x2be4b5b9 => w8=0x7c96b725

0x83f66e1d 是f_old

0x2be4b5b9 = 0xa7ee479c + 0x83f66e1d(f_old)
0xa7ee479c = 0xf6002500 + 0xa7d96c5c + 0xa14b640
				T[i]			M[g]     +    a

从这儿推出来的 T 表是 0xf6002500,但是我用脚本提出来的 T 表是 0xf61e2562,所以对不上,
接着,又往后面看发现第 20 轮的时候又对不上了,不会又改了 T 表吧,很好,发现 T 表是 0xe20011c9
接着,发现后面的第 30 轮数据对不上了,发现 T 表是 0xe9100000

第三大轮魔改

接着接着,发现后面的第 40 轮数据又对不上了,接着发现第 41,第 42,第 43 轮都有点不对劲,将日志拷贝出来
通过观察日志,发现将第 40 轮和 41 轮左移的位数交换了一下,第 42 轮和第 43 轮左移的位数交换了一下
但是还是不对,接着分析的时候发现,为啥第 43 轮的时候,会用到第 42 轮的 a
后面问了下 ai 同志,ai 同志说这四轮两两交换了,结果最后得到了正确的值了

终于对了

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import struct

class CustomMD5:
    # 魔改MD5的T表 (64个元素)
    @staticmethod
    def left_rotate(x, c):
        return ((x << c) | (x >> (32 - c))) & 0xFFFFFFFF

    @staticmethod
    def FF(i, g, a, b, c, d, x, s, ac):
        f = ((c ^ d) & b) ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def GG(i, g, a, b, c, d, x, s, ac):
        f = ((b ^ c) & d) ^ c
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def HH(i, g, a, b, c, d, x, s, ac):
        f = b ^ c ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:8x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    @staticmethod
    def II(i, g, a, b, c, d, x, s, ac):
        f = (c ^ (b | ~d)) & 0xFFFFFFFF
        res = (f + a + ac + x) & 0xFFFFFFFF
        new_val = (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF
        print(f"i: {i+1:2d}, g: {g:2d}, M[{g:2d}]: 0x{x:8x}, f_old: 0x{f:08x}, f: 0x{res:8x}, a: 0x{d:8x}, b: 0x{new_val:8x}, c: 0x{b:8x}, d: 0x{c:8x}, S: {s:2d}, T: 0x{ac:8x}")
        return new_val

    def __init__(self, message: bytes):
        self.message = message
        # 标准 MD5 初始向量
        # self.A = 0x67452301
        # self.B = 0xefcdab89
        # self.C = 0x98badcfe
        # self.D = 0x10325476
        self.A = 0x2abdd689
        self.B = 0xe08fa1a5
        self.C = 0xd41eaea4
        self.D = 0x266582ff
    def hexdigest(self):
        original_bit_len = len(self.message) * 8
        original_bit_len = 80 * 8
        message = bytearray(self.message)
        message.append(0x80)
        while len(message) % 64 != 56:
            message.append(0x00)
        message += struct.pack("<Q", original_bit_len)

        for offset in range(0, len(message), 64):
            chunk = message[offset: offset + 64]
            M = struct.unpack("<16I", chunk)
            A, B, C, D = self.A, self.B, self.C, self.D

            # ================= 第一轮 (0-15) =================
            A = self.FF(0,  0,  A, B, C, D, M[0],   6, 0xE9C9B756)
            D = self.FF(1,  1,  D, A, B, C, M[1],  13, 0xD71BA479)
            C = self.FF(2,  2,  C, D, A, B, M[2],  17, 0x241081DB)
            B = self.FF(3,  3,  B, C, D, A, M[3],  21, 0x681088D9)
            A = self.FF(4,  4,  A, B, C, D, M[4],   7, 0x9B14F7AF)
            D = self.FF(5,  5,  D, A, B, C, M[5],  12, 0xFF1F5BB1)
            C = self.FF(6,  6,  C, D, A, B, M[6],  17, 0x881CD7BE)
            B = self.FF(7,  7,  B, C, D, A, M[7],  20, 0x66666122)
            A = self.FF(8,  8,  A, B, C, D, M[8],   7, 0xF6666193)
            D = self.FF(9,  9,  D, A, B, C, M[9],  12, 0xA619639E)
            C = self.FF(10, 10, C, D, A, B, M[10], 16, 0x49140921)
            B = self.FF(11, 11, B, C, D, A, M[11], 22, 0xC11DCEEE)
            A = self.FF(12, 12, A, B, C, D, M[12],  7, 0xF51C0FAF)
            D = self.FF(13, 13, D, A, B, C, M[13], 13, 0x4717C62A)
            C = self.FF(14, 14, C, D, A, B, M[14], 17, 0xA9104613)
            B = self.FF(15, 15, B, C, D, A, M[15], 22, 0xFD169501)

            # ================= 第二轮 (16-31) =================
            A = self.GG(16,  1, A, B, C, D, M[1],   5, 0xF6002500)
            D = self.GG(17,  6, D, A, B, C, M[6],   9, 0x02741453)
            C = self.GG(18, 11, C, D, A, B, M[11], 14, 0xD221E691)
            B = self.GG(19,  0, B, C, D, A, M[0],  20, 0xE20011C9)
            A = self.GG(20,  5, A, B, C, D, M[5],   5, 0x2261CDE6)
            D = self.GG(21, 10, D, A, B, C, M[10],  9, 0xF2D50D97)
            C = self.GG(22, 15, C, D, A, B, M[15], 14, 0x425A14ED)
            B = self.GG(23,  4, B, C, D, A, M[4],  20, 0xA277E905)
            A = self.GG(24,  9, A, B, C, D, M[9],   5, 0xF277A3F9)
            D = self.GG(25, 14, D, A, B, C, M[14],  9, 0x626F12D9)
            C = self.GG(26,  3, C, D, A, B, M[3],  14, 0x922A4C9A)
            B = self.GG(27,  8, B, C, D, A, M[8],  20, 0xC040B340)
            A = self.GG(28, 13, A, B, C, D, M[13],  5, 0x265E5A51)
            D = self.GG(29,  2, D, A, B, C, M[2],   9, 0xE9100000)
            C = self.GG(30,  7, C, D, A, B, M[7],  14, 0xD63F105D)
            B = self.GG(31, 12, B, C, D, A, M[12], 20, 0xC35707D6)

            # ================= 第三轮 (32-47) =================
            A = self.HH(32,  5, A, B, C, D, M[5],   4, 0xFFFC3942)
            D = self.HH(33,  8, D, A, B, C, M[8],  11, 0x977CD691)
            C = self.HH(34, 11, C, D, A, B, M[11], 16, 0xA4BCEA44)
            B = self.HH(35, 14, B, C, D, A, M[14], 23, 0x4BDCCFA9)
            A = self.HH(36,  1, A, B, C, D, M[1],   4, 0xBEBCBC70)
            D = self.HH(37,  4, D, A, B, C, M[4],  11, 0x288C7EC6)
            C = self.HH(38,  7, C, D, A, B, M[7],  16, 0xF6CC4B60)

            A = self.HH(40, 13, A, B, C, D, M[13],  4, 0xD4EC1095)
            B = self.HH(39, 10, B, C, D, A, M[10], 23, 0xEAAC27FA)
            C = self.HH(42,  3, C, D, A, B, M[3],  16, 0xE6DC88E5)
            D = self.HH(41,  0, D, A, B, C, M[0],  11, 0xD9DCD039)

            B = self.HH(43,  6, B, C, D, A, M[6],  23, 0x048C1D05)
            A = self.HH(44,  9, A, B, C, D, M[9],   4, 0x1FA27CF9)
            D = self.HH(45, 12, D, A, B, C, M[12], 11, 0x6D9D6122)
            C = self.HH(46, 15, C, D, A, B, M[15], 16, 0xC4AC5665)
            B = self.HH(47,  2, B, C, D, A, M[2],  23, 0xFDE5391C)

            # ================= 第四轮 (48-63) =================
            A = self.II(48,  0, A, B, C, D, M[0],   6, 0xF4292244)
            D = self.II(49,  7, D, A, B, C, M[7],  10, 0xAB9423A7)
            C = self.II(50, 14, C, D, A, B, M[14], 15, 0xF593A039)
            B = self.II(51,  5, B, C, D, A, M[5],  21, 0x655B59C3)
            A = self.II(52, 12, A, B, C, D, M[12],  6, 0x452AFF97)
            D = self.II(53,  3, D, A, B, C, M[3],  10, 0xF5EF247D)
            C = self.II(54, 10, C, D, A, B, M[10], 15, 0x85845DD1)
            B = self.II(55,  1, B, C, D, A, M[1],  21, 0x850CCC92)
            A = self.II(56,  8, A, B, C, D, M[8],   6, 0xF99926E0)
            D = self.II(57, 15, D, A, B, C, M[15], 10, 0xF9997E4F)
            C = self.II(58,  6, C, D, A, B, M[6],  15, 0xA9994314)
            B = self.II(59, 13, B, C, D, A, M[13], 21, 0xC5537E82)
            A = self.II(60,  4, A, B, C, D, M[4],   6, 0x450811A1)
            D = self.II(61, 11, D, A, B, C, M[11], 10, 0x450811A6)
            C = self.II(62,  2, C, D, A, B, M[2],  15, 0xBD3AF235)
            B = self.II(63,  9, B, C, D, A, M[9],  21, 0xEB86D391)

            self.A = (self.A + A) & 0xFFFFFFFF
            self.B = (self.B + B) & 0xFFFFFFFF
            self.C = (self.C + C) & 0xFFFFFFFF
            self.D = (self.D + D) & 0xFFFFFFFF

            print("last: ", hex(self.A), hex(self.B), hex(self.C), hex(self.D))

        res = bytearray()
        res.extend(struct.pack("<I", self.A))
        res.extend(struct.pack("<I", self.B))
        res.extend(struct.pack("<I", self.C))
        res.extend(struct.pack("<I", self.D))
        return res.hex()


if __name__ == "__main__":
    test = bytes.fromhex("71DBB96E5C6CD9A710C189EFC86E6085")
    result = CustomMD5(test).hexdigest()
    print("Result:  ", result)

这个魔改的 md5 算法是对了,但是还是需要找到这个魔改的 md5 算法真正的输入
还有,这个毕竟是中间状态的 abcd,它不是初始的 abcd
使用正则进行搜索,比如说 a 是 0x2abdd689,那么他是被算出来的,就可以写正则匹配 add.*=>.*0x2abdd689,就能够搜到了,初始的 a 是 0x10325476,他这个初始的 abcd 应该一开始就有的,或者是从内存中取的,跟 t 表一样,直接在日志中进行搜索
就得到了初始的 abcd,如下:

1
2
3
4
self.A = 0x10325476
self.B = 0x98badcfe
self.C = 0xefcdab89
self.D = 0x67452301
寻找魔改 md5 的明文

因为 71DBB96E5C6CD9A710C189EFC86E6085 只是一个中间状态,还需要找一下他前面的明文是什么,我发现如果从第一轮的 T 表出去去搜索明文,我竟然搜索出来了 42 个,这我这么看下去,下辈子吧,但是有一点可以肯定的是,他是从构造请求 request 添加的一大段请求头中提取的,从之前的那个 dump 插件也可以零星的看一些出来
先来看一下之前构造 request 请求参数所添加的请求头吧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Request request = new Request.Builder()
	.url(url)
	.addHeader("xy-direction", "38")
	.addHeader("X-B3-TraceId", "777700b345160600")
	.addHeader("x-xray-traceid", "ceca78da03ce9848fa576069e7bdf287")
	.addHeader("xy-scene", "fs=1&point=0")
	.addHeader("x-legacy-did", "e1dcca27-a4a5-39da-a2b2-a8329886cb4a")
	.addHeader("x-legacy-fid", "")
	.addHeader("x-legacy-sid", "session.1776320133450187008622")
	.addHeader("x-mini-gid", "7cb6ae24c7605483a8ccb7edde8ef825baf6db7947359c6577b650a6")
	.addHeader("x-mini-s1", "AAoAAAABLmNS+MnxaB7hxU//kJDFEV53wH2QVO1mtwaZ3P0QV/Chn3rue+cnuqD7UciyLeVK5AER8wRGTAI=")
	.addHeader("x-mini-sig", "b986cb1f75c1d9c39649744fc333389789a6561cb7a8198d31354bb0d1560502")
	.addHeader("x-mini-mua", "eyJhIjoiRUNGQUFGMDEiLCJjIjoxMCwiayI6IjhiNTU5NGY0NjYyZTE0M2JiOTIzZmRhZWIyNGZlZGYyZGZhMjE0MzE4NDNhNGNmYTQ0NmU4MTEzZDkyOWZkMTMiLCJwIjoiYSIsInMiOiJhNzQ2OGFjZGMwNTgzN2ZjYjk2ZTAyMjBkMmNmN2FmZDA4MjYxMTM0MjExZWM3YzgxMGNhMjM1YTY3ODQ2MGYyOWFlMzA2Yjk3ZjdkOWM1ZDY5ZDI1Y2VmYmVhNDFlZjhlNTVkZmUzY2VkYjEzNDM2Mjc4ODgzZjUwODE4YzExMiIsInQiOnsiYyI6MCwiZCI6MCwiZiI6MCwicyI6NDA5OCwidCI6MCwidHQiOltdfSwidiI6IjIuOS42MyJ9.eOARI_bLRPIXQ9lUttkOthjbdeqhEQaJQVmiFmVezt7qb2lF9BmMb-XeXl-Z2LcmhivfmAZLcG1-rwKEEkqL4KLEtMf2BsEf03abgZoTUwbSrBy8H7jEBq8T3h7KX7I--bg4kQe6E_xLJan9ae7zHLESFTKipfScEBUOlyNkvjctqB8Svw-4cAWNc86GY0ZXEhKvouVvGMJr4EjVwlwujx2gnHfvnus_JL1vA42HF9bdW4Jt5ANUekOM7pm5jLNveJAnMN9rTALlqoJqfmmTSDv6_Efcc5NBLtiagvfNs9sDUDnsGIn9XkKcoAoq3PIc0NiT8PVhSzL6_js4SKN-Tk2RCZB9Qu7yiZLNWWS2hDGCJ2mv2SpxMkatSrrQa-J3ylk1oJZ7ug3oyHShZo9bsT3rLURNo8T2cr7V0O4se6oYOhMB97AlNYbLmwjkwrXpIvsuJzkDiRTHJF9GqbLkMQ0mqBR_9AZaEmDWu6DmWBL5Np0pJwy0aYMXUgmjfas_GCWWwGmCxjaDqQ3GhwoLTISQFi-JSSWyv-quGb0bbwqyroveh1aQUPzigh5MDZ2VbV1cTOFUpkY-iaRCs6ZM9ZVEjerSG-J9Jwsxm38t4361En9R1XXg1I79wAnj5IOhgBVC7f-Uvj6KuD-EHenGjtR7rKJFpAQqJknDUqn41GTsfmG2fw5PBcIB_6QZUT7LY0zP9vd1QisrkIanSQYQ8SrXfzwjq_0pB-yPKhdePhdU-3tnRPCb1Zovwko1VMMgdFJt5FhQlnkkcZZbGoJQ0PEe7sa2ufHDsYrtaeQf8Pz_oU1fX6xw5_fR7e081TRtgNn5uckv7oBH1j82SA79IuJAhHVDH8Y5MscCmFqUi_aRQhvMGqwyyLNw6Fv2PPk7nwnU8NRIlrpBHqbDhvK7mgGbyMztQBHaKHEaHGRAmsAZG1DxQqhc1nXFPz6qbh56Nbr56FLrPD8S46cr2CYDGqMuQuodwf4PeEdGJjmcE3nomPhNOMSf_w1aM7VVN5h44SibtpE3kgyUfhC0tpLs7Q.")
	.addHeader("xy-common-params", "fid=&gid=7cb6ae24c7605483a8ccb7edde8ef825baf6db7947359c6577b650a6&device_model=phone&tz=Asia%2FShanghai&channel=Vivo&versionName=9.24.0&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4a&platform=android&sid=session.1776320133450187008622&identifier_flag=4&cpu_abi=&nqe_score=&project_id=ECFAAF&x_trace_page_current=app_loading_page&lang=zh-Hans&app_id=ECFAAF01&uis=light&teenager=0&active_ctry=CN&cpu_name=&dlang=zh&data_ctry=CN&SUE=1&launch_id=1776320359&id_token=&device_level=&origin_channel=Vivo&overseas_channel=0&mlanguage=zh_cn&folder_type=none&auto_trans=0&t=1776320361&build=9240811&holder_ctry=CN&did=1c6dc4223567c0ca5bca77caa23a5b56&preload_build_type=0")
	.addHeader("xy-platform-info", "platform=android&build=9195803&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4a")
	.addHeader("User-Agent", "Dalvik/2.1.0 (Linux; U; Android 10; Pixel 3 XL Build/QQ3A.200805.001) Resolution/1440*2960 Version/9.24.0 Build/9240811 Device/(Google;Pixel 3 XL) discover/9.24.0 NetType/WiFi")
	.build();

那这么多,我都需要看吗?这未免对我也太残忍了吧,可以使用排除法,多次跑 unidbg,看谁影响到了结果,如果他影响了结果,说明他应该被当作明文传递到加密算法中,我已经通过多次运算,得到了会影响到结果的几组值了,还有 url 应该也会影响到结果

1
2
3
4
.addHeader("xy-direction", "38")
.addHeader("xy-scene", "fs=1&point=0")
.addHeader("xy-common-params", "fid=&gid=7cb6ae24c7605483a8ccb7edde8ef825baf6db7947359c6577b650a6&device_model=phone&tz=Asia%2FShanghai&channel=Vivo&versionName=9.24.0&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4a&platform=android&sid=session.1776320133450187008622&identifier_flag=4&cpu_abi=&nqe_score=&project_id=ECFAAF&x_trace_page_current=app_loading_page&lang=zh-Hans&app_id=ECFAAF01&uis=light&teenager=0&active_ctry=CN&cpu_name=&dlang=zh&data_ctry=CN&SUE=1&launch_id=1776320359&id_token=&device_level=&origin_channel=Vivo&overseas_channel=0&mlanguage=zh_cn&folder_type=none&auto_trans=0&t=1776320361&build=9240811&holder_ctry=CN&did=1c6dc4223567c0ca5bca77caa23a5b56&preload_build_type=0")
.addHeader("xy-platform-info", "platform=android&build=9195803&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4a")

首先,目前所知道的中间状态的 md5 值,初始的 abcd 值,T 表,左移位数,最后生成的 md5 值,当然,都是魔改的,中间的这个 71DBB96E5C6CD9A710C189EFC86E6085 值的生成应该就是明文经过这个魔改之后的 md5 值生成的,所以,也就相当于从魔改的 md5 值倒推明文
先将这串值进行分组,注意,需要用小端序

1
2
3
4
5
6
# 中间状态的md5值
71DBB96E5C6CD9A710C189EFC86E6085
A = 0x6eb9db71
B = 0xa7d96c5c
C = 0xef89c110
D = 0x85606ec8

这里的 abcd 值是中间算出来的,所以可以使用正则进行搜索 add.*=>.*0x6eb9db71
image.png
发现只有一个唉,跳过去
image.png
其中的 0x8d651ffa 不知道是啥,我往上面看他的写入,也没发现个啥,看 0xe154bb77
image.png
这里的 0x450811A1 就是 T 表的值,发现在上面能够发现明文,2988 是 xy-platform-info 的一部分,继续往上面看
再往上看的时候,发现 T 表还是跟明文相加,803 刚好对应上面明文的一部分
image.png
那么会不会 T 表都是跟明文相加的啊,T 表的值很多的啊,64 个,每一个搜索出来还不一定只有一个值
那这里就可以写一个批量提取明文的脚本
image.png
好了已经提取出来了,但是这里得到的数据还是不对,无法得到 71DBB96E5C6CD9A710C189EFC86E6085,问了一下 ai
image.png
发现中间进行 Hmac,这玩意儿就相当于给 md5 加盐了,那继续问 ai 同志
image.png
同时让 ai 写了一个提取 ipad,opad 和 key 的脚本,运行就能得到
image.png
那没啥事情了,把数据填进去,发现能得到想要的结果了

  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
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import struct


class CustomMD5:
    INIT_A = 0x10325476
    INIT_B = 0x98BADCFE
    INIT_C = 0xEFCDAB89
    INIT_D = 0x67452301

    @staticmethod
    def left_rotate(x, c):
        return ((x << c) | (x >> (32 - c))) & 0xFFFFFFFF

    @staticmethod
    def FF(a, b, c, d, x, s, ac):
        f = ((c ^ d) & b) ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        return (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF

    @staticmethod
    def GG(a, b, c, d, x, s, ac):
        f = ((b ^ c) & d) ^ c
        res = (f + a + ac + x) & 0xFFFFFFFF
        return (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF

    @staticmethod
    def HH(a, b, c, d, x, s, ac):
        f = b ^ c ^ d
        res = (f + a + ac + x) & 0xFFFFFFFF
        return (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF

    @staticmethod
    def II(a, b, c, d, x, s, ac):
        f = (c ^ (b | ~d)) & 0xFFFFFFFF
        res = (f + a + ac + x) & 0xFFFFFFFF
        return (b + CustomMD5.left_rotate(res, s)) & 0xFFFFFFFF

    def __init__(self, message: bytes, total_bit_len: int):
        self.message = message
        self.total_bit_len = total_bit_len
        self.A = self.INIT_A
        self.B = self.INIT_B
        self.C = self.INIT_C
        self.D = self.INIT_D

    def hexdigest(self):
        message = bytearray(self.message)
        message.append(0x80)
        while len(message) % 64 != 56:
            message.append(0x00)
        message += struct.pack("<Q", self.total_bit_len)

        for offset in range(0, len(message), 64):
            chunk = message[offset : offset + 64]
            M = struct.unpack("<16I", chunk)
            A, B, C, D = self.A, self.B, self.C, self.D

            # ================= 第一轮 (0-15) =================
            A = self.FF(A, B, C, D, M[0], 6, 0xE9C9B756)
            D = self.FF(D, A, B, C, M[1], 13, 0xD71BA479)
            C = self.FF(C, D, A, B, M[2], 17, 0x241081DB)
            B = self.FF(B, C, D, A, M[3], 21, 0x681088D9)
            A = self.FF(A, B, C, D, M[4], 7, 0x9B14F7AF)
            D = self.FF(D, A, B, C, M[5], 12, 0xFF1F5BB1)
            C = self.FF(C, D, A, B, M[6], 17, 0x881CD7BE)
            B = self.FF(B, C, D, A, M[7], 20, 0x66666122)
            A = self.FF(A, B, C, D, M[8], 7, 0xF6666193)
            D = self.FF(D, A, B, C, M[9], 12, 0xA619639E)
            C = self.FF(C, D, A, B, M[10], 16, 0x49140921)
            B = self.FF(B, C, D, A, M[11], 22, 0xC11DCEEE)
            A = self.FF(A, B, C, D, M[12], 7, 0xF51C0FAF)
            D = self.FF(D, A, B, C, M[13], 13, 0x4717C62A)
            C = self.FF(C, D, A, B, M[14], 17, 0xA9104613)
            B = self.FF(B, C, D, A, M[15], 22, 0xFD169501)

            # ================= 第二轮 (16-31) =================
            A = self.GG(A, B, C, D, M[1], 5, 0xF6002500)
            D = self.GG(D, A, B, C, M[6], 9, 0x02741453)
            C = self.GG(C, D, A, B, M[11], 14, 0xD221E691)
            B = self.GG(B, C, D, A, M[0], 20, 0xE20011C9)
            A = self.GG(A, B, C, D, M[5], 5, 0x2261CDE6)
            D = self.GG(D, A, B, C, M[10], 9, 0xF2D50D97)
            C = self.GG(C, D, A, B, M[15], 14, 0x425A14ED)
            B = self.GG(B, C, D, A, M[4], 20, 0xA277E905)
            A = self.GG(A, B, C, D, M[9], 5, 0xF277A3F9)
            D = self.GG(D, A, B, C, M[14], 9, 0x626F12D9)
            C = self.GG(C, D, A, B, M[3], 14, 0x922A4C9A)
            B = self.GG(B, C, D, A, M[8], 20, 0xC040B340)
            A = self.GG(A, B, C, D, M[13], 5, 0x265E5A51)
            D = self.GG(D, A, B, C, M[2], 9, 0xE9100000)
            C = self.GG(C, D, A, B, M[7], 14, 0xD63F105D)
            B = self.GG(B, C, D, A, M[12], 20, 0xC35707D6)

            # ================= 第三轮 (32-47) =================
            A = self.HH(A, B, C, D, M[5], 4, 0xFFFC3942)
            D = self.HH(D, A, B, C, M[8], 11, 0x977CD691)
            C = self.HH(C, D, A, B, M[11], 16, 0xA4BCEA44)
            B = self.HH(B, C, D, A, M[14], 23, 0x4BDCCFA9)
            A = self.HH(A, B, C, D, M[1], 4, 0xBEBCBC70)
            D = self.HH(D, A, B, C, M[4], 11, 0x288C7EC6)
            C = self.HH(C, D, A, B, M[7], 16, 0xF6CC4B60)
            A = self.HH(A, B, C, D, M[13], 4, 0xD4EC1095)
            B = self.HH(B, C, D, A, M[10], 23, 0xEAAC27FA)
            C = self.HH(C, D, A, B, M[3], 16, 0xE6DC88E5)
            D = self.HH(D, A, B, C, M[0], 11, 0xD9DCD039)
            B = self.HH(B, C, D, A, M[6], 23, 0x048C1D05)
            A = self.HH(A, B, C, D, M[9], 4, 0x1FA27CF9)
            D = self.HH(D, A, B, C, M[12], 11, 0x6D9D6122)
            C = self.HH(C, D, A, B, M[15], 16, 0xC4AC5665)
            B = self.HH(B, C, D, A, M[2], 23, 0xFDE5391C)

            # ================= 第四轮 (48-63) =================
            A = self.II(A, B, C, D, M[0], 6, 0xF4292244)
            D = self.II(D, A, B, C, M[7], 10, 0xAB9423A7)
            C = self.II(C, D, A, B, M[14], 15, 0xF593A039)
            B = self.II(B, C, D, A, M[5], 21, 0x655B59C3)
            A = self.II(A, B, C, D, M[12], 6, 0x452AFF97)
            D = self.II(D, A, B, C, M[3], 10, 0xF5EF247D)
            C = self.II(C, D, A, B, M[10], 15, 0x85845DD1)
            B = self.II(B, C, D, A, M[1], 21, 0x850CCC92)
            A = self.II(A, B, C, D, M[8], 6, 0xF99926E0)
            D = self.II(D, A, B, C, M[15], 10, 0xF9997E4F)
            C = self.II(C, D, A, B, M[6], 15, 0xA9994314)
            B = self.II(B, C, D, A, M[13], 21, 0xC5537E82)
            A = self.II(A, B, C, D, M[4], 6, 0x450811A1)
            D = self.II(D, A, B, C, M[11], 10, 0x450811A6)
            C = self.II(C, D, A, B, M[2], 15, 0xBD3AF235)
            B = self.II(B, C, D, A, M[9], 21, 0xEB86D391)

            self.A = (self.A + A) & 0xFFFFFFFF
            self.B = (self.B + B) & 0xFFFFFFFF
            self.C = (self.C + C) & 0xFFFFFFFF
            self.D = (self.D + D) & 0xFFFFFFFF

        res = bytearray()
        res.extend(struct.pack("<I", self.A))
        res.extend(struct.pack("<I", self.B))
        res.extend(struct.pack("<I", self.C))
        res.extend(struct.pack("<I", self.D))
        return res.hex()


if __name__ == "__main__":
    # inner 第一块 = K xor ipad
    ipad_block = bytes.fromhex(
        "817df4c081ca3cb69222b594b114b3be0aacc42eb1bc0f11deaaf28b2d8b00d4"
        "6426fea808a41cf05b16f3ad7891e10a7985a9595507a55ffab9482a47ea63c8"
    )
    # outer 第一块 = K xor opad
    opad_block = bytes.fromhex(
        "eb179eaaeba056dcf848dffedb7ed9d460c6ae44dbd6657bb4c098e147e16abe"
        "0e4c94c262ce769a317c99c712fb8b6013efc3333f6dcf3590d322402d8009a2"
    )

    key = bytes(b ^ 0x36 for b in ipad_block)
    plaintext = (
        "/api/sns/v6/homefeedoid=homefeed_recommend&cursor_score=&geo=eyJsYXRpdHVkZSI6MC4wMDAwMDAsImxvbmdpdHVkZSI6MC4wMDAwMDB9%0A"
        "&trace_id=cdcdca3d-3aa7-32d3-ac40-22f3d73e4cdd&note_index=0&refresh_type=2&client_volume=0.32"
        "&known_signal=%7B%22session_id%22%3A%225193da8de11c45c489f06fd6502d4179-000%22%2C%22hp_con%22%3A0%2C%22hp_type%22%3A0"
        "%2C%22m_active%22%3A0%2C%22device_level%22%3A3%2C%22device_model%22%3A%22Pixel%203%20XL%22%2C%22nqe_level%22%3A1"
        "%2C%22brightness%22%3A6%2C%22battery%22%3A98%2C%22power_save_mode%22%3Afalse%2C%22notification_enable%22%3Atrue"
        "%2C%22volume%22%3A0.32%2C%22i18n%22%3A%7B%22region%22%3A%22CN%22%2C%22timezone%22%3A%22Asia%2FShanghai%22%7D"
        "%2C%22s_code%22%3A0%2C%22i28d%22%3A0%2C%22fin%22%3A0%2C%22fcn%22%3A0%2C%22fvin%22%3A0%2C%22fvcn%22%3A0%2C%22scnt%22%3A0"
        "%2C%22ug_user%22%3A%7B%22first_mf_time%22%3A0%2C%22last_act_time%22%3A1776268800000%2C%22reg_time%22%3A1776250262000"
        "%2C%22user_types%22%3A%5B1%5D%7D%2C%22ug_device%22%3A%7B%22device_types%22%3A%5B%5D%2C%22last_act_time%22%3A1776268800000"
        "%2C%22reg_time%22%3A1776250261000%7D%2C%22ugInstallFirstOpen%22%3A1%7D&unread_begin_note_id=&unread_end_note_id="
        "&unread_note_count=0&preview_ad=&preview_type=&loaded_ad=%7B%22ad_extra_info%22%3A%22%7B%5C%22last_impression_ad_id%5C%22"
        "%3A%5C%22%5C%22%2C%5C%22loaded_ad_count%5C%22%3A%5C%220%5C%22%7D%22%2C%22ads_id_list%22%3A%5B%5D%2C%22loaded_ad_pos_list%22"
        "%3A%5B%5D%2C%22loaded_ad_real_pos_list%22%3A%5B%5D%7D&home_ads_id=&feed_oaid=&user_action=0&personalization=1"
        "&is_break_down=0&orientation=portrait_split&last_card_position=-1&last_live_position=-1&last_live_id=&enable_live_shooting=false"
        "&enable_location_permission=falsefid=&gid=7cb6ae24c7605483a8ccb7edde8ef825baf6db7947359c6577b650a6&device_model=phone"
        "&tz=Asia%2FShanghai&channel=Vivo&versionName=9.24.0&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4a&platform=android"
        "&sid=session.1776320133450187008622&identifier_flag=4&cpu_abi=&nqe_score=&project_id=ECFAAF&x_trace_page_current=app_loading_page"
        "&lang=zh-Hans&app_id=ECFAAF01&uis=light&teenager=0&active_ctry=CN&cpu_name=&dlang=zh&data_ctry=CN&SUE=1&launch_id=1776320359"
        "&id_token=&device_level=&origin_channel=Vivo&overseas_channel=0&mlanguage=zh_cn&folder_type=none&auto_trans=0&t=1776320361"
        "&build=9240811&holder_ctry=CN&did=1c6dc4223567c0ca5bca77caa23a5b56&preload_build_type=038platform=android&build=9195803"
        "&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4aplatform=android&build=9240811&deviceId=e1dcca27-a4a5-39da-a2b2-a8329886cb4afs=1&point=0"
    ).encode()


    # inner = CustomMD5((K xor ipad) || plaintext)
    inner_data = ipad_block + plaintext
    inner_digest = CustomMD5(inner_data, (64 + len(plaintext)) * 8).hexdigest()
    print("inner_digest =", inner_digest)

    # outer = CustomMD5((K xor opad) || inner_digest)
    outer_data = opad_block + bytes.fromhex(inner_digest)
    final_digest = CustomMD5(outer_data, (64 + 16) * 8).hexdigest()
    print("final_digest =", final_digest)

运行结果如下:

1
2
inner_digest = 71dbb96e5c6cd9a710c189efc86e6085
final_digest = 31403731641e81a8bc2c6b7ca3beb6ee