紧急警告:SQLite数据库存在严重漏洞,众多应用可能遭受攻击!

发表时间: 2018-12-16 10:00

腾讯Blade安全团队发现,SQLite允许攻击者在受害者的计算机上运行恶意代码,并在危险较小的情况下泄漏程序内存或导致程序崩溃。由于SQLite嵌入在数千个应用程序中,因此该漏洞会影响各种软件,包括物联网设备、桌面软件、Web浏览器、Android与iOS等应用。

如果底层浏览器支持SQLite和Web SQL API,那么将漏洞利用代码转换为常规SQL语法也可以通过访问网页等操作远程利用此漏洞。Chromium浏览器引擎支持此API,这意味着像Chrome、Vivaldi、Opera 和Brave等浏览器都会受到影响,而Firefox和Edge 由于不支持此API,因此不受影响。

想想OpenSSH,Intel CPU(Meltdow和Spectre问题),每个产品都是在不断发展中日趋完善,无论是开源还是闭源,软件还是硬件。针对SQLite出现的问题,通过升级浏览器和修补API漏洞来解决。作为SQLite的忠实使用者,无论从产品开发还是应用体验,SQLite都是不错的选择,尤其是轻量化关系数据库应用中。借此机会,下面给读者普及下SQLite基础知识。

SQLite是一个开源的、内嵌式的嵌入式关系型数据库。SQLite和Oracle和Access一样是文件型数据库,就是说,一个数据库就是一个文件,此数据库里可以建立很多的表,可以建立索引、触发器等等,但是它实际上得到的就是一个文件。SQLite数据库具有下面的特点:

  • (1)首先SQLite数据库服务器就在你的数据库应用程序中,其好处是不需要网络配置和管理,也不需要通过设置数据源访问数据库服务器。
  • (2)其次SQLite数据库的服务器和客户端运行在同一个进程中。这样可以减少网络访问的消耗,简化数据库管理,使你的程序部署起来更容易。
  • (3)再次SQLite在处理数据类型时与其它的数据库不同。区别在于它所支持的类型以及这些类型是如何存储、比较、强化(enforc)和指派(assign)。

SQLite的域完整性被称为域亲和性(affinity)。为了理解类型亲和性,你必须先要理解存储类和弱类型(manifesttyping)。SQLite有Integer、Real、Text、Blob和Null五个原始的数据类型,被称为存储类。存储类这个词表明了一个值在磁盘上存储的格式,其实就是类型或数据类型的同义词。

数据库SQLite的数据库架构

SQLite在架构上采用了模块的设计,它由公共接口、编译器系统、虚拟机和后端四个子系统组成。

1、接口(Interface)。由SQLite C API组成,也就是说不管是程序、脚本语言还是库文件,最终都是通过它与SQLite交互的(我们通常用得较多的ODBC/JDBC最后也会转化为相应C API的调用)。

2、编译器(Compiler)。在编译器中,分词器(Tokenizer)和分析器(Parser)对SQL进行语法检查,然后把它转化为底层能更方便处理的分层的数据结构---语法树,然后把语法树传给代码生成器(code generator)进行处理。而代码生成器根据它生成一种针对SQLite的汇编代码,最后由虚拟机执行。

3、虚拟机(Virtual Machine)。架构中最核心的部分是虚拟机,或者叫做虚拟数据库引擎(Virtual Database Engine,VDBE)。它和Java虚拟机相似,解释执行字节代码。VDBE的字节代码由128个操作码(opcodes)构成,它们主要集中在数据库操作。它的每一条指令都用来完成特定的数据库操作(比如打开一个表的游标)或者为这些操作栈空间的准备(比如压入参数)。

4、后端(Back-End)。后端由B-树(B-tree),页缓存(page cache,pager)和操作系统接口(即系统调用)构成。B-tree和page cache共同对数据进行管理。B-tree的主要功能就是索引,它维护着各个页面之间的复杂的关系,便于快速找到所需数据。而pager的主要作用就是通过OS接口在B-tree和Disk之间传递页面。

嵌入式数据库SQLite的数据类型

在SQLite数据库中,不仅提供了储存基本数据的功能,而且提供了对二进制数据存储的能力,这样可以确保把采集到的农业图像信息存放到数据库中,由数据库统一操作和管理,在SQLite数据库中提供的基本数据类型不但可以进行转化,还可以进行大小比较。数据类型如下:

  • (1)Integer整数值。有正负之分,它是由8个字节(Bytes)表示。SQLite数据库可以根据整数值的大小自动控制其所占字节的个数。
  • (2)Real实数类型。在SQLite由8个字节来表示。
  • (3)Text文本字符数据类型。用来保存文本信息。SQLite数据库支持多种字符编码类型,包括UTF-8和UTF-16。字符串的大小是没有限制的。
  • (4)Blob二进制数据对象类型。保存二进制数据,大小没有限制。
  • (5)Null空类型。一个具有NULL存储类型的值比所有其它类型值都小。SQLite数据库对Null完全支持。

