chenchenchen777 发表于 2025-1-19 17:02

某幸咖啡协议算法

本帖最后由 chenchenchen777 于 2025-1-19 17:09 编辑

## 某幸咖啡协议算法

数字加固的,FART就能脱干净

抓包结果:

```apl
POST /resource/m/user/login h2
Host: capi.lkcoffee.com
content-type: application/x-www-form-urlencoded
content-length: 989
accept-encoding: gzip
cookie: uid=1b97928d-4f49-4608-a8cf-fe1c68c79ca31735372261909
user-agent: okhttp/3.10.0

sign=41353263181759105410239409212020611079&q=8pR_VhJIrWrz_tZtV6BCHViaW-VcsIem1HE6z7xNRYyVtK9SqWgrrgfb3iFkKIEGsfrSjR3dQhXSTLVGCRHEigsSIniYTUc1GbWaPMS1lOY9vZ2x3z0mK7gNza9lU9h8N-SzBmY1heejAwpKAaGGZXcpEjvXKst3PxekVny9uCmrt0fR9QtBPjVeKuG6m30HPQ_w5pNwRmJY2om_-ilphzED6B5rgds3pueiqbdGUeR4QY_YtsT6c4S-envoPapy_4N_UKjFQQNrX6W2k1uVUhOICA2exoewlwX8kardXpC1U9iXi60LOiH9l5UW7dEyD-U-mF1GTiD0Nm6yCw9ujurcONQ8EMqCWYNor05CgCVQbgsDLfVPq6jb4HhaYZhTMfJFtfFTjcUsGUDpvTbbwcErFNfpXCgpWMTpMLskGw-WQTSxNu2aR_Ksm1vRraRbZmKgp87DpaKLF7e2AfngErqxikJBS6MVCSP_1JAVAHR6_mmXNoIT4RE91bjUmrE7m6URIOXtanYGhad5kY00nTlZLMiP89hRZ-bWWt-pdSM6O3SVGMuQgY2HVPgF9N2jBFAHlExu_GhNfXV68g_qF6sHALbWHgdJ_KD17xvIcJHoiYbpEYk7I_GlIsZrf-F_9-ZHoaebmnPXCu__MeQuzm4yWsEukX3PE1-J2BW9Qpqtf1tQj9QU0yLO9oqBKk2UMWt6_ZNGMPQTi8uiqd_H15xb3NmhyOR5HmmOLd5AY9_4byqPXorZpwib32EhmV-C6KFYlrb4g_m0H9p6GoIAdOBHe4WFXhFs9TPr5zFiLQ8TrT5tdHk2R1tZmQyKovAsIPTi4-rxjQzCM5u0V9sWf65GI0YFnkwRU9PKWaulUQg%3D&uid=1b97928d-4f49-4608-a8cf-fe1c68c79ca31735372261909&cid=210101
```

这里是老版本的APP,没有抓包检测,直接进行协议逆向就是了。

关键代码函数定位:(这里是因为我已经抓取过才会直接判断key是sign和q的)

```js
function get_maincode() {
    function showstack(){
      console.log(
            Java.use("android.util.Log").getStackTraceString(
                Java.use("java.lang.Throwable").$new()
                )
            );
      }
    Java.perform(function(){
      var textUtils = Java.use("android.text.TextUtils")
      textUtils.isEmpty.overload('java.lang.CharSequence').implementation=function(a) {
            showstack();
            console.log("textUtils.isEmpty arg:.implementation:" + a);
            return this.isEmpty(a);
      };
      var hashMap = Java.use("java.util.HashMap");//HOOK系统函数HashMap去实现打印
      hashMap.put.implementation = function(key, value) {
            console.log("hashMap.put key: " + key + " value: " + value);
            if(key.equals("sign")|| key.equals("q")){
                showstack();
            }
            return this.put(key, value);
      }
    });
}
```

**sign:**



**q:**

这里的q是在isEmpty和hashMap都能得到堆栈信息,能够看到的是在sign的位置是在

