SQLCipher之攻与防

0x00 SQLCipher

在移动端,不管是iOS还是Android,开发人员用的最多的本地数据库非SQlite莫属了。SQLite是一个轻量的、跨平台的、开源的数据库引擎,它的在读写效率、消耗总量、延迟时间和整体简单性上具有的优越性,使其成为移动平台数据库的最佳解决方案。

但是,用SQLite保存一些不是很敏感的信息还可以接受,存储敏感信息就值得商榷了,免费版的SQLite中的信息是明文存放的,你甚至直接用文本编辑器打开都可以看到敏感内容,比如下面这个:

其实SQLite是提供了加密功能的,即SQLite Encryption Extension (SEE),但是,要收费!还不便宜,2000刀。Coder们穷啊,于是大家寻找免费的解决方案,最终SQLCipher成了比较理想的选择,因为它免费、开源,而其它家的SQLiteEncrypt、SQLiteCrypt等还是要收费。

SQLCipher也有收费版本,但收费版本只是在集成、依赖等易用性方面有优势,功能和免费差不多,因此免费版的SQLCipher成了Coder们的选择对象。

SQLCipher采用的是数据库文件整体加密的策略,使用256-bit AES加密,从算法角度来看是相当强悍了。于是,加密后的文件看起来及时这样了:

完全是天书嘛,ok,可以洗洗睡了。但真的就安全了吗?呵呵。

0x01 谋攻篇

密码学中有一句比较经典的话:一切秘密寓于密钥之中。只要我们拿到了密钥,剩下的问题都不是问题了。下面我们就来分析一下SQLCipher的密钥和加密过程。
根据官方的描述,SQLCipher是整体加密方案:

> Transparent – An application doesn’t require any special knowledge of the underlying database security. Applications use the standard SQLite API to manipulate tables using SQL. Behind the scenes the library silently manages the security.

> On-the-fly – SQLCipher encrypts and decrypts in chunks called pages, as needed, so it doesn’t operate on the database all at one time. This means SQLCipher
starts up and closes down quickly
performs very well even with very large databases
works with SQLite indexing (i.e. retrieving a single record using an indexed search can incur as little as 5% overhead above a standard SQLite database)

它并不是对表或者列进行加密,也就是说他的数据库密钥只有一个,拿下这一个密钥就成。

SQLCipher在iOS上是这样用的:

1)在项目中加入sqlite3.h和sqlite3.m;

2)使用下面代码连接数据库:

其中的key就是我们需要的密钥!然后通过sqlite3_key函数将该key与数据库关联起来,接下来就可以执行sql语句操作了。程序员肯定觉得好用啊,就增加了两行语句就完成了加密功能,直接拷贝官方的代码到自己的工程中,把“StrongPassword”换成自己的密钥就成。真实容易啊,那么我攻击也很easy啦。

一般来说,对付这种程度的加密,只需要使用静态分析即可。对于iOS程序,将程序拖到ida中,找到sqlite3_key函数相关引用地址,顺藤摸瓜,基本就搞定了。下面就是某应用中定位该函数在setKey方法中,该方法是在它的数据open方法中调用的,可以看到已经明文硬编码了数据库的密码(图中抹红部分)。

拿到密码后有个偷懒的方法可以直接打开加密数据库:使用SQLiteManager,它支持SQLCipher加密的数据库,会提示输入数据库密码,不需要自己写程序了,而且浏览数据也很方便。

对于Android版的程序,如果没加壳的话,也是很好分析的。Android调用SQLCipher使用下面的方式:

openOrCreateDatabase方法的第二个参数就是密钥了。在jeb反编译出来的代码中寻找相关的方法就可以摸到相应的数据库密钥。

到这儿程序员可能要想了,我不硬编码看你咋整,我在代码中经过加减乘除、异或后在算出key,或者更狠一点用用户口令等进行动态解密变换,密钥就不会在程序中出现了。

但攻击者还有n多招数还没上场了,比如动态调试,在输入key的方法入口打断点,对key的明文进行拦截。
还有就是hook技术,对于Android上的应用,我们可以使用CydiaSubstrate,使用MS.hookClassLoad 方法将openOrCreateDatabase进行hook处理:

 
static void hookCryptoKey() {
MS.hookClassLoad("net.sqlcipher.database.SQLiteDatabase",
new MS.ClassLoadHook() {
public void classLoaded(Class<?> arg0) {
Log.d("MyHook", "##### Class Loaded\n");
 
Method openOrCreateDatabase;
try {
openOrCreateDatabase = arg0.getMethod(
"openOrCreateDatabase",
new Class[] { File.class, String.class, SQLiteDatabase.CursorFactory.class });
 
} catch (NoSuchMethodException e) {
openOrCreateDatabase = null;
Log.d("MyHook",
"##### Unable to find method\n");
}
 
if (openOrCreateDatabase != null) {
Log.d("MyHook", "##### Found method\n");
try {
final MS.MethodPointer old = new MS.MethodPointer();
MS.hookMethod(arg0,
openOrCreateDatabase,
new MS.MethodHook() {
public Object invoked(Object arg0,
Object... args)
throws Throwable {
Log.d("MyHook", "##### Error: " + Thread.currentThread().getStackTrace());
Log.d("MyHook",
"###### Method hooked, stealing key: " + args[1]);
 
return old.invoke(arg0, args);
}
}, old);
} catch (Exception e) {
System.out
.println("#### Unable to find class\n");
}
}
}
});
}
static void initialize() {
hookCryptoKey();
}
}

这段代码可以将openOrCreateDatabase方法钩住并将第二个参数key在日志中打印出来,无视你前期经过了多少隐藏、变换。至于Substrate的用法这里就不细讲了,各位看官可以参考网上的资料。当然,你也可以尝试其它hook框架,如Xposed。

程序员可能不服气,我还可以上加壳,在iOS端我也可以上混淆、lua动态网上加载等手段啊?不过这貌似超出了SQLCipher攻防的地盘了,是程序本地保护的大范畴,有机会再研究。

0x02 防?

写到这儿,其实对本地数据库加密是感到沮丧的,这完全就是把门锁和钥匙放在一起的做法嘛,不管你在本地上任何软的手段(硬件另说,如果你的密钥、算法都放在硬件中,比如USBKey中,安全性应该是有本质上的提升),不管是简单变换还是混淆、加壳、反调试、反hook,都难保不被攻击者破解掉,不过是或多或少延缓找到钥匙的时间罢了。

因此,建议除了以下情况,不要使用SQLCipher之类的本地数据库加密:

1. 非敏感数据,爱咋用咋用;
2. 用户自己的数据,可以采用用户口令、指纹等方式直接或间接保护,因为这种情况下key其实是随用户走的,可以做到key和算法、数据的分离。

其它情况推荐数据放在APP的服务端,将客户端做瘦,将安全的防护重点放在通信和服务端的防护上。