SQLite 数据库本身一共有80多个API调用接口,功能简单的数据库程序用三个接口完成: sqlite3_open(),sqlite3_exec(), 和 sqlite3_close()。 要是想更好的控制数据库引擎,使用sqlite3_prepare()函数把SQL语句编译成字节码再通过sqlite3_step()函数来执行。在SQLite中,绝大多数接口提供了UTF-8和UTF-16两个版本,大多数接口成对出现。

此外,SQLite数据库中同时也提供了用户对API接口扩充的机制。SQLite的扩充API用来支持用户定义的函数、聚合和排序法。实现一个用户自定义的函数分为两步。首先,写句柄。句柄实现一些你想通过SQL完成的功能。然后,注册句柄。为它提供SQL名称、参数的数量和一个指向句柄的指针。

数据库控制和语句执行方式

从数据库打开开始,SQLite就要为sqlite3 *类型准备好内存,一直到数据库关闭整个过程。打开数据库时,这个类型的变量指向你将操作的数据库。SQLite数据库和大多数据库操作相同,其过程连接并打开数据库、处理事务和断开连接并关闭数据库构成:

1、连接并打开数据库。每个SQLite数据库都存储在单独的操作系统文件中,数据库与文件一一对应。连接并打开数据库的接口调用为sqlite3_open(),它用来打开一个数据库文件,该数据库文件中可以包含许多个关系表。该接口调用成功返回SQLITE_OK。

数据库打开成功。SQLite还可以创建内存数据库。如果你使用:memory:或一个空字符串做数据库名,数据库将在RAM中创建。内存数据库将只能被创建它的连接所存取,不能与其它连接共享。另外,内存数据库只能存活于连接期间,一旦连接关闭,数据库就将从内存中被删除。

2、处理事务。SQLite 是支持事务处理的。默认情况下,事务自动提交,也就是每一个SQL语句都在一个独立的事务下运行。任何SQL语句命令都在事务下执行。可以通过Begin、Commit和Rollback等命令手动提交事务。如开始、提交、回滚事务过程如下:

3、数据库SQL语句执行。在SQLite数据库中执行事务最常用的接口是sqlite3_exec(),在这个函数中有指向sqlite3_exec()的回调函数的指针参数。例如做 insert 操作或做 delete 操作时,就没有必要使用回调,该函数指针可设为NULL。而当你做 select 时,就要使用该回调函数,因为 sqlite3 把数据查出来,得通过回调告诉你查出了什么数据。

数据库对SQL语句的执行方式在下一节详述,回调函数的形式为:

typedef int (*sqlite3_callback)(void*,int,char**, char**)。

在SQLite数据库中执行事务的接口也可以是sqlite3_get_table(),它返回一个表格化的结果集。不需要提供回调函数。

4、执行SQL语句。SQL语句的执行过程由三个阶段完成,下面会详述。

5、格式化(动态构造)SQL语句,利用sqlite接口函数构造SQL语句,并调用相关接口执行语句。首先调用sqlite3_mprintf()或sqlite3_vprintf()格式化SQL语句,接着调用sqlite3_exec()执行语句,最后调用sqlite3_free()释放内存资源。也可以调用sqlite3_exec_printf()完成整个过程。

6、断开连接并关闭数据库。前面如果用 sqlite3_open 开启了一个数据库,结尾时不要忘了还要关闭所有附加的数据库文件。SQLite提供接口为SQLite3_close()。

SQL语句的执行在SQLite数据库中分两种情况,预编译查询和封装查询。

预处理查询是SQLite执行所有SQL命令的方式,每一个阶段都关联于语句句柄的一种状态(prepared、active和finalized)。Pepared表示所有资源都已分配,对应接口函数为sqlite3_prepare(),语句已经可以执行但还没有执行。现在还没有申请锁,一直到调用sqlite3_step()时才会申请锁。Active状态开始于对sqlite3_step()的调用,此时语句正在被执行并拥有某种锁。Finalized意味着语句已经被关闭且所有相关资源已经被释放。

