
1.准备
首先肯定是抓包啊,我抓的是这个 login 这个接口,它里面会携带一个 newSign 的参数
{ "cipherParam": "userName", "countryCode": 86, "loginToken": "", "newSign": "b119ee2cd2aa8e3a7bff1d18abf63027", "password": "ca8f119a27ec17f98b463807cd0b6b62", "platform": "android", "timestamp": "1766477585241", "type": "pwd", "userName": "df0751c510410bb57cd24c5e5dc90b86_1", "v": "5.75.5"}其他的好像除了 adi 不是固定的,其他都是固定的

这个 newSign 长度是 32 位,很有可能是 md5 或者 hmacMd5 之类的算法
hook 一下 newStringUTF 这个函数,如果是 so 层生成的算法,应该就会返回相应的结果,因为需要将 C 字符串转换成 java 字符串
var symbols = Module.enumerateSymbolsSync("libart.so");//2 定义一个变量,用来接收一会找到的NewStringUTF的地址var addrNewStringUTF = null;//3 循环找出libart.so中所有成员,匹配是NewStringUTF的函数,取出地址,赋值给上面的变量for (var i = 0; i < symbols.length; i++) { //3.1 取出libart.so的一个个方法对象 var symbol = symbols[i]; //3.2 判断方法对象的名字是不是包含 NewStringUTF和CheckJNI---》因为在真正底层,函数名不叫NewStringUTF,前后有别的字符串 // 实际它真正的名字:asdfa_NewStringUTF_dadsfasfd if (symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) { // 3.3 找到后,把地址赋值个上面的变量 addrNewStringUTF = symbol.address; // 3.4 控制台打印一下 console.log("NewStringUTF is at ", symbol.address, symbol.name); break }}// 4 如果不为空,我们开始hook它(通过地址hook,有onEnter和onExit,所有的参数都给了args,通过位置取到每个参数)if (addrNewStringUTF != null) { Interceptor.attach(addrNewStringUTF, { onEnter: function (args) { // 4.1 取出NewStringUTF传入的第一个参数 var c_string = args[1]; // 4.2 第一个参数是c的字符串,我们把它转一下,变成真正的字符串 var dataString = c_string.readCString(); // 4.3 改字符串不为空,且长度为32,我们输出一下,并且打印出它的调用栈 if (dataString) { // 只需要改这里后期 console.log(dataString); // 4.4 读取当前在执行那个so文件,及so文件中的地址 console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n'); // 4.5 打印调用栈 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new())); }
} });}但是当我 hook 上去之后,发现并没有返回哪个 md5 值,那有没有可能是 so 层生成的一串密文,然后 Java 层得到这串密文进行的 md5 的结果呢?可以用 hook 脚本看一下是否在 java 层生成的 md5 加密结果

发现确实是在 Java 层生成的 md5 值
那接着就需要找这个明文是在哪儿生成的,再次 hook newStringUTF
得到了它的调用栈
dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5RSg82smLag2bANscc9kl6AYGRYCtwcvmKHI5NH8DB84aGmjSiqtgZBmq19mDo6H/vq4W20lY8vhiPbLZjRm7v0nbhKx0wBmg5NvC3qt4bxAA0JZKeGf/03ivn7QFFKJo6J2QjBBVujmzF1vg4Bdhhgg==0x6f01f83ab8 frida-agent-64.so!0x1eaab80x6f01f83618 frida-agent-64.so!0x1ea6180x6f01f83618 frida-agent-64.so!0x1ea618
java.lang.Throwable at com.shizhuang.duapp.common.helper.ee.DuHelper.encodeByte(Native Method) at lte.NCall.IL(Native Method) at com.shizhuang.duapp.common.helper.ee.DuHelper.doWork(Unknown Source:18) at lte.NCall.IL(Native Method) at com.shizhuang.duapp.common.utils.RequestUtils.e(Unknown Source:28) at lte.NCall.IL(Native Method) at com.shizhuang.duapp.common.helper.net.interceptor.HttpRequestInterceptor.intercept(Unknown Source:18) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1) at cb.b.intercept(SourceFile:130) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:10) at okhttp3.internal.http.RealInterceptorChain.proceed(SourceFile:1)去看一下 encodeByte 这个方法,它是在 native 层的方法,果然

