这次分析的app是:五菱汽车(8.2.1)
登录,抓包
发现请求体只有sd字段,看见加密的时候,可以先使用算法助手hook java层所有加解密方法
发现我们所需要的sd加密字段在java层hook不到,那加密算法应该是写在了so层,因为这个app是bb加固企业,得有脱壳机才能脱。
jadx加载dex,直接搜"sd"
发现这里有个变量定义,直接跟进
这里貌似是实现了post字段的装填,我们可以frida hook这个方法看看
function hook_addCheckcode(){Java.perform(function(){let CheckCodeUtils = Java.use("com.cloudy.linglingbang.model.request.retrofit2.CheckCodeUtils");CheckCodeUtils["addCheckCode"].implementation = function (str, i) {console.log(`CheckCodeUtils.addCheckCode is called: str=${str}, i=${i}`);let result = this["addCheckCode"](str, i);console.log(`CheckCodeUtils.addCheckCode result=${result}`);return result;};})
}
发现确实hook到了登录数据
** 随后跟进encrypt方法**
继续跟进
最后跟进到checkcode方法,这个含有三个参数的checkcode是写在了native层,hook看一下
没问题,查看这个方法是在so库中
直接ida打开这个so文件
一开始可以现在搜索框输入java,看看方法是不是静态注册,如何是动态注册的方法,可以hook registernatives函数进行查看偏移地址和分析,脚本如下:
var ENV = null;
var JCLZ = null;var method01addr = null;
var method02addr = null;
var method02 = null;var addrNewStringUTF = null;var NewStringUTF = null;function hook_RegisterNatives() {var symbols = Module.enumerateSymbolsSync("libart.so");var addrRegisterNatives = null;for (var i = 0; i < symbols.length; i++) {var symbol = symbols[i];//_ZN3art3JNI15RegisterNativesEP7_JNIEnvP7_jclassPK15JNINativeMethodiif (symbol.name.indexOf("art") >= 0 &&symbol.name.indexOf("JNI") >= 0 && symbol.name.indexOf("NewStringUTF") >= 0 && symbol.name.indexOf("CheckJNI") < 0) {addrNewStringUTF = symbol.address;console.log("NewStringUTF is at ", symbol.address, symbol.name);NewStringUTF = new NativeFunction(addrNewStringUTF,'pointer',['pointer','pointer'])}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);}}if (addrRegisterNatives != null) {Interceptor.attach(addrRegisterNatives, {onEnter: function (args) {console.log("[RegisterNatives] method_count:", args[3]);var env = args[0];ENV = args[0];var java_class = args[1];JCLZ = args[1];var class_name = Java.vm.tryGetEnv().getClassName(java_class);//console.log(class_name);var methods_ptr = ptr(args[2]);var method_count = parseInt(args[3]);for (var i = 0; i < method_count; i++) {var name_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3));var sig_ptr = Memory.readPointer(methods_ptr.add(i * Process.pointerSize * 3 + Process.pointerSize));var fnPtr_ptr = Memory.readPointer(methods_ptr.add(i * 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);console.log("[RegisterNatives] java_class:", class_name, "name:", name, "sig:", sig, "fnPtr:", fnPtr_ptr, "module_name:", find_module.name, "module_base:", find_module.base, "offset:", ptr(fnPtr_ptr).sub(find_module.base));if(name.indexOf("method01")>=0){// method01addr = fnPtr_ptr;continue;}else if (name.indexOf("decrypt")>=0){method02addr = fnPtr_ptr;method02 = new NativeFunction(method02addr,'pointer',['pointer','pointer','pointer']);method01addr = Module.findExportByName("libroysue.so", "Java_com_roysue_easyso1_MainActivity_method01")}else{continue;}}}});}
}function invokemethod01(contents){console.log("method01_addr is =>",method01addr)var method01 = new NativeFunction(method01addr,'pointer',['pointer','pointer','pointer']);var NewStringUTF = new NativeFunction(addrNewStringUTF,'pointer',['pointer','pointer'])var result = null;Java.perform(function(){ console.log("Java.vm.getEnv()",Java.vm.getEnv())var JSTRING = NewStringUTF(Java.vm.getEnv(),Memory.allocUtf8String(contents))result = method01(Java.vm.getEnv(),JSTRING,JSTRING);console.log("result is =>",result)console.log("result is ",Java.vm.getEnv().getStringUtfChars(result, null).readCString())result = Java.vm.getEnv().getStringUtfChars(result, null).readCString();})return result;
}function invokemethod02(contents){var result = null;Java.perform(function(){ var JSTRING = NewStringUTF(Java.vm.getEnv(),Memory.allocUtf8String(contents))result = method02(Java.vm.getEnv(),JSTRING,JSTRING);result = Java.vm.getEnv().getStringUtfChars(result, null).readCString();})return result;
}
rpc.exports = {invoke1:invokemethod01,invoke2:invokemethod02
};setImmediate(hook_RegisterNatives);/*
java_class: com.example.demoso1.MainActivity name: method01 sig: (Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x73e2cd1018 module_name: libnative-lib.so module_base: 0x73e2cc1000 offset: 0x10018
java_class: com.example.demoso1.MainActivity name: method02 sig: (Ljava/lang/String;)Ljava/lang/String; fnPtr: 0x73e2cd0efc module_name: libnative-lib.so module_base: 0x73e2cc1000 offset: 0xfefcfunction hookmethod(addr){Interceptor.attach(addr,{onEnter:function(args){console.log("args[0]=>",args[0])console.log("args[1]=>",args[1])console.log("args[2]=>",Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())},onLeave:function(retval){console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString())}})
}function replacehook(addr){//> 能够hook上,就能主动调用var addrfunc = new NativeFunction(addr,'pointer',['pointer','pointer','pointer']);Interceptor.replace(addr,new NativeCallback(function(arg1,arg2,arg3){// 确定主动调用可以成功,只要参数合法,地址正确var result = addrfunc(arg1,arg2,arg3)console.log(arg1,arg2,arg3)console.log("result is ",Java.vm.getEnv().getStringUtfChars(result, null).readCString())return result;},'pointer',['pointer','pointer','pointer']))
}*/
因为这里是静态注册,我们直接查看该函数就行
点击该方法,先更改一下传参变量名,第一个类型是JNIEnv*的指针变量,改完以后,后面JNI函数会比较好分析
我们发现,点击来这个函数的时候,消耗了挺多的时间,我们看一下汇编,
可以发现是加了ollvm混淆,基于这个混淆效果不是很强,我们可以直接关注关键函数进行分析
往下看一下代码,发现有个aes_encrypy1和aes_encrypt2
点进去aes_encrypy1
是一个函数,有三个参数,我们可以来hook看这三个参数到底是什么东西,我们现在java层写一个checkcode方法的主动调用,这样后面分析就不用频繁登录
function call(){Java.perform(function () {let CheckCodeUtils = Java.use("com.cloudy.linglingbang.model.request.retrofit2.CheckCodeUtils");let instanc = CheckCodeUtils.$new();let result = instanc.encrypt("abcdefghij", 1);console.log(`CheckCodeUtils.encrypt result=${result}`);})
}
这样我们在frida界面输入call()就可以调用一次加密
好的,现在开始hook native层的函数,代码如下
先找到函数偏移先,然后直接hook
function hook_aesencry1(){let baseaddr = Module.findBaseAddress('libencrypt.so'); Interceptor.attach(baseaddr.add(0xA5BC), {onEnter:function(args){console.log('aes_encry1 is called');console.log('args[0]:'+ hexdump(args[0]));console.log('args[1]:'+ hexdump(args[1]));console.log('args[2]:'+ hexdump(args[2]));},onLeave:function(retval){}})
}
可以发现ase_encrypy1被调用,第一个参数就是我们输入的字符串,说明加密应该是这里
跟进WBACRAES128_EncryptCBC
分析一下代码,可以看到这个函数有两个主要方法
InsertCBCPadding,这个判断是aes加密的填充函数
还有这个WBACRAES_EncryptOneBlock,跟进
根据上面代码分析,这个this指针应该是我们传入的字符串,我们可以在EncryptOneBlock这里hook看一下,这里就不放代码,可以发现就是我们传入的字符串,我们继续跟进
可以发现出现了一点小问题,我们看一下汇编代码
主要看一下这个BLR指令,这是一个跳转指令,跳转地址是在x4这个寄存器里面,我们现在是看不懂x4寄存器内容,可以hook这个地址,使用frida的上下文查看x4寄存器的地址,代码如下
function hook_x4(){let baseaddr = Module.findBaseAddress('libencrypt.so');let targetaddr = baseaddr.add(0xA03C);Interceptor.attach(targetaddr, {onEnter:function(args){console.log("寄存器的值: " + this.context.x4);},onLeave:function(retval){}})
}
这里我们先hook_x4()挂钩先,然后主动调用call()就行
x4的地址为0x7cea2086f8,ida里面的偏移就是86f8,我们直接按G跳转该位置
成功进入到该函数内部,还是一样,先hook这四个参数看看
可以发现第二个参数就是我们传入的字符串,这里对他进行命名为indata,我们接着分析代码
这里出现了PrepareAESMatrix这个函数,参数是我们输入的字符串,其他几个参数我们暂时不知道,跟进这个函数看看
这里看着像是对明文的一些操作,我们hook一下看一下参数,第三个参数需要在函数返回时进行hook
function hook_peraes(){let baseaddr = Module.findBaseAddress('libencrypt.so');let targetaddr = baseaddr.add(0x7874);let args2= nullInterceptor.attach(targetaddr, {onEnter:function(args){// console.log('args[0]:'+ hexdump(args[0]));// console.log('args[1]:'+ hexdump(args[1]));// console.log('args[2]:'+ hexdump(args[2]));args2 = args[2];},onLeave:function(retval){console.log('处理后的结果' + hexdump(args2));}})
}
确实是对明文进行处理,这里先命名为indata_state,aes加密就是使用对明文进行处理,然后与密钥处理后的结果进行十轮加密,十轮加密都会对indata_state进行修改,因为没找到分析密钥,分析应该是白盒aes加密
白盒AES是一种将密钥嵌入到加密算法中的技术,使得即使攻击者能够完全访问加密算法的执行过程,也无法轻易提取出密钥。这种技术主要用于防止逆向工程和密钥泄露。
网上对白盒aes加密的分析很多,这里主要是分析crack的过程
DFA故障攻击是一种目前处理白盒aes加密的主流方法,过程就是在进行加密是修改indata_state的内容,注入随机字节,原理网上也很多,这里不在赘述,我们回到代码
可以发现,indata_state确实在被修改处理,我们现在要找到没有列混淆的最后一轮之前之前注入就行
可以发现这里有个特征值,int10 ==10,往前看代码
int10是a4赋值,也是这个函数的第四个参数,我们看一下第四个参数是什么
可以发现第四个参数是0xa,也就是数字10,这里判断应该就是加密轮次
再看v9的值
初始被赋值为0,这里可以看一下v9到底被++了多少次,
查看汇编
这里w20存的就是v9的值,我们可以在hook偏移8970,查看寄存器的值,判断到底cmp了多少次
代码如下
function hook_w20(num){let baseaddr = Module.findBaseAddress('libencrypt.so');let targetaddr = baseaddr.add(0x8970);Interceptor.attach(targetaddr, {onEnter:function(args){console.log("寄存器w20的值为====>>>", this.context.x20);},onLeave:function(retval){}})
}
可以发现这里应该进行了9轮加密,符合aes加密轮数,那我们在进入第9轮时,更改indata_state的内容,注入故障文就行,(对于10轮函数的AES-128,DFA攻击通常将最后两轮设为目标。当故障注入到最后2轮中时,密文的某一些字节会受到故障的影响。)这里为了方便查看,call()主动调用采用16字节"0123456789abcdef"
function call(){Java.perform(function () {let CheckCodeUtils = Java.use("com.cloudy.linglingbang.model.request.retrofit2.CheckCodeUtils");let instanc = CheckCodeUtils.$new();let result = instanc.encrypt("0123456789abcdef", 1);console.log(`CheckCodeUtils.encrypt result=${result}`);})
}
然后我们直接写出代码
let num = null;
function hook_change_state(){let baseaddr = Module.findBaseAddress('libencrypt.so');let data_state = null; //明文加密后的地址,故障文注入地址let w20_addr = baseaddr.add(0x8970);let aes_result = null; //aes加密结果Interceptor.attach(baseaddr.add(0x86f8), {onEnter:function(args){aes_result = args[2] },onLeave:function(retval){//console.log("aes加密结果为====>>>>>>",hexdump(aes_result))let aes_data_to_print = Memory.readByteArray(aes_result, 16); // 将字节数组转换为十六进制字符串let hexString = Array.from(new Uint8Array(aes_data_to_print)).map(b => ('0' + b.toString(16)).slice(-2)) // 转换为十六进制,确保两位.join('');console.log("aes加密结果为====>>>>>>", hexString);}})Interceptor.attach(baseaddr.add(0x7874), {onEnter:function(args){//console.log("args[2]===>>>", hexdump(args[2]));data_state = args[2];},onLeave:function(retval){//console.log("state_addr==>>", hexdump(data_state));}})Interceptor.attach(w20_addr, {onEnter:function(args){if(this.context.x20 == 0x8){//console.log("state_data=====>>>>>>>>>>>>>>>>>>>>>>",hexdump(data_state));Memory.writeByteArray(data_state, [num])//console.log("修改的state如下:"+hexdump(data_state))}},onLeave:function(retval){}})
}function hook_call(){let nums=[0x39,0x20,0x36,0x72,0x27]hook_change_state()for (let i = 0; i < nums.length; i++) {num = nums[i]//console.log("num====>>>", num)call()console.log("================================================")}
}
我们先回到之前indata_state,位置看看改成16字节的输入,indata_state是怎么样
我们在这里四个字节分别修改五次,总共20次,修改内存的数据使frida Memory.writeByteArray(data_state, [num])api进行修改
第二个字节的修改Memory.writeByteArray(data_state.add(0x1), [num])就行,以此类推
修改20次故障文后aes加密结果
9a39f8f250d9a12988803093cefe4f80
9139f8f250d9a1d988806293ce944f80
c639f8f250d9a19e88800d93ce154f80
1539f8f250d9a13588804c93ce984f80
9739f8f250d9a10288802593ce874f80
50c4f8f2b0d9a106888065e2cefc1280
508bf8f257d9a1068880655dcefc4980
50b5f8f202d9a106888065cdcefc9980
50d2f8f266d9a1068880651dcefc0c80
5015f8f243d9a10688806598cefc8580
50392bf2505ba106de806593cefc4f73
50393df2501da1060f806593cefc4fdb
5039b2f25020a106e6806593cefc4f6c
503999f250e7a10667806593cefc4f02
5039b4f250dda106da806593cefc4f0b
5039f81b50d97f0688a865939dfc4f80
5039f8e250d9270688ea6593abfc4f80
5039f85450d92106887d65938ffc4f80
5039f8ed50d9170688ad65934afc4f80
5039f89250d9f00688c36593f2fc4f80
然后使用phoenixAES还原出第十轮的密钥
import phoenixAESphoenixAES.crack_file('crackfile', [], True, False, verbose=2)
crackfile只需要在上述故障文第一行加入正常的数据就行
第十轮密钥是8A6E30D74045AE83634D6ECDE1516CA1
接着使用https://github.com/SideChannelMarvels/Stark,编译aes_keyschedule.c
还原出密钥
密钥是8A6E30D74045AE83634D6ECDE1516CA1,根据上面明文的处理,indata_state数据其实没有改变,可判断iv其实就是0,因为明文需要与iv先异或,iv填充16个0就行,至此分析完毕,加密后base64就行