这次分析的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_jclassPK15JNINativeMethodi
if (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: 0xfefc
function 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= null
Interceptor.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 phoenixAES
phoenixAES.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就行
标签:function,aes,console,log,白盒,args,result,var,frida From: https://www.cnblogs.com/GGbomb/p/18621516