frida hook 一下
Java.perform(function () { var DuHelper = Java.use("com.shizhuang.duapp.common.helper.ee.DuHelper"); function bytesToString(byteArray) { if (byteArray === null) return "null"; try { return Java.use("java.lang.String").$new(byteArray); } catch (e) { return "decode error"; } } DuHelper["encodeByte"].implementation = function (bArr, str) { console.log(`DuHelper.encodeByte is called: bArr=${bytesToString(bArr)}`); console.log(`DuHelper.encodeByte str=${str}`); let result = this["encodeByte"](bArr, str); console.log(`DuHelper.encodeByte result=${result}`); return result; };})得到的结果如下:
bArr=cipherParamuserNamecountryCode86loginTokenpasswordca8f119a27ec17f98b463807cd0b6b62platformandroidtimestamp1766477470308typepwduserNamedf0751c510410bb57cd24c5e5dc90b86_1uuid8271c326e36d0dc4v5.75.5DuHelper.encodeByte str=010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100DuHelper.encodeByte result=dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==参数 1,data 里面的值直接拼接起来,参数 2 这些 01 字符串好像是固定的,不固定再说 hook 一下 registerNative 看一下是在哪个 so 中注册的
==========================
来自:<libdewuhelper.so>函数名字:encodeByte函数签名:([BLjava/lang/String;)Ljava/lang/String;当前函数绝对地址:0x6ee2afa724偏移地址:0x1724
==========================2.Unidbg 模拟调用
package com.dewu;
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.*;import com.github.unidbg.linux.android.dvm.array.ByteArray;import com.github.unidbg.memory.Memory;
import java.io.File;
public class DeWu extends AbstractJni implements IOResolver { private final AndroidEmulator emulator; private final VM vm; private final Module module; private final DvmClass DuHelper;
public DeWu() { // 创建模拟器实例(根据 SO 架构选择 32/64 位,这里使用 64 位) emulator = AndroidEmulatorBuilder.for64Bit() .setProcessName("com.shizhuang.duapp") .build();
// 获取内存操作接口 final Memory memory = emulator.getMemory(); // 设置 Android 版本(23 对应 Android 6.0) memory.setLibraryResolver(new AndroidResolver(23));
// 创建 Dalvik 虚拟机(可传入 APK 文件进行解析) vm = emulator.createDalvikVM(new File("apks/dewu/得物5.75.5.apk")); vm.setJni(this); // 设置 JNI 接口 vm.setVerbose(true); // 开启详细日志(调试时建议打开)
// 加载目标 SO DalvikModule dm = vm.loadLibrary("dewuhelper", true); dm.callJNI_OnLoad(emulator); // 调用 JNI_OnLoad
module = dm.getModule();
// 获取目标 Java 类 DuHelper = vm.resolveClass("com/shizhuang/duapp/common/helper/ee/DuHelper"); }
public String encodeByte(byte[] bArr, String str) { DvmObject<?> result = DuHelper.callStaticJniMethodObject( emulator, "encodeByte([BLjava/lang/String;)Ljava/lang/String;", new ByteArray(vm, bArr), new StringObject(vm, str) );
return result.getValue().toString(); }
public static void main(String[] args) { DeWu deWu = new DeWu();
String newSign = deWu.encodeByte( "cipherParamuserNamecountryCode86loginTokenpasswordca8f119a27ec17f98b463807cd0b6b62platformandroidtimestamp1766477470308typepwduserNamedf0751c510410bb57cd24c5e5dc90b86_1uuid8271c326e36d0dc4v5.75.5".getBytes(), "010110100010001010010010000011000111001011101010101000101110111010011010101101101010001000101100010110100010001010011010110011001111001011100010101000100100110010110010100010101011110010111100" );
System.out.println("NewSign:: " + newSign); }
@Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println("file open::"+pathname); return null; }}这个参数直接返回结果了
JNIEnv->NewStringUTF("dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==") was called from RX@0x1200185c[libdewuhelper.so]0x185cNewSign:: dWWoXlbR3K87j2N27Dkv4uOPUnOsh0xrJ5t+atsiQXC+/dIN8Kof9Gm2x1kil7S/8E3s013q5Me8muLoeGr7kDHtlbjkJRJP84YRY4V2sjuZnELPxA8zqBmuSkbZUY5R6UalVRwH6x+GSNamBQsKTwEzP7gb2kPhj7/7yLrvQ95ipDatc7p/W38oetoOdXRWRISLjDrLzA9PnQhm+Lk2r/jKJYnSxepP8bY/ZX4WU0XFAXAehqQ1BJBFGjsSnetxvzAHF4+DtgFTBeeRX1EtTA==而且这个参数我每次运行,它的结果都是一样的,说明并没有时间戳或者随机数这些随机因子的参与
我把 so 放到 ida 中,发现直接打不开,是 so 加壳了

先用大佬的脚本把 so dump 下来,接着使用 soFixer 进行脱壳修复
function dump_so(so_name) { Java.perform(function () { var currentApplication = Java.use("android.app.ActivityThread").currentApplication(); var dir = currentApplication.getApplicationContext().getFilesDir().getPath(); var libso = Process.getModuleByName(so_name);
console.log("[name]:", libso.name); console.log("[base]:", libso.base); console.log("[size]:", ptr(libso.size)); console.log("[path]:", libso.path);
var file_path = dir + "/" + libso.name + "_" + libso.base + "_" + ptr(libso.size) + ".so"; var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) { Memory.protect(ptr(libso.base), libso.size, 'rwx'); var libso_buffer = ptr(libso.base).readByteArray(libso.size); file_handle.write(libso_buffer); file_handle.flush(); file_handle.close(); console.log("[dump]:", file_path); } });}
dump_so("libdewuhelper.so");接着进行修复
.\SoFixer-Windows-64.exe -s libdewuhelper.so_0x6ed92e7000_0x7000.so -o libdewuhelper_fix.so -m 0x6ed92e7000 -d将修复过后的 so 文件拖到 ida 中看一下吧,发现现在就可以正常打开了,使用 ida 的插件 findcrypt 过一下

