app登录协议逆向分析
随*记app
版本 v13.2.21.0
一.过frida检测
hook dlopen看加载了哪些so
function hook_dlopen(soName) {
Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
}
})
Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
}, onLeave: function(retval){
console.log(`dlopen onLeave: ${this.fileName}`)
}
})
}
hook_dlopen()

程序在dlopen enter libmsaoaidsec.so 之后,dlopen leave libmsaoaidsec.so之前终止,检测的操作应该就是在libmsaoaidsec.so中,并且是在init函数中进行的
因此需要hook call_constructors 去做进一步的定位
要进行frida检测就需要创建线程,在hook call_constructors 后,进一步去hook pthread_create
let is_hook_pthread_create = false;
function hook_pthread_create(soName){
if(is_hook_pthread_create) {
return;
}
let module = Process.findModuleByName(soName);
if (module) {
Interceptor.attach(Module.findExportByName('libc.so','pthread_create'),{
onEnter(args){
// do something
let func_addr = args[2]
console.log(`The thread Called function offset is: ${func_addr.sub(module.base)}`)
}
})
is_hook_pthread_create = true;
}
}
let is_hook_call_constructors = false;
function hook_call_constructors(soName) {
if(is_hook_call_constructors) {
return;
}
let symbols = Process.findModuleByName("linker64").enumerateSymbols()
let call_constructors = null;
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
// __dl__ZN6soinfo17call_constructorsEv
if(symbol.name.indexOf("call_constructors") != -1){
call_constructors = symbol.address;
}
}
if (call_constructors) {
Interceptor.attach(call_constructors, {
onEnter: function (args) {
// do something
hook_pthread_create()
}
});
is_hook_call_constructors = true;
}
}
function hook_dlopen(soName) {
Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
}
})
Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`dlopen onLeave: ${this.fileName}`)
}
})
}
hook_dlopen("libmsaoaidsec.so")
init中创建的线程函数有三个,分别在偏移0x1c544,0x1b8d4,0x26e5c处,relpace这三个函数为空
function hook_dlopen(soName) {
Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
//hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
pass_frida_check()
}
})
Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
//hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`dlopen onLeave: ${this.fileName}`)
pass_frida_check()
}
})
}
let is_pass_frida_check = false;
function pass_frida_check() {
if (is_pass_frida_check) {
//console.log('Function already hooked, skipping...');
return;
}
let secmodule = Process.findModuleByName("libmsaoaidsec.so");
if (secmodule) {
Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
is_pass_frida_check = true; // Mark as hooked
}
}

绕过检测了
二.抓包与关键函数定位
登录过程抓包

有两个我们感兴趣的键password和sign
java层自吐算法没有获得结果
hook libart中的jni函数
获得sign,在libme.so!_Z5getDgP7_JNIEnvP7_jclassP8_jobjecthhi+0x118中
[GetStringUTFChars] result:businessType: moduleType:base tag:Networker message:--> GET https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 http/1.1
<-- 403 https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 (4999ms, 91-byte body)
extraData:null fullReport:false
0x6f565486cc libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120
获得password,在libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120中
[GetStringUTFChars] result:businessType: moduleType:base tag:Networker message:--> GET https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 http/1.1
<-- 403 https://auth.feidee.net/v2/oauth2/authorize?encode_version=v2&password=0fd69633c48a5c2a095ee5b50daac234&grant_type=password&scope=accountbook%3Buser&username=18559707719 (4999ms, 91-byte body)
extraData:null fullReport:false
0x6f565486cc libmarsxlog.so!Java_com_tencent_mars_xlog_Xlog_logWrite2+0x120
定制系统trace

sign字符串由com.mymoney.util.DGUtil.getDG方法返回

password字符串由com.mymoney.util.AESEncryptUtil.aesEncrypt方法返回
hook验证
function hook_java() {
Java.perform(function () {
let AESEncryptUtil = Java.use("com.mymoney.util.AESEncryptUtil");
AESEncryptUtil["aesEncrypt"].implementation = function (str, i2) {
console.log(`AESEncryptUtil.aesEncrypt is called: str=${str}, i2=${i2}`);
let result = this["aesEncrypt"](str, i2);
console.log(`AESEncryptUtil.aesEncrypt result=${result}`);
return result;
};
let DGUtil = Java.use("com.mymoney.util.DGUtil");
DGUtil["getDG"].implementation = function (context, z, z2, i2) {
console.log(`DGUtil.getDG is called: context=${context}, z=${z}, z2=${z2}, i2=${i2}`);
let result = this["getDG"](context, z, z2, i2);
console.log(`DGUtil.getDG result=${result}`);
return result;
};
})
}