```
com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:14)
com.lucky.lib.http2.AbstractLcRequest.getRequestParams(SourceFile:7)
```

一个在7行,一个在14行,我们去看看这里的函数不过两个都是通过hashMap来定位的


### 先来看q,因为sign值也需要传入q值。

```
String b2 = c.b(com.alibaba.fastjson.a.toJSONString(map));
```




在这里可以看到加密函数了,至于是AESwork,还是AESwork4Api,其实能够判断到是后者,因为前面进行了base64的decode,不过可以去HOOK一下this.f26285e.a();的返回值,可以看看条件判断走的哪


最后发现是Native的函数,同时我们再去看看sign值的位置,看看函数往哪走


顺着 return c.f26578b.a(a(hashMap));



找到了md5_crypto,也就是另外一个native方法

这里的字符串是加密的,我们通过主动调用去查看一下是哪个so加载进行的native

```js
function decrypto_str(input){
    Java.perform(function(){
      var Class = Java.use('com.stub.StubApp');
      
      // 检查是否传入参数
      if (!input) {
            console.log('没有提供参数!');
            return;
      }

      var result = Class.getString2(input);
      console.log('调用结果: ' + result);
    });
}
```

加载的是cryptoDD的so



能够看到是ollvm的混淆,左下角也能看到对应的流程图,然后我们再去搜索了java_查看是不是静态注册的函数,结果发现并不是的


那就是动态注册的函数了,那我们要先去找到对应的动态注册的函数。

```js
var RegisterNativesarray= [];
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols;
    if (symbol.name.indexOf("art") >= 0 &&symbol.name.indexOf("JNI") >= 0 && symbol.name.indexOf("RegisterNatives") >= 0
      ) {
      RegisterNativesarray.push(symbol.address);
      console.log("RegisterNatives is at ", symbol.address, symbol.name);
      continue;
    }
}
```

这里去获取了一个可能为函数注册的函数数组


是有两个的,那么我们去遍历这两个函数,看看是不是有对应去动态注册的函数

```java
jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
```

这里有些代码是从如画那里改的,我为了确定要是在哪个动态注册的函数,所有使用闭包确保当前的 i 被正确捕获

```apl
struct JNINativeMethod {
    const char* name;       // 0: 方法的名称(指向 C 字符串)
    const char* signature;// 1: 方法的签名(指向 C 字符串)
    void* fnPtr;            // 2: 本地方法的实现函数指针(指向 C 函数)
};
```

这里获取到的 JNINativeMethod 是结构体,所以在获取指针的时候是这样写的

```js
for (var j = 0; j < method_count; j++) {// 这里使用另一个 i 作为方法索引
                            var name_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3));
                            var sig_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize));
                            var fnPtr_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize * 2));
```

```js
if (RegisterNativesarray.length > 0) {
    for (let i = 0; i < RegisterNativesarray.length; i++) {
      // 使用闭包确保当前的 i 被正确捕获
      (function(i) {
            Interceptor.attach(RegisterNativesarray, {
                onEnter: function (args) {
                  console.log("come to addrRegisterNatives[" + i + "]"); // 输出正确的 i
                  var env = args;      // jni对象
                  var java_class = args; // 类
                  var class_name = Java.vm.tryGetEnv().getClassName(java_class);
                  var taget_class = "com.luckincoffee.safeboxlib.CryptoHelper"; // 目标类名
                  if (class_name === taget_class) {
                        console.log("\n method_count:", args);
                        var methods_ptr = ptr(args);
                        var method_count = parseInt(args);
                        for (var j = 0; j < method_count; j++) {// 这里使用另一个 i 作为方法索引
                            var name_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3));
                            var sig_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize));
                            var fnPtr_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize * 2));
                            var name = Memory.readCString(name_ptr);
                            var sig = Memory.readCString(sig_ptr);
                            var find_module = Process.findModuleByAddress(fnPtr_ptr);
                            var offset = ptr(fnPtr_ptr).sub(find_module.base);
                            console.log('class_name:', class_name, "name:", name, "sig:", sig, 'module_name:', find_module.name, "offset:", offset);
                        }
                  }
                }
            });
      })(i);// 在此传入当前的 i
    }
}
```