发现这个不是很复杂,点 AES 的进去看一下
这么好心吗,符号都没去

debugger.addBreakPoint(module.base+0x2EC4);看一下参数 1 和参数 2 代表什么
mx0
>-----------------------------------------------------------------------------<[20:21:12 851]x0=RW@0x1231a000, md5=02696f4b9754b0aa1bf0b8668de15e38, hex=636970686572506172616d757365724e616d65636f756e747279436f646538366c6f67696e546f6b656e70617373776f72646361386631313961323765633137663938623436333830376364306236623632706c6174666f726d616e64726f696474696d657374616d70313736363437size: 1120000: 63 69 70 68 65 72 50 61 72 61 6D 75 73 65 72 4E cipherParamuserN0010: 61 6D 65 63 6F 75 6E 74 72 79 43 6F 64 65 38 36 amecountryCode860020: 6C 6F 67 69 6E 54 6F 6B 65 6E 70 61 73 73 77 6F loginTokenpasswo0030: 72 64 63 61 38 66 31 31 39 61 32 37 65 63 31 37 rdca8f119a27ec170040: 66 39 38 62 34 36 33 38 30 37 63 64 30 62 36 62 f98b463807cd0b6b0050: 36 32 70 6C 61 74 66 6F 72 6D 61 6E 64 72 6F 69 62platformandroi0060: 64 74 69 6D 65 73 74 61 6D 70 31 37 36 36 34 37 dtimestamp176647^-----------------------------------------------------------------------------^mx1
>-----------------------------------------------------------------------------<[20:21:15 101]x1=RW@0x12319000, md5=e608ea25e8d9a43a3bcbdfaa22e60d30, hex=64323435613062613864363738613631000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000size: 1120000: 64 32 34 35 61 30 62 61 38 64 36 37 38 61 36 31 d245a0ba8d678a610010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................^-----------------------------------------------------------------------------^参数 1 是明文啊,就是传进去的参数,参数 2 就是 AES 的 key,没有 iv,ECB 模式
那 d245a0ba8d678a61 大概率是 key 了,刚好 AES128,16 个字节

接着看一下 userName 和 password,这个直接在 Java 层加密的,之前的 hook 脚本就能够体现到
这个是 userName

这个是 password,就一个 md5 加密

分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
最后更新于 ,距今已过 201 天
部分内容可能已过时
评论
还没有评论,来写第一条吧。