发布于 

RPC 开放到公网,更多 API 与源码分析

前言

在上一篇文章中,我们深入学习了 Frida RPC 远程调用的概念和实战技巧。当你的逆向分析需要在 PC 端批量调用手机上的加密函数时,RPC 是最高效的方案。然而,当场景升级到"多人协作"或"远程机房"时,你需要将 Frida RPC 服务暴露到公网——这同时带来了便利和安全挑战。

本文将系统讲解如何安全地将 Frida RPC 开放到公网,并深入剖析 Frida 的核心 API 与源码架构,帮助你从"会用"进化到"理解原理"。

Frida RPC 开放到公网的安全考虑

为什么需要开放到公网

典型场景包括:

  • 团队协作:多名逆向工程师共享同一台设备的 RPC 服务
  • 远程调试:设备在机房或异地,需要在办公室进行调试
  • 自动化流水线:CI/CD 中集成 Frida 脚本,需要远程调用

公网暴露的核心风险

将 Frida 直接暴露到公网是极其危险的:

  1. 任意代码执行:Frida RPC 本质上允许远程注入和执行任意 JavaScript 代码
  2. 数据泄露:中间人可以窃取 RPC 通信中的敏感数据
  3. 设备被控:攻击者可通过 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 公网部署的完整知识链:

  1. 安全部署:通过 SSH 隧道、ngrok/frp 等方式安全暴露 RPC 服务
  2. 防护措施:TLS 加密 + Token 认证,防止未授权访问
  3. 核心 API:Module 枚举、内存搜索、线程操作、文件操作、指令解析
  4. 源码架构:理解 frida-core、frida-gum、frida-agent 三层架构
  5. 实战案例:完整的远程 RPC 服务搭建流程

安全第一:在将任何服务暴露到公网之前,务必实施认证和加密措施。Frida 的强大能力意味着一旦被恶意利用,后果将非常严重。

在下一篇文章中,我们将通过综合案例来实践算法还原的完整思路,将这些知识融会贯通。