APK 注入位置权限弹窗与上报——百度极速版改包实录
核心总结: 给安卓应用apk扩展功能,如果没有加壳或者拖过壳,那么结合ai可玩性就很高了,我发现ai写代码目前不局限代码,连java的smali都能搞定,我们只要给需求就行了,ai帮我们出。
本文记录了一次完整的 APK 改包实战:给百度极速版注入「启动时弹出位置权限、拒绝则关闭应用、同意则获取位置并上报到指定 URL」的逻辑,并处理「有权限但 GPS/网络定位未开」的边界情况。全程采用只替换单个 DEX 再重签名的方式,避免整包回编译导致的资源错误。
特别说明:文中的 DEX / smali 修改与脚本编写,均在与大模型(LLM)对话中完成。由人提出需求与约束,大模型完成反编译分析、smali 注入、bash/python 脚本及迭代修改;本文末尾附有「与大模型协作实现:对话与迭代摘要」,便于复现思路与排查时对照。
一、目标与约束
- 目标:启动应用时弹出系统位置权限对话框;用户拒绝则直接退出,用户同意则进入应用,并在后台将位置(或空数据)上报到指定 URL(如
http://192.168.1.204:4000/ip?lat=&lon=)。 - 约束:不破坏原 APK 的资源和其余 DEX,只改入口相关逻辑并替换对应 DEX,保证安装后能正常进入主界面。
二、APK 结构与入口
- 包名:
com.baidu.searchbox.lite(百度极速版)。 - 结构:多 DEX(
classes.dex~classes16.dex),Manifest 为二进制 AXML,需 apktool 反编译后才能读。 - 启动入口:
AndroidManifest.xml中MAIN+LAUNCHER的 Activity 为com.baidu.searchbox.SplashActivity,对应 smali 在smali_classes10/com/baidu/searchbox/SplashActivity.smali。
Manifest 中已声明 ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION,因此无需改 Manifest,只需在代码里请求权限并处理回调。
三、为何不整包回编译
使用 apktool d 反编译后再 apktool b 回编译整包时,易出现:
- 资源链接错误(如
res/values/styles.xml中裸字0、foregroundServiceType="0x00000800"等)。 - aapt2 校验比原包更严,修复成本高且易连锁报错。
因此采用只替换 DEX 的方式:用 smali 汇编器把改动的 smali_classes10 编成 classes10.dex,再用 zip -u 在原 APK 上仅更新该文件,最后 zipalign + 重签名。这样资源与其余 DEX 完全不动,避免 Resources$NotFoundException 等崩溃。
四、工具链
| 工具 | 用途 |
|---|---|
| apktool | 反编译 APK,得到可读 AndroidManifest 与 smali |
| smali (baksmali/smali) | 仅将 smali_classes10 汇编为 classes10.dex |
| zip -u | 在原 APK 上仅替换 classes10.dex |
| zipalign + apksigner | 对齐与签名(需 Android SDK build-tools) |
脚本 replace-dex-and-sign.sh 已实现通用用法:./replace-dex-and-sign.sh [原始.apk] [DEX编号]。不传参时默认 baidujishu.apk、DEX 编号 10。流程:下载 smali jar → 汇编 {apk名}_decoded/smali_classes{N} → 复制原 APK 并用 zip -u 更新 classes{N}.dex → 若存在 ANDROID_HOME 则 zipalign 并用 debug.keystore 签名,输出 {apk名}_modified_signed.apk。
五、注入逻辑概览
5.1 入口:SplashActivity.onCreate
- 在
onCreate末尾(changeScreenOrientation之后、return-void之前)插入:checkSelfPermission(ACCESS_FINE_LOCATION);- 若未授权:
requestPermissions([ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION], 0x2711)并 return; - 若已授权:进入「有权限」分支。
5.2 有权限分支(已授权时)
- 获取
LocationManager,检查isProviderEnabled("gps")与isProviderEnabled("network")。 - 若两者都未开启:Toast「请开启定位以正常使用」→
startActivity(LOCATION_SOURCE_SETTINGS)→ finish(),即视为「没有位置 = 没有权限」,不进入应用。 - 若至少一个开启:启动上报线程
SplashActivity$LocReport。
⚠️ 已知问题:上述逻辑会导致已授权用户每次启动都走「有权限分支」;若设备上 GPS/网络定位未开(或系统返回未开),就会每次都弹 Toast 并跳转系统定位设置页,体验很差。修正建议:有权限分支不再强制检查 GPS/network,直接启动上报线程并进入应用;无位置时由上报线程按现有逻辑上报空参数即可。若仍希望「没开定位时提示」,可仅做 Toast 提示、不跳转不 finish,或改为仅在首次进入时检查一次。
5.3 权限回调:onRequestPermissionsResult
requestCode != 0x2711或grantResults为空:直接super.onRequestPermissionsResult并 return。- 用户拒绝(
grantResults[0] == -1):打日志、finish()。 - 用户同意:
- 仅检查 GPS 是否开启;若未开:Toast「请开启定位以正常使用」→ 打开系统定位设置页(不 finish)。
- 无论是否弹设置,都启动上报线程
LocReport。
5.4 上报线程:SplashActivity$LocReport.run()
- 循环最多 5 次,每次间隔 2 秒:
- 先
getLastKnownLocation("gps"),若有则拼lat/lon并跳出; - 否则
getLastKnownLocation("network"),若有则拼lat/lon并跳出。
- 先
- 若 5 次后仍无位置:使用默认 URL,即
lat=、lon=为空的上报。 - 对最终 URL 发 GET 请求(如
http://192.168.1.204:4000/ip?lat=xx&lon=xx或空参数),并打 Log.e(“SplashLoc”, …) 便于 logcat 排查。
六、调试与本地监听
- 日志:所有关键分支用
Log.e("SplashLoc", msg)打印(请求权限、已授权、回调 requestCode、granted/denied、report run/url/ok/fail),便于adb logcat -s SplashLoc:E观察。 - 本地收包:在电脑上运行
scripts/apk/listen-4000.py,在 4000 端口起一个 HTTP 服务,打印每次 GET 的 URL 与 query(含lat/lon),方便确认是否上报、是否有位置)。
七、文件与脚本一览
以下资源已随本文一起提供,位于 assets/reverse/apk-location-permission 目录,可直接下载使用:
| 文件 | 说明 |
|---|---|
| replace-dex-and-sign.sh | 通用:./replace-dex-and-sign.sh [原始.apk] [DEX编号],汇编对应 smali_classesN → 仅替换 classesN.dex → 对齐与签名 |
| listen-4000.py | 本地 4000 端口 HTTP 服务,打印 APP 上报的 GET 请求(含 lat/lon) |
本地复现时,将上述 assets/reverse/apk-location-permission 中的脚本与 README 放到同一目录(并准备 baidujishu.apk、baidujishu_decoded/),按 README 执行即可。原工程路径对应关系:
| 原路径(示例) | 说明 |
|---|---|
scripts/apk/baidujishu.apk | 原版 APK |
scripts/apk/baidujishu_decoded/ | apktool 反编译结果(含 smali 与 Manifest) |
.../SplashActivity.smali | 入口 Activity,注入权限与回调逻辑 |
.../SplashActivity$LocReport.smali | 上报 Runnable:取位置、拼 URL、发 GET |
八、注意事项
- 合规:修改第三方 APK 可能违反其使用条款或相关法律,仅建议在自有或已获授权的应用上操作,本文仅供学习与合规场景参考。
- 加固/校验:若应用带完整性校验或加固,修改后可能无法启动或需额外处理。
- GPS 无法「自动开启」:普通应用无法代码层面打开 GPS,只能通过
ACTION_LOCATION_SOURCE_SETTINGS跳转系统设置,由用户手动开启。
8.1 DEX 引用超 65535 时的可选方案
整包用 smali 汇编时若报错 Unsigned short value out of range: 65536,说明该 DEX 的方法/字段/类型等引用数在重排后超过了 DEX 格式的 16 位上限。可考虑:
| 方案 | 说明 |
|---|---|
| 从原 DEX 反编再替换 | 用 baksmali 反汇编原 APK 里的 classesN.dex,只替换你改动的 smali 文件后再用 smali 汇编。脚本已支持:汇编失败时自动尝试该流程(需同目录下有原 APK)。若仍超限,说明 smali 汇编时的引用顺序与原 DEX 不同,会触顶。 |
| 删减无用类再汇编 | 在反编译得到的 smali_classesN 中删除确定不会用到的类(如部分 Titan/插桩相关类),减少引用数后再汇编。需保证不删到启动链与你要用的类。 |
| 只替换方法体(不重排引用) | 用 dexlib2 写小工具:读原 classesN.dex,只替换指定类/方法的 code_item(指令区),其余表与引用顺序不变,写回。这样不会因重排而新增超限引用,但需要编程实现。 |
| DexPatcher 等补丁工具 | 使用 DexPatcher 等对原 APK 做「补丁 dex」式修改,可能避免整包重编带来的引用重排。 |
九、与大模型协作实现:对话与迭代摘要
以下为本次实现过程中,人与大模型对话的要点摘要,便于理解「需求如何一步步落地」以及「为何采用当前方案」。
需求与文档
先提出「分析 baidujishu.apk,如何加弹窗获取位置权限」。大模型分析 APK 结构、入口 Activity(SplashActivity)、Manifest 已有权限,并写出 README:反编译 → 改 Manifest(可选)→ 在 smali 中注入 requestPermissions 与 onRequestPermissionsResult → 回编译与重签名。同时说明 zipalign/apksigner 来自 Android SDK build-tools。按文档改 APK
要求「按文档改这个 apk」。大模型用 apktool 反编译,在 SplashActivity.onCreate 末尾注入请求位置权限的 smali,并新增 onRequestPermissionsResult(拒绝则 finish,同意则启动上报线程)。新建SplashActivity$LocReport,在 run() 里对https://baidu.com/ip发 GET。整包回编译时出现资源链接错误(styles.xml、foregroundServiceType),修复后仍易有新报错。替代方案:只替换 DEX
确认「整包回编译」不可靠后,改为「只替换 dex 再重签名」:用 smali 汇编器仅编smali_classes10→ 得到classes10.dex→ 在原 APK 上仅用 zip -u 替换该文件(不解包重打),再 zipalign + 签名。编写replace-dex-and-sign.sh自动化该流程,并说明 smali jar 下载与 ANDROID_HOME 检测。上报逻辑细化
- 无位置时只上报空参数(
lat=&lon=),不再尝试两种 provider 各报一次;上报地址改为测试用http://192.168.1.204:4000/ip。 - 需要「获取位置」:在 LocReport.run() 中用 LocationManager 取 GPS,有则拼
lat/lon,无则用空 URL。 - 本地监听:写
listen-4000.py,在 4000 端口打印每次 GET 的 URL 与 query,便于确认是否走到上报逻辑。
- 无位置时只上报空参数(
安装后崩溃:Resources$NotFoundException
整包用 unzip 再 zip 重打会导致资源错乱。改为:复制原 APK → 仅zip -u更新classes10.dex,不再解包重打,资源与其余 DEX 完全保持原样,崩溃消失。调试
为确认「弹了授权但不知道有没有走回调」:在 SplashActivity 与 LocReport 中用Log.e("SplashLoc", ...)打日志(请求权限、已授权、onRequestPermissionsResult 的 requestCode、granted/denied、report run/url/ok/fail),便于adb logcat -s SplashLoc:E观察。位置获取与「持续获取」
- 解释 getLastKnownLocation 常为 null(仅缓存、未请求过、室内等),无位置时上报空即可。
- 要求「启动后持续用任何方式获取位置」:在 LocReport 中循环最多 5 次、每次间隔 2 秒,先试 GPS 再试 network,任一个有值即拼 URL 并上报,5 次后仍无则上报空。
GPS 未开时的行为
- 要求「没开 GPS 怎么自动开启」:说明无法代码开启,只能跳转系统定位设置页。在「已有权限」与「刚授权」两处检测 isProviderEnabled(gps/network),若都关则 Toast「请开启定位以正常使用」并 startActivity(LOCATION_SOURCE_SETTINGS)。
- 要求「需要提示一下」:在跳转前增加上述 Toast。
- 要求「有权限但没有位置也当没有权限」:若 GPS 与 network 都关,则 Toast + 跳设置 + finish(),不进入应用。
- 要求「授权之后检查 GPS 有没有开,没有开弹过去」:在 onRequestPermissionsResult 同意分支里,仅检查 GPS;若未开则 Toast + 跳设置(不 finish),然后照常启动上报线程。
授权后每次都跳转 GPS 的问题
- 现象:用户授权位置权限后,每次启动应用都会跳转到系统定位设置页。
- 原因:有权限分支(5.2)在每次启动时都检查 isProviderEnabled(gps/network),两者都关则跳转 + finish;不少设备默认或经常处于「定位关」状态,导致已授权用户每次打开都被打断。
- 修正:有权限分支去掉「检查 GPS/network → 未开则跳转并 finish」的逻辑,改为直接启动上报线程并进入应用;无位置时上报空参数即可。若需保留提示,可仅 Toast、不跳转不 finish。
以上即为本 session 的完整总结:从 APK 分析、为何只换 DEX、到权限与定位检查、上报逻辑与调试方式,以及与大模型协作的迭代过程,形成可复现的改包与验证流程。