9、不安全的数据存储:SharedPreferences、SQLite、内部/外部存储检测
数据存储这块,说白了就是看 App 把用户隐私藏哪儿了。我见过太多开发图省事,直接把密码、Token 明文往 SharedPreferences 里一扔,结果反编译 APK 后一览无余。今天咱们就聊聊这些存储方式的安全隐患,以及怎么用自动化工具和手动手段把它们揪出来。
核心观点: 任何存储在客户端的数据,默认都是不安全的。我们能做的,是尽量提高攻击者的获取成本。
9.1 SharedPreferences:最容易被忽视的“保险箱”
SharedPreferences 是 Android 最轻量的键值对存储。嗯,用起来确实方便,但问题也出在这儿——默认情况下,它存的是明文 XML 文件。
风险点:
- 文件路径:
/data/data/<包名>/shared_prefs/<文件名>.xml - 权限问题:如果设置了
MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE(API 17 已废弃,但旧设备仍存在),其他 App 也能读。 - 明文存储:密码、Token、身份证号直接以字符串形式保存。
自动化检测方法:
# 使用 drozer 扫描
dz> run app.provider.info -a <包名>
dz> run app.provider.read content://<包名>/shared_prefs/xxx.xml
# 使用 MobSF 静态分析
# 上传 APK 后,查看 "Shared Preferences" 报告项
手动检测步骤:
- 用 adb 进入设备 shell:
adb shell - 切换到 App 数据目录:
run-as <包名>(需 debuggable 或 root) - 查看 XML 文件:
cat /data/data/<包名>/shared_prefs/*.xml
我的经验: 有一次我测试某银行 App,发现他们把用户登录后的 sessionId 明文存在 SharedPreferences 里。更离谱的是,这个文件权限是 644,任何 App 都能读。我写了个小工具,遍历所有第三方 App 的 shared_prefs 目录,结果捞出来一堆敏感信息。
9.2 SQLite 数据库:加密了吗?
SQLite 是 Android 内置的关系型数据库。很多 App 用它存聊天记录、缓存数据甚至用户资料。默认情况下,数据库文件也是明文的。
风险点:
- 文件路径:
/data/data/<包名>/databases/<数据库名>.db - 未加密:直接用 SQLite 浏览器就能打开查看。
- SQL 注入:如果 ContentProvider 暴露了数据库接口,可能被注入攻击。
自动化检测:
# 使用 objection 动态分析
objection -g <包名> explore
android sqlite list
android sqlite dump <数据库名>
手动检测:
- 拉取数据库文件:
adb pull /data/data/<包名>/databases/xxx.db - 用 SQLite 工具打开:
sqlite3 xxx.db - 执行
.tables查看所有表,SELECT * FROM <表名>;查看内容
注意: 有些 App 会用 SQLCipher 或 Room 配合加密。但别高兴太早——我见过把加密密钥硬编码在 Java 代码里的,反编译后直接拿到密钥,跟没加密一样。
9.3 内部存储 vs 外部存储:权限的陷阱
内部存储(/data/data/<包名>/files/)默认只有 App 自己能访问。外部存储(/sdcard/Android/data/<包名>/ 或 /sdcard/<自定义目录>/)则可能被其他 App 或用户直接读取。
常见问题:
- 把敏感文件写到
/sdcard/根目录下,任何文件管理器都能看到。 - 使用
getExternalStorageDirectory()而不是getExternalFilesDir(),导致文件暴露。 - 日志文件、缓存文件包含用户隐私信息。
检测方法:
# 查看外部存储文件
adb shell ls -la /sdcard/<包名>/
adb shell ls -la /sdcard/Android/data/<包名>/
# 查看内部存储文件(需 root 或 debuggable)
adb shell run-as <包名> ls -la files/
避坑指南: 我曾经测试一个社交 App,他们把用户头像缓存到
/sdcard/<包名>/cache/ 目录下。文件名直接用用户手机号命名,比如 13800138000.jpg。这等于把用户手机号公开了。后来我建议他们用哈希值命名,并且把缓存目录改到内部存储。
9.4 知识体系与检测流程
下面这张图总结了不安全数据存储的检测思路,从静态分析到动态验证,一步步来。
9.5 自动化工具实战
光靠手动太慢了。我一般先用自动化工具扫一遍,再针对可疑点手动验证。
| 工具 | 检测类型 | 命令/操作 | 输出示例 |
|---|---|---|---|
| MobSF | 静态分析 | 上传 APK → 查看 "Data Storage" 章节 | 列出所有 SharedPreferences 文件名及内容 |
| drozer | 动态分析 | run app.provider.finduri |
发现可读的 ContentProvider 暴露数据库 |
| objection | 运行时 Hook | android sqlite dump <db> |
直接输出数据库所有表内容 |
| Frida | 自定义 Hook | Hook SharedPreferences.getString() |
拦截所有读取的键值对 |
小技巧: 用 Frida 写个脚本,Hook 住
android.content.SharedPreferences 的 getString() 和 putString() 方法,运行时所有读写操作都会打印到控制台。这样不用翻文件,直接看调用栈就知道哪些数据被存了。
9.6 修复建议
发现问题后,怎么修?我总结了几条原则:
- SharedPreferences: 使用 EncryptedSharedPreferences(AndroidX Security 库),自动加密键和值。
- SQLite: 使用 SQLCipher 或 Room 配合加密,密钥从 KeyStore 获取,不要硬编码。
- 外部存储: 敏感数据一律写到内部存储。如果必须用外部存储,使用
getExternalFilesDir()并设置文件权限为MODE_PRIVATE。 - 日志: 发布版本关闭所有日志输出,避免 Logcat 泄露数据。
再次提醒: 不要以为用了加密就万事大吉。密钥管理才是真正的难点。我见过把密钥放在 Native 层 so 文件里的,结果用 IDA Pro 一分析,字符串直接暴露。密钥必须走 Android KeyStore 或远程获取。
好了,关于不安全数据存储就聊到这儿。记住一句话:客户端没有绝对的安全,我们能做的只是让攻击者多费点功夫。下一章咱们聊聊网络通信安全,看看 HTTPS 是不是真的靠谱。