Frida是一个面向开发人员、逆向工程师和安全研究人员的动态检测工具包。是一个基于 Python + JavaScript 的 Hook 与调试框架。将您自己的脚本注入黑盒流程。挂钩任何函数、监视加密 API 或跟踪私有应用程序代码,无需源代码。编辑,点击保存,立即看到结果。全部没有编译步骤或程序重新启动。
支持众多平台:Windows、Linux、Android、Mac、iOS等。且支持从Java层到Native层的完全hook。
Frida分为服务端与客户端,客户端将运行我们编写的代码并提交到服务端来运行。客户端可以选择与哪个服务端交互。有两种客户端,一种可以直接使用命令,运行javascript进行hook操作。另一种是通过python编写Hook前的准备动作,比如为某个设备运行某个JavaScript代码,具体的hook实现仍然需要JavaScript进行。
Frida与XPosed的区别:
Frida被归类于调试框架,它不能伴随应用启动,调试应用程序时,被调试的程序由Frida启动并进行hook操作。而XPosed则伴随整个系统启动而启动,它总是能在应用程序启动之前对它进行修改。
Frida支持实时热补丁,随时修改Hook代码并应用,不需要重新编译,也不需要重启应用,这是一个很酷的功能。XPosed每次修改需要重启系统。这个限制和第一点区别相互对应。
Frida还可以Hook 其他的多个平台,并且可以Hook 二进制库。
部署十分方便
不过完全可以先使用Frida确定一个可行的方案,再到XPosed中实现。
安装步骤:
客户端:pip install frida,frida-tools
服务端:https://github.com/frida/frida/releases 根据目标平台与位数选择合适的服务端。
Android在root下运行服务端,之后执行 frida-ls-devices,可以看到已经连接的设备。
一般会有一个local一个remote,分别对应着本地调试和本地套接字调试,这两个都表示本机,其他的则是远程设备。使用local可以直接调试本机程序。
frida-ps -D <ID>可以显示指定设备的进程列表。
可以通过python对设备信息获取和操作。
-
通过frida.get_device_manager获得设备管理器
-
通过device_manager.enumerate_devices枚举设备。
-
通过device.enumerate_processes枚举进程
-
通过device.attach挂接进程,并获得会话
-
通过script.load会话创建脚本并执行
-
编写JavaScript代码来执行进程内操作。
-
通过script.on("message", on_message)注册后在JavaScript中使用send可以发送消息给python
import frida
def on_message(message,data):
print(message)
print(data)
device_manager = frida.get_device_manager()
devices = device_manager.enumerate_devices()
index = 1
for device in devices:
print("{0}:{1} => {2}".format(index,device.id, device.name))
index += 1
index = int(input("选择设备:"))
device = devices[index - 1]
processes = device.enumerate_processes()
index = 1
for process in processes:
print("{0}: {1}".format(index, process.name))
index += 1
index = int(input("选择进程:"))
process = processes[index - 1]
session = device.attach(process.name)
script = session.create_script('''
console.log("123")
send("456")
''')
script.on("message", on_message)
script.load()
另一种可以使用命令来完成这个动作
frida -D emulator-5554 -l .agent.js -f com.example.seccon2015.rock_paper_scissors
-D 选择设备
-l 选择JavaScript hook代码
-f 选择目标进程
在这之后,剩下的hook工作将在JavaScript中进行。JavaScript的API在官网有详细描述 https://frida.re/docs/javascript-api/
比如Hook一下MainActivity的onCreate方法:
需要在Java环境中进行Hook操作需要将代码放在Java.perform的回调中。
Java.use获得某个类,通过<类对象>.<需要Hook的方法>.impementation = function(){xxx} 就可以hook某个对象。此时不会调用原来的方法,如果需要调用原来的方法需要自行this.Hook的方法。
Java.perform(() => {
var mainActivityClass = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity")
mainActivityClass.onCreate.implementation = function (arg)
{
console.log("Hello world")
this.onCreate(arg)
}
});
具体安装参考 https://github.com/oleavr/frida-agent-example
安装好后前往agent/index.ts下编写相同的代码:
import { log } from "./logger.js";
Java.perform(() => {
var mainActivityClass = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity")
mainActivityClass.onCreate.implementation = function (args:any)
{
console.log("Hello world")
this.onCreate(args)
}
});
与JavaScript是相同的Api,在需要时将代码翻译为JavaScript并执行。
如果不使用npm run watch命令,每次运行前需要使用npm install再次安装。
使用npm run watch命令后每次修改TypeScript都会自动编译并应用修改到应用程序中。
不过,要注意像MainActivity.onCreate之类的方法它一般只在启动时执行,此时仍然需要重启脚本才能生效。
它是SECCON的SECCON Quals CTF 2015 APK1例题,可以在链接中下载。官网中只提供了一种解法,我这里为了演示更多功能,使用另外三种不同的方法进行解题。
首先通过观察代码:
代码中判断结果的逻辑被放在了一个匿名类中,需要当cnt等于1000时才会得到flag。
因此我们需要让cnt等于999并且让最后一次猜拳是胜利的。
在frida教程中是在onClick时修改n、m、cnt的值。由于onClick处于MainActivity下,直接获得对象就可以改(参考frida教程的做法)。
而我们会尝试在showMessageTask去修改MainActivity中的值。展示如何在内部类中访问外部类的成员:
来自于jadx的指引我们知道了匿名内部类的名字为com.example.seccon2015.rock_paper_scissors.MainActivity$1。
Java.perform(() => {
const showMessageTask = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1")
showMessageTask.run.implementation = function()
{
console.log("called")
this.this$0.value.cnt.value = 999
this.this$0.value.m.value = 0
this.this$0.value.n.value = 1
this.run()
}
});
点三个之中的一个按钮后就可以得到 flag。
Java.perform(() => {
const showMessageTask = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity$1")
showMessageTask.run.implementation = function()
{
console.log("SECCON{" +(this.this$0.value.calc()+ 1000)*107 + "}")
}
});
Java.perform(() => {
const MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity")
MainActivity.onCreate.implementation = function(arg:any)
{
console.log("SECCON{" +(this.calc()+ 1000)*107 + "}")
this.onCreate(arg)
}
});
第三种方法是直接调用二进制库:
Java.perform(() => {
const MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity")
MainActivity.onCreate.implementation = function(arg:any){
var modules = Process.enumerateModules()
for (var index in modules)
{
if (modules[index].name == "libcalc.so")
{
var exports = modules[index].enumerateExports()
for (var export_index in exports)
{
console.log(exports[export_index].name)
}
}
}
this.onCreate(arg)
}
});
为了执行时lib库已经加载,选择onCreate作为挂载点。
Process.enumerateModules可以枚举当前进程的所有模块
Modules.enumerateExports可以枚举导出表
最后我们可以通过:
Module.findExportByName获得指定库的导出函数的地址,它是一个NativePointer对象。
通过new NativeFunction让他变成一个函数,并为其设置它的返回值与参数,之后直接调用call就可以调用到二进制库中的函数了。
Java.perform(() => {
const MainActivity = Java.use("com.example.seccon2015.rock_paper_scissors.MainActivity")
MainActivity.onCreate.implementation = function(arg:any)
{
var pointer = Module.findExportByName("libcalc.so","Java_com_example_seccon2015_rock_1paper_1scissors_MainActivity_calc")
var nativeMethod = new NativeFunction(pointer as NativePointer, 'int', [])
console.log("SECCON{" +(nativeMethod.call()+1000)*107 + "}")
this.onCreate(arg)
}
});
除了Android的hook,它在Windows下也表现得十分出色。我们可以通过将设备ID改为local来访问本机程序。
这里演示劫持网易云音乐的网络访问:
首先找到网易云程序路径,把启动命令改成类似于这样:
frida -D local -l ._agent.js -f D:CloudMusiccloudmusic.exe
Hook代码就像这样:
使用Interceptor.attach挂接Native函数,第一个参数为一个指针,通过Module.getExportByName可以获得。
第二个匿名类中包含onEnter与onLeave两个回调函数,分别对应着进入函数时与退出函数时,它们都是可选的。
onEnter(args)中args为参数列表,可以通过下标访问某个参数。
onLeave(retval)中retval为函数返回值。
Hook connect需要先了解函数原型:
第一个为SOCKET对象。第二个就是我们关注的连接信息。
其中sin_port为端口,in_addr为ip地址,但它们无法直接使用,它们现在处于网络传送内存顺序(大端顺序),我们需要将其转换回小端顺序,并且还原回字符串形式的ip地址。
我们需要两个方法,分别是:
inet_ntoa:负责将十六进制的ip地址转换为字符串。
ntohs:将端口号转换为小端顺序。
使用Module.getExportByName获得函数地址。
new SystemFunction把它转换为可以调用的函数,并指定好参数和调用约定。要注意inet_ntoa传入的参数是in_addr结构,而不是in_addr指针,in_addr总长度为4。
之后就可以通过call方法调用这两个函数。
剩下的就是读取出第二个参数的数值,并传递给上面两个函数并记录返回值。
point = args[1]:取出第二个参数sockaddr。
point .readUShort():取出sin_family==AF_INET,判断是不是IPv4协议,不是则放过。注意read并不会让这个指针向后偏移。
point.add(2):将会指向sin_port
point.add(4):将会指向in_addr
得到的返回值通过.value就可以取出数值,注意读取字符串需要用readXXXString。
import { log } from "./logger.js";
var AF_INET = 2
var inet_ntoa = new SystemFunction(Module.getExportByName("ws2_32.dll","inet_ntoa"), 'pointer', ['uint32'], 'stdcall')
var ntohs = new SystemFunction(Module.getExportByName("ws2_32.dll","ntohs"), 'uint16', ['uint16'], 'stdcall')
Interceptor.attach(Module.getExportByName("ws2_32.dll","connect"), {
onEnter(args) {
let socket = args[0]
let point = args[1]
let family = point.readUShort()
if (family != AF_INET)
return
let ip_port = ntohs.call(null, point.add(2).readUShort())
let ip_addr = inet_ntoa.call(null, point.add(4).readUInt())
console.log(`socket=${socket},ip=${ip_addr.value.readAnsiString()},port=${ip_port.value}`)
},
});
效果:
Frida基本用法就是这些,如果有其他需求可以前往api文档查阅。
https://frida.re/docs/javascript-api/
原文始发于微信公众号(锋刃科技):Frida介绍与实践
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论