Frida是一款基于Python和JavaScript的进程级Hook框架,其中JavaScript语言承担了Hook的主要工作,而Python语言则相当于提供给外界的绑定接口,使用者可以通过Python语言将JavaScript脚本注入进程中,官方也在下方仓库中提供了Python远程调用JavaScript中函数的方式:

https://github.com/frida/frida-python

下面简单介绍下通过Python实现Frida注入的基本方式。

0x00 开发环境准备

工欲善其事,必先利其器。编写 frida js 时让 IDE 智能提示【使用 IDE 编写 frida js 时智能提示】

方法一

  • git clone https://github.com/oleavr/frida-agent-example.git
  • **cd frida-agent-example/**,执行命令 npm install
  • 然后使用 VSCode、pycharm、idea 等 IDE 打开此工程,在 agent 目录下编写 JavaScript 代码时就会有智能提示。

JS单步调试

https://bbs.pediy.com/thread-265160.htm

能愉快的单步调试 frida 的 js 脚本,可以方便不少。首先运行 frida 脚本

frida -l </Users/name/path/test.js> --debug --runtime=v8 <port/name>

或者:

session = dev.attach(app.pid)
script = session.create_script(jscode, runtime="v8")
session.enable_debugger()

启动后会回显 Inspector 正在监听 9229 默认端口

Chrome Inspector server listening on port 9229

chrome

打开 chrome://inspect, 点击 Open dedicated DevTools for Node。

image.png

此时 debug 已经连接,切换至 Sources,按 Command + P 加载要调试的脚本,即可下断调试了。

image.png

pycharm

首先安装 Node.js 插件,重启。添加调试器 Attaching to Node.js/Chrome,端口默认即可。Attach to 应选择 Node.js < 8 started with --debug, 下面的自动重连选项可选可不选。

image.png

触发断点需要在 debug 窗口切换到 script 选项卡,右键要调试的脚本,选择 Open Actual Source,在新打开的 Actual Source 窗口设置好断点后,需要再取消/启用一次所有断点作为激活,发现断点上打上对勾才真正可用了。

image.png

接下来就可以正常调试了

image.png

优缺点

  1. 用 Chrome 调试支持的更为顺滑,调试脚本自动重加载,断点也能正确响应。
  2. 用 PyCharm 调试断点有时需要手动激活有点麻烦,但可以使用PyCharm 的Debug 窗口和快捷键。
  3. PyCharm 使用 ts 环境调试时,可以直接在ts文件上下断,也不需要手动激活断点。

方法二

https://bbs.pediy.com/thread-258513.htm

基础Frida代码完成

frida 代码提示插件。如果你安装了 Frida, 不管你熟不熟悉 nodejs 的生态, 肯定已经安装好了

npm install @types/frida-gum

你需要在你编写注入 js 文件的目录下运行 ( 可以不事先创建 package.json,只是会出现一条警告 )

然后使用可以TypeScript代码完成功能的编辑器 ( 比如 vscode、pycharm、idea ) 打开js文件即刻。

基础 frida 代码完成“ 就可以补全 frida js 代码,如果想要 类成员函数及成员变量的类型等功能,可以安装插件 frida-tsplugin

下载并安装插件 frida-tsplugin

在任意目录下:git clone https://github.com/tacesrever/frida-tsplugin ,然后在 frida-tsplugin 目录下运行

npm install
npm run compile

Frida-tsplugin 特性

  1. 可以识别 Java.use 和 Java.cast
  2. 可以追踪变量赋值传递
  3. 可以识别并追踪类成员函数及成员变量的类型
  4. 可以根据重载函数的参数类型识别对应的重载函数
  5. 可以识别 someJavaFunction[.overload(...)].implementation = function(...) {...} 函数块中的参数类型和this类型

ps. 对于未能追踪到的类型, 可以使用 Java.cast 来为其做一个声明

0x01 简单介绍

获取设备

import frida
# 无线连接
# /data/local/tmp/frida-server -l 0.0.0.0:6666
# Wifi ADB监听IP和端口为192.168.2.111:5555
device = frida.get_device_manager().add_remote_device("192.168.2.111:6666")

# 有线连接
device = frida.get_usb_device()

注入进程

import frida
import time

# spawn方式注入进程
pid = device.spawn(['com.example.luodemo'])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)

# attach模式注入进程
session = device.attach('com.example.luodemo')

注入脚本

import frida

script = session.create_script('''
setImmediate(Java.perform(function(){
console.log('hello python frida')
}))
''') #读入Hook脚本内容

script.load() #将脚本加载进进程空间中

文件方式注入进程

with open("LuoHook.js", encoding="UTF-8") as f:
script = session.create_script(f.read())
script.load()

调用函数

import frida

session = frida.get_usb_device().attach('目标应用包名')
script = session.create_script(open('myscript.js').read())

def on_message(message, data):
print(message)

script.on('message', on_message)
script.load()

# 现在可以通过 RPC 调用函数
result = script.exports.myFunction(arg1, arg2)
print(result)

备注

script.on

在 Frida 的脚本中使用 script.on 方法是为了设置一个事件监听器,它允许你捕获并处理脚本中发出的消息。这通常用于以下两种情况:

  1. 接收脚本中的输出:脚本可以通过 send 方法发送消息到附加的进程(例如,打印调试信息或者返回结果)。
  2. 处理脚本中的异常:如果脚本中发生错误,Frida 会自动发送一个包含错误信息的消息。

以下是 script.on 方法的基本用法:

script.on('message', function(message, data) {
// message 参数包含消息的类型和内容
// data 参数包含与消息相关的原始数据(如果有的话)

if (message.type === 'send') {
// 处理脚本通过 send 方法发送的消息
console.log("Received message from script:", message.payload);
} else if (message.type === 'error') {
// 处理脚本中的错误
console.log("Script reported an error:", message.description);
}
});

这里 message 对象通常具有以下属性:

  • type:消息类型,可以是 'send''error''console'
  • payload:当消息类型是 'send' 时,这是脚本发送的实际数据。
  • description:当消息类型是 'error' 时,这是错误描述。

data 参数通常是一个包含原始数据(例如,缓冲区)的对象,它仅在消息类型是 'send' 时有意义。

使用 script.on 方法,你可以确保在脚本运行时能够接收和处理任何重要的通知或数据,从而实现与脚本的交互。这在调试和与脚本进行通信时非常有用。

rpc.exports

rpc.exports 是在 Frida 脚本中使用的一个属性,它用于定义可以从外部通过 RPC(远程过程调用)调用的函数。通过这种方式,你可以将 Frida 脚本中的某些功能暴露给外部程序,使得外部程序可以远程执行这些功能。

以下是如何使用 rpc.exports 的基本步骤:

第一种方式,函数直接嵌套在RPC导出中,直接导出。

test.js代码:

rpc.exports = {
hello: function () {
return 'Hello'
},
failPlease: function () {
return 'oops'
}
}

python代码:

import frida

device = frida.get_usb_device()
session = device.attach("设置")
with open("test.js") as f:
script = session.create_script(f.read())
script.load()

api = script.exports
print(api.hello())
print(api.fail_please())

第二种方式,先写函数在 RPC ,然后导出。

test.js 代码

function hello() {
return 'Hello'
}

function failPlease() {
return 'oops'
}

rpc.exports = {
hi: hello,
fail: failPlease
}

python代码:

import frida

device = frida.get_usb_device()
session = device.attach("设置")
with open("test.js") as f:
script = session.create_script(f.read())
script.load()

api = script.exports
print(api.hi())
print(api.fail())

0x02 ARIDA-管理PRC脚本&自动生成 http 接口的工具

下载地址:GitHub - tcc0lin/arida: 基于FastAPI实现的Frida-RPC工具,自动解析JavaScript文件生成对应API接口

实现原理

Python执行PyexecJs通过Js的AST树结构获取Frida-Js脚本中rpc.exports的方法以及对应方法的参数个数,根据方法名和参数个数通过types.FunctionDefPython AST字节码来动态生成新的Function对象,并且结合pydanticcreate_model自动生成的参数模型注册到FastAPI的路由系统中,实现Frida-RPC的功能。

工作流程

image.png

核心功能

  1. 管理JavaScript文件,具备APP-文件的映射关系
  2. 自动针对现有的JavaScript方法生成相应的API方法
  3. 自动生成Open API文档

安装&使用

参考上面官方地址

效果展示

img

img

0x03 send/recv/wait

FRIDA RPC - 奋斗的安卓勇士Blog (linqi.net.cn)

介绍

1、send

  • send() 函数用于从Frida脚本向主机控制台发送消息。
  • 这个函数可以用来传递任何可以被序列化的数据,包括基本数据类型、对象和数组。
  • 使用示例:
send('Hello from Frida script!');

2、recv():

  • recv() 函数用于在脚本中设置一个回调函数,该函数将在主机控制台发送消息时被调用。
  • 你可以使用这个函数来处理从主机控制台接收到的命令或数据。
  • 使用示例:
recv(function onData(data) {
console.log('Received data:', data);
});

3、wait():

  • wait() 函数用于暂停脚本的执行,直到主机控制台调用resume()
  • 这可以用于同步操作,尤其是在你需要等待某个外部事件或条件时。
wait();
// 脚本将在这里暂停,直到主机控制台调用resume()

案例

要实现的功能是,我们不仅仅可以在kali主机上调用安卓app里的函数。我们还可以把数据从安卓app里传递到kali主机上,在主机上进行修改,再传递回安卓app里面去。

编写这样一个app,其中最核心的地方在于判断用户是否为admin,如果是,则直接返回错误,禁止登陆。如果不是,则把用户和密码上传到服务器上进行验证。

package com.roysue.demo04;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Base64;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

EditText username_et;
EditText password_et;
TextView message_tv;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

password_et = (EditText) this.findViewById(R.id.editText2);
username_et = (EditText) this.findViewById(R.id.editText);
message_tv = ((TextView) findViewById(R.id.textView));

this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

if (username_et.getText().toString().compareTo("admin") == 0) {
message_tv.setText("You cannot login as admin");
return;
}
//hook target
message_tv.setText("Sending to the server :" + Base64.encodeToString((username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), Base64.DEFAULT));

}
});

}
}

最终程序运行之后,效果如下:

img

我们的目标就是在kali主机上“得到”输入框输入的内容,并且修改其输入的内容,并且“传输”给安卓机器,使其通过验证。也就是说,我们哪怕输入admin的账户和密码,也可以绕过本地校验,进行登陆的操作。

所以最终安卓端的js代码的逻辑就是,截取输入,传输给kali主机,暂停执行,得到kali主机传回的数据之后,继续执行。形成代码如下:

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;
send(string_to_send); // 将数据发送给kali主机的python代码
recv(function (received_json_object) {
string_to_recv = received_json_object.my_data
console.log("string_to_recv: " + string_to_recv);
}).wait(); //收到数据之后,再执行下去
return this.setText(string_to_recv);
}
});

kali主机端的流程就是,将接受到的JSON数据解析,提取出其中的密码部分,然后将用户名替换成admin,这样就实现了将adminpw发送给“服务器”的结果。

import time
import frida

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
data = data.decode("base64")
user, pw = data.split(":")
data = ("admin" + ":" + pw).encode("base64")
print "encoded data:", data
script.post({"my_data": data}) # 将JSON对象发送回去
print "Modified data sent"

device = frida.get_usb_device()
pid = device.spawn(["com.roysue.demo04"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("s4.js") as f:
script = session.create_script(f.read())
script.on("message", my_message_handler) # 注册消息处理函数
script.load()
raw_input()

我们只要输入任意用户名(非admin)+密码,非admin的用户名可以绕过compareTo校验,然后frida会帮助我们将用户名改成admin,最终就是admin:pw的组合发送到服务器。

$ python loader4.py

Script loaded successfully
{u'type': u'send', u'payload': u'Sending to the server :YWFhYTpiYmJi\n'}
None
Sending to the server :YWFhYTpiYmJi

message: {u'type': u'send', u'payload': u'Sending to the server :YWFhYTpiYmJi\n'}
data: aaaa:bbbb
pw: bbbb
encoded data: YWRtaW46YmJiYg==

Modified data sent
string_to_recv: YWRtaW46YmJiYg==

动态修改输入内容就这样实现了。