某幸咖啡协议算法
数字加固的,FART就能脱干净

image-20241229133301349.png (45.72 KB, 下载次数: 0)
下载附件
2025-1-19 16:41 上传
这里是老版本的APP,没有抓包检测,直接进行协议逆向就是了。
关键代码函数定位:(这里是因为我已经抓取过才会直接判断key是sign和q的)
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:

image-20241229133726026.png (836.68 KB, 下载次数: 0)
下载附件
2025-1-19 16:41 上传
q:

image-20241229133905998.png (976.59 KB, 下载次数: 0)
下载附件
2025-1-19 16:41 上传
一个在7行,一个在14行,我们去看看这里的函数不过两个都是通过hashMap来定位的

image-20241229134234156.png (40.64 KB, 下载次数: 0)
下载附件
2025-1-19 16:41 上传
先来看q,因为sign值也需要传入q值。 String b2 = c.b(com.alibaba.fastjson.a.toJSONString(map));

image-20241229134513667.png (14.77 KB, 下载次数: 0)
下载附件
2025-1-19 16:41 上传

image-20241229134531631.png (25.6 KB, 下载次数: 0)
下载附件
2025-1-19 16:44 上传
在这里可以看到加密函数了,至于是AESwork,还是AESwork4Api,其实能够判断到是后者,因为前面进行了base64的decode,不过可以去HOOK一下this.f26285e.a();的返回值,可以看看条件判断走的哪

image-20241229141604264.png (15.95 KB, 下载次数: 0)
下载附件
2025-1-19 16:44 上传

image-20241229141721015.png (22.99 KB, 下载次数: 0)
下载附件
2025-1-19 16:44 上传

image-20241229141739212.png (18.56 KB, 下载次数: 0)
下载附件
2025-1-19 16:44 上传
找到了md5_crypto,也就是另外一个native方法
这里的字符串是加密的,我们通过主动调用去查看一下是哪个so加载进行的native
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); }); }

image-20241229150131073.png (330.85 KB, 下载次数: 0)
下载附件
2025-1-19 16:47 上传

image-20241229163015930.png (266.5 KB, 下载次数: 0)
下载附件
2025-1-19 16:47 上传
能够看到是ollvm的混淆,左下角也能看到对应的流程图,然后我们再去搜索了java_查看是不是静态注册的函数,结果发现并不是的

image-20241229163146844.png (31.81 KB, 下载次数: 0)
下载附件
2025-1-19 16:47 上传
这里去获取了一个可能为函数注册的函数数组

image-20241229163532299.png (288.67 KB, 下载次数: 0)
下载附件
2025-1-19 16:47 上传
这里有些代码是从如画那里改的,我为了确定要是在哪个动态注册的函数,所有使用闭包确保当前的 i 被正确捕获
struct JNINativeMethod { const char* name; // 0: 方法的名称(指向 C 字符串) const char* signature; // 1: 方法的签名(指向 C 字符串) void* fnPtr; // 2: 本地方法的实现函数指针(指向 C 函数) };这里获取到的 JNINativeMethod 是结构体,所以在获取指针的时候是这样写的
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)); if (RegisterNativesarray.length > 0) { for (let i = 0; i < RegisterNativesarray.length; i++) { // 使用闭包确保当前的 i 被正确捕获 (function(i) { Interceptor.attach(RegisterNativesarray[i], { onEnter: function (args) { console.log("come to addrRegisterNatives[" + i + "]"); // 输出正确的 i var env = args[0]; // jni对象 var java_class = args[1]; // 类 var class_name = Java.vm.tryGetEnv().getClassName(java_class); var taget_class = "com.luckincoffee.safeboxlib.CryptoHelper"; // 目标类名 if (class_name === taget_class) { console.log("\n[RegisterNatives] method_count:", args[3]); var methods_ptr = ptr(args[2]); var method_count = parseInt(args[3]); 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 } }

image-20241229164646477.png (1.5 MB, 下载次数: 0)
下载附件
2025-1-19 16:47 上传
这里我们通过了找到的这个动调注册函数的函数,并且对于这个函数进行了HOOK,在每一次进行函数的动态注册时就直接去打印对于的注册的函数以及对应的偏移地址,然后我们就去实现unidbg的算法复现。
unidbg环境 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); } }