封装查询是预处理查询的封装。在使用上比较简单,允许你在单一的函数调用中执行SQL命令。一个函数是sqlite3_exec(),特别适合执行不需要返回数据的查询。为了让查询结果数据返回,必须实现回调函数,传递给sqlite3_exec()函数作为参数。另一个是sqlite3_get_table(),返回一个表格化的结果集。封装查询也可以格式化SQL语句之后再执行。

二进制数据的数据库处理技术

在农业信息采集嵌入式系统中,为了满足对采集信息有效地在数据库中储存、管理、查询和传输的要求,必须要对数据库处理的关键技术进行研究。这些技术包括:记录和字符串处理、字段处理、错误控制、操作控制和线程处理。

1、记录和字符串的处理是数据库中最常用的处理方式。在前面也提到过sqlite3_exec()和sqlite3_get_table()函数是处理事务和执行SQL语句接口,区别在于sqlite3_exec()在处理记录,如执行SELECT时,要得到处理结果需提供回调函数。提供回调函数的作用就是当在执行完对记录的操作后,让回调函数按需要得到处理结果。字符串处理主要用在对SQL语句的格式化上,在SQLite数据库中常用接口为sqlite3_mprintf()或sqlite3_vprintf()。

2、字段处理是在记录操作的基础上进行的。在SQLite中提供了以对字段操作的函数接口,其特点是sqlite3_column_开头。这些接口可以获得字段名称、字段的存储类型和字段的值等信息。如获得一个字段的名称、储存类型和声明类型的函数调用如下:

3、SQLite数据库预定义了许多错误代码宏以方便程序设计错误处理的应用。很多API接口函数的返回类型是整形,这表示返回错误码,所以在接口调用中可以根据返回码进行错误处理,可以使用函数sqlite3_errmsg()获得附加的错误信息。

图像数据在sqlite数据库中是按照二进制形式存放的。 操作二进制数据需要用一个辅助的数据类型:sqlite3_stmt * 。它是一个已经把SQL语句进行了解析,并用sqlite自己标记记录的内部数据结构来表示的SQL语句。正因为这个结构已经被解析了,所以你可以往这个语句里插入二进制数据。

把二进制数据插到 sqlite3_stmt 结构的过程,必须用sqlite 提供的函数来插入。SQLite数据库提供了一种通配符机制用来表示SQL语句中不确定的字符值,这些通配符如?、aaa、nnn等。做这些通配符所代表的值在sqlite3_bind_开头的接口函数中被填充,在SQLite数据库中提供了很多以sqlite3_bind_开头的接口用来给SQL声明中的通配符赋值。

二进制数据的存储相对比较复杂一点,但从数据库对二进制数据的操作来看,主要分为数据写入和数据读出。

1、数据写入是指将二进制数据写到数据库中,让数据库统一管理保存。在SQLite中,二进制数据时保存在Blob数据类型中的。数据的写入步骤是:创建数据库关系表,构造含有通配符的插入二进制数据的SQL语句,然后把该语句解析到sqlite3_stmt 结构中。如:

调用接口函数sqlite3_bind_blob()给SQL申明的通配符赋值,该函数第二个参数为通配符的索引号,从1开始,有多个通配符时,要多次调用该函数。第三个参数为二进制数据开始指针,第四个参数为二进制数据长度。

sqlite3_bind_blob( stat, 1, pdata, (int)(length_data), NULL );

之后,二进制数据存放到了SQL语句。现在需要调用sqlite3_step()把sqlite3_stmt 结构表示的SQL语句就被写到了数据库里。最后调用sqlite3_finalize()释放sqlite3_stmt 结构的内存。

2、数据的读出是把二进制数据从数据库按照记录读出的过程。该过程在循环中完成,在农业信息采集系统中可以进行图像信息的显示和无线传输。准备sqlite3_stmt* 结构,然后把读数据库的SQL语句解析到sqlite3_stmt 结构中。

sqlite3_prepare( db, “select * from tab”, -1,&stat, 0 );

接着开始循环查询数据,sqlite3_step( )返回SQLITE_ROW表示记录没有结束,否则表示结束。在循环中可获取ID值、二进制数据、二进制数据长度和数据处理,之后释放sqlite3_stmt* 结构内存。

SQLite的特性还体现在它既是一个数据库,一个程序库,一个命令行工具,也是一个学习关系型数据库的很好的工具。确实有很多途径可以把它使用到内嵌环境、网站、操作系统服务、脚本语言和应用程序。对于程序员来说,SQLite就像一个数据传送带,提供了一种方便的将应用程序绑定的数据的方法。