
样本地址
初步分析
是否加固
先看一下有没有壳,大厂一般是不会进行加壳的,但还是看一下
也确实发现没加固
抓包分析
直接抓的登录 login 的包,发现参数还挺多的
其中 x-mini-gid, x-mini-s1, x-mini-sig, x-mini-mua, shield 等应该就是加密参数了
本篇文章主要分析的也就是 shield 这个加密参数的生成
加密位置定位
直接 hook NewStringUTF 就能够定位到相关的位置,还是需要把 libmsaoaidsec.so 的检测给过掉
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())); } } });}然后就能够得到下面的堆栈信息了
到 jadx 中跳到这个堆栈中进行分析
网上有很多这样的分析了,直接说结论吧:
他会按照这样的顺序执行 Native.initializeNative()=>Native.initialize(this.token)=>Native.intercept(chain, this.cPtr)
unidbg 模拟执行
还是先搭个架子
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 这个方法
public void initializeNative(){ Native.callStaticJniMethodObject(emulator,"initializeNative()V");}然后报错,开始补环境
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)这个看起来跟日志相关,直接返回即可
@Overridepublic 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);}接着,报错
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)直接实例化这个类,将方法的调用传进去
@Overridepublic 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);}接着报下面的这个错误
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 即可
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,直接去补上
接着,报错
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 一下
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 结果如下:
[Pixel 3 XL::com.xingin.xhs ]-> Native.initialize is called: str=mainNative.initialize result=3624499008Native.initialize is called: str=mainNative.initialize result=3624496928可以看到,传进去的是”main”这个字符串,直接调用
public void initialize(){ cPtr = Native.callStaticJniMethodLong(emulator,"initialize(Ljava/lang/String;)J","main"); System.out.println(cPtr);}发现环境缺失了,继续
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 的持久化存储的数据
@Overridepublic 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);}接着报错
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 进行获取
直接补吧,值都出来了
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"); }
}接着,又报错了
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)这个直接补就行了
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 的值了,继续调用
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();}发现报错
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
接着,直接补就行了
case "okhttp3/Interceptor$Chain->request()Lokhttp3/Request;":{ return vm.resolveClass("okhttp3/Request").newObject(request);}接着,报错,补环境
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)这儿,直接补就行了
case "okhttp3/Request->url()Lokhttp3/HttpUrl;":{ return vm.resolveClass("okhttp3/HttpUrl").newObject(request.url());}然后,继续报错
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)直接补环境
case "okhttp3/HttpUrl->encodedPath()Ljava/lang/String;":{ HttpUrl httpUrl = (HttpUrl)dvmObject.getValue(); return new StringObject(vm,httpUrl.encodedPath());}继续报错
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 后面添加一个 ?
case "okhttp3/HttpUrl->encodedQuery()Ljava/lang/String;":{ HttpUrl httpUrl = (HttpUrl)dvmObject.getValue(); return new StringObject(vm,httpUrl.encodedQuery());}继续报错
java.lang.UnsupportedOperationException: okhttp3/Request->body()Lokhttp3/RequestBody; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417) at com.xhs.Shield.callObjectMethodV(Shield.java:187) at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)直接补上
case "okhttp3/Request->body()Lokhttp3/RequestBody;":{ return vm.resolveClass("okhttp3/RequestBody").newObject(request.body());}接着又报错了
java.lang.UnsupportedOperationException: okhttp3/Request->headers()Lokhttp3/Headers; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417) at com.xhs.Shield.callObjectMethodV(Shield.java:193) at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)直接补上
case "okhttp3/Request->headers()Lokhttp3/Headers;":{ return vm.resolveClass("okhttp3/Headers").newObject(request.headers());}继续报错
java.lang.UnsupportedOperationException: okio/Buffer-><init>()V at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:803) at com.github.unidbg.linux.android.dvm.AbstractJni.newObjectV(AbstractJni.java:758) at com.github.unidbg.linux.android.dvm.DvmMethod.newObjectV(DvmMethod.java:214)直接补上
@Overridepublic 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);}好了,又在接着报错了
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)直接补就行
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);}接着,他还有报错
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)那我接着补
@Overridepublic 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);}但是还是在报错
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)那就继续接着补
case "okhttp3/Headers->name(I)Ljava/lang/String;":{ Headers headers = (Headers)dvmObject.getValue(); return new StringObject(vm,headers.name(vaList.getIntArg(0)));}仍在报错
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)继续补
case "okhttp3/Headers->value(I)Ljava/lang/String;":{ Headers headers = (Headers)dvmObject.getValue(); return new StringObject(vm,headers.value(vaList.getIntArg(0)));}报错
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)继续补
@Overridepublic 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);}报错
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)继续补
case "okio/Buffer->clone()Lokio/Buffer;":{ Buffer buffer = (Buffer)dvmObject.getValue(); return vm.resolveClass("okio/Buffer").newObject(buffer.clone());}报错
java.lang.UnsupportedOperationException: okio/Buffer->read([B)I at com.github.unidbg.linux.android.dvm.AbstractJni.callIntMethodV(AbstractJni.java:563) 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)继续补
case "okio/Buffer->read([B)I":{ Buffer buffer = (Buffer)dvmObject.getValue(); return buffer.read((byte[]) vaList.getObjectArg(0).getValue());}报错
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)继续补
case "okhttp3/Request->newBuilder()Lokhttp3/Request$Builder;":{ Request request = (Request) dvmObject.getValue(); return vm.resolveClass("okhttp3/Request$Builder").newObject(request.newBuilder());}报错
java.lang.UnsupportedOperationException: okhttp3/Request$Builder->header(Ljava/lang/String;Ljava/lang/String;)Lokhttp3/Request$Builder; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417) 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)还是要继续补啊
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));}仍在报错
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)接着补
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);}还报错啊
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)直接补
case "okhttp3/Interceptor$Chain->proceed(Lokhttp3/Request;)Lokhttp3/Response;":{ return vm.resolveClass("okhttp3/Response").newObject(null);}继续报错
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)继续补就是
case "okhttp3/Response->code()I":{ return 200;}然后就能够得到结果了