这里打印出来就可以发现是第二个RegisterNatives。RegisterNatives is at0xe3ab15ad _ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi

```apl
method_count: 0x4
class_name: com.luckincoffee.safeboxlib.CryptoHelper name: localAESWork sig: ([BI[B)[B module_name: libcryptoDD.so offset: 0x1984d
class_name: com.luckincoffee.safeboxlib.CryptoHelper name: localConnectWork sig: ([B[B)[B module_name: libcryptoDD.so offset: 0x1978d
class_name: com.luckincoffee.safeboxlib.CryptoHelper name: md5_crypt sig: ([BI)[B module_name: libcryptoDD.so offset: 0x1a981
class_name: com.luckincoffee.safeboxlib.CryptoHelper name: localAESWork4Api sig: ([BI)[B module_name: libcryptoDD.so offset: 0x1b1cd
```

这里我们通过了找到的这个动调注册函数的函数,并且对于这个函数进行了HOOK,在每一次进行函数的动态注册时就直接去打印对于的注册的函数以及对应的偏移地址,然后我们就去实现unidbg的算法复现。

## unidbg环境

```java
package com.luckycoffee;

import com.alibaba.fastjson.util.IOUtils;
import com.bytedance.frameworks.core.encrypt.TTEncrypt;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Emulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.HookStatus;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.arm.context.Arm32RegisterContext;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.DebuggerType;
import com.github.unidbg.hook.HookContext;
import com.github.unidbg.hook.ReplaceCallback;
import com.github.unidbg.hook.hookzz.*;
import com.github.unidbg.hook.xhook.IxHook;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.XHookImpl;
import com.github.unidbg.linux.android.dvm.AbstractJni;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.DvmClass;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.array.ByteArray;
import com.github.unidbg.memory.Memory;
import com.github.unidbg.utils.Inspector;
import com.github.unidbg.virtualmodule.VirtualModule;
import com.github.unidbg.virtualmodule.android.AndroidModule;
import com.sun.jna.Pointer;

import java.io.File;

public class Luck extends AbstractJni {
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass CryptoHelper;
    private final boolean logging;
    Luck(boolean logging) {
      this.logging = logging;
      emulator = AndroidEmulatorBuilder.for32Bit()
                .setProcessName("com.lucky.luckyclient")
                .addBackendFactory(new Unicorn2Factory(true))
                .build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
      final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
      memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
      vm = emulator.createDalvikVM(new File("E:\\android\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\luckycoffee\\110_4b45e34150992a9236e68e10bc69c35c.apk")); // 创建Android虚拟机
      vm.setJni(this);
      vm.setVerbose(logging); // 设置是否打印Jni调用细节
      //      new AndroidModule(emulator, vm).register(memory);
      DalvikModule dm = vm.loadLibrary(new File("E:\\android\\unidbg-master\\unidbg-android\\src\\test\\java\\com\\luckycoffee\\libcryptoDD.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
      dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
      module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
      CryptoHelper = vm.resolveClass("com/luckincoffee/safeboxlib");
    }
    public static void main(String[] args) throws Exception {
      Luck test = new Luck(true);
    }
}
```



这里有一个小报错,是load dependency libandroid.so failed 这个依赖没有,这里别去找一个系统的so了,因为这个so也需要依赖其他的so,停不下来的

```apl
libandroid.so 的模拟:libandroid.so 是 Android 系统的核心库之一,提供了许多低级别的系统功能,如内存管理、文件操作、线程管理等。 可以确保这些基础库的符号(如 JNI、memcpy、malloc 等)被正确加载到内存中。
```

我们这里使用new AndroidModule(emulator, vm).register(memory);直接去虚拟出来这个需要的API就可以了

