枚举so库函数地址
枚举导入表
var imports=Module.enumerateImports("libencryptlib.so")
for(var i=0;i<imports.length;i++){
// console.log(JSON.stringify(imports[i]));
console.log(imports[i].name+" "+imports[i].address);
}
枚举导出表
var exports=Module.enumerateExports("libencryptlib.so")
for(var i=0;i<exports.length;i++){
console.log(exports[i].name+" "+exports[i].address);
}
枚举符号表
var symbols=Module.enumerateSymbols("libencryptlib.so")
for(var i=0;i<symbols.length;i++){
console.log(symbols[i].name+" "+symbols[i].address);
}
枚举导出表中某个函数的地址
var funcaddr=Module.findExportByName("libencryptlib.so","_ZN7MD5_CTX11MakePassMD5EPhjS0_");
console.log(funcaddr);
枚举模块,再枚举其中的导出或导入表
var modules=Process.enumerateModules();
console.log(JSON.stringify(modules));
console.log(JSON.stringify(modules[0].enumerateExports()));
console.log(JSON.stringify(modules[0].enumerateImports()[0]));
实战
以XX48为例
SO库中找到MD5的加密函数。
函数有四个参数,我们比较关注后面3个参数。
Interceptor.attach(funcaddr,{
onEnter:function(args){
console.log("funcaddr args[1]:"+hexdump(args[1]));//hexdump打印地址的内存
console.log("funcaddr args[2]:"+args[2]);
console.log("funcaddr args[3]:"+hexdump(args[3]));//打印了内存发现为空,猜测是最后返回值的缓存区。所以在onLeave中打印一下内存。
this.args3=args[3];
},onLeave:function(retval){
console.log("funcaddr args[3]:"+hexdump(this.args3));
}
})
关于hexdump的参数:
hexdump(args[0],{offset:0,length:16,header:true,ansi:true};
offset 显而易见,就是偏移
length 打印的长度
header 有无顶栏的01234…ef这一行
ansi 配色
成功拿到返回值,和登录页面抓包中的值一样
模块基址的几种获取方式与函数地址的计算
模块基址的获取方式
如果在导入、导出、符号表中找不到的函数,就需要自己手动计算地址了(基址+偏移)。
var module1=Process.findModuleByName("libencryptlib.so");
console.log("module1:"+module1.base);
var module2=Process.getModuleByName("libencryptlib.so");
console.log("module2:"+module2.base);
var module3=Module.findBaseAddress("libencryptlib.so");
console.log("module3:"+module3);
var module4=Process.enumerateModules();
for(var i=0;i<module4.length;i++){
if(module4[i].name=="libencryptlib.so")
console.log("module4:"+JSON.stringify(module4[i].base));
}
函数地址的计算
thumb指令(thumb1指令为2字节,thumb2包含2字节和4字节的指令)
函数地址计算: so基址+函数在so中的偏移+1
arm指令(A32和A64,指令长度为4字节)
函数地址计算: so基址+函数在so中的偏移
一般来说,用IDA打开,设置下显示指令长度,如果长度都为4,那么就是arm指令。长度为2或者2,4交替,则是thumb指令。
示例
继续以这个函数为例
var soaddr=Module.findBaseAddress("libencryptlib.so");
//soaddr =0x708bbd2000
var funcaddr =soaddr.add(0x1fa38);
//此为另一种写法
//var so=0x708bbd2000
//funcaddr=ptr(so).add(0x1fa38); ptr=new NativePointer
Interceptor.attach(funcaddr,{
onEnter:function(args){
console.log("funcaddr args[1]:"+hexdump(args[1]));
console.log("funcaddr args[2]:"+args[2]);
console.log("funcaddr args[3]:"+args[3]);
this.args3=args[3];
},onLeave:function(retval){
console.log("funcaddr args[3]:"+hexdump(this.args3));
}
})
最后输出和之前的一样
so库hook 打印参数的集成脚本
function print_arg(addr){
var module = Process.findRangeByAddress(addr);
if(module != null) return hexdump(addr) + "\n";
return ptr(addr) + "\n";
}
function hook_native_addr(funcPtr, paramsNum){
var 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]);
this.logs.push("this.args" + i + " onEnter: " + print_arg(args[i]));
}
}, onLeave: function(retval){
for(let i = 0; i < paramsNum; i++){
this.logs.push("this.args" + i + " onLeave: " + print_arg(this.params[i]));
}
this.logs.push("retval onLeave: " + print_arg(retval) + "\n");
console.log(this.logs);
}
});
}
var soaddr=Module.findBaseAddress("libwtf.so");//要hook的so库
var funcaddr =soaddr.add(0x8bc+1);//偏移
hook_native_addr(funcaddr,2)//args2是参数的个数
hook过java层root检测
弹出了一个Toast,告诉我们它有root检测╭(╯^╰)╮。
那我们就直接hook Toast,打一下调用栈。
function main(){
Java.perform(function(){
function showstacks(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
Java.use("android.widget.Toast").show.implementation=function(){
showstacks();
console.log("toast show");
return this.show();
}
});
}
setImmediate(main)
然后很头疼,Frida spawn模式启动,app会崩溃,真不懂… 然后只能附加,而且手速还要快,不然每次都是Toast弹出后才附加上去。
重点就看com.hoge…WelcomeActivity这个类了.$指的是匿名内部类。(这里$2$1说明嵌套了两个匿名内部类)
我们jadx打开,这apk居然没有加壳,很快就能定位到关键代码。
我们查看checkSuFile和checkRootFile这两个函数,发现返回值就是简单的True/False 以及null。
frida直接hook这两个函数,直接修改返回值呗。
function main(){
Java.perform(function(){
function showstacks(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
}
// Java.use("android.widget.Toast").show.implementation=function(){
// showstacks();
// console.log("toast show");
// return this.show();
// }
Java.use("com.hoge.android.factory.util.system.SystemUtils").checkSuFile.implementation=function(){
return false;
}
Java.use("com.hoge.android.factory.util.system.SystemUtils").checkRootFile.implementation=function(){
return null;
}
});
}
setImmediate(main)
然后再注入一遍,就发现app不会弹出Toast了。
再附上一个查看APP是否检测root的代码
Java.perform(function(){
function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}
Java.use("java.io.File").$init.overload("java.lang.String").implementation = function (str) {
if (str.toLowerCase().endsWith("/su") || str.toLowerCase() == "su") {
console.log("发现检测su文件");
showStacks();
}
return this.$init(str);
}
Java.use("java.lang.Runtime").exec.overload("java.lang.String").implementation = function (str) {
if (str.endsWith("/su") || str == "su") {
console.log("发现尝试执行su命令的行为");
showStacks();
}
return this.exec(str);
}
Java.use("java.lang.Runtime").exec.overload("[Ljava.lang.String;").implementation = function (stringArray) {
for (var i = 0; i < stringArray.length; i++){
if (stringArray[i].includes("su") || stringArray[i].includes("/su") || stringArray[i] == "su"){
console.log("发现尝试执行su命令的行为");
showStacks();
break;
}
}
return this.exec(stringArray);
}
Java.use("java.lang.ProcessBuilder").$init.overload("[Ljava.lang.String;").implementation = function (stringArray){
for (var i = 0;i < stringArray.length; i++) {
if (stringArray[i].includes("su") || stringArray[i].includes("/su") || stringArray[i] == "su") {
console.log("发现尝试执行su命令的行为");
showStacks();
break;
}
}
return this.$init(stringArray);
}
});
jnitrace
jnitrace项目地址
用法: jnitrace -m attach -l xxx.so com.xxxx(不加attach的话,就以spawn模式启动)
然后他会将jni函数以及其参数都打印出来。
混淆的字符串查看
hook地址,比较繁琐
对于一些混淆的字符串,我们可以等它加载之后再去hook它的地址
比如
这是一个jni函数,按理说后面d010是类的路径,可是我们查看发现被混淆了。
这时候,就可以通过frida去hook加载到内存后已经解密的字符串。
Java.perform(function(){
var soAddr=Module.findBaseAddress("liblogin_encrypt.so");
console.log(hexdump(soAddr.add(0xD010)));/./不用+1
})
然后等app启动之后再attach上,就能看到内存中字符串的结果
jnitrace,但是只能看jni函数
这个例子比较特殊,因为是jni函数,其实也可以用jnitrace去查看后面的类路径!
内存中dump so,但是需要修复
dump的脚本
function dumpso(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.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);
}
});
}
dumpso("liblogin_encrypt.so");
dump后,cp到sdcard,然后在pull到本地。
修复so
因为so文件在执行时和存储时不一样的结构,所以我们dump下来的so文件直接放到IDA里,会解析错误。
我们可以使用别人写好的工具来修复so文件。
食用方法:
sofixer -s orig.so -o fix.so -m 0x0 -d
-s 待修复的so路径
-o 修复后的so路径
-m 內存dump的基地址(16位) 0xABC
-d 输出debug信息
修复后打开,完美。
修改so库函数参数以及其返回值
var soAddr=Module.findBaseAddress("libxiaojianbang.so");
var add=soAddr.add(0x165C);
Interceptor.attach(add,{
onEnter:function(args){
args[2]=ptr(1000);//修改参数
console.log(args[2]);
console.log(args[3]);
console.log(args[4]);
},onLeave:function(retval){
//retval.replace(1000);//修改返回值
console.log(retval.toInt32());
}
});
修改字符串参数
法一、把char*指向的字符串修改掉,新字符串一般不超出原字符串长度(但是其他函数访问这个地址就会出错!)
function stringToBytes(str){
return hexToBytes(stringToHex(str));
}
// Convert a ASCII string to a hex string
function stringToHex(str) {
return str.split("").map(function(c) {
return ("0" + c.charCodeAt(0).toString(16)).slice(-2);
}).join("");
}
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
// Convert a hex string to a ASCII string
function hexToString(hexStr) {
var hex = hexStr.toString();//force conversion
var str = '';
for (var i = 0; i < hex.length; i += 2)
str += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
return str;
}
var soAddr=Module.findBaseAddress("libxiaojianbang.so");
var add=soAddr.add(0x1D68);
Interceptor.attach(add,{
onEnter:function(args){
if(args[1].readCString()=="xiaojianbang"){//由于这个函数调用多次,如果每次都修改,可能会导致程序奔溃,于是就加一个判断。
var newstr="Fup1p1";//修改后的字符串
args[1].writeByteArray(hexToBytes(stringToHex(newstr)+"00"));
args[2]=ptr(newstr.length);
}
console.log(hexdump(args[1]));
console.log(args[2]);
},onLeave:function(retval){
}
});
法二、把so中已有的字符串地址传给函数(但是只能是已有的字符串,不能自定义)。
移花接木
var soAddr=Module.findBaseAddress("libxiaojianbang.so");
var add=soAddr.add(0x1D68);
Interceptor.attach(add,{
onEnter:function(args){
if(args[1].readCString()=="xiaojianbang"){//由于这个函数调用多次,如果每次都修改,可能会导致程序奔溃,于是就加一个判断。
args[1]=soAddr.add(0x885);
args[2]=ptr(soAddr.add(0x885).readCString().length)
}
console.log(hexdump(args[1]));
console.log(args[2]);
},onLeave:function(retval){
}
});
法三、构建新的字符串,需要注意构建的字符串变量的作用域
var soAddr=Module.findBaseAddress("libxiaojianbang.so");
var add=soAddr.add(0x1D68);
var newstrAddr;
Interceptor.attach(add,{
onEnter:function(args){
if(args[1].readCString()=="xiaojianbang"){//由于这个函数调用多次,如果每次都修改,可能会导致程序奔溃,于是就加一个判断。
var newstr="Fup1p1_1p1puF";
newstrAddr=Memory.allocUtf8String(newstr);/*为什么需要定义成全局变量,而不是直接var,因为javascript的垃圾回收机制,
newstrAddr变量作用域在onEnter函数中,如果onEnter函数结束,垃圾回收机制就会回收这块内存,所以之后在
执行原函数的时候,args[1]只会指向一个newstrAddr这个地址,而这个地址内部数据已经被清除了!所以需要定义成全局变量
确保在整个 Interceptor 生命周期内保留内存分配。*/
args[1]=newstrAddr;
console.log(hexdump(args[1]));
args[2]=ptr(newstr.length);
}
//console.log(hexdump(args[1]));
console.log(args[2]);
},onLeave:function(retval){
}
});
frida 修改so函数代码
以这个函数为例,我们的目的是修改使他的值为a3+a4-a5。
我们先去armconverter,看看修改后的ARM指令的机器码
然后开始hook
function hook_dlopen(addr,soname,callback){
Interceptor.attach(addr,{
onEnter:function(args){
var path=args[0].readCString();
if(path.indexOf(soname)!=-1){
this.hook=true;
}
console.log(hexdump(args[0]));
},onLeave:function(retval){
if(this.hook==true){
callback();
}
}
});
}
var android_dlopen_ext_addr=Module.findExportByName(null,"android_dlopen_ext");
function hexToBytes(hex) {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return bytes;
}
function xiugai(){
var soaddr=Module.findBaseAddress("libxiaojianbang.so");
var add_addr=soaddr.add(0x165C);
var write_addr=soaddr.add(0x1684);
console.log(hexdump(write_addr));
Memory.protect(write_addr,4,"rwx");
write_addr.writeByteArray(hexToBytes("0001094B"));
console.log("hooked "+hexdump(write_addr));
}
//另一种写法
//function xiugai(){
// var codeaddr=soaddr.add(0x1684);
// Memory.patchCode(codeaddr,4,function(code){
// var writer=new Arm64Writer(code,{pc:codeaddr});
// writer.putBytes(hexToBytes("0001094B"));
// writer.flush();
// });
//}
hook_dlopen(android_dlopen_ext_addr,"libxiaojianbang.so",xiugai);
Hook成功
console.log(Instruction.parse(write_addr).toString())//打印汇编指令
console.log(Instruction.parse(write_addr).next)//打印吓一跳指令的地址
so层主动调用任意函数
比如,我们想去调用一个自实现的jstring2cstr函数
var soaddr=Module.findBaseAddress("libxiaojianbang.so");
var func_addr=soaddr.add(0x124C);//函数地址
var jst2cstr=new NativeFunction(func_addr,'pointer',['pointer','pointer']);//函数地址,函数返回值类型,函数参数类型
var env=Java.vm.tryGetEnv();
console.log(JSON.stringify(env));
var jstring=env.newStringUtf("Fup1p1");
var retval=jst2cstr(env,jstring);
console.log(hexdump(retval));
console.log(retval.readCString());
hook libc读写文件
var addr_fopen=Module.findExportByName("libc.so","fopen");
var addr_fclose=Module.findExportByName("libc.so","fclose");
var addr_fputs=Module.findExportByName("libc.so","fputs");
console.log("addr_fopen:"+addr_fopen,"addr_fclose:"+addr_fclose,"addr_fopen:"+addr_fopen);
var fopen=new NativeFunction(addr_fopen,"pointer",["pointer","pointer"]);
var fclose=new NativeFunction(addr_fclose,'int',["pointer"]);
var fputs=new NativeFunction(addr_fputs,'int',["pointer","pointer"]);
var path=Memory.allocUtf8String("/sdcard/test.txt");
var openmode=Memory.allocUtf8String("w");
var data=Memory.allocUtf8String("114514");
var file=fopen(path,openmode);
fputs(data,file);
fclose(file);
jni函数的hook
手动计算函数地址
JNIEnv本质上就是一个结构体,其对JNINativeInterface结构体做了一个封装,所以如果我们能够在内存中定位到这个结构体,就可以通过偏移去去到函数指针,然后再去调用或者Hook。
console.log(hexdump(Java.vm.tryGetEnv().handle.readPointer()));//记得加上readPointer
var env_addr=Java.vm.tryGetEnv().handle.readPointer();
var findclass_addr=env_addr.add(48).readPointer();//在 jni.h中找到FindClass对应的偏移
Interceptor.attach(findclass_addr,{
onEnter:function(args){
console.log("FindClass: " + args[1].readCString());
},
onLeave:function(retval){
}
});
Hook libart 来hook jni的相关函数
Android 10.0之前,libart.s0(64位的)存放在/system/lib64目录下;Android 10.0之后,libart.so存放在/system/apex/com.android.runtime.release/lib下
以hook NewStringUTF为例子
我们要hook的是下面那个不带Check的。
var symbols=Process.getModuleByName("libart.so").enumerateSymbols();//新API
//var symbols=Module.enumerateSymbols();//旧API
console.log(JSON.stringify(symbols[0]));
var newstringutf=null;
for(let i=0;i<symbols.length;i++){//枚举获得需要的JNI函数
var symbol=symbols[i];
if(symbol.name.indexOf("CheckJNI")==-1&&symbol.name.indexOf("NewStringUTF")!=-1){//过滤掉check
console.log(symbol.name,symbol.address);
newstringutf=symbol.address;//保存地址
}
}
Interceptor.attach(newstringutf,{
onEnter:function(args){
console.log("NewStringUTF:",args[1].readCString());
}, onLeave:function(retval){
}
});
JNI函数的主动调用
以NewStringUTF为例子。
使用frida封装好的函数来调用jni
env=Java.vm.tryGetEnv()
env 与env.handle的区别
env: Frida 包装好的env
env.handle:记录了JNI中真实的env的地址
这两个一定程度上可以通用,某些情况会自动转换。但是在使用Frida封装好的API的时候,必须使用env。当参数需要JNIEnv* 的时候两个都可以。
var retval=Java.vm.tryGetEnv().newStringUtf("Fup1p1"); //js string-> jstring
console.log(retval);
var value=Java.vm.tryGetEnv().getStringUtfChars(retval).readCString(); //jstirng -> Cstring
console.log(value);
NativeFunction来主动调用
var symbols=Process.getModuleByName("libart.so").enumerateSymbols();
var newstringutf_addr=null;
for(let i=0; i<symbols.length; i++){
if(symbols[i].name.indexOf("CheckJNI")==-1&&symbols[i].name.indexOf("NewStringUTF")!=-1){
newstringutf_addr=symbols[i].address;
}
}
var jstring=Memory.allocUtf8String("Fup1p1");
var newfunction=new NativeFunction(newstringutf_addr,"pointer",["pointer","pointer"]);
var Jstring=newfunction(Java.vm.tryGetEnv().handle,jstring);
var getstringutfchars=Java.vm.tryGetEnv().handle.readPointer().add(0x548).readPointer();
var getstringutfchars_func=new NativeFunction(getstringutfchars,"pointer",["pointer","pointer","pointer"]);
console.log(getstringutfchars_func(Java.vm.tryGetEnv().handle,Jstring,ptr(0)).readCString());
函数堆栈与内存读写的追踪
so层的堆栈打印
console.log(Thread.backtrace(this.context,Backtracer.FUZZY).map(DebugSymbol.fromAddress).join('\n')+'\n')//FUZZY也可以改成ACCURATE
Thread.backtrace(this.context,Backtracer.FUZZY)//返回函数栈的所有地址(但是是LR寄存器的值,体现在代码中就是函数返回的下一行代码的地址)
map(DebugSymbol.fromAddress)// 拿到符号信息,然后替换掉地址,所以这样打印出来的时候你能看到函数名+偏移的值。
join('\n') map返回的是一个数组,用join给它分隔。
但是要注意,它返回的地址是LR的地址,体现在程序中就是函数的下一行,这一点要注意。
frida-trace
也是可以打印so库函数的调用流程
插件链接
IDA 安装好插件后,直接edit->plugins 找到插件。
使用插件后会默认在桌面生成一个文件,然后直接在终端中输入命令 frida-trace -UF -O txt的路径
然后会在当前目录下生成一个文件夹,里面保存了很多js文件,每个js文件其实就对应这一个函数,你也可以修改这些函数,实现自己的功能。
然后就会打印出函数的调用顺序。
内存读写监控
frida的脚本实现的内存读写不是很精确,后面会学习unidbg的内存读写
思路就是使用Frida的API 去修改内存页的权限,然后代码执行到此处就会触发异常。so库中遇到异常就会执行你覆写进去的异常处理的handler。
不精确的原因也是每次修改的是一个页的权限,学过操作系统的也都知道,为了保持系统的效率,都是按页管理的,所以不能精确到一个具体的地址上去!
function hook_dlopen(addr,soname,callback){
Interceptor.attach(addr,{
onEnter:function(args){
var path=args[0].readCString();
if(path.indexOf(soname)!=-1){
this.hook=true;
}
// console.log(hexdump(args[0]));
},onLeave:function(retval){
if(this.hook==true){
callback();
}
}
});
}
var android_dlopen_ext_addr=Module.findExportByName(null,"android_dlopen_ext");
console.log(android_dlopen_ext_addr);
hook_dlopen(android_dlopen_ext_addr,"libxiaojianbang.so",set_raed_write);
function set_raed_write(){
Process.setExceptionHandler(function(details){
console.log(JSON.stringify(details,null,2));
console.log("lr:",DebugSymbol.fromAddress(details.context.lr));
console.log("pc:",DebugSymbol.fromAddress(details.context.pc));
Memory.protect(details.memory.address,Process.pointerSize,"rwx");
console.log(Thread.backtrace(details.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n')+'\n');
return true;
});
var addr=Module.findBaseAddress("libxiaojianbang.so").add(0x3cfd);
Memory.protect(addr,8,"---");
}
快速定位到JNI静态/动态注册的函数
定位静态注册的函数
JNI静态注册的时候会用到dlsym函数去找到JNI_OnLoad符号所对应的方法地址,所以我们hook dlsym函数就行。
var dlsymAddr=Module.findExportByName("libdl.so","dlsym");
Interceptor.attach(dlsymAddr,{
onEnter:function(args){
this.args1=args[1];
// console.log("dlsym: "+args[1].readCString());
},onLeave:function(retval){
var module=Process.findModuleByAddress(retval);
// console.log(JSON.stringify(Process.findModuleByAddress(retval)));
if(module==null)return;
console.log("module_name:"+module.name,"method_name:"+this.args1.readCString(),"method_addr:"+retval,"offset:"+retval.sub(module.base));
}
});
定位动态注册的函数
动态注册的时候需要用到RegisterNatives这个函数,下图为函数的参数,JNINativeMethod结构体。
熟悉NDK开发的也都知道,method是有一个映射表的,可以有很多个函数表。
对于结构体,主要就是有三个成员,第一个是函数名,第二个是函数参数的签名,第三个就是最重要的函数指针。还要注意64位中,一个指针的大小是8字节。后面代码中体现的就是Process.pointerSize了。
var symbol=Process.getModuleByName("libart.so").enumerateSymbols();
var RegisterNative_addr = null;
for(let i=0;i<symbol.length;i++){
if(symbol[i].name.indexOf("CheckJNI")==-1&&symbol[i].name.indexOf("RegisterNatives")!=-1){
RegisterNative_addr=symbol[i].address;//通过枚举取得RegisterNatives函数的地址
}
}
console.log("RegisterNative_addr:"+RegisterNative_addr);
Interceptor.attach(RegisterNative_addr,{
onEnter: function(args){
var env=Java.vm.tryGetEnv();
var className=env.getClassName(args[1]);
var method_count=args[3].toInt32();
for(let i=0;i<method_count;i++){//遍历函数映射表,每个结构体的大小是24字节,所以后面要add(Process.pointerSize*3),写成add(24也行)。
var method_name=args[2].add(Process.pointerSize*3*i).readPointer().readCString();
var method_signature=args[2].add(Process.pointerSize*3*i).add(Process.pointerSize).readPointer().readCString();
var method_pointer=args[2].add(Process.pointerSize*3*i).add(Process.pointerSize*2).readPointer();
var module=Process.findModuleByAddress(method_pointer);
console.log("className:"+className,"method_name:"+method_name,"method_signature:"+method_signature,"module_name:"+module.name,"method_pointer:"+method_pointer,"method_offset:"+method_pointer.sub(module.base));
}
},onLeave:function(retval){
}
})
Frida InlineHook
hook某一行汇编代码,以之前的a3+a4+a5为例子
var soaddr=Module.findBaseAddress("libxiaojianbang.so");
var hookAddr=soaddr.add(0x17BC);
Interceptor.attach(hookAddr,{
onEnter:function(){
console.log(this.context.x8);
}
,onLeave:function(){
console.log(this.context.x8.toInt32());
}
})
Frida检测
好文:https://bbs.kanxue.com/thread-217482.htm
ptrace占坑
此图中,下面一条进程就是上面一条的子进程,可以通过观察PPID来确定。
在linux中,一个进程只能被一个进程附加
一个进程在任何时候只能被一个进程附加。这是因为当一个进程使用ptrace附加到另一个进程时,目标进程的状态会发生变化,例如,目标进程可能会停止运行,以便进行调试操作。如果允许多个进程同时附加到同一个目标进程,那么这可能导致混乱和不一致的状态。
而frida在注入中就需要用到ptrace,附加到程序上,然后修改程序寄存器以及内存,从而在目标程序中加载frida-agent。
如果一个程序,通过子进程来ptrace自己,那么frida将注入失败。
那有哪些办法呢?
spawn模式启动
frida并不需要一直ptrace程序,以spawn模式启动,这样frida在ptrace结束后,程序的子程序也能正常的去ptrace父进程。
修改代码逻辑
静态修改 APK:你可以先反编译 APK,然后修改其代码以禁用或绕过自我 ptrace 的部分。完成修改后,重新编译并签名 APK。这样,当你运行修改后的 APK 时,它将不再尝试自我 ptrace,使得 Frida 能够附加到进程。
但是有的时候,子进程ptrace父进程并不都是为了反frida,有的时候只是为了守护进程或者普通的多进程而已。
其他注入方式,比如gadget等,这里不详细介绍
进程名检测
通过遍历进程名,来查看是否启动了frida-server
这个就简单了,重命名一下frida-sever的名字就能避开检测,和win里面一样(突然想起的了我的adi.exe)
端口检测
frida-server的启动默认端口是27042。
anti
/data/local/tmp/fs64 -l 0.0.0.0:1234
adb forward tcp:1234 tcp:1234
frida -H 127.0.0.1:1234 package_name -l hook.js
D-Bus协议通信
Frida使用D-Bus协议通信,可以遍历/proc/net/tcp文件,或者直接从0-65535
向每个开放的端口发送D-Bus认证消息,哪个端口回复了REJECT,就是frida-server
anti
其实就hook strcmp 或者 strstr 这些函数,修改它的返回值或者参数都行。
maps
每个进程在/ proc / [PID]中都有自己的文件夹。这里的文件和子文件夹包含有关该过程的许多有用和重要的信息,其中/proc/ [PID]/maps显示进程的映射内存的图表。
maps文件用于显示当前app中加载的依赖库
Frida在运行时会先确定路径下是否有re.frida.server文件夹
若没有则创建该文件夹并存放frida-agent.so等文件,该so会出现在maps文件中
在frida注入进程序后,进入proc/PID(程序PID) 目录下,查看maps文件,可以找到frida-agent
扫描task目录
扫描目录下所有/task/pid/status中的Name字段
寻找是否存在frida注入的特征
具体线程名为gmain、gdbus、gum-js-loop、pool-frida等
readlink去查看proc/self/…
通过readlink查看/proc/self/fd、/proc/self/task/pid/fd下所有打开的文件,检测是否有Frida相关文件。
fd文件下记录了app打开的文件
ls -l 可以查看详细的信息,可以找到re.frida.server(这个文件夹,只要打开frida-server客户端)
扫描内存
扫描内存中是否Frida库特征出现,列如字符串LIBFRIDA
读ART源码下的System.loadLibrary
先阅读一下安卓源码
Blog Link1
Blog Link2
主要就看下面的loadLibrary0了,之前的findlibrary主要就是对libraryname进行一个补全,比如加上了lib和.so。有兴趣的话可以自己看看。
我们接下来关注的重点就是nativeLoad这个函数
是个Native函数,我们继续查看.c
继续跟踪JVM_NativeLoad这个函数
然后再看LoadNativeLibrary这个函数,它传入了filename…
继续往下看,终于找到了关键函数
发现一个无比熟悉的函数
一路跟进…
一直找到了do_dlopen函数
在构造函数中找对Init和init_array的调用
JNI_OnLoad的调用时机
hook initarray
前面看过了安卓的源码都知道,init和initarray的调用时机是在do_dlopen函数中。所以直接hook do_dlopen函数是不可取的,因为无论是在On:Enter(此时还init和initarray函数还没有被调用)还是On:Leave(此时,这两个函数已经调用完毕,再hook也没用了)。所以,我们hook的最佳时机,还是在call_constructors这个函数处。
以一个小demo为例子
我们要先hook dlopen函数,然后再OnEnter的时候,就可以得到so库的地址,然后hook call_constructors这个函数。
var ishooked=0;
function hook_dlopen(addr,soname,callback){
Interceptor.attach(addr,{
onEnter:function(args){
var sopath=args[0].readCString();
console.log(sopath);
if(sopath.indexOf(soname)!=-1){
start_hook();
}
},onLeave:function(){
}
})
}
function hook_init_array(){
var so_addr=Module.findBaseAddress("libxiaojianbang.so");
var init_test1_addr=so_addr.add(0x1C54);
var init_test2_addr=so_addr.add(0x1C7C);
var init_test3_addr=so_addr.add(0x1C2C);
Interceptor.replace(init_test1_addr,new NativeCallback(function(){
console.log("init_test1_addr has been replaced");
},'void',[]));
Interceptor.replace(init_test2_addr,new NativeCallback(function(){
console.log("init_test2_addr has been replaced");
},'void',[]));
Interceptor.replace(init_test3_addr,new NativeCallback(function(){
console.log("init_test3_addr has been replaced");
},'void',[]));
ishooked=1;
}
function start_hook(){
var call_constructors_addr=null;
var symbols=Process.getModuleByName("linker64").enumerateSymbols();
for(let i=0;i<symbols.length;i++){
var symbol=symbols[i];
if(symbol.name.indexOf("call_constructors")!=-1){
console.log(symbol.name,symbol.address);
call_constructors_addr=symbol.address;
}
}
Interceptor.attach(call_constructors_addr,{
onEnter:function(args){
if(!ishooked)hook_init_array();
},onLeave:function(){
}
})
}
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(android_dlopen_ext,"libxiaojianbang.so",ptr(0));
hook JNI_Onload
由前面的源码可知,JNI_Onload的调用时机是在dlopen后,所以hook JNI_Onload的方法就只需要在dlopen 的OnLeave处hook就行了
function hook_dlopen(addr,soname,callback){
Interceptor.attach(addr,{
onEnter:function(args){
this.args0=args[0];
},onLeave:function(){
var sopath=this.args0.readCString();
if(sopath.indexOf(soname)!=-1){
callback();
}
}
})
}
function hook_JNI_onload(){
var so_addr=Module.findBaseAddress("libxiaojianbang.so");
console.log(so_addr);
var init_test1_addr=so_addr.add(0x1CCC);
Interceptor.replace(init_test1_addr,new NativeCallback(function(){
console.log("JNI_OnLoad has been replaced");
},'void',[]));
}
var android_dlopen_ext = Module.findExportByName("libdl.so", "android_dlopen_ext");
hook_dlopen(android_dlopen_ext,"libxiaojianbang.so",hook_JNI_onload);
hook pthread_create函数
有一些检测函数会在so库加载的时候,通过pthread_create去创建多个线程。所以我们可以通过hook pthread_create函数,来查看它开启了哪些进程,把和检测相关的函数直接干掉。
function pthread_create(){
var pthread_create_addr=Module.findExportByName("libc.so","pthread_create");
console.log("pthread_create_addr:"+pthread_create_addr);
Interceptor.attach(pthread_create_addr,{
onEnter:function(args){
console.log(args[0],args[1],args[2],args[3]);
var module=Process.findModuleByAddress(args[2]);
if(module!=null)console.log(module.name,args[2].sub(module.base));
},onLeave:function(retval){
console.log("retval is -> "+retval);
}
});
}
pthread_create();
评论区