算法分析
base64 编码
看一下 XYAAQABAAAAAEAAABTAAAAUzUWEe0xG1IbD9/c+qCLOlKGmTtFa+lG438MeOFeRawXxNDjzedlS52p+7Faz8MjiJx+gKBgEgwfGWaKbbP82ngygrWkkUvuxqUQQShXUDT1nUFV 这段密文是如何生成的
首先还是需要 trace 一份日志吧,这样如果后面分析不出来的时候,可以参照这个日志进行分析
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 函数
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,接着是剩余的部分的值
前面的这个 XY 是固定的,不用分析,主要还是看后面的这个部分,后面这段值的源地址是 0x123d8140,在 unidbg 中提供了一个 api,traceWrite 可以找到这个地址值的写入
emulator.traceWrite(0x123d8140, 0x123d8140 + 132);接着发现在 libxyass.so 的 0x4bd60 偏移的这些地方在不断的进行写入

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

尝试去 cyberChef 中看一下这个是否是标准的 base64 编码
发现并没有进行魔改,就是标准的 base64 编码
那接着对这 99 个字节进行分析,溯源
emulator.traceWrite(0x123e3070, 0x123e3070 + 99);发现在控制台,将这段数据分成了两个部分
前 0x10 个字节和后 0x53 个字节,那先来分析前 0x10 个字节吧
前 0x10 个字节分析
继续 trace,看一下写入
emulator.traceWrite(0xe4ffefb9L, 0xe4ffefb9L + 0x10);它的结果如下:

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