验证完毕
接下来确定这两个Native方法在哪个so中
对于动态注册的native方法,hook RegisterNatives,对于静态注册的native方法hook dlsym
先尝试hook RegisterNatives
function find_RegisterNatives(params) {
let symbols = Module.enumerateSymbolsSync("libart.so");
let addrRegisterNatives = null;
for (let i = 0; i < symbols.length; i++) {
let symbol = symbols[i];
//_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodi
if (symbol.name.indexOf("art") >= 0 &&
symbol.name.indexOf("JNI") >= 0 &&
symbol.name.indexOf("RegisterNatives") >= 0 &&
symbol.name.indexOf("CheckJNI") < 0) {
addrRegisterNatives = symbol.address;
console.log("RegisterNatives is at ", symbol.address, symbol.name);
hook_RegisterNatives(addrRegisterNatives)
}
}
}
let is_hook_RegisterNatives = false;
function hook_RegisterNatives(addrRegisterNatives) {
if (is_hook_RegisterNatives) {
return;
}
if (addrRegisterNatives != null) {
Interceptor.attach(addrRegisterNatives, {
onEnter: function (args) {
console.log("[RegisterNatives] method_count:", args[3]);
let java_class = args[1];
//let class_name = Java.vm.tryGetEnv().getClassName(java_class);
let methods_ptr = ptr(args[2]);
let method_count = parseInt(args[3]);
for (let i = 0; i < method_count; i++) {
let name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));
let sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));
let fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize * 2));
let name = Memory.readCString(name_ptr);
let sig = Memory.readCString(sig_ptr);
let _module = Process.findModuleByAddress(fnPtr_ptr);
let baseAddr = _module.base;
console.log("[RegisterNatives] name:", name, "sig:", sig, "libName:", Process.findModuleByAddress(fnPtr_ptr).name, " fnOffset:", fnPtr_ptr.sub(baseAddr), " callee:", DebugSymbol.fromAddress(this.returnAddress));
//console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "libName:", Process.findModuleByAddress(fnPtr_ptr).name, " fnOffset:", fnPtr_ptr.sub(baseAddr), " callee:", DebugSymbol.fromAddress(this.returnAddress));
}
}
});
is_hook_RegisterNatives = true;
}
}
let is_hook_dlsym = false;
function hook_dlsym() {
if (is_hook_dlsym) {
return;
}
let dlsymAddr = Module.findExportByName(null, "dlsym");
Interceptor.attach(dlsymAddr, {
onEnter: function (args) {
this.funcName = args[1].readCString();
},
onLeave: function (retval) {
let module = Process.findModuleByAddress(retval);
if (module) {
console.log(module.name, this.funcName, retval.sub(module.base));
}
}
})
is_hook_dlsym = true;
}
function hook_dlopen(soName) {
Interceptor.attach(Process.findModuleByName("libandroid.so").getExportByName("android_dlopen_ext"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`android_dlopen_ext onEnter: ${this.fileName}`)
//hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`android_dlopen_ext onLeave: ${this.fileName}`)
pass_frida_check()
find_RegisterNatives()
}
})
Interceptor.attach(Process.findModuleByName("libdl.so").getExportByName("dlopen"), {
onEnter: function (args) {
this.fileName = args[0].readCString()
console.log(`dlopen onEnter: ${this.fileName}`)
//hook_call_constructors(soName)
}, onLeave: function(retval){
console.log(`dlopen onLeave: ${this.fileName}`)
pass_frida_check()
find_RegisterNatives()
}
})
}
let is_pass_frida_check = false;
function pass_frida_check() {
if (is_pass_frida_check) {
//console.log('Function already hooked, skipping...');
return;
}
let secmodule = Process.findModuleByName("libmsaoaidsec.so");
if (secmodule) {
Interceptor.replace(secmodule.base.add(0x1c544), new NativeCallback(function () {
console.log(`hook_sub_1c544 >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
Interceptor.replace(secmodule.base.add(0x1b8d4), new NativeCallback(function () {
console.log(`hook_sub_1b8d4 >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
Interceptor.replace(secmodule.base.add(0x26e5c), new NativeCallback(function () {
console.log(`hook_sub_26e5c >>>>>>>>>>>>>>>>> replace`);
}, 'void', []));
is_pass_frida_check = true; // Mark as hooked
}
}


均在libme.so中
三.算法逆向(AES逆向与差分故障攻击,MD5逆向)
1.unidbg 模拟AES算法执行
首先分析aesEncrypt函数
反编译aesEncrypt,执行的两个最关键的函数是

其中8AE0似乎初始化了一个很大的数组v21
9E28实际上有三个参数,包括输入的字节数组和输出结果缓冲区,unidbg hook这个函数验证
package com.kanxue.test2;
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.backend.Backend;
import com.github.unidbg.arm.backend.ReadHook;
import com.github.unidbg.arm.backend.UnHook;
import com.github.unidbg.arm.context.RegisterContext;
import com.github.unidbg.debugger.BreakPointCallback;
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.memory.Memory;
import com.github.unidbg.memory.MemoryBlock;
import com.github.unidbg.pointer.UnidbgPointer;
import com.github.unidbg.utils.Inspector;
import unicorn.Arm64Const;
import unicorn.ArmConst;
import java.awt.*;
import java.io.*;
import java.util.Random;
public class MyMoney extends AbstractJni{
private final AndroidEmulator emulator;
private final VM vm;
private final Module module;
private final boolean logging;
private final DvmClass AESEncryptUtil;
MyMoney(boolean logging) {
this.logging = logging;
emulator = AndroidEmulatorBuilder.for64Bit().setProcessName("com.mymoney").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/kanxue/test2/MyMoney.apk")); // 创建Android虚拟机
vm.setJni(this);
//vm.setVerbose(logging); // 设置是否打印Jni调用细节
DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/java/com/kanxue/test2/libme.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
// 加载声明native方法的类
AESEncryptUtil = vm.resolveClass("com.mymoney.util.AESEncryptUtil");
}
void destroy() throws IOException {
emulator.close();
if (logging) {
System.out.println("destroy");
}
}
public static void main(String[] args) throws Exception {
MyMoney test = new MyMoney(true);
test.callFunc();
test.destroy();
}
void callFunc() {
emulator.attach().addBreakPoint(module.base + 0x4261C, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
Number number2 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X2);
byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);
byte[] bytes2 = emulator.getMemory().pointer(number2.longValue()).getByteArray(0, 32);
Inspector.inspect(bytes1, "1:");
Inspector.inspect(bytes2, "2:");
return true;
}
});
StringObject retVal = AESEncryptUtil.callStaticJniMethodObject(emulator, "aesEncrypt(Ljava/lang/String;I)Ljava/lang/String;",
"123456789ABCDEF", 0); // 执行Jni方法
System.out.println(retVal.getValue());
}
}

