一、问题描述
使用frida工具,hook安卓程序中一个类的函数,虽然听起来简单,但对于对一次接触这个东西的来说,却是很容易被绕晕的。
本次实验的对象是我在网上找的一个非常简单的安卓记事本程序,
可以直接在这里下载:notepad, 提取码: 55dc
。
下载点击运行即可解压,内有两个文件,一个是源码压缩包,另一个是对应的程序安装包:
二、开始操作
这里默认你已经搭建好了frida环境,你可以直接通过下面的命令将其安装到模拟器上:
adb install app-note.apk
注意最后面是这个安装包的路径,这里假设你的终端路径就在这个文件夹下,那么可以直接运行上面的命令进行安装。
安装完成后,在模拟器中点击程序运行,然后输入命令frida-ps -Ua
,找到该记事本的标识符:
然后新建一个test.js
文件,存入以下代码:
function main() {
var MainActivity = Java.use("cn.itcast.notepad.NotepadActivity"); //得到主组件的类
//获取该类上的startActivityForResult函数
var startActivityForResult = MainActivity.startActivityForResult.overload('android.content.Intent', 'int');
//hook该函数
startActivityForResult.implementation = function (v, c) {
console.log('test:', v, c); //尝试打印程序传入该函数的两个参数
startActivityForResult.call(this, v, c); //继续让其调用原本的函数逻辑
console.log('Done:' + JSON.stringify(this));//打印一下该实例对象
};
}
if (Java.available) { //如果java虚拟器已经启动
//则执行perform函数传入的函数对象
Java.perform(()=>{
main(); //在该函数中跳转执行main函数
});
}
保存后,执行命令:frida -U -f cn.itcast.notepad -l test.js
此时,只要你在模拟器中点击这个加号,就会调用上面我们写好的函数,并打印出来对应的日志。
三、代码详解
上面是操作过程,不是很复杂,但麻烦的是中间各种步骤的逻辑,以及为什么要这么做。
首先是拿到apk程序的标识符:cn.itcast.notepad
,有了这个我们就可以通过后面的命令:
frida -U -f cn.itcast.notepad -l test.js
将我们写的js
代码注入到该程序中去。
-U
:连接USB设备-f
:注入代码的目标文件,其后跟着的就是apk的标识符。-l
:加载其后的js脚本文件注入到目标程序中。
然后是我们写的js代码,里面有很多对象,比如这里用到的Java
对象,就是frida
为我们提供好的。
通过Java.available
属性来判断目标程序是否已经加载完成,如果加载完成,那就可以通过Java.perform
传入我们希望在目标程序中执行的函数代码。
这里为了便于观察,又在传入的这个函数中调用我们前面写的main
函数,main
函数中才是我们写代码的地方。
了解了这个,其实你就可以直接将其简写成下面这样了:
然后来到我们的主函数main
函数中,首先是第一行代码:
var MainActivity = Java.use("cn.itcast.notepad.NotepadActivity"); //得到主组件的类
这里通过Java.use
直接获取目标程序中的主类,这个类可以通过一些工具获得,比如我用到的一个:GDA4.10.exe, 提取码: s1w9
。
将apk文件拖进去,它就会完成分析:
点击右上角的那个图标,就可以进入入口函数中,往最上面翻,就可以看到这个包的类,将其复制下来进入代码中即可。
得到这个类后,我们还需要hook这个类中具体的函数,更准确的来说,是我们要分析点击记事本中这个按钮时会执行哪些函数,我们就hook哪些函数:
最方便的方式自然是直接分析源码,前面也已经提供了源码的压缩包,通过android studio将其打开,直接查看布局文件,看它的id,为add
:
然后进入代码文件,就可以看到为它添加了监听事件,并在事件中调用了本类的startActivityForResult
函数:
所以我们的目标就是hook该函数,第一步就是先得到该函数:
var startActivityForResult = MainActivity.startActivityForResult.overload('android.content.Intent', 'int');
直接通过.
的方式就能拿到,但由于这个函数有一系列重载的函数,所以我们还需要用overload
来获取指定类型的重载函数,也就是上面代码中所调用的那个函数。
这个不用记,你不用overload
也可以继续往下写,只不过后面会报错,并且报错信息有相关的函数签名,然后直接将合适的复制过来用即可。
这里不调用overload
,就会报错让你选择一个相应的重载函数,直接将其复制过来即可。
最后就是hook
该函数了:
//hook该函数
startActivityForResult.implementation = function (v, c) {
console.log('test:', v, c); //尝试打印程序传入该函数的两个参数
startActivityForResult.call(this, v, c); //继续让其调用原本的函数逻辑
console.log('Done:' + JSON.stringify(this));//打印一下该实例对象
};
通过函数的implementation
实现,让其等于一个你想要hook的函数即可,这个函数的参数,也就是startActivityForResult
函数的参数,由于它有两个,所以这里我也写了两个。
然后在这个函数中,我将传入的这两个参数直接打印了一下,并且还重新调用了原函数:startActivityForResult
,不影响其正常工作逻辑。
注意:由于startActivityForResult
只是一个函数,作为类函数是需要实例对象的,开发时由于是通过对象调用,不需要自己手动填充,这里就不行了,必须要填入调用这个函数的实例对象,也不难,就是this
。
毕竟我们hook掉的是这个类函数,能调用这个hook函数的必然是这个类的实例化对象,那么this
自然也就指向该对象了,所以可以直接用。
四、没有源码时
很多时候我们拿不到源码,可能就不是那么容易找到相应的hook函数,但通过前面提到的按个工具,其实我们依旧可以直接看出一点痕迹。
因为这个应用程序主页就只有一个可以点击的地方,所以当你在这个反编译工具中,双击右上角进入入口出,看到setOnClickListener
添加监听对象时,就基本可以推测出这就是我们的目标:
然后此时,你可以直接鼠标双击后面的NotepadActivity
对象,它会跳转到对应的代码处:
这里可以看到,这里的点击函数中,就通过传入的this$0
来调用这个函数,这个传入的this$0
参数不就是外面的那个实例对象吗?
所以这样其实没有源码,也能分析出来。