9、不安全的数据存储:SharedPreferences、SQLite、内部/外部存储检测

数据存储这块,说白了就是看 App 把用户隐私藏哪儿了。我见过太多开发图省事,直接把密码、Token 明文往 SharedPreferences 里一扔,结果反编译 APK 后一览无余。今天咱们就聊聊这些存储方式的安全隐患,以及怎么用自动化工具和手动手段把它们揪出来。

核心观点: 任何存储在客户端的数据,默认都是不安全的。我们能做的,是尽量提高攻击者的获取成本。

9.1 SharedPreferences:最容易被忽视的“保险箱”

SharedPreferences 是 Android 最轻量的键值对存储。嗯,用起来确实方便,但问题也出在这儿——默认情况下,它存的是明文 XML 文件。

风险点:

  • 文件路径:/data/data/<包名>/shared_prefs/<文件名>.xml
  • 权限问题:如果设置了 MODE_WORLD_READABLEMODE_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" 报告项

手动检测步骤:

  1. 用 adb 进入设备 shell:adb shell
  2. 切换到 App 数据目录:run-as <包名>(需 debuggable 或 root)
  3. 查看 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 <数据库名>

手动检测:

  1. 拉取数据库文件:adb pull /data/data/<包名>/databases/xxx.db
  2. 用 SQLite 工具打开:sqlite3 xxx.db
  3. 执行 .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 知识体系与检测流程

下面这张图总结了不安全数据存储的检测思路,从静态分析到动态验证,一步步来。

不安全数据存储检测流程 SharedPreferences SQLite 数据库 内部/外部存储 静态分析 反编译查看 XML/DB 文件 动态分析 运行时 Hook 抓取数据 文件系统检查 adb pull 拉取文件 明文敏感信息 密码/Token/身份证 未加密/硬编码密钥 SQLCipher 密钥泄露 权限配置错误 MODE_WORLD_READABLE 报告输出:高风险 → 需整改

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.SharedPreferencesgetString()putString() 方法,运行时所有读写操作都会打印到控制台。这样不用翻文件,直接看调用栈就知道哪些数据被存了。

9.6 修复建议

发现问题后,怎么修?我总结了几条原则:

  • SharedPreferences: 使用 EncryptedSharedPreferences(AndroidX Security 库),自动加密键和值。
  • SQLite: 使用 SQLCipher 或 Room 配合加密,密钥从 KeyStore 获取,不要硬编码。
  • 外部存储: 敏感数据一律写到内部存储。如果必须用外部存储,使用 getExternalFilesDir() 并设置文件权限为 MODE_PRIVATE
  • 日志: 发布版本关闭所有日志输出,避免 Logcat 泄露数据。
再次提醒: 不要以为用了加密就万事大吉。密钥管理才是真正的难点。我见过把密钥放在 Native 层 so 文件里的,结果用 IDA Pro 一分析,字符串直接暴露。密钥必须走 Android KeyStore 或远程获取。

好了,关于不安全数据存储就聊到这儿。记住一句话:客户端没有绝对的安全,我们能做的只是让攻击者多费点功夫。下一章咱们聊聊网络通信安全,看看 HTTPS 是不是真的靠谱。


公众号:蓝海资料掘金营,微信deep3321