Frida 基础操作2

1.FRIDA 初级

1.1.Frida远程过程调用(rpc)

有时候分析一个应用的算法时找到了算法的实现方法但方法的逻辑特别复杂或被混淆了怎么办?
这时候我们可以选择一个简单而粗暴的方式,直接刚,啊呸,直接调。
frida支持在js代码中使用rpc.exports关键字对js函数进行导出,导出方法可在python代码中通过script.exports.导出符号()进行调用。配合之前说过的frida的方法主动调用,可以很好的完成对应用中的算法或核心方法进行主动单独调用。

function Hook10(){
    
    
	Java.perform(function(){
    
    
		console.log("Frida Test Hook10");
		// 主动调用类静态方法
		var clszz = Java.use("cn.gemini.k.fridatest.FridaHook1");
		console.log("func3_verify_static  ret:"+clszz.func3_verify_static(">>>pwd<<<"));
	});
}

rpc.exports = {
    
    
    exporthook10: Hook10 //将方法导出为exporthook10符号,这里要注意导出名千万不能有大写字母或下划线
};

python代码

# -*- coding: UTF-8 -*-
import frida
import sys

# 目标包名
appPacknName = "cn.gemini.k.fridatest"
scriptFile = "hook_script.js"

# 输出日志的回调方法
def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

device = frida.get_usb_device()
# spawn模式,找到目标包名并重启,在启动前注入脚本
pid = device.spawn([appPacknName])
session = device.attach(pid)
# 注意这里需要将device.attach(pid)这句代码写在前面,这样执行才符合预期(启动时程序白屏,等待下面这行代码来恢复执行)
# 其实在https://www.jianshu.com/p/b833fba1bffe这篇文章中有提到
device.resume(pid)

# 方式一: 通过js文件创建hook代码
with open(scriptFile, encoding='UTF-8') as f:
    script = session.create_script(f.read())
# 方式二: 直接将hook代码写在python文件中
# script = session.create_script(js_code)

script.on("message", on_message)
script.load()  # 把js代码注入到目标应用中
# 避免结束
# sys.stdin.read()

# frida RPC测试
script.exports.exporthook10()  # exportmain为js文件中的导出符号

1.2.Frida手动加载dex

破解应用时如果想在应用中执行我们自己写的Java类代码通过frida怎么实现?
可以将自定义的Java类编译成一个dex文件,懒得单独编译也可以直接去apk文件中获取,需要注意的是有些apk可能存在多个dex文件,这里需要找一下一定要是含有我们Java类的那个dex。
dex文件准备好后使用adb工具push到设备的"/data/local/tmp/“目录下方便我们的frida代码加载,之后通过Java.openClassFile(”/data/local/tmp/my.dex").load()对目标dex进行加载。这样就可以通过Java.use调用我们直接写的方法了。

function hook() {
    
    
    Java.perform(function () {
    
    
        Java.openClassFile("/data/local/tmp/my.dex").load();
        var cls = Java.use("xx.xxxxxx.x.xxxxxxxxxxxx"); // 自定义的Java类
		console.log(cls);
        var obj = cls.$new();   // 实例化自定义的Java类
		console.log(obj);
    });
}

1.3.Frida Hook动态加载的dex

如果需要hook的类所在dex是应用在运行过程中动态加载的怎么hook?
首先怎么判断是否为动态加载:内存中确实存在该类,但apk中的dex却找不到该类,那么可能就是动态加载的。
这种情况下通过frida的一般hook流程是hook不上这个类的,因为frida使用的默认classloader与加载该类的classloader是不一样,此时就需要通过frida的enumerateClassLoaders方法来枚举当前进程的classloader,再通过loader.findClass方法在每个枚举到的classloader中寻找是否存在我们想要的类,找到后再通过Java.classFactory.loader=loader来切换一下当前frida使用的classloader,切换完成后就可以通过Java.use进行类的查找了。

