文章总结: 本文介绍一种通用的AndroidApp动态攻击面挖掘方法:通过UI广度优先遍历(BFS)自动点击每个页面触发动态加载,结合FridahookWebView的loadUrl、onPageStarted等关键点,抓取静态分析无法发现的动态H5/JSURL。核心要点包括候选排序(底部tab优先)、状态去重(归一化指纹)和自愈机制(卡死重启),工具已开源。 综合评分: 85 文章分类: 移动安全,渗透测试,逆向分析,安全工具,实战经验
Android UI BFS:把 App 的每一页点开,抓出动态加载的 JS
Lior1969 Lior1969
Moonlight安全
2026年9月5日 00:00 北京
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
MOONLIGHT SECURITY · ISSUE 04
MoonLight安全 · 2026-09-04
一篇通用的 Android App 动态面挖掘方法:UI 广度遍历 + Frida 抓 H5/JS URL
很多 App 的接口、H5、JS 在静态分析里根本看不到——它们不是打包时就写死的,而是”点进去”才动态加载的。文章讲一个通用方法:用 UI 广度优先遍历把每个页面点一遍,触发动态加载,再用 Frida hook 住 WebView,把动态 H5 / JS URL 一条条抓出来。
01 为什么静态分析抓不全
大部分 App 的页面/JS 依赖动态加载:WebView 里的 H5、动态 feature、分包、懒加载 JS。manifest 里最多给你一个壳 Activity,真正的内容——接口地址、JS 文件、H5 页面——是运行时才拼出来、或者才从服务端拉下来的。只看 manifest + 反编译做静态分析,会漏掉一大块攻击面。
对应的办法就是反过来:把每个页面点进去,让动态加载被真实触发。这就是 UI 广度优先遍历(BFS)要做的事。
02 整体思路:一个 BFS 循环
核心就是一个队列驱动的广度遍历。状态 = 当前屏 UI 层级快照,队列 = 这个快照里每个可点控件的中心坐标。逐点 tap → 长等待 → 重新 dump → 去重 → 把新页面的可点候选继续入队。伪代码:
# UI BFS 主循环(通用 App)
state = ui_dump(pkg) # uiautomator dump 出的 UI 层级
queue = pick_candidates(state) # 抽取可点候选 (cx, cy)
while queue and states_visited < MAX_STATES:
(x, y), label, depth = queue.pop(0)
tap(x, y) # 点击候选
sleep(DELAY) # 长等待,触发动态加载
new_state = ui_dump(pkg) # 重新 dump
h = state_hash(new_state) # 归一化指纹
if h in visited: # 已见过 -> 返回,死胡同
back()
continue
visited.add(h) # 记录新状态
record_urls(h) # 记 H5/WebView/接口 URL
if depth < MAX_DEPTH:
queue += pick_candidates(new_state)
03 可点候选怎么挑
从 dump 出的 XML 里逐条解析 <node> 节点:取 text / content-desc、clickable、class、resource-id、bounds,算出中心坐标和面积。过小的节点(宽高小于 8px)和容器节点(action_bar_root / content / viewPagerLayout)直接丢掉,不作点击目标。
排序是最关键的一步:很多 App 的主导航在底部 tab 栏,如果不优先点它,BFS 很容易卡在某个”空态页 / 个人页”这样的死胡同,只遍历 1 个状态就结束。所以候选排序遵循三条:
越靠底越优先(cy 越大越先,底部 tab 优先)
有 text / content-desc 的优先(能点出导航意图)
面积从大到小,保证点的是主要控件
04 状态去重:state_hash
每个状态用”class + resource-id”拼成一个归一化指纹,再 md5 取前 12 位。这样同一页面就算文字变了、内容刷新了,也不会被当成新状态,队列才有可能收敛。否则 BFS 会在同一个页面反复点击,永远跑不完。
def state_hash(xml):
# 只取 class + resource-id 做归一化,忽略文字/位置变化
body = re.findall(r'class="([^"]*)"[^>]*?resource-id="([^"]*)"', xml)
s = "|".join("%s#%s" % (cls.split(".")[-1], rid) for cls, rid in body)
return hashlib.md5(s.encode()).hexdigest()[:12]
05 自愈:卡死就重启
Android 冷启动有个 AMS start-timeout(约 10s),App 启动慢了会被系统直接杀掉;WebView / 复杂页面也可能卡死 UI 线程。所以遍历一定要带自愈:检测到进程没了、或者卡在 logo 启动页(截图判定为低亮度低纹理),就 force-stop + 重新 am start + 竞速重挂 Frida 续跑。否则一次卡死,整个 BFS 就白干了。
踩坑:很多 App 的网络库被壳混淆(OkHttp 的 RealCall、Request$Builder 按名 hook 会 ClassNotFound)。上层按名 hook 不可用时,别在一条路上硬撞,退一步走 WebView 层,或直接 hook native。
3 个关键参数一表看懂
| 环节 | 作用 | 不做的后果 | | — | — | — | | 候选排序 | 底部 tab 优先 | BFS 卡死胡同,只跑 1 个状态 | | state_hash 去重 | 状态归一化收敛 | 同一页反复点击,跑不完 | | 长等待 + 自愈 | 等动态加载 / 卡死重启 | 加载没触发、或一次卡死全崩 |
本节要点
候选排序 + 状态去重 + 自愈,这三件事决定了 BFS 能否真正跑通。缺一个,遍历要么卡死、要么无限循环、要么被系统杀掉。
06 抓动态 JS:hook 住 WebView
BFS 把动态加载触发之后,要真正抓到”加载了哪些 URL”,就用 Frida hook 住 WebView 的三个关键点:loadUrl(App 主动加载 H5)、onPageStarted(页面开始加载)、shouldOverrideUrlLoading(拦截跳转)。每个都 send("[URL] ...") 上报,runner 端收集去重。
// hook WebView 抓动态 H5 / JS URL,上报给 Python runner 收集
var WV = Java.use("android.webkit.WebView");
WV.loadUrl.overload("java.lang.String").implementation = function (u) {
send("[URL] web-load " + u);
return this.loadUrl(u);
};
var WVC = Java.use("android.webkit.WebViewClient");
WVC.onPageStarted.overload(
"android.webkit.WebView", "java.lang.String", "android.graphics.Bitmap"
).implementation = function (wv, url, fav) {
send("[URL] page-start " + url);
return this.onPageStarted(wv, url, fav);
};
WVC.shouldOverrideUrlLoading.overload(
"android.webkit.WebView", "java.lang.String"
).implementation = function (wv, url) {
send("[URL] override " + url);
return this.shouldOverrideUrlLoading(wv, url);
};
runner 端解析 [URL] 前缀,过滤过短的、去重,汇总成动态 URL 清单。这些 URL 就是静态分析拿不到的 H5 / JS 入口,也是后续分析的真实攻击面。
这样的方法对任意 App 可复用:换包名(–pkg)、必要时指定启动 Activity(–start-activity),其余逻辑通用。
扫码关注 · MoonLight安全
每周一篇,专注一线安全实战与方法论
本文工具 · 开源地址
本文用到的 app_crawl.py 已清洗为通用可复用代码,随项目 8eeth0ven / bfs-clicker 一起开源在:
https://github.com/8eeth0ven/bfs-clicker
如果对排查隐藏页面、逆向动态接口有帮助,欢迎 Star、在看或分享给同好。关注 MoonLight安全,持续输出脱壳、抓包、签名逆向与黑盒测试实战。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Moonlight安全 Lior1969 Lior1969《Android UI BFS:把 App 的每一页点开,抓出动态加载的 JS》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论