他的计算流程也就是下面这样的
v155 << 16 = 0x000400000x00040000 | 4 = 0x00040004bswap32(0x00040004): 字节: 00 04 00 04 翻转: 04 00 04 00结果 = 0x04000400这也就需要看 x16 这个值是从哪儿来的了
看一下 v155 的交叉引用
发现来自于 a4 这个参数,a4 就是当前这个子函数的参数啊

继续找他的交叉引用,发现只有一个,跳转过去
发现参数是 v5,又往上溯源
怕跟错了,继续 hook 一下这个子函数
emulator.attach().addBreakPoint(module.base+0x467DC);发现没跟错,继续
然后对 x2 寄存器的数据进行溯源
emulator.traceWrite(0xe4fff630L,0xe4fff630L+0x4);继续跳转
跳转到 0x4ae64,看不太懂
直接看汇编吧
将 w8 寄存器的值存到 x21 寄存器,但是 w8 寄存器的值又是从 x29 偏移 0xa8 这个地址处进行取的,hook 过去,发现 fp 的地址是 0xe4fff500,偏移 0xa8,地址就变成了 0xe4fff458

从 0xe4fff458 地址处取 32 位的值到 x8 寄存器,也就刚好是 0x00000004 即 0x4,继续 trace
跳转到 0x4a5dc,发现在这儿正好是在将 w9 寄存器的值放进 x29 寄存器偏移 0xa8 的位置啊,而 w9 寄存器的值来源于 x22 寄存器取值
继续 hook,得到
trace 一下 0x123e00c0 地址的来源,发现又来源于 memcpy
继续 trace

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

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

直接 hook 一下
emulator.attach().addBreakPoint(module.base+0x80A54);得到 x8 的地址为 0x12015c50,对应着在 ida 中的偏移就是 0x15c50,跳转过去
发现 q1就是固定的, .rodata 段的值是只读的
接下来看一下 q0 这个值,他跟 q1 不同的地方也就是 0x35,继续 trace 一下这段数据的生成
emulator.traceWrite(0xe4fff088L, 0xe4fff088L + 0x10);trace 结果如下:
跳转到 0x88714
这一堆看着头有点大,直接看 trace 日志吧
0x35 是从 w22 寄存器来的,接着需要找 w22 寄存器的值是在哪儿生成的,w22 寄存器的值是 w24 逻辑右移 18 位得到的
w24 又是 w22 和 w25 异或得到的结果,接着就需要看 w22 和 w25 寄存器的值是从哪儿来的了
首先看 w22 寄存器的值
w22 是 orr(按位或) w22 寄存器和 w24 寄存器的值得到的
首先看这个 0x66 吧,他是 w24 寄存器得到的值,往上看,是取的 "ldrb w24, [x24, x26]" x24=0x7a41ab7 x26=0xa5d5f84 => w24=0x66 这一行,取的地址也就是 0x12017a3b,即偏移 0x17a3b
又是 .rodata 段固定的值,有点怀疑这个 0x35 也是固定的值了,现在 w24 的值得到了,继续看 w22 寄存器的值,看 0xa1b97700 是怎么来的,继续看

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

0xa1b90000 是将 w24 的取低 8 位,插入到 w22 的 16到23 的位置,即
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 取的,是个只读的数据,固定的
继续看 0xb9,他是 0x12017a5e 取的,也是固定的,那所有的都是只读固定的,那 0x35 也是固定的了
第 5 ~8 字节
跳转到 0x4bbfc
这个值就是固定的,不需要分析了
第 9 字节
继续回到之前的位置,跳转到 0x4bc2c 的位置
这里可能是 ida 识别的问题,索引下标数字不对,这儿也是固定的
第 10~13 字节
跳转到 0x4bc34