```apl
AndroidModule 在 Unidbg 中的作用是模拟 Android 环境中的 .so 文件并加载相应的符号和依赖。通过 register(memory) 方法注册该模块,可以确保 Unidbg 模拟器能够正确加载相关的库并处理它们的符号。
```

这里我们去尝试着去调用(首先是确定传入的参数是什么)刚刚在动态加密的加密函数localAESWork4Api,不过这里是通过的动态注册的方式来实现的函数注册,所以不能直接去通过静态注册的方式直接实现

```js
// 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。
var RegisterNativesarray= [];
var symbols = Module.enumerateSymbolsSync("libart.so");
for (var i = 0; i < symbols.length; i++) {
    var symbol = symbols;
    if (symbol.name.indexOf("art") >= 0 &&
      symbol.name.indexOf("JNI") >= 0 &&
      symbol.name.indexOf("RegisterNatives") >= 0
      ) {
      RegisterNativesarray.push(symbol.address);
      console.log("RegisterNatives is at ", symbol.address, symbol.name);
      continue;
    }
}
if (RegisterNativesarray.length > 0) {
    for (let i = 0; i < RegisterNativesarray.length; i++) {
      // 使用闭包确保当前的 i 被正确捕获
      (function(i) {
            Interceptor.attach(RegisterNativesarray, {
                onEnter: function (args) {
                  console.log("come to addrRegisterNatives[" + i + "]"); // 输出正确的 i
                  var env = args;      // jni对象
                  var java_class = args; // 类
                  var class_name = Java.vm.tryGetEnv().getClassName(java_class);
                  var taget_class = "com.luckincoffee.safeboxlib.CryptoHelper"; // 目标类名
                  if (class_name === taget_class) {
                        console.log("\n method_count:", args);
                        var methods_ptr = ptr(args);
                        var method_count = parseInt(args);
                        for (var j = 0; j < method_count; j++) {// 这里使用另一个 i 作为方法索引
                            var name_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3));
                            var sig_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize));
                            var fnPtr_ptr = Memory.readPointer(methods_ptr.add(j * Process.pointerSize * 3 + Process.pointerSize * 2));
                            var name = Memory.readCString(name_ptr);
                            var sig = Memory.readCString(sig_ptr);
                            var find_module = Process.findModuleByAddress(fnPtr_ptr);
                            var offset = ptr(fnPtr_ptr).sub(find_module.base);
                            console.log('class_name:', class_name, "name:", name, "sig:", sig, 'module_name:', find_module.name, "offset:", offset);
                            if(name.indexOf("localAESWork4Api") != -1){
                              console.log(`Found target method: ${name}`);
                              Interceptor.attach(fnPtr_ptr, {
                                    onEnter: function(args) {
                                        // 获取第一个参数 byte[] bArr
                                        var byteArray = args; // byte[] 类型参数
                                        // 获取第二个参数 int i2,假设它是数组的长度(根据实际情况调整)
                                    
                                       
                                        // 打印 byte[] 内存地址及长度
                                        console.log(`localAESWork4Api called with bArr (Memory Address): ${byteArray}, length: ${200}`);
                                       
                                        // 使用 Frida 的 hexdump 打印内存中的字节内容
                                        try {
                                          var byteArrayMemory = Memory.readByteArray(byteArray, 200);
                                          console.log("localAESWork4Api called with bArr (Hexdump):");
                                          console.log(hexdump(byteArray, { length: 200, offset: 0 }));
                                        } catch (e) {
                                          console.error(`Error reading byte array: ${e.message}`);
                                        }
                                       
                                        // 输出第二个参数 int i2
                                        var intParam = args.toInt32();
                                        console.log(`localAESWork4Api called with i2: ${intParam}`);
                                    },
                                    onLeave: function(retval) {
                                        // 输出返回值(可以根据需要进行分析)
                                        console.log(`localAESWork4Api returned: ${retval}`);
                                    }
                              });
                            }
                        
                        
                        }
                  }
                }
            });
      })(i);// 在此传入当前的 i
    }
}
```