注意到在输入明文少于16字节时输出为16字节,而在明文等于或多于16字节时输出为32字节,说明明文应当是做了填充的,且至少填充一个分组
同时,重复一个明文分组,例如明文为“0123456789ABCDEF0123456789ABCDEF”时,得到密文“C2226C9369A87C2FC370D04C29323794C2226C9369A87C2FC370D04C293237941058C97167132CE1FCBC1643544D010B”
密文中出现重复分组,说明加密模式是ECB的。接着进一步分析sub_9E28
有如下关键部分,做简单的分析可知sub_9D8C的第二个参数与明文有关,第三个是密文缓冲区,hook sub_9D8C


注意到明文被填充了一个字节01,填充方式就明白了
进入函数sub_9D8C

这里的逻辑较为简单,仅有两个函数sub_8FD4和sub_9D14,其中v9和明文有关,hook这两个函数
emulator.attach().addBreakPoint(module.base + 0x8FD4, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
byte[] bytes = emulator.getMemory().pointer(number0.longValue()).getByteArray(0, 32);
byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);
Inspector.inspect(bytes, "0x8FD4 0:");
Inspector.inspect(bytes1, "0x8FD4 1:");
return true;
}
});
emulator.attach().addBreakPoint(module.base + 0x9D14, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X0);
Number number1 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X1);
Number number2 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X2);
byte[] bytes = emulator.getMemory().pointer(number0.longValue()).getByteArray(0, 32);
byte[] bytes1 = emulator.getMemory().pointer(number1.longValue()).getByteArray(0, 32);
byte[] bytes2 = emulator.getMemory().pointer(number2.longValue()).getByteArray(0, 32);
Inspector.inspect(bytes, "0x9D14 0:");
Inspector.inspect(bytes1, "0x9D14 1:");
Inspector.inspect(bytes2, "0x9D14 2:");
return true;
}
});
sub_8FD4处的v9
sub_9D14处的v9