image-20241229172358052.png (545.1 KB, 下载次数: 0)
下载附件
2025-1-19 16:49 上传
这里有一个小报错,是load dependency libandroid.so failed 这个依赖没有,这里别去找一个系统的so了,因为这个so也需要依赖其他的so,停不下来的
libandroid.so 的模拟:libandroid.so 是 Android 系统的核心库之一,提供了许多低级别的系统功能,如内存管理、文件操作、线程管理等。 可以确保这些基础库的符号(如 JNI、memcpy、malloc 等)被正确加载到内存中。我们这里使用new AndroidModule(emulator, vm).register(memory);直接去虚拟出来这个需要的API就可以了
AndroidModule 在 Unidbg 中的作用是模拟 Android 环境中的 .so 文件并加载相应的符号和依赖。通过 register(memory) 方法注册该模块,可以确保 Unidbg 模拟器能够正确加载相关的库并处理它们的符号。这里我们去尝试着去调用(首先是确定传入的参数是什么)刚刚在动态加密的加密函数localAESWork4Api,不过这里是通过的动态注册的方式来实现的函数注册,所以不能直接去通过静态注册的方式直接实现
// 获取 RegisterNatives 函数的内存地址,并赋值给addrRegisterNatives。 var RegisterNativesarray = []; var symbols = Module.enumerateSymbolsSync("libart.so"); for (var i = 0; i < symbols.length; i++) { var symbol = symbols[i]; 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[i], { onEnter: function (args) { console.log("come to addrRegisterNatives[" + i + "]"); // 输出正确的 i var env = args[0]; // jni对象 var java_class = args[1]; // 类 var class_name = Java.vm.tryGetEnv().getClassName(java_class); var taget_class = "com.luckincoffee.safeboxlib.CryptoHelper"; // 目标类名 if (class_name === taget_class) { console.log("\n[RegisterNatives] method_count:", args[3]); var methods_ptr = ptr(args[2]); var method_count = parseInt(args[3]); 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[0]; // 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[1].toInt32(); console.log(`localAESWork4Api called with i2: ${intParam}`); }, onLeave: function(retval) { // 输出返回值(可以根据需要进行分析) console.log(`localAESWork4Api returned: ${retval}`); } }); } } } } }); })(i); // 在此传入当前的 i } }

image-20250115155247381.png (758.53 KB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传
那么这里就去主动调用这个加密函数了,看看我们加密的之前和之后的结果是什么。
这里我们调用的这个函数是在我们HOOK动态注册函数时得到的结果,同时在函数加密结束之后再进行HOOK

image-20250119141456382.png (855.1 KB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传

image-20250119140658601.png (1.32 MB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传

image-20250119140824077.png (232.51 KB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传

image-20250119140915242.png (107.31 KB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传

image-20250119141603534.png (47.05 KB, 下载次数: 0)
下载附件
2025-1-19 16:51 上传
在加密函数中,其实虽然混入了混淆,但是其实也很明显得可以看到这里的加密过程,也不用想办法去除混淆。看着函数的名称大概率就是白盒的aes128,至于是CBC模式还是EBC模式或者是其他,就可以直接去尝试将输入设置和两组相同的明文,看密文是否是一样的来判断

image-20250119142110239.png (1010.95 KB, 下载次数: 0)
下载附件
2025-1-19 16:52 上传
在aes128_enc_wb_coff函数中,我们可以看到很多查表法的异或操作,其实就已经很像是aes的内部算法了,同时,我们要去实现密钥的破译,其实是需要进行DFA攻击的,那么就需要就找到故障注入的位置,所以要去明确加密过程

image-20250119142336042.png (79.73 KB, 下载次数: 0)
下载附件
2025-1-19 16:52 上传

image-20250119142603100.png (34.07 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传

image-20250119143034283.png (1.08 MB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传

image-20250119143117332.png (316.39 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
这里给出了部分的测试结果
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)

image-20250119143311567.png (66.05 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传

image-20250119143510935.png (70.7 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
k0:644A4C64434A69566E44764D394A5570

image-20250119144312375.png (46.13 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传

image-20250119144322903.png (220.16 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
这样是的定位也是通过动态注册找的

image-20250119151136963.png (1.42 MB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
按照同样的方法先去试试主动调用,看看我们的结果和标准有差别没,没差别就不用分析了

image-20250119151240125.png (37.17 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
首先是在208的位置找到了MD5的位置

image-20250119161337234.png (88.83 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
这里去HOOK一下看看参数:
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; } }); }

image-20250119161523736.png (475.57 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传

image-20250119161538421.png (433.83 KB, 下载次数: 0)
下载附件
2025-1-19 16:58 上传
可以看到这里,在我们输入的明文的后面添加了新的字符串进去,经过多次的尝试,发现其实是一样。不是随机生成的
dJLdCJiVnDvM9JUpsom9拿着这串新的数据去MD5,发现也不是正确的结果,那么就说明是有问题的。

image-20250119161825199.png (105.61 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119161914939.png (81.82 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119162547252.png (280.6 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119162559623.png (40.9 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传
在函数结束之后去查看encrypt digest encryptoText addr的地址

image-20250119162948215.png (1.56 MB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119163055008.png (169.51 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119163149453.png (54.67 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传

image-20250119163247359.png (71.8 KB, 下载次数: 0)
下载附件
2025-1-19 16:59 上传
用python复现一下:
import binascii def bytes_to_int(src, offset): """ 还原 C 语言中的 `bytesToInt` 函数, 将字节数组中从 offset 开始的 4 个字节转换为 uint32_t 整数. 参数: - src: MD5 结果的字节数组. - offset: 从哪个位置开始读取字节. 返回: - 返回转换后的 uint32_t 整数. """ # 按照 C 代码中的顺序来组合字节 value = (src[offset + 1] << 16) | (src[offset] << 24) | (src[offset + 2] << 8) | src[offset + 3] 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}")










查看全部评分