来自于v126,v126 是反转的 v124 的值,v124 发现来自于 v160,总之跟 v160 有关系,寻找交叉引用
发现 v160 是 v25 的值,v25 又是由调用了 sub_1DD30(&v158, v22, v23) 这个函数的结果值,进去看一下这个函数干了什么
发现这个函数会进行 memcpy,好像是在进行拼接什么东西
去问一下 AI
也就是说 0x53 是拼接数据的长度,刚好后需要拼接一个长度为 0x53 的数据
第 14 ~16 字节
跳转到 0x4bc30,发现跟刚才第 10 到 13 字节数据挨着的
还是需要去寻找 v162 的交叉引用,发现到最后还是会调用 sub_1DD30 这个字符串拼接函数,说明跟上面的 0x53 是一样的,那就继续分析下面的东西,前 0x10 个字节的数据可以归结为都是固定的
后 0x53 个字节分析
RC4 算法
继续回到之前 memcpy 的地方
发现源地址是在 0x123e0060,trace 一下写入
跳转到 0x4b82c 看一下
0x35 是后面这一堆 *v66 ^ *((_DWORD *)&v164 + (unsigned __int8)(v76 + v73) + 2); 算出来的,还是异或,他在这个循环中每次会写入 8 个值,循环条件是 v70
在上面,看日志可以得到循环有 10 轮,每次写入 8 个字节,但是也才 80 字节啊,还有 3 个字节呢?当然往下面看还在写
看这个模式,逐字节异或有点像流密码的方式,或许可能是 rc4 算法
如果是 rc4 算法的话,会有 s 盒的初始化,密钥一些内容
用了一下小风大佬的魔改 unidbg,确实要省事许多,重新 trace 一份日志进行分析
从第二个字节分析一下吧,数据是如何生成的 0x16
最后就跳转到这儿了
而且看到很多从 1 开始进行一直排序的序列,很有可能这儿是在进行 S 盒初始化
跳转到 0x4b694,发现这一块应该就在做 S 盒初始化了

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

S 盒就是 X10 寄存器,在这块代码结束的地方断点,这个时候 S 盒应该初始化已经完成了
直接 hook 看一下
emulator.attach().addBreakPoint(module.base+0x4B6BC);看一下 x10 寄存器
这个时候已经初始化完成了,当 S 盒初始化完成之后,就应该密钥参数进行打乱 S 盒的顺序了
继续往下面看,S 盒参与到运算中了
让 ai 同志翻一下
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_idx4B6C8 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

可以知道 key 就是 std::abort() 了,那么明文呢?有没有魔改呢?有没有魔改看一下就行了,在最开始跳转到 0x4b82c 的地方应该就是 rc4 加密的核心逻辑了,rc4 加密的核心逻辑一般都是明文与密钥流进行异或操作
这里的这个 v66 就是明文了,hook 对应的汇编地址
哟西,明文也找到了,那去 cyberchef 中试一下呢
标准的,不用我操心其他的了,直接还原吧
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 个字节数据
首先还是先看一下这段明文的数据
x10=RW@0x123d80a0, md5=10d9b81db7889a6314a27620973ebda3, hex=00000001ecfaaf01000000020000000700000024000000103932343038313165316463636132372d613461352d333964612d613262322d61383332393838366362346131403731641e81a8bc2c6b7ca3beb6eesize: 830000: 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 ...$....9240811e0020: 31 64 63 63 61 32 37 2D 61 34 61 35 2D 33 39 64 1dcca27-a4a5-39d0030: 61 2D 61 32 62 32 2D 61 38 33 32 39 38 38 36 63 a-a2b2-a8329886c0040: 62 34 61 31 40 37 31 64 1E 81 A8 BC 2C 6B 7C A3 b4a1@71d....,k|.0050: BE B6 EE ...先 trace 以下,看是在哪儿写入的
emulator.traceWrite(0x123d80a0, 0x123d80a0 + 0x53);发现是通过 memcpy 过来的
source:RW@0x12411000, md5=657a70ce87e9ce48458f506290ee6138, hex=00000001ecfaaf01000000020000000700000024000000103932343038313165316463636132372d613461352d333964612d613262322d613833323938383663623461size: 670000: 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 ...$....9240811e0020: 31 64 63 63 61 32 37 2D 61 34 61 35 2D 33 39 64 1dcca27-a4a5-39d0030: 61 2D 61 32 62 32 2D 61 38 33 32 39 38 38 36 63 a-a2b2-a8329886c0040: 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
source:RW@0x123dd580, md5=95c44e390e50ca9292230fa9ae784d38, hex=00000001ecfaaf010000000200000007000000240000001039323430383131size: 310000: 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
去 trace 日志看一下
后面的 9240811 不用管,这个是版本号,固定的,而且版本号后面跟着的类似于 uuid 的东西,就是之前的 sDeviceId,主要看的也就是前面这一堆,跳转到 0x4b474
这发现也就是需要分析 v15 到 v20 的值,看看是在哪儿生成的
首先看 v15 吧
a1 是当前函数的参数,在他调用的时候直接传的 1 进来,是写死的,固定的