// hook动态加载的dex
function Hook11(){
    
    
	Java.perform(function(){
    
    
		console.log("Frida Test Hook11");
		// 遍历当前所有的classloader
		Java.enumerateClassLoaders({
    
    
			onMatch:function(loader){
    
    
				try {
    
    
					// 这里需要注意的是如果遍历到的loader中没有找到目标类则会报错,所以需要添加try catch
					if(loader.loadClass("xxx.xxx.xxx")){
    
    
						Java.classFactory.loader = loader;
						var Dynamic = Java.use("xxx.xxx.xxx");
						console.log(Dynamic);
						Dynamic.HHH.implementation = function(){
    
    
							return "hook 11";
						}
					}
				} catch (error) {
    
    
				}
			},
			onComplete:function(){
    
    
			}
		})
	});
}

1.4.Frida栈回溯

一般在定位应用的某个功能点时用的较多,常见用法是对一些系统API进行hook,比如系统加解密API、接受用户输入的系统API等,当程序执行到被hook的方法时再通过对当时的堆栈进行打印并分析上下栈信息即可得知该功能的代码实现所在位置。

function PrintJavaStacks1(){
    
    
	var thread = Java.use("java.lang.Thread");
	var tOBJ = thread.$new();
	var stack = tOBJ.currentThread().getStackTrace();
	var at = ""
	for(var i = 0; i < stack.length; i++){
    
    
		at += stack[i].toString()+"\n";
	}
	console.log(at);
}

1.5.Frida Hook时机

有些场景下的hook要求我们在APP应用启动前就要进行hook,比如分析被加过固的应用又或是需要hook的方法是一个静态方法,在程序启动时就被初始化,这种情况该如何控制frida的hook时机呢?
前面说过frida支持两种注入模式,一种是直接对已启动的目标应用进行附加注入。另一种是以挂起的方式重启目标应用,应用启动时会先进入等待模式,观察设备发现应用是处于白屏状态的,实际上是在等待我们唤醒主线程来继续完成启动。
对于需要在APP应用启动前就hook的场景我们就需要选择第二种注入模式。
以挂起的方式启动应用可以使用"frida -U -f com.xxx.xxx -l hook.js “来实现,之后需要我们通过手动输入%resume的方式来让主线程开始运行。
该命令没有加”–no-pause"参数,如果加上该参数程序启动时就不会进入等待而是直接启动。加与不加的区别就是启动时不等待与等待。
参数"-f"表示,需要重启这个应用并且attach上去,与之对应的是"-n"或"-p"参数,分别是通过指定进程名或进程id来attach到正在运行的应用。
参数"-l"表示本次需要加载的js注入代码。
如果是在python代码中要以挂起的方式启动则可以使用spawn()方法,唤醒的方法是resume(),需要指定pid。

1.6.Frida程序与App数据交互

主要涉及到两个方法send()/recv();
send:向Frida应用程序发送JSON可序列化消息
recv:接收来自Frida应用程序发送的一条消息
wait:直到消息已经被recv接收,并且recv的回调方法已经执行完毕并返回
js注入程序通过send方法将应用的数据发送给PC端的frida应用程序,然后frida应用程序对接收到的数据进行各种处理修改后再发送给js注入程序,这样就可以完成python对app数据动态修改的效果了。可能有些小伙伴会问,为什么要传到PC端去处理,之后又回传给js代码而不直接通过js代码处理呢?
主要是考虑到手机性能的问题,如果需要处理的数据比较复杂,那么使用PC去跑则效果更好。
python客户端处理代码

def my_message_handler(message, payload):
    print(message)
    print(payload)
    if message["type"] == "send":
        print(message["payload"])
        data = message["payload"].split(":")[1].strip()
        print('message:', message)
        print("burning data:"+data)
        data = str(base64.b64decode(data), "utf-8")# 解码
        user, pw = data.split(":") # 提取用户名和密码
        data  =  str(base64.b64encode(("admin" + ":" + pw).encode("utf-8")), "utf-8") # 组成新的组合并编码,这是使用admin登陆
        print("encoded data:", data)
        script.post({
    
    "my_data": data})  # 将JSON对象发送回去
        print("Modified data sent")

注入的js代码

Java.perform(function () {
    
    
    var tv_class = Java.use("android.widget.TextView");
    tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
    
    
        var string_to_send = x.toString();
        var string_to_recv;
        console.log("send"+string_to_send);
        send(string_to_send); // 将数据发送给PC端
        recv(function (received_json_object) {
    
    
            string_to_recv = received_json_object.my_data
            console.log("string_to_recv: " + string_to_recv);
        }).wait(); //收到数据之后,再执行下去
        var string = Java.use("java.lang.String");
        string_to_recv = string.$new(string_to_recv);//将收到的数据转化为String类型
        return this.setText(string_to_recv);
    }
});

