SQLite攻击 – iOS持久化控制实现

SQLite是世界上部署最多的软件之一。但是,SQLite的安全相关测试,目前涉及的基本是网页应用环境及本地浏览器环境。然而上述SQLite的应用场景只是冰山一角,其他大量应用场景及其对应的安全问题同样值得关注。

在我们的长期研究中,我们尝试了完全依赖SQL语言环境对内存破坏问题进行利用。使用我们查询劫持和面向查询编程的新姿势,我们实现了在SQLite引擎中可靠地利用内存损坏。下面我们将用几个真实场景来演示这些技术:用密码窃取程序攻击后端服务器、高权限实现iOS持久化控制。

我们希望通过发布研究方法及成果来鼓励安全研究社区,以便于在各种应用场景中测试SQLite。鉴于SQLite内置于几乎所有的主流操作系统、桌面系统及移动设备,因此其前景和机会是无限的。此外,本文所提及的许多数据库原语(primitives)并不是SQLite独有的,大家可以考虑移植到其他SQL引擎。欢迎大家在自己熟悉的结构化查询语言中进行新的尝试和探索。

缘由

这项研究源自于我和@omriher研究一些臭名昭著的密码窃取程序的部分泄露源码。虽然有非常多的密码窃取程序(Azorult, Loki Bot, 和 Pony 等等),但这些程序的作案手法大致相同:

计算机受到感染,恶意的密码盗窃软件要么在凭证使用的时候捕获凭证,要么就收集各终端上存储的凭证(存储在sqlite文件中,客户端软件将SQLite数据库用于凭证存储的情况并不少见)。

恶意软件收集到这些SQLite文件后,将它们发送到C2服务器,然后使用PHP进行解析,并将解析出的所有凭证整合到一个集中的数据库中。

上述攻击方式是在我们审查这些密码窃取程序的泄露源码后推测出的结论。

我们能否利用不可信数据库的负载和查询过程来进行反击呢?如果可以,这将在不计其数的场景中造成重大影响。毕竟官方都宣称:SQLite是部署最广泛的软件之一。

SQLite拥有非常复杂的代码,几乎在任何设备上都可以使用。这是我们对于这项研究的动力所在,下面开始切入正题。

SQLite简介

你此时此刻正在使用SQLite的可能性是很大的,尽管你可能根本没意识到。

引用作者的话:

“SQLite是一个C语言库,它实现了一个小型、快速、自包含、高可靠性、功能完整的SQL数据库引擎。SQLite是世界上使用最多的数据库引擎。SQLite内置在所有手机和大多数计算机中,并且人们日常使用的很多软件中还独立携带了SQLite。”

与大多数其他SQL数据库不同,SQLite没有单独的服务进程。SQLite直接对普通磁盘文件进行读写操作。一个单独的文件就可以包括一个完整的,包含多个表,索引和视图的SQL数据库。

 攻击面 

以下代码片段是密码窃取程序后端的常见示例:

鉴于我们可以控制数据库及其内容,因此可以将可用攻击面分为两个部分:数据库的加载和初始解析,以及对其执行的SELECT查询。