v16 这个值就是 sAppId 这个值补码的 16 进制表现形式
0xECFAAF01 = 3975851777 (unsigned)3975851777 - 2^32 = -319115519(sAppId)正好可以对上
v17 是根据前面的那个 a4 的值(就是前面分析的那个 0x35 和 0x31 异或的那个值)来判断的,如果 a4 有值,赋值 v17 为 2
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 个字节的位置
接着就需要找 q0 是在哪儿生成的
q0 是从 0xe4fff438 读出来的,继续找
发现这里是在 4 个字节 4 个字节写写入的,一共写 16 个字节,4 个字节,abcd,有可能是 hash 算法
然后一直向上找
发现是 add 加出来的,更怀疑是 md5 加密了,但是很有可能会进行魔改,先用一下小风的插件去看一下,发现一共有两次 md5
但是明文应该不是这个,看着不像,或许只能得到一部分明文的数据,也就是 sDeviceId+xy-scene,但是可以看到当前的 PC 寄存器保存的地址是 0x8189c,跳转过去
这里好像是在填充 0x80,这儿是在对明文进行处理吗?之前我做过关于 md5 的笔记的,看其他地方好像也是将 md5 的运算过程给拆散了,在 ida 中,他是将 md5 的 64 轮运算拆散了,直接看 ida 的话,感觉不是特别好还原,还是主要依靠 trace 日志进行分析吧
先拿一份标准的 md5 来进行对照吧,看改了哪些地方
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
先运行一下,看下状态

这个代码是标准 md5 的原因,可能这个样本的 md5 已经被深度魔改了,T 表,左移位数都搜索不到,那么他的明文一定会在 trace 日志中出现,所以也就是说一定能够搜索到,直接搜索的话效率有点低啊,可以使用正则的方式进行搜索 add.*0x6eb9db71.*=>.*,这样就只会搜索到 4 个,也就对应着在 md5 运算中的 4 大轮运算
直接找第一个,也就能够确定,这里是 4 轮运算中的第一大轮,但是不能确定是否是 16 小轮运算中的第 1 小轮
跳转过去对应着下面的日志
[libxyass.so 0x12000000+0x82a08] 0x12082a08: "add w8, w8, w4" ; w8=0xf12c7987 w4=0x6eb9db71 => w8=0x5fe654f8直接跳转到 0x82a08
这一块就很像非线性函数中的 FF 函数了,首先先来看一下 FF 函数,挨个来对应一下吧
@staticmethoddef 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 跳转,发现在另一个地方还会加一个数

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

