宋 冰,孙 莉,史大伟,韩胜杰
(1.河南警察学院 网络安全系,郑州 450000;2.驻马店正阳县公安局 刑警大队,河南 驻马店 463600;3.新乡市公安局 电信网络犯罪侦查支队,河南 新乡 453000)
近年来随着科技的飞速发展与日新月异,手机已经从最简单的语音文字通讯工具,变为搭载各种功能的智能设备,正走进千家万户,成为现代社会不可缺的一部分。手机中的各种App存储着重要的线索,在犯罪侦查中,手机中的数据往往是案件查破的关键。SQLite数据库作为一种轻量级数据库,普遍使用在手机的各种应用程序中,包含账户信息、通信记录、网页浏览记录、交易记录、位置信息等大量的用户痕迹。随着人们广泛使用智能设备,以及对智能手机各项功能的了解,犯罪分子对其中存储的数据进行有意的删除、更改、覆盖等操作,这无疑对犯罪线索的获取带来阻力。因此,针对SQLite数据库删除数据恢复的研究,对于案件侦查有着重要的意义。
目前针对SQLite数据库进行数据恢复的方法主要有三种:一是基于手机文件系统的工作机制。从文件属性入手,在存储设备中的未分配空间中恢复SQLite文件[1];二是基于SQLite文件结构。通过在SQLite文件中页面的未分配区域中遍历寻找删除记录,从而达到数据恢复的目的[2];三是基于SQLite数据库日志。对SQLite数据库自动创建的日志文件,根据记录的存储模式,分析其中的数据,恢复数据记录。SQLite数据库支持的日志类型有两种:一种是早期使用的回滚日志,另一种是预写日志(WAL),相较于回滚日志,预写日志在并发读写操作和磁盘I/O方面具有较好的性能[3],再加上手机存储容量的不断增加,从Android9开始,越来越多的手机以空间换取更好的性能,从而将预写日志作为SQLite数据库的日志模式[4]。
本文通过分析SQLite数据库及预写日志文件的文件格式、存储及删除机制,提出了一种基于SQLite数据库日志的数据恢复方法。该方法是提取SQLite数据库文件中页和WAL日志文件中各个框包含的页,通过分析记录的存储模式,综合两种页中存储的记录数据进行恢复。实验表明,该方法具有较好的恢复效果。
SQLite数据库的配置中,存在影响运行和删除数据恢复的两个重要参数。第一个参数是页大小。SQLite文件中页类似于文件系统的簇,页是SQLite数据库内部能够被寻址的最小单位,如果当前的SQLite文件不够存放新的数据时,页大小将决定新分配的空间大小。第二个重要的参数是PRAGMA命令。PRAGMA命令是一个特殊命令,可以用在SQLite 环境内,控制各种环境变量和状态标志,一个PRAGMA值可以被读取,也可以根据需求进行设置。通常在第一次创建SQLite数据库时PRAGMA就会被自定义,其中决定数据成功恢复的PRAGMA命令有三个:
(1)secure_delete。默认值是0,这个参数可设置为0、1或FAST。值为0表示关闭,通常为关闭状体,可以减少CPU周期数和磁盘I/O数量以提高性能。如果希望避免在内容被删除或更新后留下痕迹,可以将该值设置为1。这个参数特殊设置是FAST选项,它将在B+tree页中的自由列表下留下取证数据。
(2)auto_vacuum。默认值是0,表示数据库文件大小不会自动缩小。如果这个参数设置为1,数据库会删除未使用的页面,并且不会将这些页面保留在自由列表中。
(3)journal_mode。用于启用或禁用日志。这个参数有六个选项,分别是DELETE、TRUNCATE、PERSIST、MEMORY、WAL和OFF。除WAL和OFF以外,其余选项都会创建一个以“-journal”结尾的日志文件,如果将该参数设置为WAL,则会创建一个以“-wal”结尾的日志文件,在数据库打开期间,WAL日志文件永远不会被删除,但由于它有一个固定的大小,一旦数据填充到WAL的末尾,SQLite会覆盖WAL前面用于暂存数据的页。
这三个PRAGMA参数的设置都会对SQLite数据库是否在主数据库文件中和可选日志文件中保留被删除记录产生影响,从而直接影响SQLite数据恢复的效果。
SQLite数据库完整数据通常包含于磁盘上“主数据库”文件中。在事务模式下,额外的信息会存储在第二个文件中,也就是回滚日志或者预写日志。同一数据库内所有页大小相同,页的单位大小定义在数据库文件头部中。
一个SQLite数据库文件是以B+tree结构组成的[5],分为SQLite头部和SQLite主体。SQLite头部包括文件的前100个字节,它提供了该SQLite数据库文件的基本信息,如头部类型字符串、页大小、数据库文件的大小、文本编码等,其中在文件偏移18和19的十六进制值若为0x01则是回滚日志模式,值为0x02则是WAL日志模式,可以使用“PRAGMA journal_mode”命令查看日志模式。在SQLite文件偏移量100处,SQLite主体从它的第一个页开始,这个页包含一个名为“sqlite_master”表的结构,“sqlite_master”表保存着数据库的表和索引结构的基本信息,其中存储了每个表和索引的第一页入口点。“sqlite_master”表中内容如图1所示,“sqlite_master”表中显示表名称是“tel”,表所在的根页(rootpage)是第2页,“sqlite_master”表中包含创建该表的SQL语句。
图1 “sqlite_master”表中内容
SQLite数据库主体中的页分为四种不同类型,每个类型都有自己的作用和结构。其类型如下:
(1)Freelist主干页(Freelist trunk page):Freelist主干页以页面指针组织起来,每个页面指针大小是4个字节。第一个指针将指向下一个自由列表主干页的页码,之后的每一个指针都会指向一个自由列表叶子页的页码。第一个Freelist主干页的指针存储在SQLite头部的偏移量32处。
(2)Freelist叶子页(Freelist leaf page):Freelist叶子页是一个可以分配给新记录存储的自由页。这种类型的页面不保存分配数据或任何其他类型的内容。
(3)表B+tree内部页(Table B+tree interior page):表B+tree内部页中包含指向它的下一级B+tree页和最右子页(序号最大的子页)的页码。类似于Freelist主干页,指针的大小为4个字节。
高校产出具有公共品的特征,从表面上看,是培养的学生、取得的科研成果以及为社会提供的服务,实质上则是蕴含在人才和成果中的专门知识的发明和创造。由于功能具有二重属性,客观功能可以用一定的数值来表示大小,而主观功能取决于人的主观感受,缺乏可度量性,需要采用定性方法来表示。因此,在进行指标选择时,一方面要尽量选取能够量化和可以采集的指标,另一方面也要将一些概念化的指标转变为可操作化的指标。例如,在挖掘定性指标内涵的基础上,尽量选取信息重叠度高的定量指标来替代,以保证绩效价值评估方法的可操作性。
(4)表B+tree叶子页(Table B+tree leaf page):表B+tree叶子页是SQLite数据库中存储记录的关键结构。这种类型的表B+tree叶子页是唯一可以包含有效数据的B+tree页。
每个表B+tree叶子页分为页头部和页主体,B+tree叶子页的内部结构如图2所示。在一个SQLite文件中,只有这种类型的页保存着用户或应用程序数据。页头部大小占8个字节,后面是单元指针数组,单元指针数组存储了指向同一页内每个记录单元的指针(每个指针长度为2个字节)。SQLite在数据单元内容区中优先从页最后部开始逆序存储数据,即第一个单元指针指向页的最后一个记录单元,而最后一个单元指针指向单元内容区域中的第一个记录单元。单元指针数组的最后一个指针之后至单元内容区域中的第一个单元格之前的区域则是未分配区。
图2 B+tree叶子页的内部结构
页头部中的一个重要指针是偏移量为1处的自由块指针。自由块包含了单元内容区中被删除的单元记录,并以链表的形式组织起来,自由块指针是链表的入口点(即第一个自由块),每个自由块都包含一个指向下一个自由块的指针。一个自由块被分隔成三个字段:(1)指向下一个自由块的指针,大小2个字节;(2)当前自由块的长度,大小2个字节;(3)空闲区域的内容,被删除的数据就存储在此处。如果一个自由块是链表的最后一个,那么第一个字段将被设置为0。对于我们而言,值得关注的是自由块的第三个字段。
SQLite数据库中最小的实体是单元(cell)。单元中的数字由一个可变长度的整数表示,称为varint编码。varint的长度为1~9个字节。计算方法是在输入的字节数组(例如,单元的序列类型)上进行运算,遍历所有的字节,直到找到一个最高位等于0的字节(即作为无符号整数的字节数值小于128)。移除所有获得字节中的最高位,重新组合就得到实际的数值。
每个单元以两个varint整数开始,表示有效载荷的字节数和行的id,后面是一个字节数组,最后是一个可选的4个字节指针,指向overflow页。字节数组以varint编码存储,其中包括字节数组的头部长度、数据类型和实际的有效载荷。除了TEXT和BLOB类型之外,所有的数据类型都是用一个特定值来编码。BLOB类型由大于或等于12的偶数数字定义,而TEXT类型由大于或等于13的奇数数字定义。最后,存储在有效载荷中记录的大小可以通过数据类型计算出来。
WAL日志文件的存储结构头部字段如表1所示,头部字段由日志头部和固定大小的帧(Frame)组成[6]。WAL日志头部大小固定为32个字节,以4个字节为一组,分为8组代表日志文件的8个属性。magic number是固定值0x377f0682或0x377f0683,页面大小与对应数据库相同;checkpoint序列表示经过的检查点的次数;salt-1与salt-2是随机数,随着checkpoint的执行,前者增加1,后者随机变化;checksum-1与checksum-2分别是文件头校验和的第1部分与第2部分。
表1 WAL日志文件的存储结构头部字段
WAL日志帧头部字段如表2所示。WAL日志头部后面是0到多个帧。每个帧由一个24字节的帧头(frame-header)和一个大小等于数据库页大小的页组成。帧头部为6个32位大端无符号整数。page number表示该帧数据对应的数据库页;size after commit表示帧数据提交后,数据库的大小;salt-1/2与日志文件头中对应数值相同,若不同则表示该帧无效。帧头后是大小等于页大小的帧数据部分。
表2 WAL日志帧头部字段
在SQLite数据库中,用户进行删除、更新等操作时,表B+tree叶子页上的单元数据将成为自由块形式存在,自由块与单元数据互相交错。根据上述对表B+tree叶子页结构的分析,页头部中偏移量为1处的自由块指针指向第一个自由块,每个自由块指针大小为4个字节,前2个字节指向下一个自由块,一直到该自由块指针值为0,表示结束,从而形成自由块链表。每个自由块指针的后2个字节表示该自由块的大小。因此,表B+tree叶子页上的单元数据在删除后,只是前4个字节的值发生变化,其余字节保持不变。
在表B+tree叶子页中,数据单元头的记录大小、行id、载荷大小、数据类型等字段都可能根据删除操作变为自由块后,发生改变。因此,由数据类型字段来匹配具体数据字段会变得困难。数据类型字段以1~n顺序排列形式出现,对应着数据字段中相应的1~n的数据,并且储存着数据字段的各项信息,包括大小位置等。所以,要恢复数据字段中的数据,必须从数据类型字段入手,使两者一一对应,形成映射关系。
(1)当记录大小、行id、载荷大小总共占用3个字节时,删除记录数据后,由于自由块指针大小为4个字节,此时数据类型字段的第一个varint被覆盖,而数据类型字段剩余的varint字段和数据字段均保持不变,此时数据可以绝大部分恢复。被覆盖的数据类型字段的第一个varint表示的是数据库表定义中的第一个字段。通常情况下,该值为自增型关键字,用于表示该条记录的序号。
(2)当记录大小、行id、载荷大小总共占用4个字节或超过4个字节时,删除记录数据后,数据类型varint字段和数据字段均保持不变,此时数据可以完全恢复。
综上所述,在原数据单元成为自由块过程中,数据类型字段的第一个varint被覆盖,而其余都完整,通过划分识别这些数据,就可以较为精确地恢复出已删除的数据。
对数据库进行记录删除、更新等操作后,只要未触发checkpoint,则数据预先存储在WAL日志文件中,并未对主数据库文件进行修改,每次进行的事务操作将在WAL日志文件中留下记录。因此,可以从WAL日志文件中得到前一段时间更新操作留下的记录。当触发checkpoint后,如果WAL日志文件中帧头部的salt-1′字段值与日志文件头部中salt-1字段值相同,则表示该帧内的数据已经被覆盖,数据无法恢复;如果帧头部的salt-1′字段值比日志文件头部中salt-1字段值小1,则表示该帧内的数据尚未被覆盖,数据库对应页中的数据可以被恢复。由于前面已对数据库文件中页的单元结构进行过解析,而WAL日志文件中的帧包含的页与数据库页结构一致,因此,可使用上述方法直接对WAL日志文件中满足条件的帧进行解析,从而恢复数据库记录。
本文采用对SQLite主数据库文件和WAL日志文件中的页包含的数据综合分析,进行数据恢复,以达到较好的数据恢复效果。首先通过对SQLite主数据库文件中各个页包含的单元内容区进行数据恢复,其次对SQLite数据库对应的WAL日志文件中各个框包含的页进行数据恢复。
首先根据文件头部签名“SQLite format 3 00”,判断此文件为SQLite主数据库文件,根据SQLite主数据库文件的前100个字节,提取页大小、数据库文件大小、文本编码等基本参数。然后从该文件偏移100字节处,提取到1页,获得表名、根页号、创建表的SQL语句等信息。进而根据根页号获得所有其下首字节为“0x0D”的B+tree叶子页,并根据在3.1中提到的删除机制,通过偏移字节形成的指针,查找下个自由块,首尾相接,形成完整的删除链表。最后恢复收集到的删除链表中自由块的数据类型字段以及数据字段。
首先读取WAL日志文件,根据对应同名的db数据库文件中的页大小,按照WAL日志结构进行解析,保存至帧集合中。然后,分别将帧集合中的帧依次取出,将各个帧头的salt-1字段值与文件头部中salt-1字段值进行比较,如果帧头的salt-1字段值比文件头部中salt-1字段值小1,则将根据数据库页的单元结构对该帧进行解析,获得type以及Data数据,将获得的数据库记录保存。SQLite删除数据恢复流程如图3所示。
图3 SQLite删除数据恢复流程
通过实验验证上述恢复方法的可行性和有效性。考虑到不同品牌手机系统获取镜像文件的差异性,本文采用手机模拟器Genymotion创建Android10版本的手机。
首先,查看通讯录数据库的参数设置,通讯录数据库参数设置如图4所示,通讯录数据库的secure_delete的值为0,表示未开启;auto_vacuum的值为1,表示自动缩小;journal_mode的值为wal,表示开启WAL日志模式。
图4 通讯录数据库参数设置
通过创建Genymotion脚本对手机通讯录进行操作,分为四种情况:对通讯录数据全部删除、部分随机删除、部分随机删除后添加数据、反复删除添加,模拟用户实际使用过程。最后,针对每种情况采用本文方法对手机中通讯录存储数据库contacts2.db的数据进行恢复。四种情况具体实验步骤如下:
(1)对通讯录数据全部删除。首先在通讯录中创建1000条联系人信息,然后按照顺序依次删除1000条联系人信息。使用本文算法对contacts2.db和contacts2.db-wal文件综合扫描,分别得到数据库主文件页和WAL日志文件的帧。恢复的数据库主文件页和WAL日志文件中的帧如图5所示,每个页或帧中包含若干条联系人信息,其中WAL日志文件中恢复的帧数据详情如图6所示。
图5 恢复的数据库主文件页和WAL日志文件中的帧
图6 WAL日志文件中恢复的帧数据详情
(2)部分随机删除。首先在通讯录中创建1000条联系人信息,然后随机删除100条联系人信息。实验步骤同上。
(3)部分随机删除后添加数据。首先在通讯录中创建1000条联系人信息,然后随机删除100条联系人信息,之后再添加100条联系人信息。实验步骤同上。
(4)反复删除添加。首先在通讯录中创建1000条联系人信息,在随机删除100条联系人信息后再添加100条联系人信息,然后再次随机删除100条联系人信息后再次添加100条联系人信息。实验步骤同上。
为了保证实验过程互不干扰,每次实验前需要重置手机,保证手机中没有残留数据。
对通讯录数据库不同操作下恢复效果如图7所示。实验结果表明,在实验(1)和实验(2)的情况下,当通讯录数据全部删除或随机删除时,由于没有添加数据对删除的数据造成覆盖,数据恢复率和准确率较高。在实验(3)和实验(4)的情况下,随着数据添加操作对删除数据的覆盖和破坏,对恢复数据造成一定影响,但在数据恢复率和准确率上,仍达到较高的恢复效果。
图7 对通讯录数据库不同操作下恢复效果
在目前电子数据取证过程中,手机中的数据具有重要价值。SQLite数据库被手机中各种功能的App用来存储数据,包含大量的用户痕迹。基于手机中SQLite数据库具有特有的删除机制,本文详细研究了SQLite数据库中影响删除数据恢复效果的三个参数,分析了SQLite数据库文件格式和WAL日志文件格式,并着重分析其数据删除机制及恢复原理。根据SQLite数据库删除机制,提出了一种综合提取SQLite主数据库文件中页和WAL日志文件中各个帧包含的页,对删除数据进行恢复的方法。实验表明,该方法具有较好的恢复效果。
由于SQLite数据恢复工具多为国外公司的商业版本,实验中不便获取,故缺少与其它恢复方法的横向对比;针对不同品牌的手机获取手机完整镜像文件存在差异,从而会影响WAL日志文件的恢复效果。下一步研究将考虑采集大量具有客观性、普遍性的数据集来对此方法进行验证。