最近有个朋友找我,说他接手了一个老项目,数据库用 SQLCipher 加密了,想导出数据做分析却无从下手。这种问题我以前也踩过不少坑,正好借这个机会把 iOS App 数据解密导出到分析结构方法完整梳理一遍。无论你是开发者要查自己 App 的数据,还是测试、数据分析岗位需要从 iOS 端拿数据做分析,这篇文章都能给你一条能直接落地的路径。
先说清楚这个内容能解决什么问题:iOS App 的数据拿不到通常有几种情况,沙盒机制挡着看不到文件、数据库被 SQLCipher 加密打不开、业务层又做了自定义加密导致导出的内容全是乱码。这篇文章会把“数据到底存哪里”“怎么把沙盒数据弄出来”“加密怎么识别和解除”“最后怎么整理成 CSV、JSON 这些分析友好的结构”全部讲透。而且全程不用越狱也能覆盖大部分场景,适合 iOS 开发新手、测试工程师、爬虫逆向入门者以及想从 App 里回收数据做分析的运营同学。
1. 先搞清楚数据到底“藏在”哪
1.1 iOS 沙盒机制带来的天然屏障
iOS 和 Android 最大的不同之一,就是每个 App 都有自己独立的沙盒目录。这个目录只有 App 自己能读写,其他 App 访问不了,你通过电脑上的文件管理器也看不到。系统层面这么做是为了安全和隔离,但对开发者来说就有点头疼了——尤其是你想导出数据的时候,第一步就卡在“东西到底放哪了”。
沙盒路径在真机和模拟器上不一样。在模拟器里,路径是这样的:
~/Library/Developer/CoreSimulator/Devices/<设备ID>/data/Containers/Data/Application/<AppUUID>在真机上,没有越狱的话,你直接通过 Finder 是看不到这个目录的。只能通过 Xcode 的 Devices 窗口、iTunes 备份、或者第三方工具把沙盒内容捞出来。这也是很多人觉得 iOS 数据“拿不到”的根本原因——不是数据不存在,而是入口被系统焊死了。
我见过不少刚入行的同学,上来就问“我的 App 数据库在手机哪个位置”,这个问题本身就问错了。正确的问题是“我通过什么路径能把沙盒目录同步到电脑上”。搞懂这个区别,后面的事就顺了。
1.2 常见数据存储方式与路径
在 iOS 里,数据存放的位置基本有固定规律。你拿到一个 App 的沙盒目录后,不用瞎翻,直奔这几个关键路径就行。
| 路径 | 存放内容 | 典型文件格式 |
|---|---|---|
| Documents/ | 用户生成的文档、导出文件、需要持久化的数据 | .sqlite .db .json .txt |
| Library/Preferences/ | NSUserDefaults 偏好设置 | .plist |
| Library/Application Support/ | Core Data、SQLite、业务数据 | .sqlite .store .db .dat |
| Library/Caches/ | 缓存数据,系统可能随时清理 | .db .tmp .cache |
| tmp/ | 临时文件,不保证持久 | 不定 |
绝大多数 App 的核心业务数据都在 Documents 和 Library/Application Support 里面。比如用 Core Data 的 App,默认会在 Application Support 下生成一个 .sqlite 文件;直接拼 SQLite 的 App,一般会把数据库放在 Documents 或 Application Support;用 Realm 的,则会在 Documents 下放一个 .realm 文件。
拿到沙盒目录之后,第一件事不是急着解密,而是先列一遍目录结构,看看有哪些文件、文件大小是多少。文件大小往往能直接透露信息——一个几十 MB 的 .sqlite 文件,里面大概率是核心业务表;一个几 KB 的 .plist,通常只是偏好设置,参考价值有限。
1.3 “拿不到”是哪种拿不到
我在实际工作里遇到过几种典型的“拿不到”,每种的原因和解法完全不一样。
第一种是开发者自己调试自己的 App。这种最简单,用模拟器跑起来后直接就能在 Finder 里打开沙盒目录,或者用 Xcode 的 Devices 窗口点 Download Container 一键导出。如果你是这个情况,直接看本文第 3 章的模拟器部分就行。
第二种是接手别人的项目,数据库是加密的。这种场景下你手里通常有源码或者至少有一部分源码,可以顺着代码找到密钥和加密逻辑。但现实往往更残酷,源码可能不全,密钥可能存在钥匙串里,加密逻辑可能是第三方 SDK 包了一层黑盒。这种情况最麻烦,也是本文重点讲解的部分。
第三种是非开发场景,比如运营或者数据分析岗位的人,要从某台 iPhone 上的 App 里把用户数据导出来做分析。这种人往往没有任何源码,甚至不是技术人员,但需求很明确——把 App 里的数据变成能导入 Excel 或 BI 工具的结构化数据。这种情况我会推荐备份法加工具导出,不需要写代码也能搞定大部分明文数据库的场景。
1.4 加密形态分三种,识别不了就白忙
很多人在第一步就栽了:拿着一个加密数据库,用常规工具打开直接报错“file is not a database”。然后就开始乱试,浪费时间。我建议先搞清楚加密的三种常见形态,再对症下药。
第一种是 SQLCipher 整库加密。这是 iOS 上最流行的开源数据库加密方案。它本质上是 SQLite 的一个加密分支,整个数据库文件从第一页开始就是密文,用文本编辑器打开看不到任何“SQLite format 3”的明文头,也看不到表名。识别方法是看文件头部,正常 SQLite 文件的头 16 字节包含“SQLite format 3”字样,而 SQLCipher 加密后的文件头是一串无法解读的随机字节。
第二种是系统的 Data Protection 加密。这是 iOS 系统级的功能,文件系统层面的加密,文件本身还是明文 SQLite,但你从备份里导出来之后,有可能因为缺少对应的 keybag 而无法读取。这种加密通常发生在 App 设置了 NSFileProtection 属性之后。识别方法是文件头正常,但读取时提示无权限或者内容为空。
第三种是业务层的自研加密。这是最让人头疼的,App 在写入数据库之前,自己先把字段内容做了 AES、DES 或者 XOR 加密,再存进 SQLite。这种情况下数据库本身是明文格式,能打开,但表里的数据全是乱码,像什么“U2FsdGVkX1+abc123”这种 Base64 字符。
我见过很多人在没识别加密类型的情况下就盲目尝试,结果浪费了大半天。正确的姿势是先做静态识别,用file命令、hexdump和strings三个工具,花五分钟就能定位是哪种加密形态。
2. 工具准备与访问路线选型
2.1 三条路线,先选对再动手
在开始搞数据之前,先想清楚一个问题:你的设备是什么状态?不同状态对应不同的访问路线,选错了就是白忙。
| 路线 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 模拟器路线 | 自己开发调试,App 跑在 Mac 的 Simulator 里 | 路径直接可见,导出最快 | 仅限模拟器,无法覆盖真机环境 |
| 越狱机 + Frida/SSH | 逆向分析、测试环境 | 灵活,能动态调试拿密钥 | 需要越狱设备,门槛高 |
| 非越狱备份路线 | 真机环境、无越狱设备 | 最稳,适用于绝大多数场景 | 需要 iTunes 备份,速度偏慢 |
我的建议是:如果你只是自己在开发调试,优先用模拟器路线,省时省力。如果你需要从真机上导数据,优先用备份路线,不需要越狱,iOS 15、16、17 都能用。只有在你需要动态调试、从内存里挖密钥的时候,才需要上 Frida 那套越狱方案。
2.2 常用工具清单
我整理了一份工具清单,都是实际工作中用得上的。不需要全部装,按需选就行。
- Xcode:自带
simctl,模拟器沙盒目录提取靠它。 - libimobiledevice:开源工具集,包含
idevicebackup2、idevice_id等命令,用于真机备份。 - 爱思助手 / iMazing:图形化工具,新手友好,一键导出沙盒和应用数据。
- sqlite3:macOS 自带,处理明文 SQLite 必备。
- sqlcipher:SQLCipher 的命令行版本,解密加密库靠它。
- Frida:动态插桩工具,用于 Hook 函数、抓取密钥。
- plutil:macOS 自带,处理 plist 文件的利器。
- jq:JSON 处理工具,导出后整理数据时用。
- class-dump:导出 App 的 Objective-C 类信息,逆向分析时用。
这些工具大部分是免费的,idevicebackup2在 macOS 上通过 Homebrew 安装就行。Frida 稍微特殊一点,需要先在电脑上安装frida-tools,然后在越狱手机上安装 frida-server,两边版本要对应,否则连不上。
2.3 为什么备份法最稳妥
在非越狱场景下,备份法是我的首选,没有之一。原因很简单:它不依赖设备状态,不需要特殊权限,只要是正常的 iPhone 都能用。
iTunes 备份会把 App 的沙盒数据打包进一个备份目录,这个目录在 macOS 上有固定的格式。更关键的是,如果你在备份的时候设置了加密备份密码,连 Keychain 里的数据也能一起导出来。Keychain 里往往存放着数据库的密钥、Token、密码等敏感信息,有了这些,后面解密你就会从容很多。
备份法还有一个额外的好处:不干扰原设备。你只是做了一次只读的备份,不会在手机上安装任何额外的东西,不影响 App 正常运行。对于生产环境的数据导出,这个优势非常重要。
有一点一定要记住:如果备份时不设置加密备份密码,那么即使备份成功,Keychain 数据也不会包含在内。所以,当你发现导出的备份里缺少钥匙串数据时,先检查备份是不是加密的。
2.4 快速搭建环境
环境搭建这件事,我在不同电脑上折腾过好几轮,这里给出一套最少步骤的搭建方案。前提是你有一台 macOS。
安装 libimobiledevice:
brew install libimobiledevice安装 frida-tools:
pip3 install frida-tools然后验证环境是否正常。真机通过 USB 连接 Mac,解锁屏幕,信任这台电脑,然后在终端里执行:
idevice_id -l如果输出了设备的 UDID,说明 libimobiledevice 工作正常。Frida 的验证需要手机端也装了 frida-server,并且两边版本一致。执行frida-ps -U,能列出手机上的进程列表就说明通了。
这里插一句,很多新手在装完 libimobiledevice 后发现idevice_id能识别设备,但idevicebackup2执行报错,大概率是备份目录权限问题。用sudo跑,或者确保当前用户对备份目录有读写权限,基本就能解掉。
3. 完整实操:从沙盒到分析结构
3.1 拿到沙盒数据的几种姿势
模拟器路径最简单。先用xcrun simctl list确认模拟器名字或 UDID,然后执行:
xcrun simctl get_app_container booted com.example.yourapp data这个命令会直接输出沙盒 data 目录的完整路径,接到 Finder 里就能看。注意,要先保证 App 在模拟器里安装过,并且设备是 booted 状态。
真机走备份法。先用idevicebackup2做一次全量备份:
idevicebackup2 backup --full --password yourpassword /path/to/backup--full表示全量备份,--password后面跟的是备份的加密密码。如果密码这一步漏了,后面会拿不到 Keychain 里的数据。备份完成后,在/path/to/backup目录下会生成一堆随机文件名,这些文件本质上是备份数据的分片。
要找到指定 App 的数据,需要用idevicebackup2的另一个功能,或者用第三方工具解析。实际操作中,我更喜欢用爱思助手这类图形化工具,它能在备份后直接列出所有 App 的沙盒文件,点击导出就能把整个 Documents 或 Library 目录拉到电脑上。对于不想折腾命令行的同学,这个方式最省心。
3.2 识别库文件是什么格式
拿到沙盒数据后,先别急着打开,先识别文件格式。我用一个多 MB 的数据库文件举例:
file database.sqlite如果输出结果是:
SQLite 3.x database那说明它是明文的 SQLite,直接上sqlite3就行。如果输出是“data”或者“application/octet-stream”,那就要警惕了,很可能是加密文件。
再用 hexdump 确认:
hexdump -C database.sqlite | head -n 2明文 SQLite 的开头是:
00000000 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 |SQLite format 3.|如果用strings也搜不到任何表名或者字段名,那就基本可以断定是 SQLCipher 加密了。我遇到过一个比较“狡猾”的情况,文件头不是标准 SQLite 头,但用strings能找到一些表名碎片,这种是部分加密或者自定义存储格式,处理起来要另想办法。
3.3 明文 SQLite 直接导出
对于明文 SQLite,导出其实是水到渠成的事。先把表结构看一遍:
sqlite3 database.sqlite .schema这个命令会把所有建表语句列出来。分析结构的第一步就是看 schema,了解有哪些表、表之间有什么关系、字段类型是什么。看明白了再导数据。
导出成 CSV:
sqlite3 -header -csv database.sqlite "SELECT * FROM user_table;" > user_table.csv-header会把字段名作为第一行输出,-csv指定输出格式。导出来之后直接可以用 Excel 打开。
导出成 JSON 稍微麻烦一点,需要借助工具。我用 Python 写过一个简单脚本,逻辑就是查询全部数据,然后逐行写入 JSON 数组:
sqlite3 database.sqlite "SELECT * FROM user_table;" | python3 -c " import sys, json rows = [line.strip().split('|') for line in sys.stdin] print(json.dumps(rows, ensure_ascii=False)) "这种方式适合临时用。如果数据量大或者字段多,建议直接用 Python 的 sqlite3 模块读取再序列化成 JSON,可控性更强。
3.4 SQLCipher 解密实战
SQLCipher 的解密流程已经比较成熟了。前提是你得拿到密钥,也就是 passphrase。密钥一般来自几种渠道:源码里硬编码的字符串、Keychain 中存储的值、服务端下发后缓存在内存中的数据。拿到之后,用 sqlcipher 命令行工具操作。
sqlcipher 的安装也很简单:
brew install sqlcipher解密的核心思路不是直接修改原文件,而是把加密的库导出到一个新的明文库文件里。这样做的好处是不动原文件,万一搞砸了还能重来。
操作流程如下:
sqlcipher encrypted.db进入 sqlite 的交互界面后,先设置密钥:
PRAGMA key = 'your-secret-key';如果密钥正确,后续查询就能成功。但有些库是旧版本的 SQLCipher,需要先做一次迁移:
PRAGMA cipher_migrate;接着附加一个新的明文数据库:
ATTACH DATABASE 'decrypted.db' AS plaintext KEY '';然后把加密库的内容导出到明文库:
SELECT sqlcipher_export('plaintext');最后分离并退出:
DETACH DATABASE plaintext; .exit执行完之后,decrypted.db就是一份明文 SQLite 数据库,用普通的sqlite3工具就能正常打开。我实际测试过几回,这个方法对 SQLCipher 4 的默认配置也适用,只要密钥正确,基本不会出问题。
如果你没有密钥,那就麻烦多了。暴力破解是理论上可行但现实中不太划算的方式,SQLCipher 的密钥派生函数(PBKDF2)计算成本很高,跑字典都嫌慢。更现实的思路是动态调试:用 Frida Hook 住sqlite3_key函数,在 App 运行的时候把密钥从内存里抓出来。我后面会详细讲这个方法。
3.5 自研 AES 解密思路
自研加密比 SQLCipher 更灵活也更乱,因为它没有统一标准。我在一个项目里遇到过这种情况:数据库本身是明文 SQLite,但里面的字段值都是类似U2FsdGVkX1+...的 Base64 字符串。一看就知道是 OpenSSL 的 AES 加密格式,加密的时候带了随机盐,解密必须靠正确的密码。
这种加密的破解思路分四步走。
第一步,在 App 的二进制文件或源码里搜索加密相关的字符串。用strings命令扫一遍:
strings AppBinary | grep -i "AES\|CCCrypt\|encrypt\|decrypt"如果找到了CCCrypt的调用,说明用的是系统自带的 CommonCrypto;如果找到 OpenSSL 相关的符号,那多半是 AES-256-CBC 或 AES-128-CBC。
第二步,找密钥和 IV(初始化向量)。密钥可能硬编码在代码里,也可能来自 Keychain,还有可能每一轮都随机生成然后存到服务端。对于前两种情况,静态分析就够了;对于第三种情况,就得抓包或者动态调试。
第三步,写解密脚本。以常见的 AES-256-CBC 为例,用 Python 的pycryptodome库就能搞定:
from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 def decrypt_aes_cbc(encrypted_b64, key, iv): encrypted = base64.b64decode(encrypted_b64) cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = unpad(cipher.decrypt(encrypted), AES.block_size) return plaintext.decode('utf-8') key = b'your-32-byte-key-here!!!!' iv = b'your-16-byte-iv!' print(decrypt_aes_cbc('U2FsdGVkX1+xxx...', key, iv))第四步,把所有字段都跑一遍解密,输出成明文 CSV。
这里有个筛选思路,可以先批量解密一部分数据,看输出是否是可读文本。如果解密失败(比如 Unpad 报错),多半是 key 或 iv 不对,调整后再试。你不需要一次性搞定全表,先拿几条数据验证,确认没问题再上全量。
3.6 转成分析友好的结构
数据解密完了,最后一步是整理成分析结构。我理解的分析结构,不只是一张 CSV,而是要让数据“可被查询、可被筛选、可被关联”。我通常按这个顺序来做:
先导出表结构。无论数据量多大,表结构是骨架。这里我做的第一件事永远是导出 schema:
sqlite3 decrypted.db .schema > schema.sql导出全部数据为 CSV,每个表一个文件。如果表之间有外键关系,文件名用“主表_子表”这种命名方式来标识,比如orders.csv、order_items.csv,后面分析的时候一眼就能看出来关系。
再生成一份 JSON 版本,方便程序化处理。数据量大的情况下,CSV 在 Excel 里会有行数上限,JSON 配合 Python 或 pandas 更实用。
最后也是很多人忽略的:生成 ER 关系图。虽然标题里写的是“分析结构方法”,但我认为表之间的关联关系本身就是最重要的分析结构。你可以根据 schema 手动梳理外键关系,然后用graphviz或者drawio画出来。我在实际项目中用 graphviz 画过一次,几十行的 dot 代码就能生成一份清晰的关系图,比嘴上说十遍都直观。
数据导出之前一定要处理好脱敏。手机号、身份证、地址这些敏感字段,要么直接删掉,要么用***替换。尤其是导出的数据要发给别人或者上传到分析平台的时候,脱敏这步不能省。
4. 常见问题与排查技巧实录
4.1 备份里找不到 App 数据
这是我被问过最多的问题。明明做了备份,但导出的备份文件里没有目标 App 的沙盒数据。大概率是两个原因。
第一个原因是 App 本身设置了“不备份”属性。iOS 开发中,有些数据被标记为NSURLIsExcludedFromBackupKey,这类数据不会进入 iTunes 备份。如果 App 的数据库存放在这个标记的目录下,备份里自然是找不到的。这种时候就别死磕备份了,改用 Frida 直接读取沙盒更靠谱。
第二个原因是备份没有包含加密数据。确认一下你备份的时候是不是勾选了“加密本地备份”并输入了密码。我犯过这个错,第一次做备份时没加密,结果 Keychain 数据全部缺失,数据库文件还在,但钥匙串里的密钥信息全没了。后来改成加密备份,数据才完整。
4.2 文件打不开,显示“not a database”
这个报错几乎每个和 SQLite 打交道的人都见过。出现这个报错,先不要慌,用file命令看下文件类型,再用hexdump看文件头。
如果文件头不是标准 SQLite 头,那可能是 SQLCipher 加密。如果文件头是标准 SQLite 头但仍然打不开,那可能是文件损坏。我之前遇到过一次,是 App 在写入过程中被杀掉,SQLite 的 WAL 文件没有合并,导致主库文件不完整。处理方式是找到同目录下的-wal文件,把它和主库放在一起,再用sqlite3打开,SQLite 会自动进行恢复。
4.3 密钥找不到
密钥是整个解密流程里最关键也最容易卡住的一环。我总结了几种寻找密钥的渠道,按性价比排序。
第一是搜源码。如果手上有源码,直接 grep:
grep -r "password\|passphrase\|secret\|key" --include="*.swift" --include="*.m" --include="*.h"第二是搜二进制文件里的硬编码字符串。用strings配合 grep:
strings AppBinary | grep -i "key\|pass\|secret"第三是用 Frida 动态 Hook。这是最接近“万能解法”的手段。比如想拿 SQLCipher 的密钥,可以 Hooksqlite3_key函数:
if (ObjC.available) { Interceptor.attach(Module.findExportByName(null, "sqlite3_key"), { onEnter: function(args) { var keyPtr = new NativePointer(args[1]); var keyLen = args[2].toInt32(); var key = keyPtr.readUtf8String(keyLen); console.log("SQLCipher key: " + key); } }); }用 Frida 启动目标 App 并加载这个脚本,App 一调用sqlite3_key,密钥就会打印到终端里。这个方法需要越狱环境或者经过重签名的调试版本,但对逆向分析来说已经算是最常规的手段了。
第四是抓 Keychain。如果密钥存在 Keychain 里,在越狱机上用keychain-dumper可以直接导出来。非越狱机上,就只能靠加密备份加合理推导了。
4.4 导出数据中文乱码
CSV 用 Excel 打开中文乱码,这是 Windows 和 macOS 编码差异的经典问题。Excel 默认用 GBK 编码打开 CSV,而 sqlite3 默认导出的是 UTF-8。
解决方法有两个。一个是导数据之前先转换编码:
iconv -f UTF-8 -t UTF-8-BOM user_table.csv > user_table_bom.csv加上 BOM 之后,Excel 就能识别编码了。另一个是干脆不用 CSV,改用 Excel 原生支持的格式,比如直接导成 xlsx。Python 的pandas库一行代码就能做到:
import pandas as pd df = pd.read_csv('user_table.csv') df.to_excel('user_table.xlsx', index=False)从实际体验来说,数据量大或者字段多的时候,xlsx 比 CSV 稳得多,不会出现字段被截断或者编码错乱的问题。
4.5 备份密码忘了怎么办
加密备份如果忘了密码,那 Keychain 数据基本等于丢了,没什么好办法恢复。但沙盒数据还有救。备份文件本身虽然没有明文 Keychain,但沙盒目录下的文件未必全部加密。你可以尝试用idevicebackup2的--unencrypted选项重新做一次备份,或者通过第三方工具直接浏览备份内容。
我在实际项目中还踩过一个坑:备份的时候设了密码,但后来想用明文方式读取备份里的文件,却被系统拦住了。后来发现idevicebackup2有--decrypt相关操作,可以用备份密码把备份解密成明文目录。具体命令在不同版本上略有差异,建议先看idevicebackup2 --help的输出。
我个人的经验是,备份密码这玩意儿一定要记好。如果项目涉及长期数据导出,最好在项目文档里留一份加密备份的说明,密码单独放一个安全的密码管理工具里,不要贴在聊天记录里。
我自己在实际操作中的体会是,iOS App 的数据导出这件事,百分之八十的问题都出在“没识别清楚数据形态”上。拿到一个文件,先花几分钟看清楚它是明文 SQLite、SQLCipher 还是自研加密,后面的路就顺了。最后再分享一个小技巧:不管数据量多大,先导出 schema 看表结构,比一上来就全表 dump 高效得多——结构清楚了,你知道每一张表里装的是什么,再去取数就有方向了。这一套流程我在不下十个项目里验证过,不能说百分之百覆盖所有情况,但至少能帮你避开大部分常见的坑。