Frida注入跳转所有Activity:把隐藏页面和动态接口一网打尽

admin 2026-09-07 04:18:10 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍使用Fridagadget注入Android应用实现进程内跳转所有Activity的方法,通过同uid绕过非导出限制、ARouter带参路由解决数据依赖页白屏问题,并hookOkHttp与WebView抓取动态接口和H5地址。同时涵盖FLAG_SECURE绕过、SSL校验处理及自愈枚举驱动器实现,最终输出报告展示测试页面数、到达Activity及去重URL列表,为安全测试提供攻击面枚举方案。 综合评分: 85 文章分类: 移动安全,渗透测试,逆向分析,安全工具,红队


Frida 注入跳转所有 Activity:把隐藏页面和动态接口一网打尽

原创

Lior1969 Lior1969

Moonlight安全

2026年9月4日 18:50 北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

MOONLIGHT SECURITY · ISSUE 07

MoonLight安全 · 2026-09-04

进程内 startActivity 绕过非导出 + ARouter 带参跳转 + 动态 URL 抓取

真机 App 动辄几百个页面组件,可一个静态列表加 am start -n 下来大半都打不开:非 exported 被安全模型直接拦、藏在深层引导流、数据依赖页不带参数是白屏。这篇用 frida gadget 注入做「进程内跳转」,逐个打开隐藏 Activity,并 hook OkHttp/WebView 把动态加载的接口和 H5 全量抓出来。读完能带走一套可复用的「隐藏页面 + 动态接口」枚举驱动器。

01 为什么「跳转所有 Activity」能挖出隐藏页面

静态反编译能列出一个 App 的全部 Activity 组件,但它只是「壳」。真机上一跑,你就会发现三个挡路的现实:非 exported 组件、深层引导流、数据依赖页。它们共同决定了「静态列表 + 直接启动」永远摸不到真实业务面。

非 exported 组件:am start -n 是外部(跨 uid)调用,被 Android 导出校验直接丢 SecurityException。

深层引导流:订单、活动、积分这类页面藏在串行流程里,正常导航根本点不进去。

数据依赖页:详情/聊天/订单页要带 ID 或 type 才渲染,裸启动是空白页,截图也没用。

对策就三招,正好对应三个坑:进程内 startActivity(同 uid 绕过导出校验)、ARouter 带参路由(让数据依赖页渲染真实内容)、hook OkHttp/WebView(把运行时求的接口与 H5 抓出来)。下面逐个展开。

02 进程内 startActivity:同 uid 绕过非导出

Android 的安全模型规定:非 exported 组件只能被同 uid调用。外部用 am start 属于跨 uid,在 AMS 调度阶段就会抛 SecurityException。但只要把 frida gadget 注入进 app 进程,用 ActivityThread.currentApplication().getApplicationContext().startActivity() 启动,就是「自己调自己」——uid 相同,导出校验直接被跳过。

// rpc.exports.launch —— 同 uid 打开任意组件, 不校验 exported
function launch(pkg, cls, extrasJson) {
  var ActivityThread = Java.use("android.app.ActivityThread");
  var app = ActivityThread.currentApplication();
  var ctx = app.getApplicationContext();
  var Intent = Java.use("android.content.Intent");
  var it = Intent.$new();
  it.setClassName(pkg, cls);   // 非导出的私有 Activity 也能开
  it.addFlags(NEW_TASK);
  ctx.startActivity(it);       // 同 uid -> 绕过非导出 SecurityException
}

坑:为什么一定要 NEW_TASK?因为从 ApplicationContext 启动 Activity 需要一个新任务栈,不带这个 flag 会抛异常或起不来。extras 参数也在这里传:数字走 putExtra、布尔走 putBoolean、其余转字符串。

03 ARouter 带参路由:数据依赖页要传参才渲染