这里的最后一个参数的int值是加密的mode,不是对应的传入的长度,所以我随便固定了200,发现其实这里的结果固定

那么这里就去主动调用这个加密函数了,看看我们加密的之前和之后的结果是什么。


这里我们调用的这个函数是在我们HOOK动态注册函数时得到的结果,同时在函数加密结束之后再进行HOOK

```java
    public void call_localAESWork4Api(){
      ArrayList<Object> list_arg = new ArrayList<>(10);
      list_arg.add(vm.getJNIEnv());//Env
      list_arg.add(0);//jclass
      String str = "chenchenchen777";
      byte[] bytes = str.getBytes();
      ByteArray byteArray = new ByteArray(vm, bytes);
      list_arg.add(vm.addLocalObject(byteArray));//str
      list_arg.add(0);
      Number number = module.callFunction(emulator, 0x1b1cd, list_arg.toArray());
      ByteArray object = vm.getObject(number.intValue());
      String encodeToString = Base64.getEncoder().encodeToString(object.getValue());
      System.out.println("AES encode result: " + encodeToString);
    }
    public void call_encryptdata(){
      attach.addBreakPoint(module.base+0x17BD4,new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext context = emulator.getContext();
                Inspector.inspect("encrypt plainText addr :", (int) context.getPointerArg(0).peer);
                Inspector.inspect("encrypt plainText len :", (int) context.getPointerArg(1).peer);
                Inspector.inspect("encrypt encryptoText addr :", (int) context.getPointerArg(2).peer);
                attach.addBreakPoint(context.getLRPointer().peer,new BreakPointCallback() {
                  @Override
                  public boolean onHit(Emulator<?> emulator, long address) {
                        Inspector.inspect("Already encrypted",0x00);
                        return false;
                  }
                });
                return true;
            }
      });
    }
```



其实这里我们都能看到的是,这里的加密存储的位置是连续的


同时这里的加密结果也和cyber结果一样




在加密函数中,其实虽然混入了混淆,但是其实也很明显得可以看到这里的加密过程,也不用想办法去除混淆。看着函数的名称大概率就是白盒的aes128,至于是CBC模式还是EBC模式或者是其他,就可以直接去尝试将输入设置和两组相同的明文,看密文是否是一样的来判断


其实是可以看到每组之间的加密是互不影响的,先确实EBC。

在aes128_enc_wb_coff函数中,我们可以看到很多查表法的异或操作,其实就已经很像是aes的内部算法了,同时,我们要去实现密钥的破译,其实是需要进行DFA攻击的,那么就需要就找到故障注入的位置,所以要去明确加密过程


在185行的位置,我们找到了关键代码

我们故障注入的位置其实是在八轮列混合之后,九轮列混合之前,那么在第九轮的shiftRows的就可以作为我们故障注入的位置。通过判断调用函数的次数来明确我们混淆的轮数,来实现故障。

```java
public void inline_Hook(){
    num = 0;
    attach.addBreakPoint(module.base+0x154E8,new BreakPointCallback() {
      @Override
      public boolean onHit(Emulator<?> emulator, long address) {
            RegisterContext context = emulator.getContext();
            num++;
            Inspector.inspect("num :",num);
            if(num%9==0){
                Inspector.inspect("already to 9 \n The state addr :",(int) context.getPointerArg(0).peer);
                UnidbgPointer pointer = memory.pointer((int) context.getPointerArg(0).peer);
                byte randomByte = (byte) randInt(0, 0xff); // 随机生成一个字节值
                pointer.setByte(randInt(0, 15), randomByte);
            }
            return true;
      }
    });
}
// 生成指定范围内的随机整数
public static int randInt(int min, int max) {
    return (int) (Math.random() * (max - min + 1)) + min;
}
```



最后,我们通过多次的故障来约束对应的密钥的范围实现密钥的破解

