核心总结: 给安卓应用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.dexclasses16.dex),Manifest 为二进制 AXML,需 apktool 反编译后才能读。
  • 启动入口AndroidManifest.xmlMAIN + 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 中裸字 0foregroundServiceType="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 != 0x2711grantResults 为空:直接 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.apkbaidujishu_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」式修改,可能避免整包重编带来的引用重排。

九、与大模型协作实现:对话与迭代摘要

以下为本次实现过程中,人与大模型对话的要点摘要,便于理解「需求如何一步步落地」以及「为何采用当前方案」。

  1. 需求与文档
    先提出「分析 baidujishu.apk,如何加弹窗获取位置权限」。大模型分析 APK 结构、入口 Activity(SplashActivity)、Manifest 已有权限,并写出 README:反编译 → 改 Manifest(可选)→ 在 smali 中注入 requestPermissions 与 onRequestPermissionsResult → 回编译与重签名。同时说明 zipalign/apksigner 来自 Android SDK build-tools。

  2. 按文档改 APK
    要求「按文档改这个 apk」。大模型用 apktool 反编译,在 SplashActivity.onCreate 末尾注入请求位置权限的 smali,并新增 onRequestPermissionsResult(拒绝则 finish,同意则启动上报线程)。新建 SplashActivity$LocReport,在 run() 里对 https://baidu.com/ip 发 GET。整包回编译时出现资源链接错误(styles.xml、foregroundServiceType),修复后仍易有新报错。

  3. 替代方案:只替换 DEX
    确认「整包回编译」不可靠后,改为「只替换 dex 再重签名」:用 smali 汇编器仅编 smali_classes10 → 得到 classes10.dex → 在原 APK 上仅用 zip -u 替换该文件(不解包重打),再 zipalign + 签名。编写 replace-dex-and-sign.sh 自动化该流程,并说明 smali jar 下载与 ANDROID_HOME 检测。

  4. 上报逻辑细化

    • 无位置时只上报空参数(lat=&lon=),不再尝试两种 provider 各报一次;上报地址改为测试用 http://192.168.1.204:4000/ip
    • 需要「获取位置」:在 LocReport.run() 中用 LocationManager 取 GPS,有则拼 lat/lon,无则用空 URL。
    • 本地监听:写 listen-4000.py,在 4000 端口打印每次 GET 的 URL 与 query,便于确认是否走到上报逻辑。
  5. 安装后崩溃:Resources$NotFoundException
    整包用 unzip 再 zip 重打会导致资源错乱。改为:复制原 APK → 仅 zip -u 更新 classes10.dex,不再解包重打,资源与其余 DEX 完全保持原样,崩溃消失。

  6. 调试
    为确认「弹了授权但不知道有没有走回调」:在 SplashActivity 与 LocReport 中用 Log.e("SplashLoc", ...) 打日志(请求权限、已授权、onRequestPermissionsResult 的 requestCode、granted/denied、report run/url/ok/fail),便于 adb logcat -s SplashLoc:E 观察。

  7. 位置获取与「持续获取」

    • 解释 getLastKnownLocation 常为 null(仅缓存、未请求过、室内等),无位置时上报空即可。
    • 要求「启动后持续用任何方式获取位置」:在 LocReport 中循环最多 5 次、每次间隔 2 秒,先试 GPS 再试 network,任一个有值即拼 URL 并上报,5 次后仍无则上报空。
  8. GPS 未开时的行为

    • 要求「没开 GPS 怎么自动开启」:说明无法代码开启,只能跳转系统定位设置页。在「已有权限」与「刚授权」两处检测 isProviderEnabled(gps/network),若都关则 Toast「请开启定位以正常使用」并 startActivity(LOCATION_SOURCE_SETTINGS)。
    • 要求「需要提示一下」:在跳转前增加上述 Toast。
    • 要求「有权限但没有位置也当没有权限」:若 GPS 与 network 都关,则 Toast + 跳设置 + finish(),不进入应用。
    • 要求「授权之后检查 GPS 有没有开,没有开弹过去」:在 onRequestPermissionsResult 同意分支里,仅检查 GPS;若未开则 Toast + 跳设置(不 finish),然后照常启动上报线程。
  9. 授权后每次都跳转 GPS 的问题

    • 现象:用户授权位置权限后,每次启动应用都会跳转到系统定位设置页。
    • 原因:有权限分支(5.2)在每次启动时都检查 isProviderEnabled(gps/network),两者都关则跳转 + finish;不少设备默认或经常处于「定位关」状态,导致已授权用户每次打开都被打断。
    • 修正:有权限分支去掉「检查 GPS/network → 未开则跳转并 finish」的逻辑,改为直接启动上报线程并进入应用;无位置时上报空参数即可。若需保留提示,可仅 Toast、不跳转不 finish。

以上即为本 session 的完整总结:从 APK 分析、为何只换 DEX、到权限与定位检查、上报逻辑与调试方式,以及与大模型协作的迭代过程,形成可复现的改包与验证流程。