详情、聊天、订单这类页面的渲染完全由参数决定。直接 startActivity 一个类名、不传参数,打开就是空壳。这类 App 普遍集成阿里巴巴 ARouter,路由路径既标识页面又能承载参数——build(path) 后用 withString/withInt 带参,比裸组件更接近真实路由。某些页即便 startActivity 能开,但没走 ARouter 就把路由参数丢了,一样白屏。

// rpc.exports.jump —— ARouter 带参路由跳转
function jump(path, paramsJson) {
  var ARouter = Java.use("com.alibaba.android.arouter.launcher.ARouter");
  var pc = ARouter.getInstance().build(path);
  pc.withString("chatId", "1");   // 数据依赖页带参, 页面才渲染
  pc.withInt("type", 2);
  pc.navigation();                // 走 ARouter 路由, 而非裸组件
}

04 抓动态加载的接口与 H5

页面能跳了,接下来是真正值钱的部分:运行时才请求的接口 URL。这些在静态反编译里看不到——它们由 OkHttp 在页面渲染那一刻才构造。在 agent 里 hook 两个层:okhttp3.internal.connection.RealCall 的 execute/enqueue(请求真正发出处,抓服务接口),以及 okhttp3.Request$Builder.url(构造请求处);WebView 那边 hook loadUrl 与 onPageStarted 抓动态 H5。

// OkHttp 接口层: 请求真正发出处
var RC = Java.use("okhttp3.internal.connection.RealCall");
RC.enqueue.implementation = function (cb) {
  var r = this.request();
  send("[URL] api " + r.method() + " " + r.url().toString());
  return RC.enqueue.call(this, cb);
};

// WebView H5 层
var WV = Java.use("android.webkit.WebView");
WV.loadUrl.overload("java.lang.String").implementation = function (u) {
  send("[URL] web-load " + u);
  return WV.loadUrl.overload("java.lang.String").call(this, u);
};

统一用 send("[URL] <kind> <url>") 上报,PC 端 runner 收到后去重、落盘。下表给你对照「拦截点 — 上报格式 — 抓什么」。

| 拦截点 | 上报格式 | 抓什么 | | — | — | — | | RealCall.execute / enqueue | api 方法 地址 | 服务端接口 URL | | Request$Builder.url | rq 地址 | 请求构造处的 URL | | WebView.loadUrl | web-load 地址 | 动态加载的 H5 页 | | WebViewClient.onPageStarted | page-start 地址 | 网页开始加载 |

05 前置 bypass:没有这些,导航会被杀、截图全黑

跳转和抓 URL 之前,先要把「看」和「活」这两件事解决。否则一套遍历跑下来,要么 app 被 root/watchdog 杀掉,要么截图全是黑帧。FLAG_SECURE(0x2000)是「防截屏」位,支付/钥匙/二维码页面会点亮它,导致 screencap 黑帧;更坑的是一个 setSecure(true) 会毒化整个进程,之后每个页面截图都是 0 字节。所以 SurfaceControl 和 Builder 两个层面的 setSecure 都要强制 false。

// 剥 FLAG_SECURE + SurfaceControl.setSecure 强制 false
var W = Java.use("android.view.Window");
W.addFlags.implementation = function (f) { return this.addFlags(f & ~FLAG_SECURE); };
Java.use("android.view.SurfaceControl").setSecure.implementation = function () {
&nbsp; var a = Array.prototype.slice.call(arguments);
&nbsp; a[a.length - 1] = false; &nbsp; // 强制 un-secure surface
&nbsp; return this.setSecure.apply(this, a);
};
// SSL: hostname/cert 校验在 BoringSSL 内, 必须 native 拦截
var gvr = Module.findExportByName("libssl.so", "SSL_get_verify_result");
if (gvr) Interceptor.replace(gvr, new NativeCallback(function () { return 0; }, "long", ["pointer"]));

坑:为什么 native SSL 才是关键?因为 hostname/cert 校验发生在 BoringSSL 内部,光 hook 上层 Java HostnameVerifier 根本够不到,登录页连裸 IP 都会报「Hostname not verified」。所以必须用 SSL_get_verify_result 返回 0 兜底,再加 Java 层证书链 cleaner + HostnameVerifier。