说明加密是在sub_8FD4中进行的,进一步分析sub_8FD4
函数进行了主要三部分的操作
第一部分,对明文和最开始初始化的大缓冲区进行操作,疑似进行初始密钥加的操作

第二部分进行了10轮操作,必然就是轮函数了

最后一部分给state赋值

2.直接获取主密钥
这里的AES似乎不是查表法,我们尝试直接获取主密钥,转到a1[520]的汇编代码处
取a1[520]对应如下汇编

hook该地址,查看a1数组
emulator.attach().addBreakPoint(module.base + 0x903C, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X20);
byte[] bytes = emulator.getMemory().pointer(number0.longValue() + 520).getByteArray(0, 32);
Inspector.inspect(bytes, "a1[520] 0:");
return true;
}
});

结果错误,正确的结果应该是“C2226C9369A87C2FC370D04C293237941058C97167132CE1FCBC1643544D010B”

尝试改变字节顺序,B1取作第1个字节,B5取作第2个字节,B9取作第3个字节,B13取作第4个字节,B2取作低5个字节。。。即取作“262A2824484A44474834383637252654”
结果对应上了

3.差分故障攻击获取主密钥
轮函数的位置我们也已经获取到了,再来尝试一下差分故障攻击
首先确定hook点以找到正确的攻击时机
就选择在轮函数的while处


hook 9338处,在进行第8轮的轮函数后,改变state
public void dfaAttack() {
emulator.attach().addBreakPoint(module.base + 0x9338, new BreakPointCallback() {
int count = 0;
UnidbgPointer pointer;
@Override
public boolean onHit(Emulator<?> emulator, long address) {
count += 1;
pointer = emulator.getMemory().pointer(addr);
if (count % 10 == 8) {
pointer.setByte(randInt(0, 15), (byte) randInt(0, 0xff));
}
return true;
}
});
}
当然还要先确定state的位置,由最后一部分的state保存的位置可以推导,轮函数中的state保存在如下汇编的X19处

addr保存state的地址,供dfaAttack使用
emulator.attach().addBreakPoint(module.base + 0x92B8, new BreakPointCallback() {
@Override
public boolean onHit(Emulator<?> emulator, long address) {
Number number0 = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_X19);
addr = number0.longValue();
return true;
}
});
dfaAttack前的结果795F567258F4110E73D1F74998E1EC7B
dfaAttack后的结果4E5F567258F4116373D1BE499888EC7B
符合在第8此列混淆后,第9轮列混淆前的结果,进行多次差分攻击,将结果进行phoenixAES crack。注意trace_file的第一行为正确密文
795FE072587E110E8DD1F74998E1EC16
795F377258F0110ED5D1F74998E1ECA7
7981567234F4110E73D1F78C98E19E7B
795F56F658F47A0E7351F749B3E1EC7B
245F567258F411C473D1234998B6EC7B
795F56A958F47E0E73CCF74912E1EC7B
795F6372580F110E3BD1F74998E1EC5C
795F567E58F4820E73EBF74988E1EC7B
795F560258F4C40E736DF74943E1EC7B
795F117258E3110E4DD1F74998E1ECF0
E25F567258F411EF73D1C749982BEC7B
799B56728CF4110E73D1F7CA98E1A87B
。。。
import phoenixAES
phoenixAES.crack_file('trace_file', [], True, False, 3)

得到第10轮密钥后,再反推主密钥,WBAES得到如下结果
.\WBAES.exe 7977097D71EE280461F122CEB112BE57 10
K00: 262A2824484A44474834383637252654
K01: 18DD08BE50974CF918A374CF2F86529B
K02: 5EDD1CAB0E4A505216E9249D396F7606
K03: F2E573B9FCAF23EBEA460776D3297170
K04: 5F4622DFA3E9013449AF06429A867732
K05: 0BB30167A85A0053E1F506117B737123
K06: A41027460C4A2715EDBF210496CC5027
K07: AF43EBD6A309CCC34EB6EDC7D87ABDE0
K08: F5390AB75630C67418862BB3C0FC9653
K09: 5EA9E70D08992179101F0ACAD0E39C99
K10: 7977097D71EE280461F122CEB112BE57
K00: 262A2824484A44474834383637252654 与上面直接获取的主密钥一致
ex: 轮函数快速定位
AES的加密必然要进行一定的轮运算,执行路径必然呈现出一定的周期性,用如下代码追踪指令执行
public void traceAESCode() {
emulator.getBackend().hook_add_new(new CodeHook() {
@Override
public void hook(Backend backend, long address, int size, Object user) {
long now = emulator.getBackend().reg_read(Arm64Const.UC_ARM64_REG_PC).longValue();
if ((now > module.base) & (now < (module.base + module.size))) {
System.out.println(now - module.base);
}
}
@Override
public void onAttach(UnHook unHook) {
}
@Override
public void detach() {
}
}, module.base, module.base + module.size, null);
}
结果由以下python代码处理
import matplotlib.pyplot as plt
import numpy
input = numpy.loadtxt("trace.txt", int)
plt.ylim(30000, 51000)
plt.plot(range(len(input)), input)
plt.show()
发现明显的10轮循环周期,想必这就是轮函数的位置了