1.7.Frida类型转换、参数构造

1.7.1.类型转换
我们在hook某个方法分析其参数值时,如果参数不是string类型,那么打印出来的很可能就是[object Object],这种情况我们就需要对参数做一些转换处理才能打印出来真实的值。

  • 打印HashMap或Map类型的参数x

frida给我们提供了类型强转方法:Java.cast()

var map = Java.use("java.util.HashMap");
var args_x = Java.cast(x,map);  //将参数x转换为HashMap类型
console.log(args_x.toString()); //调用HashMap的toString方法
  • 打印char[]类型的参数x
var arr = Java.use("java.util.Arrays");
console.log("参数对应数组:" + arr.toString(x));
  • 打印byte[]类型的参数x
var arr = Java.use("java.util,Arrays")
var JavaString = Java.use("java.lang.String")
console.log("参数对应数组:" + arr.toString(x)); //调用Arrays类的toString方法将byte[]类型参数x转为string类型
console.log("参数对应字符串:" + JavaString.$new(x));//将byte[]类型参数x做为String类构造参数

1.7.2.参数构造

frida获取应用context
我们在主动调用app的某个方法时经常会遇到参数是一个context对象,此时我们就可以通过下面的方式获取context对象。

var currentApplication= Java.use("android.app.ActivityThread").currentApplication();
var context = currentApplication.getApplicationContext();

frida构造任意类型的数组
frida还给我们提供了构造任意类型数组的方法java.array()
用法格式:Java.array(‘type’,[value1,value2,…])
使用构造char[]类型:

var charArray = Java.array('char',['你','好']);

使用构造Object[]类型:

var objectclass = Java.use("java.lang.Object");
var objectArray = Java.array('char',[objectclass.class]);

1.8.打印non-ascii
应用场景一般是apk被混淆了,混淆后的方法名都是一些乱码或不显示的非ASCII字符,这时候因为我们没法确定方法名所以就没法指定需要hook的方法,这时候可以通过先编码打印出来,再用编码后的字符串去hook即可解决方法名乱码的问题。
https://api-caller.com/2019/03/30/frida-note/#non-ascii
示例方法:

int ֏(int x) {
    
    
    return x + 100;
}

js代码:

Java.perform(function x() {
    
    
        var cls = Java.use("com.example.hooktest.MainActivity");
        var methods = cls.class.getDeclaredMethods();   // getDeclaredMethods方法会获取该类的所有方法,包括私有的,公有的,保护的
        for (var i in methods) {
    
        // 遍历获取到的所有方法
            console.log(methods[i].toString()); // 打印原始方法名
            console.log(encodeURIComponent(methods[i].toString().replace(/^.*?\.([^\s\.\(\)]+)\(.*?$/, "$1")));    // 过滤处理并编码
        }
        // %D6%8F == ֏
        cls[decodeURIComponent("%D6%8F")].implementation = function (x) {
    
    
                console.log("original call: fun(" + x + ")");
                var result = this[decodeURIComponent("%D6%8F")](900);
                return result;
            }
    }
)

1.9.收集的一些工具方法
string转byte[]

function stringToBytes(str) {
    
      
    var ch, st, re = []; 
    for (var i = 0; i < str.length; i++ ) {
    
     
        ch = str.charCodeAt(i);  
        st = [];                 
        do {
    
      
            st.push( ch & 0xFF );  
            ch = ch >> 8;          
        }    
        while ( ch );  
        re = re.concat( st.reverse() ); 
    }  
    return re;  
} 

frida读写std::string

function readStdString(str){
    
    
    const isTiny = (str.readU8()&1) === 0;
    if (isTiny)
        return str.add(1).readUtf8String();
    return str.add(2*Provess.pointerSize).readPointer().readUtf8String();
}

function writeStdString(str, content){
    
    
    const isTiny = (str.readU8()&1) === 0;
    if (isTiny)
        str.add(1).writeUtf8String(content);
    else
        str.add(2*Process.pointerSize).readPointer().writeUtf8String(content);
}

Guess you like

Origin blog.csdn.net/weixin_38819889/article/details/119846548