白Einzz
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

前述
通过修改smali,java层hook,so层hook等多种思路解决apk签名校验问题。下载某签名校验典型案例APP,对其重打包签名后打开应用即闪退,我们通过搜索signatures找到对应代码
若无法直接通过关键词确定代码位置,也可以通过hook signatures堆栈的方式进行追踪
Java.perform(function(){
var signature = Java.use('android.content.pm.Signature');
signature.hashCode.implementation = function(){
console.log("find signature.hashCode");
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return this.hashCode();
}
signature.toByteArray.implementation = function(){
console.log("find signature.toByteArray");
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
return this.toByteArray();
}
});
获取堆栈信息
当我们通过hook修改方法qian()传入参数i的取值时,使用spawn模式hook程序直接退出,说明该方法确实为检测点
查看引用qian()的代码片段可以看到qian默认传入的就是226776851
在smali代码中直接注释掉qian(226776851)
对app进行第一次重打包签名(磁盘搜索_killer.apk)并在测试机上安装发现仍然闪退无法正常运行,看代码发现另一个if语句this.eee.equals("")同样执行了Toast.makeText(this, 1, 1).show(),因此我们还需要解决bug()方法
解决方式一:
通过代码可以知道只要eee的值不是空值就可以绕过该检测,,编写脚本时发现一旦执行bug()方法程序就会退出,该方法内部存在相关检测,因此不能让其执行同时又要返回一个非空值,hook脚本如下
Java.perform(function(){
let MainActivity = Java.use("com.bug.bt.MainActivity");
MainActivity["bug"].implementation = function () {
console.log('bug is called');
//let ret = this.bug();
var bb="1";
console.log('bug ret value is ' + bb);
return bb;
}
});
使用spawn模式启动上一步中第一次编译过的app(磁盘搜索_killer.apk)
frida -U -f com.bug.bt -l hook.js --no-pause
程序正常运行
解决方式二:
在第一次编译后的基础上再将smile代码中this.eee = bug()注释掉,同时将空字符修改为非空的内容
保存并编译,安装第二次重新编译后的app(磁盘搜索_killer2.apk)正常运行
解决方式三:
实际上bug()方法实现在so层中
使用IDA分析找到可疑方法Java_com_bug_bt_MainActivity_bug
查看伪c代码
发现其中调用了getSignHashCode()方法,其中if语句通过对比result与签名是否一致,当不一致时exit掉
因此我们只需绕过if中的条件判断即可,把BEQ指令改为相反的BNE指令
将02 D0 字节修改为02 D1
在第一次编译后的app(磁盘搜索_killer.apk)基础上进行如上修改,替换掉原本的so文件后再次重新打包app(磁盘搜索_killer3.apk),正常运行
也可以直接将exit替换为nop指令,将前几位字节改为00 BF 00 BF或者00 00 00 00即可
解决方式四:
方式四-一:通过IDA分析并编写hook脚本将相关方法置空
使用IDA找到对应方法,获取其偏移量BFC
查看其伪c可知传参和返回值类型
静态地址 ( 基地址 ) + 偏移量 = 动态地址,同时Thumb指令需要加1
function main() {
var libnativeaddress=Module.findBaseAddress("libbug.so");
if(libnativeaddress!=null){
bypass_native(libnativeaddress);
}
}
function bypass_native(libnativeaddress){
var target_addr=libnativeaddress.add(0xBFC+1);
console.log('target_addr===>',target_addr);
Interceptor.replace(target_addr,new NativeCallback(function(num1,num2){
console.log('replace success!!!!!');
return 2;
},'int',['int','int']));
}
也可以直接通过通过导出函数名定位 native 方法
function main() {
var getSignHashCode_addr = Module.findExportByName("libbug.so","getSignHashCode");
if(getSignHashCode_addr!=null){
console.log("getSignHashCode_addr is -> ",getSignHashCode_addr);
bypass_getSignHashCode(getSignHashCode_addr);
}
}
function bypass_getSignHashCode(getSignHashCode_addr){
//console.log('replace start');
Interceptor.replace(getSignHashCode_addr,new NativeCallback(function(num1,num2){
console.log('replace success!!!!!!!!!!');
return 2;
},'int',['int','int']));
}
方式四-二:将系统exit()置空
通过之前的分析就能知道最终执行了exit(),直接将exit置空即可
function main7() {
Java.perform(function(){
var openPtr = Module.getExportByName(null, 'exit');
Interceptor.replace(openPtr, new NativeCallback(function (flags) {
console.log('CCCryptorCreate called from:\n' +Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n') + '\n');
console.log('success ');
}, 'void', ['int']));
});
}
同时我们打印堆栈也能看到是libbug.so的getSignHashCode方法调用了exit
方式四-三:输出所有的导出函数后分析
hook导出函数(填写so的名字),如下图所示。
function main6() {
var exports1 = Module.enumerateExportsSync("libbug.so");
for(var i=0; i<exports1.length; i++){
console.log("name:address",exports1[i].name,exports1[i].address);
}
}
可以看到程序在崩溃之前加载了两个比较显眼的方法
编写脚本hook出该方法的偏移
在ida中直接jump到对应地址即可看到该方法
查看伪c确认其传参和反返回值类型后编写hook脚本将其置空
function main3() {
var exports1 = Module.enumerateExportsSync("libbug.so");
for(var i=0; i<exports1.length; i++){
if(exports1[i].name="getSignHashCode"){
console.log("name:",exports1[i].name,"address:",exports1[i].address);
bypass_getSignHashCode2(exports1[i].address);
break;
}
}
}
function bypass_getSignHashCode2(getSignHashCode_addr){
console.log('replace start');
Interceptor.replace(getSignHashCode_addr,new NativeCallback(function(num1,num2){
console.log('replace success!!!!!!!!!!');
return 2;
},'int',['int','int']));
}
总结
解决方式4的众多脚本中除了直接hook系统exit的方案,其他方案在执行时都可能会遇到Error: expected a pointer的错误。原因是使用spawn方式启动应用时还没有加载到该so,frida找不到libbug.so
将setImmediate()替换为setTimeout()解决以上问题,但若时间过长也会导致系统已经执行exit直接退出
需要不断的尝试设置不同时间才能成功一次
那么怎样才能更有效的解决Error: expected a pointer的错误。解决思路是先判断是否加载了libbug.so,然后在执行相关方法。在 Android 系统中,动态加载 so 库最终是通过 dlopen 和 android_dlopen_ext 完成的,而dlopen主要用于加载一些系统的库,输出dlopenext加载的so
var dlopen_ext_addr = Module.findExportByName(null,"android_dlopen_ext");
Interceptor.attach(dlopen_ext_addr,{
onEnter: function(args){
console.log("dlopen_ext ->" + args[0].readCString());
},onLeave: function(retval){
console.log(retval);
}
});
运行结果如下可以看到加载了libbug.so
编写脚本确定在加载了libbug.so后,在执行下一步
function main() {
var boo=false;
var dlopen_ext_addr = Module.findExportByName(null,"android_dlopen_ext");
Interceptor.attach(dlopen_ext_addr,{
onEnter: function(args){
if(args[0].readCString().indexOf("libbug.so") != -1){
console.log("find libbug.so");
boo=true;
}
},onLeave: function(retval){
//console.log(retval);
if(boo){
getSignHashCode();
}
}
});
}
function getSignHashCode(){
var getSignHashCode_addr = Module.findExportByName("libbug.so","getSignHashCode");
if(getSignHashCode_addr!=null){
console.log("getSignHashCode_addr is -> ",getSignHashCode_addr);
bypass_getSignHashCode(getSignHashCode_addr);
}
}
function bypass_getSignHashCode(getSignHashCode_addr){
//console.log('replace start');
Interceptor.replace(getSignHashCode_addr,new NativeCallback(function(num1,num2){
console.log('replace success!!!!!!!!!!');
return 2;
},'int',['int','int']));
}
成功
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)