由sqlite3_open完成的数据库初始加载其实是一个很受限的攻击面,它基本上只是为了打开数据库进行大量程序设置和参数配置。所以我们的主要攻击面是经过AFL模糊测试的头部解析部分(https://www.sqlite.org/fileformat.html#the_database_header)。

当我们开始对数据库进行查询时,就有点意思了。

引用SQLite作者的话:

“SELECT语句是SQL语言中最复杂的命令。”

虽然我们无法控制查询本身(因为它在我们的目标中是硬编码的),但对SELECT过程的研究最终被证明是有助于我们后续利用过程的。

因为SQLite3是一个虚拟数据库,所以每个SQL语句都需要先使用一个sqlite3_prepare*例程编译成字节码程序。

此外,prepare函数会遍历和展开所有SELECT子查询,并且验证所有涉及的对象(比如表或视图)是否存在并在master schema中进行定位。

 sqlite_master 和 DDL 

每一个SQLite数据库都有一个sqlite_master表,该表定义了数据库及其所有对象(如表,视图和索引等)的schema。

sqlite_master的定义如下:

这其中我们感兴趣的是sql字段,这个字段是用于描述对象的DDL(数据定义语言)。

从某种意义上来说,DDL类似于C语言中的头文件,DDL用于定义数据库中数据容器的结构,名称和类型,就像C中头文件通常定义的是类型定义,结构,类和其他数据结构一样。

如果我们查看数据库文件,就会看到这些DDL语句以纯文本的形式出现:

在查询准备过程中,sqlite3LocateTable() 会尝试寻找定义了被查询的表的内存结构。

sqlite3locateTable()读取sqlite_master表中可用的schema,如果是首次执行的话,还会对每个结果进行调用,既验证DDL语句是否有效,并构建必要的用于描述对象的内部数据结构。

DDL修补

了解上述处理过程之后,我们不禁想问,是否可以简单的替换掉文件中以纯文本形式呈现的DDL?如果我们可以向文件注入自己的SQL,那么便可以影响它的行为了。

根据上面的代码片段,DDL语句似乎必须以“create”开头。考虑到这种限制,我们需要评估一下我们的攻击面。

通过检查SQLite文档发现,下图中的这些对象是我们可以创建的。

CREATE VIEW这个命令给了我们一个有趣的想法。简单来说,VIEW对象只是一个预包装的SELECT语句。如果我们可以使用兼容的View来替换目标软件要使用的Table,那么机会便来了。

 劫持任意查询 

设想如下场景:

原始数据库中有一个名叫dummy的表,该表的定义如下:

目标软件使用如下命令对dummy表进行查询:

如果我们将dummy制作成VIEW的话,我们便可以劫持这个查询了:

这个视图让我们可以劫持查询——这就意味着我们可以生成一个完全受我们控制的新查询。

这种细微的差别极大的扩展了我们的攻击面。加载程序对头部的简单解析及不可控查询的执行,使得我们可以通过修改DDL来与SQLite解析器的各个部分进行交互并创建包含任意子查询的视图。

现在我们已经可以与SQLite解释器进行交互了,那么我们的下一个问题就是:在SQLite中内建了哪些原语?是否支持任意系统命令执行以及对文件系统的读写?

由于我们不是第一批从利用的角度关注到SQLite的人,因此回顾该领域先前的成果还是有意义的。So, 从最基础的部分开始。

 SQL注入 

作为研究人员,我们甚至很难拼写没有“i”(injection,注入)的SQL(由于太常见、太基础),所以从SQL注入似乎是个比较好的入手点。毕竟我们是想熟悉SQLite提供的内部原语,是否有系统命令,是否可以加载任意库。

最直接的技巧是添加一个新的数据库文件并执行如下SQL指令:

我们添加一个新的数据库,创建一个表并向其插入一行文本。新的数据库创建一个包含webshell代码的文件(SQLite中,数据库直接保持为单文件的)。

PHP解释器会解析数据库文件直至遇到PHP起始标签“<?”。

在密码窃取的场景中,能够写入webshell绝对是一种胜利。但是,还记得吗?DDL是不能以“ATTACH”开头的(需要以“Create”开头)。

另一个相关的选项是load_extension函数。虽然该函数允许我们加载任意的共享对象,但在默认情况下该函数是被禁止的。

 SQLite中的内存损坏 

与其他用C语言编写的软件一样,在评估SQLite安全性时,内存安全问题是必须要考虑的。在Michal Zalewski的博文中(blog post),他描述了如何使用AFL对SQLite进行模糊测试并获得了意想不到的结果:30分钟的模糊测试就发现了22个bug。

有趣的是,SQLite从此将AFL列入了其测试套装中。

从各个方面,内存损坏问题得到了足够的重视(Richard Hip和他的团队应该得到极大尊重)。但是,从攻击者的角度来看,如果没有合适的框架,这些漏洞还是很难利用的。

现代缓解措施是利用内存损坏问题的主要障碍,攻击者需要找到更灵活的环境。

相信安全研究社区很快就会找到完美的目标!

 Web SQL 

Web SQL数据库是一个网页API,用来将数据存储到数据库以便通过JavaScript使用SQl的变体进行查询。在2010年11月,W3C Web应用程序工作组停用了此规范,理由是缺少除SQLite之外的独立实现。

目前,Google Chrome,Opera和Safari仍然支持该API,都使用SQLite作为该API的后端。

任意网站通过一些流行的浏览器可以写入SQLite,这个问题引起了安全社区的注意,导致漏洞数量上升。

一些非常著名的浏览器中的任意网站都可以访问SQLite中那些不受信任的输入,该问题引起了安全社区的注意,随后漏洞数量开始增长。因为JavaScript解释器可以利用SQLite中的漏洞来实现浏览器攻击。

几个令人记忆深刻的研究报告如下:

1、容易实现的目标 (CVE-2015-7036)

  • 不可信信指针取消引用fts3_tokenizer()

2、Chaitin team 在黑帽17会议上演示的 更复杂利用

  • fts3OptimizerFunc()中的类型混淆

3、Exodus对最新麦哲伦漏洞的利用

  • fts3SegReaderNext()中的整数溢出
根据过去的WebSQL研究显示,名为“FTS”的虚拟表模块可能是我们的研究目标。

FTS

Full-Text Search(FTS,全文搜索)是一个虚拟表模块,实现对一组文档进行文本搜索。

从SQL语句的角度来看,虚拟表对象就跟其他表或视图一样。但在内部,对虚拟表的查询会调用影子表上的回调方法,而不是通常情况下对数据库文件进行读写。

一些如FTS的虚拟表实现会使用真实的数据库表(非虚拟)来存储内容。

举个例子,当要在FTS3虚拟表中插入一个字符串时,必须生成一些元数据以便进行有效的文本搜索。这些元数据最终会被存储在名为“%_segdir”和“%_segments”的真实表中,而字符串本身被存储在“%_content”表中,这里的%是原始虚拟表的名字。

这些用来辅助存储虚拟表数据的真实表被称为“影子表”(shadow tables)。简单来说影子表就是一个与原表结构一致但是名字不一样的普通表。

由于影子表之间相互信任,影子表之间传递数据的接口为bug的利用提供了条件。我们在RTREE虚拟表模块中发现的一个OOB(Out-Of-Band)漏洞——CVE-2019-8457,很好的证明了这一点。

用于地理索引的RTREE虚拟表是一个整数列开头的表。因此,RTREE接口也认定RTREE表中的第一列为整数。但是,如果我们建立一个第一列是字符串的新表,然后将其传入rtreenode()接口的话,就会发生OOB。

现在我们就可以使用查询劫持来控制查询了,我们也知道了在哪里可以找到漏洞,现在就可以对漏洞进行利用了。

深入SQLite内部进行漏洞利用开发

以前有关SQLite漏洞利用开发的文章都明确地表明环境的包装是必要的。无论是在这篇牛文(https://medium.com/0xcc/bypass-php-safe-mode-by-abusing-sqlite3s-fts-tokenizer-256ee2555607)中描述关于滥用SQLite tokenizer所使用的PHP解释器,还是最近在Web SQL上进行便捷利用的JavaScript解析器。

但是,由于SQLite几乎无处不在,如果限制它自身的利用潜力对我们来讲太没挑战,所以我们开始探索通过SQLite内部机制实现漏洞利用目的。

目前安全社区常用JavaScript来进行漏洞利用,那么我们是否可以使用SQL来实现类似的效果吗?

考虑到SQL是图灵完备的,我们开始基于自己的漏洞挖掘经验整理出用于EXP开发的原始清单。

用纯SQL编写具有以下功能的漏洞EXP:

  • 内存泄漏
  • 将整数打包和解包为64位指针
  • 指针算数
  • 能在内存中创建复杂的假对象
  • 堆喷射
接下来,我们逐一搞定这些原语功能并用SQL实现。

为了在PHP7上实现RCE,我们将利用一个已发布的漏洞:CVE-2015-7036。

等等,为什么一个4年前的漏洞到现在还没有被修复?这里面其实有一个很有意思的故事,它也是我们的一个论证示例。

因为只有在允许不可信来源的Web SQL时才可被利用,故而被忽略了。

 利用方案 

CVE-2105-7036是一个非常好用的漏洞。

简而言之,危险函数fts3_tokenizer()的调用使用单个参数(如”simple”、”porter”或者其他注册的tokenizer)时会返回一个令牌解析器地址(tokenizer address)。

当fts3_tokenizer()函数使用两个参数时,fts3_tokenizer会用第二个参数覆盖第一个参数的令牌解析器地址。

当指定令牌解析器地址被覆盖之后,我们就可以劫持任何使用了该令牌解析器地址的FTS表实例的程序流了。

我们的漏洞利用方案:

  • 泄露令牌解析器地址
  • 基址计算
  • 伪造一个假令牌指向我们的恶意代码
  • 使用恶意令牌覆盖原有令牌
  • 实例化fts3虚拟表以触发恶意代码
接着再回到我们的EXP开发上来。

 面向查询编程(QOP)

我们很自豪地介绍一种使用结构化查询语言进行漏洞利用开发的独特方法,我们向社区分享QOP,是希望能以此鼓励研究人员去追求在数据库利用中的无限可能。

下面的每个原语都会附带一个sqlite3命令示例。

别忘了,我们的目标是向sqlite_master表中植入我们的功能原语并在当目标软件查询我们的恶意SQLite文件时,劫持目标软件的查询。

 内存泄漏——二进制 

诸如ASLR之类的缓解措施无疑提高了内存破坏利用的门槛,对抗ASLR的一种常见办法是去学习和了解内存布局。这就是众所周知的内存泄漏。

内存泄漏有一些漏洞子类,每个漏洞也是略有不同。

在我们的例子中,SQLite内存泄漏的类型是BLOB类型(Binary Large Object,二进制大对象)。这些BLOB是一个很好的泄漏目标,因为它们有时包含内存指针。

使用一个参数来调用脆弱的fts_tokenizer()函数,随后该函数返回请求的tokenizer的内存地址,hex()可以将该地址以16进制显示。

显然,我们获得了内存地址,但是被反转成小端(little-endianity)了。

当然,我们可以使用一些SQLite内置的字符串操作来反转地址,substr()就非常适合。

现在我们可以读取BLOB了,但又出现了另一个问题,我们该如何进行存储呢?

 QOP链 

当然,用SQL存储数据要求使用INSERT语句,由于sqlite_master有严格的验证机制,所有的语句必须以“CREATE”开头,所以我们无法使用INSERT。应对这一挑战的方法就是简单的将视图下的各个查询连接起来。

举个例子:

这看起来貌似变化不是很大,但是随着我们的链变得越来越复杂,使用伪变量会更简单。

 对64-bit指针进行解包 

如果你曾经做过pwn挑战,那么指针解包和打包的概念应该不陌生。

通过该原语(指针解包)可以将十六进制(比如我们刚刚得到的内存泄漏地址)转化成整数。然后我们便能在接下来的步骤中对这些指针进行各种计算。

如上图所示的查询,使用substr()迭代十六进制串的每一位。再使用instr()确定数值并结合移位算法技巧(https://gist.github.com/ChiChou/97a53caa2c0b49c1991e)来实现将字符的转换为数值。

现在我们需要的就是对“*”后面进行正确的移位。

 指针运算 

当指针是整数形式的时候,进行运算是一个很简单的任务。例如,从上文中得到的令牌指针中提取image base:

 打包64位指针 

在读取泄漏的指针并进行处理后,需要将他们打包回原有的小端(little-endian)形式并存储起来。

安装SQLite文档资料, char()函数可以返回一个由具有整型Unicode编码值的字符组成的字符串。

但事实证明,该函数在有限的整数范围内情况良好,较大的整数会被转换成2字节的编码:

在SQLite官方文档上碰壁后,我们突然意识到:我们的EXP本身就是一个数据库,我们可以预先准备一个将整数映射到其预期值的表。

现在我们的指针打包查询就是:

 在内存中创建复杂的假对象 

编写一个指针是有用的,但这还不够。在很多利用内存安全问题进行攻击的场景中都要求攻击者在内存中伪造一些对象或者结构,甚至需要编写ROP链。

现在,我们将前文介绍的几个模块整合起来。例如,构造我们自己的tokenizer(https://www.sqlite.org/fts3.html#custom_application_defined_tokenizers)。我们伪造的tokenizer需要符合SQLite的接口:

使用上述方法和简单JOIN查询,我们就可以轻松的伪造所需的对象了。

通过底层调试器的验证,可以看到我们的确成功创建了一个伪造的Tokenizer对象。

 堆喷射 

现在我们已经构造了伪对象,有时候用伪造对象是有助于堆喷射的。然而不幸的是,SQLite并没有像MySQL一样的REPEAT()函数。

但是,这里(https://stackoverflow.com/questions/11568496/how-to-emulate-repeat-in-sqlite)给出了一个非常秀的解决方案。

zeroblob(N) 函数会返回大小为N字节的BLOB类型,同时使用replace()将其0值替换为我们伪造的对象。

对0x41的搜索结果也显示我们成功了。注意每0x20字节重复一个结果:

 内存泄漏——堆 

从我们的开发策略来看,似乎我们的方向是正确的。我们已经知道二进制图像的位置,也能够推测出必要函数,并且向堆中喷射我们的恶意tokenizer。

现在是时候用我们喷射的对象覆盖tokenizer了。但是,由于堆的地址是随机的,我们不知道我们要对那个位置进行喷射。这就需要另一个漏洞来实现堆泄漏。

我们再一次将虚拟表接口作为目标·。

由于虚拟表会使用底层的影子表,因此在不同的SQL接口之间传递原始的指针是很常见的。

 注意  在SQLite 3.20中已有相应的措施来缓解这一问题。幸运的是,PHP7使用的是早期版本来进行编译,如果版本更新了的话,也可以使用CVE-2019-8457漏洞。

要使堆的地址泄漏,我们预先创建一个fts3表使用它的MATCH接口。

正如我们之前在内存泄漏中见过的一样,该指针是个小端值,所以需要对它进行反转,当然,我们已经知道如何使用substr()函数来实现这一点。

如此,我们便知道了堆的地址,并且可以进行正确的堆喷射,最终,我们实现了用自己的恶意tokenizer覆盖原有的tokenizer。

整合

终于,前文期待的所有原语都已经有了,现在回到我们最开始:攻击密码窃取器的CC服务器。

如上所述,我们需要设置一个“陷阱”视图来启动我们的攻击。因此,我们需要审查我们的攻击目标,并为之准备相应的视图。

上图中的代码片段显示,我们的目标希望收集到的db拥有名为Notes表且含有BodyRich列。为了劫持这个查询,创建如下视图:

查询Notes时会执行3个QOP链。先分析一下第一个QOP链。

 heap_spray 

该QOP链会使用大量的恶意tokenizer对堆进行填充。

p64_simple-create、p64_simple_destrory和p64_system本质上是通过泄露和包装功能实现的工具链。

举个例子,p64_simple_destory的构造如下:

由于这些链都很复杂并且很重复,我们创建了QOP.py来执行。QOP.py以pwntools的形式使得创建如上所述的代码变得非常简单:

 Demo 

https://youtu.be/cPfYoxLOi1M (视频)

 COMMIT 

既然我们已经建立了一个框架来利用其他攻击者不确定数据库是否恶意的情况,那么让我们再来看一下SQLite漏洞利用开发的另一个例子。

iOS 持久性

因为iOS系统中所有的可执行文件都必须作为苹果安全引导(Secure Boot)的一部分进行签名,故而一般情况下很难在iOS上实现持久化。然而幸运的是,SQLite数据库不需要签名。

利用我们的新功能,我们用恶意数据库替换一个常用的数据库。在设备重新启动之后,当我们的数据库被查询时,我们便获取到了代码执行权限。

为了进行演示说明,我们替换掉了通讯录数据库“AddressBook.sqlitedb”。正如我们在PHP7利用中做的一样,我们创建两个额外的DDL语句。一个用于覆盖默认的tokenizer “simple”,另外一个DDL通过尝试实例化被覆盖的tokenizer从而触发崩溃。现在要做的就是将原始数据库的每个表重写为对应的视图。该视图将劫持所有的执行的查询重定向到恶意的DDL。

使用恶意数据库替换掉contacts DB并重启,可以看到如下系统崩溃信息:

不出所料,Contacts进程尝试在0x4141414141414149处查找我们伪造tokenizer的xCreate构造器时崩溃。

除此之外,联系人数据库实际上在许多进程之间是共享的。Contacts,Facetime,Springboard,WhatsApp,Telegram和XPCProxy只是处理对联系人数据库查询的进程中的一部分。在众多的进程中,个别进程拥有跟高级的特权,所以一点证明了可以再查询过程中执行代码,那么也就可以用这个方法进行扩展和权限提升。

我们的研究以及方法已经报告给了苹果,下面是我们的CVE:

  • CVE-2019-8600
  • CVE-2019-8598
  • CVE-2019-8602
  • CVE-2019-8577
接下来的工作

鉴于SQLite实际上几乎在所有平台上都内置的,所以我们认为,相对于SQLite的漏洞利用潜力来讲,我们所做的只不过是冰山一角罢了。我们希望安全研究社区可以采用我们的研究及其工具,并进一步的来完善它们。在这里我们给出几个建议:

  • 开发更强大的漏洞利用工具。可以通过使用sqlite_version()或sqlite_compileoption_used()等函数从预制表格中选择相关的QOP工具来动态构建利用。
  • 实现更强大的原语,比如任意读写。
  • 寻找更多“查询者无法验证数据库是否可信”的场景。
结论

我们确认,对数据库进行简单的查询也许并不像你想的那么安全。利用我们的查询劫持以及QOP技术充分证明了在SQLite中对内存损坏问题可以被可靠利用。随着权限层次结构变得比以往更加的细分,很明显我们必须重新考虑受信任与不受信任的SQL输入的边界在哪里。为了演示这些概念,我们在一个运行着PHP7的密码窃取程序后端实现了远程代码执行,并在iOS上获得了更高权限的持久化。我们相信,这些只是SQLite诸多利用中屈指可数的例子。

Check Point公司的IPS可以防范这种威胁:SQLite fts3_tokenizer Untrusted Pointer Remote Code Execution (CVE-2019-8602)。

【via@苏州极光无限信息技术有限公司