独家食用指南系列|Android端SQLCipher的攻与防新编
本帖最后由 lateautumn4lin 于 2020-10-26 18:33 编辑![](https://img-blog.csdnimg.cn/20201020210301905.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
大家好,今天给大家的是本周**技术拆解官**的第二篇文章,主题依然是沿用上一篇文章的主题--**Android端SQLite的“食用指南”**,上篇文章我们讲到了基本的**SQLite**的定义、使用方法以及开发了一个基本的**演示DEMO**,有不太了解的伙伴可以戳这里预习下。
本篇文章带来的是《**独家食用指南系列**|**Android**端**SQLCipher**的攻与防新编》,这个题目是由于之前在调研**SQLCipher**时发现的一篇文章,原文地址是这里:(https://www.freebuf.com/articles/database/108904.html),本次也是借用此标题带大家重新缕缕**SQLCipher**。
可能大家不太清楚**SQLCipher**是什么东西?没事,我们慢慢来,首先顺着之前的思路,接着来分析**Android**端**SQLite**,上周我们了解了**SQLite**的使用,那这次我们开篇先来看看**SQLite**有哪些优缺点?
>本篇文章使用到的项目源码都在我的个人Github上面:https://github.com/lateautumn4lin/TechPaoding/tree/main/practice_demo/Cattle
# 1 SQLite的缺陷以及应对方案
**SQLite**作为在**Android**端频繁使用的轻量型数据库,它的优点不言而喻:易上手、易安装、具备关系型数据库的特征等等,那它有哪些缺陷呢?而这些缺陷又是通过什么技术手段或者方案来解决的呢?
## 1.1 SQLite的缺陷
### 1.1.1 性能问题
对于一般的**APP**开发者来说,**SQLite**易上手的特点会让人很兴奋,但是对于企业级的**APP**开发来说,**性能**永远是**APP开发**的第一指标,而**SQLite**的最初的设计理念是作为一个**轻量级**的**高性能数据库**,大家可能会疑问既然这么设计为什么还会遇到性能问题呢?这里的**高性能**其实是有条件限制的,在**小数据量**、**并发低**、**查询结构简单**的场景下,在**Android**端使用**SQLite**无非是性价比非常高的选择,可是一旦超出了这些限制条件,数据库设计的问题就暴露的很明显了。
### 1.1.2 安全性
对于**安全性**这个问题是怎么理解的呢?想一想对于每个使用了**SQLite**的**APP**来说,它们或多或少都会保存些私密的数据在其中,使用**SQLite**这对于开发者来说是很方便,但是方便不仅仅是对于他们来说,同时对于一些有“额外想法”的人来说的。免费版本的**SQLite**有一个致命缺点:不支持加密。意味着,你**APP**上的数据库其实是在裸奔!对于那些想获取数据的人来说,只要让手机**ROOT**之后,就可以进入每个**APP**的目录下面获取**db文件**从而轻易的得到数据。
## 1.2 应对方案
### 1.2.1 性能问题的解法
性能方面的优化是改造**SQLite**的最重要一步,优化可以从多个方面切入去提升**SQLite**的性能。
- **ORM**框架的取舍
可能大部分应用为了提高开发效率,会引入**ORM**框架。**ORM**(Object Relational Mapping)也就是对象关系映射,用面向对象的概念把数据库中表和对象关联起来,可以让我们不用关心数据库底层的实现。在**Android**中最常用的**ORM**框架有开源(https://github.com/greenrobot/greenDAO)和**Google**官方的(https://developer.android.com/training/data-storage/room/),虽然这些**ORM**底层优化的很好,对于部分执行效率的损耗已经降到最低,但过多的使用**ORM**还是会造成性能方面的影响,所以对于**ORM**框架的取舍和依赖程度也是影响性能的关键。
- 并发问题
如果我们在项目中使用**SQLite**,那么在下面这个**SQLiteDatabaseLockedExecption**就是经常会出现的一个问题。
![](https://img-blog.csdnimg.cn/20201026102240323.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
**SQLiteDatabaseLockedExecption**归根到底是因为并发问题导致,而**SQLite**的并发有两个维度,一个是**多进程并发**,另一个是**多线程并发**。
我们首先来看看**多进程并发**
**SQLite**默认是支持多进程并发操作的,它通过文件锁来控制多进程并发。**SQLite**锁的粒度并没有非常细,它针对的是整个**db**文件,内部有5个状态,具体你可以参考下面的文章-(https://links.jianshu.com/go?to=https%3A%2F%2Fwww.sqlite.org%2Flockingv3.html)。
简单来说,多进程可以同时获取**SHARED**锁来读取数据,但是只有一个进程可以获取**EXCLUSIVE**锁来写数据库。并且**EXCLUSIVE**会阻止其它进程再获取**SHARED**锁来读取数据。在**EXCLUSIVE**模式下,数据库连接在断开前都不会释放**SQLite**文件锁,从而避免不必要的冲突,提高数据库访问的速度。
再来看看**多线程并发**
相比多进程,多线程的数据库访问可能会更加常见。**SQLite**支持多线程并发模式,需要开启**SQLITE_THREADSAFE**配置,当然系统**SQLite**会默认开启多线程 **Multi-thread**模式。
跟多进程的锁机制一样,为了实现简单,**SQLite**锁的粒度都是数据库文件级别,并没有实现表级甚至行级的锁。还有需要说明的是,同一个句柄同一时间只有一个线程在操作,这个时候我们需要打开数据库连接池 **Connection Pool**。
跟多进程类似,多线程可以同时读取数据库数据,但是写数据库依然是互斥的。**SQLite**提供了**Busy Retry**的方案,即发生阻塞时会触发**Busy Retry**,此时可以让线程休眠一段时间后,重新尝试操作。
为了进一步提高并发性能,我们可以打开**WAL** (Write-Ahead-Logging)模式。**WAL**模式会将修改的数据单独写到一个**WAL**文件中,而读操作开始时,会记下当前的**WAL**文件状态,并且只访问在此之前的数据,同时也会引入**WAL**日志文件锁。通过**WAL**模式读和写也可以完全地并发执行,不会互相阻塞。
但是需要注意的是,写之间是仍然不能并发。如果出现多个写并发操作的情况,依然有可能出现**SQLiteDatabaseLockedExecption**。这个时候我们可以让应用中捕获这个异常,然后等待一段时间再重试。
总的来说通过连接池与**WAL**模式,我们可以很大程度上提高**SQLite**的读写并发,大大减少由于并发导致的等待耗时,建议大家在应用中尝试开启。
- 查询优化
说起查询方面的优化,一般来说对于开发者第一个想到的方案就是**建立索引**,下面我们就围绕索引来看看如何优化。
如何正确建立索引在网上都有一系列的文章,比如:
1. (https://links.jianshu.com/go?to=https%3A%2F%2Fwww.cnblogs.com%2Fhuahuahu%2Fp%2Fsqlite-suo-yin-de-yuan-li-ji-ying-yong.html)
2. [官方文档:Query Planning](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.sqlite.org%2Fqueryplanner.html%23searching)
重点要说的是很多时候我们以为已经建立了索引,但事实上并没有真正生效。这里关键在于如何正确的建立索引。例如使用了**BETWEEN**、**LIKE**、**OR**这些操作符、使用表达式或者**CASE WHEN**等。
建立索引是有代价的,需要一直维护索引表的更新,比如对于一个很小的表来说就没有必要建索引;如果一个表经常是执行插入更新操作,那么也需要节制的建立索引。总的来说有几个原则:
- 建立正确的索引。这里不仅需要确保索引在查询中真正生效,我们还希望可以选择最高效的索引。如果一个表建立太多的索引,那么在查询的时候 SQLite 可能不会选择最好的来执行。
- 单列索引、多列索引与复合索引的选择。索引要综合数据表中不同的查询与排序语句一起考虑,如果查询结果集过大,还是希望可以通过符合索引直接在索引表返回查询结果。
- 索引字段的选择。整型类型索引效率会远高于字符串索引,而对于主键 SQLite 会默认帮我们建立索引,所以主键尽量不要使用复杂字段。
- 总的来说索引优化是**SQLite**优化中最简单同时也是最有效的,但是它并不是简单的建一个索引就可以了,有的时候我们需要进一步调整查询语句甚至是表的结构,这样才能达到最好的效果。
关于**SQLite**的性能方面的解法可以围绕上面三个方面去做,当然,这只是针对于**SQLite**的优化来说的,要是想要更方便的直接获取“前人果实”,可以参考2017年微信开源了内部使用**SQLite**数据库**WCDB**,这个项目也是填了很多**SQLite**遗留的坑,针对于微信的场景做了很多优化,相信大家用起来也是会避免很多不必要的麻烦。
### 1.2.2 安全问题的解法
针对于**SQLite**的安全问题的解法类似于很多数据库的安全解法,目前常用的方案主要是两类:
- 将内容加密后再写入数据库
这种方式使用起来简单,在**入库/出库**的过程中只需要将字段做对应的加解密操作即可,一定程度上解决了将数据赤裸裸暴露的问题。但也有很大弊端:
1. 这种方式并不是彻底的加密,还是可以通过数据库查看到表结构等信息。
2. 对于数据库的数据,数据都是分散的,要对所有数据都进行加解密操作会严重影响性能。
- 对数据库文件加密
将整个数据库整个文件加密,这种方式基本上能解决数据库的信息安全问题。目前已有的**SQLite**加密基本都是通过这种方式实现的,常见的几种加密方式是:
- **SQLite Encryption Extension (SEE)**
事实上**SQLite**在设计之初是有暴露加解密接口,只是免费版本没有实现而已。而**SQLite Encryption Extension (SEE)**就是**SQLite**的加密版本,收费的
- **SQLiteEncrypt**
使用**AES**加密,其原理是实现了开源免费版**SQLite**没有实现的加密相关接口,**SQLiteEncrypt**是收费的。
- **SQLiteCrypt**
使用**256-bit AES**加密,其原理和**SQLiteEncrypt**一样,都是实现了**SQLite**的加密相关接口,**SQLiteCrypt**也是收费的。
- **SQLCipher**
需要说明的是,**SQLCipher**是完全开源的,代码托管在**Github**上。**SQLCipher**同样也是使用**256-bit AES**加密,由于其基于免费版的**SQLite**,主要的加密接口和**SQLite**是相同的,但也增加了一些自己的接口。
对于大部分开发者来说,兼顾安全性和成本的同时,免费版本的**SQLCipher**也是我们优先采取的安全性加固方案。
# 2 认识SQLCipher
上面我们分析了**SQLite**的优缺点,也基本了解了目前有哪些通用的解决方案,回到我们的主题《**攻与防**》,这期我们就开始介绍我们的主角-**SQLCipher**。
## 2.1 定义
基于**SQLite**接口设计的采用**256-bit AES**加密算法的安全加密数据库。
## 2.2 特点
- 快速只有5 - 15%的性能开销加密,官方给出了一个[速度测试项目](https://github.com/sqlcipher/SQLCipherSpeed),感兴趣的朋友可以自行测试。
- 100%的数据库中的数据文件是加密的,是对所有数据文件,包括数据文件和缓存、结构文件等等进行加密。
- 使用良好的安全模式(CBC模式,密钥推导),可以自行选择加密算法。
- 零配置和应用程序级加密
- OpenSSL加密库提供算法
## 2.3 加密原理
我们现在只是了解**SQLCipher**能够给我们的数据库提供安全加固的保护,那它的实现原理是什么呢?
**SQLite**数据库设计中考虑了安全问题并预留了加密相关的接口。但是并没有给出实现。**SQLite**数据库源码中通过使用**SQLITE_HAS_CODEC**宏来控制是否使用数据库加密。并且预留了四个结构让用户自己实现以达到对数据库进行加密的效果。这四个接口分别是:
- **sqlite3_key()**: 指定数据库使用的密钥
- **sqlite3_rekey()**:为数据库重新设定密钥;
- **sqlite3CodecGetKey()**:返回数据库的当前密钥
- **sqlite3CodecAttach()**: 将密钥及页面编码函数与数据库进行关联。
而**SQLCipher**就是基于以上的四个接口以及自定义的接口来实现加密的,下面通过图片了解整个加密流程:
- 加密入口![](https://img-blog.csdnimg.cn/img_convert/564f2902ce27839fdb27679a76326ee9.png)
如上图所示为**SQLite**加密的基础**API**接口,其中蓝色部分为已有的模块,灰色部门是需要开发者去实现的部分。
- **sqlite3_key**是加密的入口,需要在调用**sqlite3_open**打开数据库后立刻调用。
- **sqlite3_key**和**sqlite3_key_v2**本质是一样的,区别是前者默认选择**main db**,后者可以通过名字选择**db**文件。
- **sqlite3_rekey**用于修改密码,使用前必须先调用**sqlite3_key**解密。
- 读写时加密
![](https://img-blog.csdnimg.cn/img_convert/c62bf68ebdf1e966d9233d18725f2db2.png)
如上图所示为加解密时**API**的具体调用。执行写操作时进行数据的加密,此时会调用**CODE2**函数,执行读操作是进行数据的解密,此时会调用**CODEC1**函数,而最终调用的都是**sqlite3Codec**这个函数,主要通过传入的参数来控制。
![](https://img-blog.csdnimg.cn/20201026114001309.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
- 加密流程
![](https://img-blog.csdnimg.cn/20201026114932940.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
上图为**SQLCipher**的实现原理,加密流程为以下步骤:
- 传入密钥,通过**Rand_bytes**算法生成16个字节的**salt**,并存储在数据库第一页的头部(**SQLite**的**db**文件,头部前**16**个字节固定为**SQLite Format**,所以可以利用文件头来存储一些数据)。
- 通过**PKCS5_PBKDF2_HMAC_SHA1**算法将密钥和**salt**一起加密并多次迭代,生成**AES加密**所用的**key**;此处是对**key**的加密,即使原始的密码泄露,也无法解密数据。
- 通过**AES对称加密算法**对每一页的文件内容(有效的内容,不包含文件头和**reserved**字段)进行加密。
- 加密时,文件执行过**AES加密**后,对文件内容,通过**Hmac算法**,获取文件校验码,填充在**page尾部**(**SQLite**提供了**reserved**字段,自动在**page尾部**预留一段空间)。
- 解密时,先调用**Hmac算法**获取文件标识码,与**page尾部**的数据进行对比,如果数据一致,则证明文件没有被篡改过,不然证明文件已经被篡改,则抛出异常。
PS:以上为默认的算法,**SQLCipher**没有固定算法,用户可以自己设置。
# 3 调试工具
## 3.1 命令行
“最基本的调试方案”,只需要在**Linux**系统安装好**SQLCipher**,其他的操作和**SQLite**无异,唯一需要注意的是**SQLCipher**是加密库,也就是我们需要给**db**手动添加密码,操作大致如下:
### 3.1.1 Linux安装SQLCipher
![](https://img-blog.csdnimg.cn/20201026112333868.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
### 3.1.2 Linux生成SQLCipher加密库
针对原始的**tech_paoding.db**数据库生成新的**decrypted_database.db**数据库
![](https://img-blog.csdnimg.cn/20201026112359787.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
## 3.2 再上神器SQLiteStudio
可视化工具调试方面我们依然采用上篇文章我们介绍的**SQLiteStudio**,不过我们需要在**Add a database**时需要选择添加一个**SQLCipher**类型的数据库。
![](https://img-blog.csdnimg.cn/20201026111519175.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
这里的**Cipher**(也就是加密算法)、**KDF**、**Page Size**的值都是默认好的,是**SQLCipher3**的默认算法的值,如果使用的**SQLCipher4**的值话,这些数据就需要改变。
# 4 SQLCipher加固流程
对于开发者来说,使用**SQLCipher**并不会对原本的**APP**逻辑入侵,只需要按照下面两个步骤即可将**SQLite**数据库加固。
## 4.1 类替换
对于正常使用**Android**端**SQLite**来说,引入的**SQLite**库是**android.database.sqlite**,比如我们要使用**SQLiteDatabase**,就需要这样引入:
```java
import android.database.sqlite.SQLiteDatabase;
```
而对于**SQLCipher**来说,没有破坏**SQLite**的类以及相关的**API**,只是重写了它们,所以我们需要更换原有的包,还是举**SQLiteDatabase**的例子,我们引入它们需要这样:
```java
import net.sqlcipher.database.SQLiteDatabase;
```
另外需要注意的是,**android.database.sqlite**是**Android**项目的内置包,而**net.sqlcipher.database.SQLiteDatabase**则需要我们自己引入,我的引入方案是从官网拉下对应的**aar包**,加入到本地的**libs**中,然后在**build.gradle**中指定引入路径
```java
implementation(name:'android-database-sqlcipher-3.5.5', ext:'aar')
```
这样,我们就可以顺利的使用**SQLCipher**进行开发了。
## 4.2 加载加密SO库
由于**SQLCipher**的算法是基于**SO**库开发的,所以我们在正常使用**SQLCipher**之前需要使用`loadLibs`方法来加载**SQLCipher**的**SO**加密库,例如:
```java
SQLiteDatabase.loadLibs(this);
```
`loadLibs`方法的逻辑如下:
![](https://img-blog.csdnimg.cn/20201026121008691.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
# 5 SQLCipher Demo开发
照例和上篇文章一样,开发基本的**Demo**
## 5.1 创建MySQLiteOpenHelper
![](https://img-blog.csdnimg.cn/20201026132614671.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
## 5.2 创建SQLiteCattleActivity
![](https://img-blog.csdnimg.cn/2020102613262232.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
## 5.3 动态效果展示
主要的代码如上图所示,下面看看开发好的**Demo**的展示:**SQLite**
和**SQLCipher**的功能已经整合在同一个**APP**中,通过不同的**Activity**来展示。
![](https://img-blog.csdnimg.cn/20201026134744206.gif#pic_center)
# 6 企业级的SQLCipher攻防案例
上面我们了解了**SQLCipher**的由来、原理、基本的使用方法,这一部分我们呼应一下标题《**攻与防**》,我们结合市面上的用到了**SQLCipher**加密数据库的**APP**来看看它们是如何做过安全防护的以及那些“不法者”是怎么去破解这些防护的。
## 6.1 百度汉语
关于**百度汉语APP**我主要下载了以下几个版本的**APK**
![](https://img-blog.csdnimg.cn/20201026133606203.png#pic_center)
我也发现了他们在不同版本对于整个**APP**的不同加固方案,这个我们在分析好整个**SQLCipher**方面的防护之后我们再总结,首先我们使用最新版本**APP**来分析下,作为词典类型的**APP**,总会有一个额外内部的数据库来作为离线时的查询方案,那么我们可以寻找下**百度汉语APP**他们的数据库方案,我们寻找下哪里是他们离线文件的入口,可以很容易在这里看到这里
![](https://img-blog.csdnimg.cn/20201026134817895.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
![](https://img-blog.csdnimg.cn/20201026134837797.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
这里有个**离线文件**下载的功能,于是可以猜测是通过下载离线**db**文件来填充他们的离线数据库,如第二幅图所示的**免费离线包**这个列表极可能是通过网络接口动态获取的,因为我们在离线状态下是获取不到的。
![](https://img-blog.csdnimg.cn/20201026140411699.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
通过抓这个接口的网络请求包可以发现,可以获取请求**db文件**的地址
![](https://img-blog.csdnimg.cn/20201026140550817.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
根据地址我们获取到了相应的**db文件**,下载完成后得到这样的文件`baidu_dict.db`,判断是基于**SQLite**的文件,放入**SQLiteStudio**以**SQLite**方式打开会提示错误,说明应该是基于**SQLCipher**的加密文件,我们需要寻找的是**SQLCipher**的秘钥,于是下面我们开始关于秘钥的追踪。
### 6.1.1 SQLCipher秘钥追踪流程
在下载阶段我们获取到了`baidu_dict.db`这个关键词,我们在**jadx**中搜索下
![](https://img-blog.csdnimg.cn/20201026141249877.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
得到的数据量不多,其他的都是跟`Baidu_Dict.apk`相关的,我们自然可以忽略,我们主要观察第一个搜索结果,进入具体的代码查看
![](https://img-blog.csdnimg.cn/20201026141458505.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
虽然这里面并没有很明显的提到`baidu_dict.db`这个关键词,可是让我们发现了一个重要的线索`DB_PATH`的值`/data/data/com.baidu.dict/databases/`,这个地址是什么呢?回想上一篇文章,这个目录是每个**APP**特有的私有文件的保存目录,而**databases**通常是大家存放**SQLite**数据文件的地方,也就是说我们离线下载的文件通常会保存在这个地方,好了,这下我们不用在每次通过下载来获取**db文件**了,我们现在可以直接去这个目录下看看有哪些**db文件**
![](https://img-blog.csdnimg.cn/20201026141929933.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
基本上相关的文件都保存在这里,下一步我们需要获取的是具体的秘钥,那么获取秘钥我们该从哪里入手呢?最简单的,我们都知道**SQLCipher**加密数据库是需要通过**getWritableDatabase**来获取**db实例**进行操作的,那我们可以直接搜索**getWritableDatabase**关键词不就好了?
![](https://img-blog.csdnimg.cn/20201026142337562.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
搜索结果很多,我们直接寻找参数非空的调用
![](https://img-blog.csdnimg.cn/20201026142446826.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
结果很明显,调用了一个**SO**函数的方法来保存秘钥,也算是对于秘钥的保护了。这当然是寻找秘钥其中的一种方案,不过这样有点取巧,我们换另一种思路来看看,离线包是服务了离线搜索的,那么我们在离线状态下看看搜索的**Activity**的调用流程是什么样的
首先获取搜索的**Activity**
![](https://img-blog.csdnimg.cn/20201026142806984.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
由图可知是`com.baidu.dict.activity.NewSearchActivity`,之后去看看这个**Activity**的代码,算了,代码太多,我们直接用**Objection**去Hook这个类看看调用流程
![](https://img-blog.csdnimg.cn/20201026143234367.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
我们重新进入这个**Activity**,可以从**Objection**界面看到这样的信息
![](https://img-blog.csdnimg.cn/20201026143447982.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
使用搜索功能,信息变成这样
![](https://img-blog.csdnimg.cn/20201026143633752.png#pic_center)
定位到了`searchAllInfo`这个方法
![](https://img-blog.csdnimg.cn/20201026143852104.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
从上面的代码我们可以发现一个类很值得关注,也就是`DictExtDBManager`,代码中调用了`DictExtDBManager.checkLocalDB`和`DictExtDBManager.getInstance`获取的结果,如果获取不到就采用`searchOnline`的在线获取方式,我们深入这个类看看,部分代码删除了
![](https://img-blog.csdnimg.cn/20201026144443783.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
可以根据这个类的代码缕我们之前的逻辑
![](https://img-blog.csdnimg.cn/20201026145121559.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
兜兜转转最后我们也找到了具体的秘钥算法位置
### 6.1.2 SQLCipher秘钥获取
现在我们找到了**APP**获取秘钥的方法,我们继续跟踪,秘钥采用了**SO**的函数,调用了`libimagerender.so`,我们使用**IDA**具体查看下,直接查看**Exports**的**tab**会发现具体的函数属于静态注册的
![](https://img-blog.csdnimg.cn/20201026145550775.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
![](https://img-blog.csdnimg.cn/2020102614571536.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzExNjkxMA==,size_16,color_FFFFFF,t_70#pic_center)
定位到了具体的函数,修复下即可获取真实的秘钥,接着我们使用这个秘钥再去解密数据库即可。
### 6.1.3 SQLCipher秘钥加固历程
上面分析好了**SQLCipher秘钥**的获取历程之后,再说说**百度汉语APP**对于它们的**SQLCipher秘钥**的加固的演变历程吧,文字就不多说了,直接上图。
![](https://img-blog.csdnimg.cn/2020102615033831.jpg#pic_center)
## 6.2 新华字典
本来还想写一段分析**新华字典APP**的流程,不过受限于文字篇幅,到这里已经是**1.6w**字了,就不继续了,之后有机会给大家再分享下,大家也可以自己动手试试。
# 7 食用荐语
以上就是本篇《**独家食用指南系列|Android端SQLCipher的攻与防新编**》的所有内容了,相比于上篇文章,这篇会更多的从**原理**、**实战**角度出发讲述下**攻与防**的内容,包括企业级的SQLCipher攻防案例。现在关于**SQlite**的系列还剩最后一篇文章,关于**SQlite**的源码剖析。 直接复制粘贴?markdown都不带转一下的吗 dongfang155 发表于 2020-10-26 18:14
直接复制粘贴?markdown都不带转一下的吗
不好意思,请问哪里的markdown格式没有转化,都转化了啊。 感谢大神分享 lateautumn4lin 发表于 2020-10-26 18:16
不好意思,请问哪里的markdown格式没有转化,都转化了啊。
5L上传了图片{:301_998:} dongfang155 发表于 2020-10-26 18:19
官方的markdown视图对于缩进两次的处理有问题,重新调整了下,感谢老哥提醒。 来看看,多谢了 有很多看不懂,不知道用学多久能看懂 厉害,楼主。