所以,就能够推出相关信息了
*(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 = 0x2abdd689self.B = 0xe08fa1a5self.C = 0xd41eaea4self.D = 0x266582ff进行替换之后,发现第一小轮算出来的 b,还是不对,说明有可能 T 常量表给改了,或者常量转换的地方也改了
这一堆算出来的值是 0x5fe654f8,接着搜索,看看后面加的那个 T 表是什么, add.*0x5fe654f8,发现
第一个 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

看一下前面哪个流程错误了,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 的地方是固定的

直接让 ai 写了个 idapython 的脚本
"""IDAPython script to dump modified MD5 T-table from address 0x94910The table contains 64 elements, each 4 bytes (DWORD)First element should be 0xe9c9b756"""
import ida_bytesimport 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
# 执行dumpif __name__ == "__main__": t_table = dump_md5_t_table()接着就能够得到魔改过后的 T 表了
# 魔改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 写了脚本来提取左移的位数
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 写了一份提取左移位数表的脚本,提取出来的左移位数如下:
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 出现错误了
f = (f_old + a + self.T_TABLE[i] + M[g]) & 0xFFFFFFFFf_old正确,a正确,T表能搜到,正确,那只能出现在明文的填充上面出问题了去日志中看一下
它最后填充的明文是 0x280,这也能理解,在这里只是取的中间状态来进行 md5 加密,而这个样本中 md5 是有多轮的,所以填充出了问题
第二大轮魔改
接着到后面没啥问题,但是到了第 17 轮的时候,又不对了,又来继续看吧
f_old 是对的,能运算出来,f 不对,说明更新 f 的时候出错了,那就要看是 T 表的问题,还是 a 的问题,还是明文的问题,或者说是左移的位数不对,看一下这段汇编
[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 同志说这四轮两两交换了,结果最后得到了正确的值了
终于对了
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,如下:
self.A = 0x10325476self.B = 0x98badcfeself.C = 0xefcdab89self.D = 0x67452301寻找魔改 md5 的明文
因为 71DBB96E5C6CD9A710C189EFC86E6085 只是一个中间状态,还需要找一下他前面的明文是什么,我发现如果从第一轮的 T 表出去去搜索明文,我竟然搜索出来了 42 个,这我这么看下去,下辈子吧,但是有一点可以肯定的是,他是从构造请求 request 添加的一大段请求头中提取的,从之前的那个 dump 插件也可以零星的看一些出来
先来看一下之前构造 request 请求参数所添加的请求头吧
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 应该也会影响到结果
.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 值倒推明文
先将这串值进行分组,注意,需要用小端序
# 中间状态的md5值71DBB96E5C6CD9A710C189EFC86E6085A = 0x6eb9db71B = 0xa7d96c5cC = 0xef89c110D = 0x85606ec8这里的 abcd 值是中间算出来的,所以可以使用正则进行搜索 add.*=>.*0x6eb9db71
发现只有一个唉,跳过去
其中的 0x8d651ffa 不知道是啥,我往上面看他的写入,也没发现个啥,看 0xe154bb77
这里的 0x450811A1 就是 T 表的值,发现在上面能够发现明文,2988 是 xy-platform-info 的一部分,继续往上面看
再往上看的时候,发现 T 表还是跟明文相加,803 刚好对应上面明文的一部分
那么会不会 T 表都是跟明文相加的啊,T 表的值很多的啊,64 个,每一个搜索出来还不一定只有一个值
那这里就可以写一个批量提取明文的脚本
好了已经提取出来了,但是这里得到的数据还是不对,无法得到 71DBB96E5C6CD9A710C189EFC86E6085,问了一下 ai
发现中间进行 Hmac,这玩意儿就相当于给 md5 加盐了,那继续问 ai 同志
同时让 ai 写了一个提取 ipad,opad 和 key 的脚本,运行就能得到
那没啥事情了,把数据填进去,发现能得到想要的结果了
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¬e_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)运行结果如下:
inner_digest = 71dbb96e5c6cd9a710c189efc86e6085final_digest = 31403731641e81a8bc2c6b7ca3beb6ee分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 137 天
部分内容可能已过时
评论
还没有评论,来写第一条吧。