这里给出了部分的测试结果

```py
import phoenixAES

# 定义要填充到tracefile的密文数据
cipher_data = """8b9099231f07c8e957b975cd9bea70a8
8be49923ad07c8e957b975959beabea8
8b4199234007c8e957b9756d9bea03a8
8b0699231b07c8e957b975de9bea4ca8
8bf599232d07c8e957b975ad9beab2a8
8bf599232d07c8e957b975ad9beab2a8
8b0b99235007c8e957b975ba9bea6ba8
8b8c99231207c8e957b9750a9bea22a8
8b719923c407c8e957b975169bea23a8
8b379923b907c8e957b975ed9bea77a8
8be599231507c8e957b9752c9bea87a8
8b8c99239c07c8e957b975819bea66a8
8b3999237507c8e957b975659beaa7a8
8b8999238b07c8e957b975c49bea06a8
8bda9923bd07c8e957b975bd9bea5ca8
8be39923c607c8e957b975589bea98a8
8b0b99235007c8e957b975ba9bea6ba8
8bf599232d07c8e957b975ad9beab2a8
8b5399231c07c8e957b9759f9bea47a8
8b0699231b07c8e957b975de9bea4ca8
8b8c99231207c8e957b9750a9bea22a8
8b619923b007c8e957b9754d9bea17a8
"""

# 将密文数据转换为小写,并写入文件
with open('tracefile11', 'wb') as t:
    # 将密文转换为小写并编码为utf-8写入文件
    t.write(cipher_data.strip().lower().encode('utf-8'))

# 调用phoenixAES的crack_file函数
phoenixAES.crack_file('tracefile11', [], True, False, 3)
```


通过这样去实现每个字节的破解

```apl
k10:869D92BBB700D0D25BD9FD3E224B5DF2
```


```apl
k0:644A4C64434A69566E44764D394A5570
```



可以看到的是这里cyber的加密结果和函数调用加密的结果是一样的,只是在unidbg里面是大写的

### sign的分析:

```java
    public void call_android_native_md5(){
      ArrayList<Object> list_arg = new ArrayList<>(10);
      list_arg.add(vm.getJNIEnv());//Env
      list_arg.add(0);//jclass
      String str = "chenchenchen777";
      byte[] bytes = str.getBytes();
      ByteArray byteArray = new ByteArray(vm, bytes);
      list_arg.add(vm.addLocalObject(byteArray));//str
      list_arg.add(1);
      Number number = module.callFunction(emulator, 0x01A981, list_arg.toArray());
      ByteArray object = vm.getObject(number.intValue());
      String result = new String(object.getValue());
      System.out.println("md5 encode result: " + result);
    }
```

这样是的定位也是通过动态注册找的



按照同样的方法先去试试主动调用,看看我们的结果和标准有差别没,没差别就不用分析了


有差别就要分析。

首先是在208的位置找到了MD5的位置

这里去HOOK一下看看参数:

```java
public void inline_Hook_md5(){
      attach.addBreakPoint(module.base + 0x13E3C, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext context = emulator.getContext();
                Inspector.inspect("encrypt md5 plainText addr :", (int) context.getPointerArg(0).peer);
                Inspector.inspect("encrypt md5 plainText len :", (int) context.getPointerArg(1).peer);
                Inspector.inspect("encrypt digest encryptoText addr :", (int) context.getPointerArg(2).peer);
                attach.addBreakPoint(context.getLRPointer().peer,new BreakPointCallback() {
                  @Override
                  public boolean onHit(Emulator<?> emulator, long address) {
                        Inspector.inspect("Already encrypted:",0x01);
                        return false;
                  }
                });
                return false;
            }
      });
    }
```


可以看到这里,在我们输入的明文的后面添加了新的字符串进去,经过多次的尝试,发现其实是一样。不是随机生成的

```apl
dJLdCJiVnDvM9JUpsom9
```

