侧边栏壁纸
博主头像
Fup1p1 's Blog 博主等级

梦想是财富自由~

  • 累计撰写 38 篇文章
  • 累计创建 24 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录

【Android】so层逆向(施工中...)

Fup1p1
2023-04-29 / 0 评论 / 0 点赞 / 2456 阅读 / 0 字 / 正在检测是否收录...

枚举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的加密函数。
image-1682771214941
函数有四个参数,我们比较关注后面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 配色

成功拿到返回值,和登录页面抓包中的值一样
image-1682771432038

模块基址的几种获取方式与函数地址的计算

模块基址的获取方式

如果在导入、导出、符号表中找不到的函数,就需要自己手动计算地址了(基址+偏移)。

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));
}

image-1682773919182

函数地址的计算

thumb指令(thumb1指令为2字节,thumb2包含2字节和4字节的指令)

函数地址计算: so基址+函数在so中的偏移+1

arm指令(A32和A64,指令长度为4字节)

函数地址计算: so基址+函数在so中的偏移

一般来说,用IDA打开,设置下显示指令长度,如果长度都为4,那么就是arm指令。长度为2或者2,4交替,则是thumb指令。

示例

image-1682776215098
继续以这个函数为例

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检测

Screenshot_20230221-223246
弹出了一个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弹出后才附加上去。
image-1682844644079
重点就看com.hoge…WelcomeActivity这个类了.$指的是匿名内部类。(这里$2$1说明嵌套了两个匿名内部类)
我们jadx打开,这apk居然没有加壳,很快就能定位到关键代码。
image-1682834014101
我们查看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);
    }
});

image-1682845314290

jnitrace

jnitrace项目地址
用法: jnitrace -m attach -l xxx.so com.xxxx(不加attach的话,就以spawn模式启动)
然后他会将jni函数以及其参数都打印出来。
image-1682854644643

混淆的字符串查看

hook地址,比较繁琐

对于一些混淆的字符串,我们可以等它加载之后再去hook它的地址
比如
image-1682856004005
这是一个jni函数,按理说后面d010是类的路径,可是我们查看发现被混淆了。
这时候,就可以通过frida去hook加载到内存后已经解密的字符串。

Java.perform(function(){
    var soAddr=Module.findBaseAddress("liblogin_encrypt.so");
    console.log(hexdump(soAddr.add(0xD010)));/./不用+1
})

然后等app启动之后再attach上,就能看到内存中字符串的结果
image-1682856149486

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文件。
image-1682863608845
食用方法:

sofixer -s orig.so -o fix.so -m 0x0 -d
-s 待修复的so路径
-o 修复后的so路径
-m 內存dump的基地址(16位) 0xABC
-d 输出debug信息

修复后打开,完美。
image-1682863087756

修改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函数代码

image
以这个函数为例,我们的目的是修改使他的值为a3+a4-a5。
我们先去armconverter,看看修改后的ARM指令的机器码
image-1688093721248
然后开始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);

image-1688100337119
Hook成功
image-1688100554592

    console.log(Instruction.parse(write_addr).toString())//打印汇编指令
    console.log(Instruction.parse(write_addr).next)//打印吓一跳指令的地址

so层主动调用任意函数

比如,我们想去调用一个自实现的jstring2cstr函数
image-1688109502746

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

手动计算函数地址

image-1688176322858
JNIEnv本质上就是一个结构体,其对JNINativeInterface结构体做了一个封装,所以如果我们能够在内存中定位到这个结构体,就可以通过偏移去去到函数指针,然后再去调用或者Hook。
image

console.log(hexdump(Java.vm.tryGetEnv().handle.readPointer()));//记得加上readPointer

image-1688176362728

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){

    }

});

image-1688177283328

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为例子
image-1688134727289
我们要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){

    }
});

image-1688135217964

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文件其实就对应这一个函数,你也可以修改这些函数,实现自己的功能。
image-1688459937937
然后就会打印出函数的调用顺序。

内存读写监控

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));
    }
});

image-1688197668081

定位动态注册的函数

动态注册的时候需要用到RegisterNatives这个函数,下图为函数的参数,JNINativeMethod结构体。
image-1688203910428
熟悉NDK开发的也都知道,method是有一个映射表的,可以有很多个函数表。
image-1688204176202
对于结构体,主要就是有三个成员,第一个是函数名,第二个是函数参数的签名,第三个就是最重要的函数指针。还要注意64位中,一个指针的大小是8字节。后面代码中体现的就是Process.pointerSize了。
image-1688203885016

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){

    }
})

image-1688204379091

Frida InlineHook

hook某一行汇编代码,以之前的a3+a4+a5为例子
image-1688212800332

    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());
        }
    })

image-1688212811158

Frida检测

好文:https://bbs.kanxue.com/thread-217482.htm

ptrace占坑

image
此图中,下面一条进程就是上面一条的子进程,可以通过观察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。
image-1683341118592
image-1683342113319

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
image-1683344046628

anti

其实就hook strcmp 或者 strstr 这些函数,修改它的返回值或者参数都行。

maps

每个进程在/ proc / [PID]中都有自己的文件夹。这里的文件和子文件夹包含有关该过程的许多有用和重要的信息,其中/proc/ [PID]/maps显示进程的映射内存的图表。

maps文件用于显示当前app中加载的依赖库
Frida在运行时会先确定路径下是否有re.frida.server文件夹
若没有则创建该文件夹并存放frida-agent.so等文件,该so会出现在maps文件中

image-1683350875363

在frida注入进程序后,进入proc/PID(程序PID) 目录下,查看maps文件,可以找到frida-agent

扫描task目录

扫描目录下所有/task/pid/status中的Name字段
寻找是否存在frida注入的特征
具体线程名为gmain、gdbus、gum-js-loop、pool-frida等
image-1683354419326

readlink去查看proc/self/…

通过readlink查看/proc/self/fd、/proc/self/task/pid/fd下所有打开的文件,检测是否有Frida相关文件。
fd文件下记录了app打开的文件
ls -l 可以查看详细的信息,可以找到re.frida.server(这个文件夹,只要打开frida-server客户端)
image-1683362256402

扫描内存

扫描内存中是否Frida库特征出现,列如字符串LIBFRIDA

读ART源码下的System.loadLibrary

先阅读一下安卓源码
Blog Link1
Blog Link2
image-1688263284780
主要就看下面的loadLibrary0了,之前的findlibrary主要就是对libraryname进行一个补全,比如加上了lib和.so。有兴趣的话可以自己看看。

image-1688263667066
我们接下来关注的重点就是nativeLoad这个函数
image-1688264117891
是个Native函数,我们继续查看.c
image-1688264161989
继续跟踪JVM_NativeLoad这个函数
image-1688264959646
然后再看LoadNativeLibrary这个函数,它传入了filename…
image-1688265411921
继续往下看,终于找到了关键函数
image-1688265505317
发现一个无比熟悉的函数
image-1688265768070
一路跟进…
image-1688265873426

image-1688265889315

一直找到了do_dlopen函数
image-1688266459125
在构造函数中找对Init和init_array的调用
image-1688266736040
JNI_OnLoad的调用时机
image-1688276278215

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这个函数。
image-1688283642425

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();

image-1688346665838

0

评论区