root 检测与风险弹窗同样要前置:把 com.sample.android.util.android.b 的 b/c/d/e 全部点成 false、apigetway.a.i() 点成 false、killProcess 置空、风险弹窗 Activity 的 onDestroy 空实现。否则遍历到一半进程被 SIGKILL,白干。

06 驱动器 web_enum.py:自愈枚举全流程

上面是 agent,下面是跑在 PC 的驱动器。它做四件事:竞速冷启动逐路由跳转+长等待自愈重启落盘报告。冷启动是这套流程最脆的一环:AMS 有个约 10 秒的 start-timeout,app 起得慢就被系统杀掉。所以驱动器不手起,而是 force-stop + am start 后立刻竞速 attach,再睡 11 秒确认 app 挺过窗口,没挺过就重试(最多 7 次)。

def restart_launch(pkg, launcher):
&nbsp; for t in range(7):
&nbsp; &nbsp; adb("am force-stop " + pkg) &nbsp; &nbsp; &nbsp; &nbsp;# 冷启动
&nbsp; &nbsp; adb("am start -n " + launcher)
&nbsp; &nbsp; pid = find_pid() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # ss -tlnp | grep 14725 解析 gadget pid
&nbsp; &nbsp; dev = frida.get_device_manager().add_remote_device("127.0.0.1:14725")
&nbsp; &nbsp; sc = dev.attach(pid).create_script(open(HOOK, encoding="utf-8").read())
&nbsp; &nbsp; sc.load(); sess.resume()
&nbsp; &nbsp; time.sleep(11) &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # 等过 ~10s AMS start-timeout 窗口
&nbsp; &nbsp; if pidof(pkg):
&nbsp; &nbsp; &nbsp; return dev, sess, sc, pid &nbsp; &nbsp; &nbsp; &nbsp;# 存活 -> 已就绪
&nbsp; &nbsp; # 否则被 AMS 杀, 循环重试

每个路由执行 sc.exports_sync.jump(path),然后长等待(wait 默认 6 秒)等页面把接口请求发出来,再截图、收集 [URL]。跳之前判断 app 是否退到桌面,退了就用 exported 的 LoginActivity 把任务带回前台(Android 10 之后进程内 startActivity 会被后台启动限制拦)。自愈逻辑:连续 2 条路由卡在 logo(splash 判定)或进程消失,就自动重启续跑,而不是整体崩掉。

07 跑完拿到什么

整个遍历结束输出 web_enum_report.json,一份报告同时回答三件事:

| 字段 | 含义 | | — | — | | pages_tested | 实际跳转/测试的页面数 | | activities_reached | 真正到达了哪些 Activity 前台 | | unique_urls | 全部跳转中抓到的去重接口与 H5 地址 |

本节要点

一套遍历跑下来,能把「静态反编译看不到」的页面和接口全部摊开:几百个组件逐个进,动态加载的接口 URL 一条不落。这既是做安全测试的攻击面底盘,也是搞清楚一个 App 到底暴露了什么的捷径。具体数字随 App 而定,但方向是稳定的——跳得动、看得见、抓得出

关注 · MoonLight安全

每周一篇,专注一线安全实战与方法论

本文工具 · 开源地址

本文用到的 web_enum.py 与 control_hook.js 已清洗为通用可复用代码,随项目 8eeth0ven / bfs-clicker 一起开源在:

https://github.com/8eeth0ven/bfs-clicker

如果对排查隐藏页面、逆向动态接口有帮助,欢迎 Star、在看或分享给同好。关注 MoonLight安全,持续输出脱壳、抓包、签名逆向与黑盒测试实战。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:Moonlight安全 Lior1969 Lior1969《Frida 注入跳转所有 Activity:把隐藏页面和动态接口一网打尽》

评论:0   参与:  0