拿着这串新的数据去MD5,发现也不是正确的结果,那么就说明是有问题的。


在239行的位置发现了doMD5sign函数,有点像关键函数


里面是调用了MD5的,还有一些像是bytesToInt的转换。这里调用的MD5,先看看是不是正常的MD5。这里的md5的第三个参数是返回值的地址



可以看到,这里是标准的MD5加密。然后就再去HOOK一下doMD5sign函数,看看这里的返回值是什么,(这里的第三个参数,是一个二级指针。存放的是返回值的地址。)

```java
    public void inline_Hook_doMD5sign(){
      attach.addBreakPoint(module.base + 0x014D54, new BreakPointCallback() {
            @Override
            public boolean onHit(Emulator<?> emulator, long address) {
                RegisterContext context = emulator.getContext();
                Inspector.inspect("encrypt md5 plainText addr :", (int) context.getPointerArg(0).peer);
                Inspector.inspect("encrypt md5 plainText len :", (int) context.getPointerArg(1).peer);
                Inspector.inspect("encrypt digest encryptoText addr :", (int) context.getPointerArg(2).peer);
                attach.addBreakPoint(context.getLRPointer().peer,new BreakPointCallback() {
                  @Override
                  public boolean onHit(Emulator<?> emulator, long address) {
                        Inspector.inspect("Already encrypted:",0x01);
                        return false;
                  }
                });
                return false;
            }
      });
    }
```

在函数结束之后去查看encrypt digest encryptoText addr的地址


这里是小端序存储的地址,可以看到这里的结果就是我们加密的结果


那么说明在MD5之后进行了数据的再加密,那么还得往下看


能够发现的是这里的结果其实全是bytesToInt之后的拼接的结果。


通过D-810简单的去除一点混淆之后其实就可以看到最后一个位置的加密加密过程了,其实也就是位运算的结果。

用python复现一下:

```py
import binascii

def bytes_to_int(src, offset):
    """
    还原 C 语言中的 `bytesToInt` 函数, 将字节数组中从 offset 开始的 4 个字节转换为 uint32_t 整数.

    参数:
    - src: MD5 结果的字节数组.
    - offset: 从哪个位置开始读取字节.

    返回:
    - 返回转换后的 uint32_t 整数.
    """
    # 按照 C 代码中的顺序来组合字节
    value = (src << 16) | (src << 24) | (src << 8) | src
    return value

# 示例 MD5 字符串(十六进制表示)
md5_hex = "5648eabd41c3a29e9ff3edb5dceda568"

# 将 MD5 十六进制字符串转换为字节数组
md5_bytes = binascii.unhexlify(md5_hex)

# 初始化结果字符串
result = ""

# 循环处理 MD5 字节数组中的每个 4 字节段
for i in range(0, len(md5_bytes), 4):
    result += str(bytes_to_int(md5_bytes, i))

print(f"Final result: {result}")

```
这里的两个抓包过程中的q以及sign就可以分析完成了,可以直接通过分析复现协议算法。
本文章中所有内容仅供学习交流使用,不用于其他任何目的,擅自使用本文讲解的技术而导致的任何意外,与作者不负责

shinian0buwan 发表于 2025-1-19 20:03

咖啡协议,可以

Clarence210 发表于 2025-1-19 21:11

贴这么多图 不容易

BruceLii 发表于 2025-1-19 21:32

好硬核的帖子啊{:1_921:}

trix0101 发表于 2025-1-19 22:46

厉害啊,原来是这样

amwquhwqas128 发表于 2025-1-19 23:30

这个协议可以,非常厉害

amin120 发表于 2025-1-20 08:32

厉害啊,原来是这样

Lty20000423 发表于 2025-1-20 08:43

难得见到这么用心的帖子

gaozhihao0329 发表于 2025-1-20 09:33

有点硬核啊

mingkongk 发表于 2025-1-20 10:15

光贴这么多图都感觉很牛逼了
页: [1] 2 3
查看完整版本: 某幸咖啡协议算法