定位如图的框选部分可定位到轮函数中

4.frida hook
接下来是函数getDg
第一个参数是Context,不知道里面有啥
public static native String getDG(Context p0,boolean p1,boolean p2,int p3);


分析不出来什么东西,查看ida反编译代码

尝试直接hook sub_C8F8,因为看着和返回结果直接相关
hook 代码
function print_ptr(addr) {
let module = Process.findRangeByAddress(addr);
if (module) return hexdump(addr) + "\n";
return ptr(addr) + "\n";
}
function hook_native_addr(funcPtr, paramsNum) {
let module = Process.findModuleByAddress(funcPtr);
Interceptor.attach(funcPtr, {
onEnter: function (args) {
this.logs = [];
this.params = [];
this.logs.push("call" + module.name + "!" + ptr(funcPtr).sub(module.base) + "\n");
for (let i = 0; i < paramsNum; i++) {
this.params.push(args[i]);
}
for (let i = 0; i < paramsNum; i++) {
this.logs.push("arg" + i + "onEnter: " + print_ptr(this.params[i]));
}
},
onLeave: function (retval){
for (let i = 0; i < paramsNum; i++) {
this.logs.push("arg" + i + "onLeave: " + print_ptr(this.params[i]));
}
this.logs.push("retval onLeave: " + print_ptr(retval) + "\n");
console.log(this.logs);
}
})
}
let base = Process.findModuleByName("libme.so").base;
hook_native_addr(base.add(0xC8F8), 3)
函数返回后前两个参数的结果

getDG实际返回 56f95141a5e5534737e99962cea0491ad274eb9fc0d1488f869808e0fe04bca3BB6E4A1F43024A6FA69538B991E12E0D
对比可发现,返回值是由arg1onLeave的结果拼接上arg0onLeave中的部分字符串,再对比抓包的结果

可以发现BB6E4A1F43024A6FA69538B991E12E0D是client-key,是固定的,可能和设备相关,
而前一部分56f95141a5e5534737e99962cea0491ad274eb9fc0d1488f869808e0fe04bca3来自arg0onLeave的字节转16进制,查看函数sub_C8F8

让ai增强一下可读性
__int64 __fastcall sub_C8F8(__int64 result, __int64 a2, int a3) {
int i = 0; // 字节索引
while (i < 16) {
// 目标位置为 a2 + 2 * i(每个字节转成两个字符)
_BYTE *dest = (_BYTE *)(a2 + 2 * i);
// 取当前字节
unsigned char byte_value = *(unsigned __int8 *)(result + i);
// 高4位(高半字节)
*dest = a0123456789abcd[byte_value >> 4];
// 低4位(低半字节)
*(dest + 1) = a0123456789abcd[byte_value & 0xF];
i++;
}
return result; // 返回原始指针
确实是这样的,接着我们来追踪arg0onLeave的16个字节从哪来的
hook sub_E3C8

这16个字节是从sub_E3C8里来的,注意到第三个参数是0x60,正好对应第二哥参数里的0x60字节的字符串
对比抓包结果,其中
首20字节: BB6E4A1F43024A6FA69538B991E12E0D,正好对应抓包中的client-key,是固定的,应该和设备相关
中间20字节:e3ca31777927466fa03e35a7242d86c8,正好对应抓包中的nonce-str,随机生成
尾20字节:1010B3E4F87F46AFA8456873BCBBE009,不知道是什么,但也是固定的
sub_E3C8应当对以上字符串进行了加密,并最后生成16字节的数据,
首先想到md5算法

和结果是对的上的:DGUtil.getDG result=6cdba91a79626731332d5fb02da66626e3ca31777927466fa03e35a7242d86c8BB6E4A1F43024A6FA69538B991E12E0D

浙公网安备 33010602011771号