RPC 开放到公网,更多 API 与源码分析
前言
在上一篇文章中,我们深入学习了 Frida RPC 远程调用的概念和实战技巧。当你的逆向分析需要在 PC 端批量调用手机上的加密函数时,RPC 是最高效的方案。然而,当场景升级到"多人协作"或"远程机房"时,你需要将 Frida RPC 服务暴露到公网——这同时带来了便利和安全挑战。
本文将系统讲解如何安全地将 Frida RPC 开放到公网,并深入剖析 Frida 的核心 API 与源码架构,帮助你从"会用"进化到"理解原理"。
Frida RPC 开放到公网的安全考虑
为什么需要开放到公网
典型场景包括:
- 团队协作:多名逆向工程师共享同一台设备的 RPC 服务
- 远程调试:设备在机房或异地,需要在办公室进行调试
- 自动化流水线:CI/CD 中集成 Frida 脚本,需要远程调用
公网暴露的核心风险
将 Frida 直接暴露到公网是极其危险的:
- 任意代码执行:Frida RPC 本质上允许远程注入和执行任意 JavaScript 代码
- 数据泄露:中间人可以窃取 RPC 通信中的敏感数据
- 设备被控:攻击者可通过 Frida 获得设备的完整控制权
防护策略总览
| 防护层 | 措施 | 说明 |
|---|---|---|
| 网络层 | SSH 隧道 / VPN | 加密传输通道 |
| 应用层 | Token 认证 | 验证调用方身份 |
| 传输层 | TLS 加密 | 防止中间人攻击 |
| 接入层 | ngrok / frp | 内网穿透 + 访问控制 |
通过 frida-server 的 --listen 参数指定监听地址
frida-server 默认监听 0.0.0.0:27042,意味着接受来自所有网络接口的连接。在生产环境中,你应该限制监听地址:
# 仅监听 localhost(最安全,配合隧道使用)
frida-server -l 127.0.0.1
# 监听指定网卡 IP(仅允许局域网访问)
frida-server -l 192.168.1.100
# 指定端口
frida-server -l 127.0.0.1:27043
关键原则:永远不要将 frida-server 直接绑定到公网 IP。正确的做法是绑定到 127.0.0.1,然后通过隧道或内网穿透工具安全地转发。
使用 SSH 隧道安全转发 Frida 连接
SSH 隧道是将 Frida RPC 安全暴露到远程网络最简单也最可靠的方式。它的原理是在 SSH 加密通道内转发 TCP 连接,无需额外配置。
本地端口转发
在 PC 端执行,将远程设备的 Frida 端口映射到本地:
# 通过 ADB 端口转发 + SSH 隧道
# 步骤1:ADB 转发设备端口到本地
adb forward tcp:27042 tcp:27042
# 步骤2:通过 SSH 将本地端口转发到远程服务器
ssh -L 0.0.0.0:27042:127.0.0.1:27042 user@your-server
现在远程服务器上的 27042 端口就安全地连接到了手机的 Frida 服务。
远程端口转发
当你无法从设备端发起连接时,可以从远程服务器建立反向隧道:
# 在远程服务器上执行
ssh -R 27042:127.0.0.1:27042 user@device-host
SSH 隧道的优势
- 零配置加密:利用 SSH 自身的加密机制,无需额外证书管理
- 认证内置:SSH 密钥认证天然提供身份验证
- 防火墙友好:只需开放 SSH 端口(22),无需额外开放端口
使用内网穿透工具
当 SSH 隧道不够灵活时(如需要给多个客户端提供访问),可以使用专业的内网穿透工具。
ngrok 方案
ngrok 提供一键式的公网隧道,适合快速调试:
# 安装 ngrok
# 下载地址: https://ngrok.com/download
# 创建隧道,转发本地 27042 端口
ngrok tcp 27042
ngrok 会返回一个公网地址和端口(如 tcp://0.tcp.ngrok.io:12345),然后你可以通过这个地址连接 Frida:
import frida
# 使用 ngrok 提供的公网地址连接
device = frida.get_device_manager().add_remote_device("0.tcp.ngrok.io:12345")
session = device.attach("com.target.app")
frp 方案
frp(Fast Reverse Proxy)适合自建内网穿透服务,可控性更强:
frps.ini(服务器端配置):
[common]
bind_port = 7000
auth.token = your_secret_token_here
# 启用 dashboard
dashboard_port = 7500
dashboard_user = admin
dashboard_pwd = admin
frpc.ini(客户端配置):
[common]
server_addr = your-server-ip
server_port = 7000
auth.token = your_secret_token_here
[frida]
type = tcp
local_ip = 127.0.0.1
local_port = 27042
remote_port = 27042
# 服务器端启动
./frps -c frps.ini
# 客户端(设备所在网络)启动
./frpc -c frpc.ini
ngrok vs frp 对比
| 特性 | ngrok | frp |
|---|---|---|
| 部署方式 | SaaS / 自托管 | 完全自托管 |
| 配置复杂度 | 极低 | 中等 |
| 并发连接 | 有限制(付费) | 无限制 |
| 自定义域名 | 支持 | 支持 |
| 数据可控性 | 依赖第三方 | 完全自主 |
| 适用场景 | 快速调试 | 生产环境 |
公网暴露的防护措施
TLS 加密通信
Frida 从 12.0 版本开始支持 TLS 加密。启用后,所有 RPC 通信都会经过 TLS 加密:
# 使用自签名证书启动 frida-server
frida-server --certificate cert.pem --key key.pem
生成自签名证书:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
客户端连接时指定证书:
import frida
device = frida.get_device_manager().add_remote_device(
"your-server:27042",
options={
"certificate": open("cert.pem", "rb").read()
}
)
Token 认证机制
在 RPC 脚本中实现简单的 Token 认证,防止未授权访问:
// rpc-server-auth.js
const AUTH_TOKEN = "your-secret-token-2024";
rpc.exports = {
// 需要认证的接口
getSign: function(token, data) {
if (token !== AUTH_TOKEN) {
throw new Error("Authentication failed: invalid token");
}
// 实际的加密函数调用
var Signature = Java.use("com.example.app.Signature");
return Signature.sign(data);
},
// 公开的健康检查接口
ping: function() {
return "pong";
}
};
Python 客户端:
import frida
TOKEN = "your-secret-token-2024"
device = frida.get_device_manager().add_remote_device("your-server:27042")
session = device.attach("com.example.app")
with open("rpc-server-auth.js") as f:
script = session.create_script(f.read())
script.load()
api = script.exports
# 使用 Token 调用
result = api.get_sign(TOKEN, "hello_world")
print(f"签名结果: {result}")
Frida API 深入分析
Module 枚举:Process.enumerateModules()
获取当前进程中加载的所有模块信息,是逆向分析的基础操作:
// 枚举所有模块
var modules = Process.enumerateModules();
modules.forEach(function(mod) {
console.log("模块名:", mod.name);
console.log("基地址:", mod.base);
console.log("大小:", mod.size);
console.log("路径:", mod.path);
});
// 查找特定模块
var targetMod = Process.findModuleByName("libnative-lib.so");
if (targetMod) {
console.log("目标模块基址:", targetMod.base);
console.log("模块大小:", targetMod.size, "字节");
}
// 查找模块中的导出函数
var exports = targetMod.enumerateExports();
exports.forEach(function(exp) {
if (exp.name.indexOf("encrypt") !== -1) {
console.log("发现加密函数:", exp.name, "地址:", exp.address);
}
});
典型应用:定位 SO 文件基址,配合 Module.findBaseAddress() 计算 ADRP 指令中的偏移量。
内存搜索:Memory.scan()
在内存中搜索特定字节模式,是寻找加密密钥、特征码的利器:
// 搜索 UTF-8 字符串
Memory.scan(Process.getModuleByName("libnative-lib.so").base,
4096 * 1024, // 搜索范围 4MB
"68 65 6C 6C 6F", // "hello" 的十六进制
{
onMatch: function(address, size) {
console.log("找到匹配:", address,
"内容:", Memory.readUtf8String(address));
},
onComplete: function() {
console.log("搜索完成");
}
});
// 搜索十六进制模式(支持通配符)
Memory.scan(module.base, module.size,
"48 8B ?? ?? ?? ?? 48", // ?? 为通配符
{
onMatch: function(address, size) {
console.log("指令匹配:", address);
},
onComplete: function() {}
});
性能提示:Memory.scan() 在大范围内搜索时可能耗时较长,建议缩小搜索范围到目标模块内。
线程操作:Process.enumerateThreads()
枚举和管理进程中的线程:
// 枚举所有线程
var threads = Process.enumerateThreads();
threads.forEach(function(thread) {
console.log("线程ID:", thread.id,
"状态:", thread.state,
"上下文:", thread.context);
});
// 修改指定线程的寄存器
var thread = Process.enumerateThreads()[0];
var context = thread.context;
console.log("PC 寄存器:", context.pc);
console.log("SP 寄存器:", context.sp);
// Stalker 跟踪线程执行
var stalker = Stalker.follow(thread.id, {
transform: function(iterator) {
var instruction = iterator.next();
do {
// 记录每条执行的指令
iterator.putCallout(function(context) {
console.log("执行:", instruction.address,
instruction.mnemonic, instruction.opStr);
});
} while ((instruction = iterator.next()) !== null);
}
});
// 停止跟踪
Stalker.unfollow(thread.id);
Stalker.flush();
文件操作:File API
Frida 提供了完整的文件操作 API,可以直接在目标进程中读写文件:
// 读取文件
var file = new File("/data/data/com.example.app/shared_prefs/config.xml", "r");
var content = file.readText();
console.log("配置内容:", content);
file.close();
// 写入文件
var outFile = new File("/data/local/tmp/dump.bin", "wb");
outFile.write(dumpBuffer);
outFile.close();
// 遍历目录
var files = File.listDirectory("/data/data/com.example.app/files/");
files.forEach(function(filename) {
console.log("文件:", filename);
});
// 检查文件是否存在
var exists = File.exists("/data/data/com.example.app/databases/data.db");
console.log("数据库存在:", exists);
指令读写:Instruction API
Instruction API 提供了反汇编能力,可以逐条解析机器码:
// 解析单条指令
var instruction = Instruction.parse(targetAddr);
console.log("地址:", instruction.address);
console.log("助记符:", instruction.mnemonic); // 如 "BL", "MOV", "STR"
console.log("操作数:", instruction.opStr); // 如 "X0, X1"
console.log("大小:", instruction.size); // 指令字节数
console.log("字节:", instruction.bytes); // 原始字节
// 遍历函数中的所有指令
function dumpFunction(funcAddr) {
var addr = funcAddr;
for (var i = 0; i < 50; i++) { // 最多解析 50 条指令
try {
var insn = Instruction.parse(addr);
console.log(addr, insn.mnemonic, insn.opStr);
addr = insn.next; // 下一条指令地址
// 遇到 RET 指令停止
if (insn.mnemonic === "RET") break;
} catch(e) {
break;
}
}
}
// 修改指令(NOP 填充)
Memory.protect(targetAddr, 4, 'rwx');
Memory.writeByteArray(targetAddr, [0x1F, 0x20, 0x03, 0xD5]); // ARM64 NOP
Frida 源码架构概览
理解 Frida 的源码架构,有助于深入掌握其工作机制和排错能力。
核心组件关系
┌─────────────────────────────────────────────┐
│ frida-tools │
│ (frida CLI, frida-trace, frida-detect...) │
├─────────────────────────────────────────────┤
│ frida-core │
│ (服务端核心:进程管理、连接管理、序列化) │
├──────────────┬──────────────────────────────┤
│ frida-gum │ frida-agent │
│ (插桩引擎) │ (目标进程内 Agent:JS 运行时) │
│ │ │
│ - Stalker │ - JavaScriptCore V8 │
│ - Interceptor│ - ObjC/Java 桥接 │
│ - Instruction│ - Native Function 桥接 │
│ - Memory │ - RPC 服务 │
└──────────────┴──────────────────────────────┘
frida-core
frida-core 是 Frida 的中枢,负责:
- 进程管理:附加/分离进程、枚举进程列表
- 连接管理:处理 USB、TCP、Remote 连接
- 协议序列化:将 JS API 调用序列化为二进制协议传输
- frida-server:运行在设备上的守护进程
frida-gum
frida-gum 是底层的动态插桩引擎,类似于 PIN 和 DynamoRIO,但更轻量:
- Stalker:代码跟踪引擎,可以记录每条执行的指令
- Interceptor:函数 Hook 引擎,支持替换和回调
- Instruction:轻量级反汇编器
- Memory:跨平台的内存操作抽象
frida-gum 使用 V8 的 JIT 编译器技术(具体来说是 SparePatch)实现高效的运行时代码修改。
frida-agent
frida-agent 是注入到目标进程中的共享库,包含:
- JavaScript 运行时:使用 QuickJS 引擎执行 JS 代码
- 桥接层:将 JS 调用转发到 frida-gum 的 C API
- RPC 服务器:在目标进程中监听 RPC 请求
- 语言绑定:自动生成 ObjC/Java/Swift 的桥接代码
源码获取与编译
# 克隆源码
git clone https://github.com/frida/frida.git
cd frida
# 查看源码结构
ls -la
# 子模块包括:
# frida-core/ - 核心服务
# frida-gum/ - 插桩引擎
# frida-agent/ - 目标进程 Agent
# frida-python/ - Python 绑定
# frida-tools/ - 命令行工具
# 编译(需要 meson + ninja)
# 参考: https://frida.re/docs/building/
实际案例:搭建远程 RPC 服务
以下是一个完整的实战案例,搭建一个安全的远程 RPC 服务,实现 PC 端远程调用手机上的加密函数。
服务端脚本(手机端)
// remote-rpc-server.js
// 远程 RPC 服务器 - 带认证和日志
const AUTH_TOKEN = "spider-secure-token-2024";
function initJava() {
if (Java.available) {
Java.perform(function() {
console.log("[+] Java 运行时已就绪");
});
}
}
rpc.exports = {
// 认证接口
authenticate: function(token) {
return token === AUTH_TOKEN;
},
// 获取 App 签名
getAppSign: function(token, data) {
if (token !== AUTH_TOKEN) {
throw new Error("[-] 认证失败");
}
var result = null;
Java.perform(function() {
var SignUtil = Java.use("com.example.app.utils.SignUtil");
result = SignUtil.getSign(data);
console.log("[+] 签名请求:", data, "->", result);
});
return result;
},
// 获取设备信息
getDeviceInfo: function(token) {
if (token !== AUTH_TOKEN) {
throw new Error("[-] 认证失败");
}
return {
system: Process.platform,
arch: Process.arch,
pid: Process.id,
modules: Process.enumerateModules().map(function(m) {
return { name: m.name, base: m.base.toString(), size: m.size };
})
};
}
};
Python 客户端(PC 端)
#!/usr/bin/env python3
# rpc_client.py - 远程 RPC 客户端
import frida
import sys
import time
SERVER_ADDR = "your-server-ip:27042"
TOKEN = "spider-secure-token-2024"
PACKAGE = "com.example.app"
class RemoteRPC:
def __init__(self, server_addr, token):
self.server_addr = server_addr
self.token = token
self.device = None
self.session = None
self.script = None
self.api = None
def connect(self):
"""连接远程 Frida 服务"""
print(f"[*] 连接到 {self.server_addr}...")
self.device = frida.get_device_manager().add_remote_device(self.server_addr)
self.session = self.device.attach(PACKAGE)
with open("remote-rpc-server.js") as f:
self.script = self.session.create_script(f.read())
self.script.on("message", self._on_message)
self.script.load()
self.api = self.script.exports
# 验证认证
if self.api.authenticate(self.token):
print("[+] 认证成功")
else:
raise Exception("[-] 认证失败:Token 无效")
def _on_message(self, message, data):
"""处理脚本消息"""
if message["type"] == "send":
print(f"[消息] {message['payload']}")
elif message["type"] == "error":
print(f"[错误] {message['stack']}")
def sign(self, data):
"""调用远程签名函数"""
return self.api.get_app_sign(self.token, data)
def device_info(self):
"""获取设备信息"""
return self.api.get_device_info(self.token)
def disconnect(self):
if self.session:
self.session.detach()
def main():
rpc = RemoteRPC(SERVER_ADDR, TOKEN)
try:
rpc.connect()
# 获取设备信息
info = rpc.device_info()
print(f"\n[*] 设备: {info['system']} {info['arch']}")
print(f"[*] 已加载模块: {len(info['modules'])} 个")
# 批量签名
test_data = ["user_001", "user_002", "user_003"]
for data in test_data:
sign = rpc.sign(data)
print(f"[+] 签名: {data} -> {sign}")
time.sleep(0.1)
finally:
rpc.disconnect()
if __name__ == "__main__":
main()
部署流程
# 1. 在 Android 设备上启动 frida-server(仅监听 localhost)
adb shell "su -c '/data/local/tmp/frida-server -l 127.0.0.1 &'"
# 2. 建立 SSH 隧道
adb forward tcp:27042 tcp:27042
ssh -N -R 27042:127.0.0.1:27042 user@your-server &
# 3. 在 PC 端运行客户端
python3 rpc_client.py
自定义 Frida Agent 的编译和加载
对于高级需求(如需要修改 Frida 的默认行为或添加自定义的原生功能),可以编译自定义 Agent。
Agent 模板
创建一个 C 文件作为自定义 Agent 的入口:
// my_agent.c
#include <frida-core.h>
// Agent 加载时的回调
static void on_load(void *user_data) {
g_print("[my_agent] Agent 已加载\n");
// 这里可以注册自定义的 Native 函数供 JS 调用
// 或者初始化自定义的插桩逻辑
}
// Agent 卸载时的回调
static void on_unload(void *user_data) {
g_print("[my_agent] Agent 已卸载\n");
}
// 导出 Agent 元信息
FRIDA_EXPORT void frida_agent_main(FridaAgentContext *context) {
frida_agent_context_set_auto_reload_script(context, TRUE);
frida_agent_context_set_idle_callback(context, NULL);
frida_agent_context_set_message_handler(context, on_message, NULL);
on_load(context);
}
编译自定义 Agent
# 使用 frida-compile 编译 JS Agent(常用方式)
npm install -g frida-compile
# 编译
frida-compile agent.js -o agent.js.bundle
# 注入自定义 Agent
frida -U -f com.example.app -l agent.js.bundle
对于需要原生代码的 Agent,需要使用 Frida 的构建系统:
# 克隆 Frida 源码并编译
git clone --recursive https://github.com/frida/frida.git
cd frida
# 使用 meson 构建系统
# 详细步骤参考: https://frida.re/docs/homebrew/
make
总结
本文覆盖了 Frida RPC 公网部署的完整知识链:
- 安全部署:通过 SSH 隧道、ngrok/frp 等方式安全暴露 RPC 服务
- 防护措施:TLS 加密 + Token 认证,防止未授权访问
- 核心 API:Module 枚举、内存搜索、线程操作、文件操作、指令解析
- 源码架构:理解 frida-core、frida-gum、frida-agent 三层架构
- 实战案例:完整的远程 RPC 服务搭建流程
安全第一:在将任何服务暴露到公网之前,务必实施认证和加密措施。Frida 的强大能力意味着一旦被恶意利用,后果将非常严重。
在下一篇文章中,我们将通过综合案例来实践算法还原的完整思路,将这些知识融会贯通。