分类目录归档:内核编程

文件沙箱系统设计与实现

前文回顾:
1. 『一种安全沙箱模型
2. 『SSDT Hook

以下进入正题:

1. 文件系统沙箱的设计

Native API是Windows平台中最底层的访问系统资源的函数。应用程序对文件系统的访问最终都要调用Native API里一组以Zw开头的函数。文件沙箱系统的实现原理是通过SSDT Hook技术截获这组API,分析并修改传入参数,把应用程序对文件系统中资源访问的路径进行重定向,从而使应用程序对系统中文件的读写操作限定在沙箱内,不对真实系统环境造成影响。图1展示了文件沙箱系统的结构框架,本系统由沙箱、SSDT Hook模块、主程序三部分组成。

sandbox-arch

图1 沙箱系统

沙箱系统中各模块功能如下:
(1) 沙箱: 沙箱是从文件系统中开辟出的一个子目录。沙箱系统启动后,应用程序试图对文件系统的修改操作都将被重定向到这里。
(2) SSDT Hook模块(Sandbox.sys): SSDT Hook模块以驱动程序的形式加载到操作系统的内核空间里,主要负责拦截Native API函数调用,分析传入参数,根据规则库制定的策略修改参数的值,使得程序对实际文件系统的资源访问重定向到沙箱中,从而达到保护真实系统环境的目的。
(3) 主程序(SbMainPanel.exe):主程序负责沙箱系统的启动与停止,配置沙箱目录,指定沙箱系统作用范围等等。

2. 文件系统沙箱的实现

Windows系统中涉及到文件操作的Native API有如下几个:ZwCreateFile()、ZwOpenFile()、ZwWriteFile()、ZwReadFile()、ZwSetInformationFile()、ZwQueryAttributesFile()。要实现沙箱系统必须拦截这几个API,并对它们参数列表中包含文件路径信息的传入参数进行修改。本小节会详细说明如何实现上述过程。为叙述方便,我们约定称沙箱中的目录为沙箱目录,文件系统中除沙箱目录以外的其他目录为原目录。

(1) 文件的打开、读/写和删除:应用程序要访问一个文件,首先必须调用ZwCreateFile()或ZwOpenFile()打开该文件。ZwCreateFile()和ZwOpenFile()函数的参数列表中有一个存储着待打开文件路径的结构体ObjectAttributes,分析并修改在这个结构体中存储的路径信息,使程序对文件系统中文件的访问重定向到沙箱目录中来,如果执行成功,会返回沙箱目录下该文件句柄。由于文件读函数ZwReadFile()、写函数ZwWriteFile()、删除函数ZwSetInformationFile()(注:使其最后一个参数FileInformationClass等于FileDispositionInformation)都是通过句柄实现对文件的操作,因此只要将返回的沙箱目录下文件句柄作为参数传入上述函数,那么程序读写、删除的对象都是沙箱目录下文件,原目录下文件并不受到影响。

(2) 文件重命名:若将ZwSetInformationFile()函数中最后一个参数FileInformationClass置为FileRenameInformation,该函数就执行文件重命名功能。修改后的文件名(含文件路径)存放在FILE_RENAME_INFORMATION结构体中,分析该结构体的成员变量FileName,如果该文件路径不在沙箱目录下,那么将它修改成沙箱目录下的文件路径。

(3) 文件信息查询:ZwQueryAttributesFile()函数的功能是根据文件路径查询文件信息。该函数参数列表中有一个存储着待查询文件路径的结构体ObjectAttributes,分析并修改在这个结构体中存储的路径信息,即可使ZwQueryAttributesFile()函数对原目录下文件信息查询转变为对沙箱目录下对应文件的信息查询。

通过SSDT Hook以上几种文件操作型Native API,在自定义替换函数中根据以上分析修改传入参数,即可完成沙箱系统关于重定向文件操作路径的功能。

--EOF--

SSDT Hook

1. Windows API调用机制

一般操作系统为保障系统安全,把程序的运行环境分为用户态和内核态两种级别,与此相对应,API也可分为两类,一类是用户态API,它们是供用户态应用程序直接调用的,这类API一般以动态链接库的形式导出。例如Windows的三大核心子系统kernel32.dll,user32.dll,gdi32.dll。另一类是内核态API,它们是供运行在内核态的程序(如设备驱动、内核进程等)调用的,这类API也称为Native API。从分层结构上看,内核态API比用户态API更加底层。图2所示的是用户态API与内核态API之间的依赖关系。

windows-api

图1 用户态API与内核态API的依赖关系

由图1可以看出,用户态API的实现依赖于内核态的Native API,它们之间通过ntdll.dll建立起联系。ntdll.dll处于用户态,它负责将Native API的接口“暴露”给应用程序,而这些Native API的实现是在内核态的ntoskrnl.exe里完成的。因此,ntdll.dll只是API调用从用户态通过软中断陷入到内核态的跳板。中断之前,ntdll.dll中被调函数把Native API的ID(系统服务调用号)和参数堆栈结构指针分别放入EAX和EDX寄存器中,中断陷入内核态后,如图2所示,中断处理函数KiSystemService()通过结构体KeServiceDescriptorTable获得系统服务分配表SSDT(System Service Dispatch Table)的基址,把EAX寄存器中的系统服务调用号当成索引从SSDT中获得该系统服务函数的函数地址,根据EDX寄存器中的参数堆栈结构指针获得参数列表,跳转到系统服务函数入口处继续执行,直至整个系统调用过程完成。

native-api-addressing

图2 内核态Native API的寻址过程

2. SSDT Hook

API拦截技术是指在API被调用时将其截断,插入开发者自己编写的代码,修改API执行代码的正常流程,从而能够改变或扩充API函数的功能。不同运行级别(用户态、内核态)下的API有不同的拦截方法。用户态API的拦截方法有静态挂钩和动态挂钩两种,其中动态挂钩方法又可细分为替换IAT函数地址、嵌入汇编代码、替换消息处理函数等,此外微软公司提供的Detours库也可实现动态拦截任意Win32 API。内核态API的拦截方法主要是SSDT Hook技术和Inline Function Hook两种方法。本文主要介绍SSDT Hook技术实现的内核态API拦截。

通过本文第1节的介绍,我们知道Native API的入口地址是中断处理函数KeSystemService()根据EAX寄存器中的ID值查找SSDT表得到的。若要拦截Native API,首先必须获得SSDT表的基址,修改SSDT表中存储的待拦截Native API的入口地址,使它指向我们自定义替换函数的入口地址。同时,保存原始的Native API入口地址,这么做的原因有两点:一、自定义替换函数中仍需要调用原始Native API,以实现系统调用功能。二、在Hook结束时需要恢复现场,还原SSDT。图3以拦截ZwCreateFile()为例说明了SSDT Hook的过程。SSDT中记录第0x25h号Native API ZwCreateFile()的入口地址为address37,经过Hook之后,我们把ZwCreateFile()的地址保存起来,将SSDT中的address37替换成自定义替换函数HookZwCreateFile()的入口地址,HookZwCreateFile()再根据已保存的address37来调用ZwCreateFile()来完成此次系统调用。

SSDT Hook

图3 SSDT Hook过程示意图

此外,自定义替换函数与被Hook的Native API必须有相同的参数类型、参数个数、函数返回类型,这样的约定可以保证SSDT Hook过程中正确保存寄存器内容,维持堆栈平衡。

--EOF--

基于Detours库的Win32 API拦截

Windows API是一组开放给程序员的应用程序编程接口,程序员可以通过各种API函数实现应用程序对系统资源的访问与利用。Windows提供了大量的API函数,这些API函数使得我们在Windows平台上开发应用程序非常方便,但是如果在不知道程序源代码的情况下想要对程序的功能进行扩充是非常困难的,此时我们就需要对程序调用的API函数进行拦截。通过API拦截,可以达到增加功能,修改程序返回值的目的。一般而言,API都采用动态链接库即DLL来实现,在Windows系统中有三大核心子系统,它们是Kernel,User,GDI,其实现分别位于kernel32.dll,user32.dll,gdi32.dll,大部分Windows API都包括在这三个DLL里。因此,拦截Windows API就是拦截应用程序对三大核心DLL导出函数的调用。API拦截技术的核心是自定义一个包含了替换函数的DLL,把这个DLL注入到一个新的或已存在进程的地址空间,使得该进程有权限访问到这个DLL的导出函数,再通过改写该进程内的二进制映像,将需要拦截的API的地址换成替换函数的地址,达到API拦截的目的。

微软公司早些年开发了Detours库,因为它可以在x86平台机器运行时动态拦截任意Win32 API函数,如今已被广泛地应用于Win32 API拦截上。Detours库能够保证无论操作系统或应用程序如何定位目标函数都能拦截成功。下图是普通函数调用和使用Detours库进行API拦截时函数调用的示意图:
Detours

上图中,目标函数是指待拦截的API函数,比如CreateFile()。Detours库会把CreateFile()函数的头几条指令换成一条无条件转移到Detour函数的跳转指令。这个Detour函数就是我们自定义的替换函数,比如Detour_CreateFile()函数。被替换出去的指令被暂时存储在Trampoline函数中。Trampoline函数是个跳板函数,比如Trampoline_CreateFile()函数,它除了保存CreateFile()函数中被替换的几条指令之外,还包含一条跳回修改后的CreateFile()函数的第二条指令。我们在Detour_CreateFile()函数中完成一系列自定义功能之后会重新调用Trampoline_CreateFlie()函数,从而完成主程序调用一个目标函数的整个过程。下图是上述过程的示意图:
Detours

Detours库对目标函数的拦截是通过修改进程内二进制映像来实现的。Detours库找到内存中目标函数的地址之后,便将目标函数的头几条指令替换成一条跳向Detour函数的无条件转移指令,同时把被替换掉的指令保存到Trampoline函数里。Trampoline函数的内容就是目标函数中被替换掉的指令再加一条无条件跳转指令,这条跳转指令是跳回到修改后的目标函数的第二条指令。由上可知,Detours库要完成一个目标函数的拦截,必须修改两个函数:目标函数和Trampoline函数。Trampoline函数既可以静态分配,也可以动态分配。动态分配时,Detours库首先为Trampoline函数分配内存,并且赋予目标函数和Trampoline函数所在内存读写的权限。接着把目标函数的开头至少5个字节的指令复制到Trampoline函数中,至少5个字节是因为32位操作系统一条无条件跳转指令长度是5个字节。如果目标函数少于5个字节,Detours库会终止操作并返回一个错误码。然后Detours库在Trampoline函数的最后添加一条无条件转移指令,这条指令跳向目标函数中没有被复制的指令开头处。同时Detours库在目标函数的开头添加一条跳向Detour函数的转移指令。最后,Detours库恢复目标函数和Trampoline函数所在内存页的原有权限并且调用FlushInstructionCache函数来刷新CPU的指令Cache。

在整个拦截过程中,必须要注意目标函数,Trampoline函数,Detour函数的参数个数、参数类型以及返回类型都必须一致,完全匹配。这样的约定可以使寄存器值得到正确保存,保证堆栈在API拦截过程中维持平衡。

--EOF--

CreateFile和ZwCreateFile函数CreationDisposition选项的对应关系

CreateFile是Win32系列的文件操作函数,由kernel32.dll导出。ZwCreateFile是Native APIs中的文件操作函数,由ntdll.dll导出,具体实现在ntoskrnl.exe中,ZwCreateFile是系统内核函数,CreateFile的实现依赖于ZwCreateFile,从分层结构上看,CreateFile在上层,ZwCreateFile在底层。

CreateFile的dwCreationDisposition参数用于指定如何打开文件,它有以下可选值:

dwCreationDisposition参数 作用
CREATE_NEW 新建文件,如果文件已存在,则函数调用失败。
CREATE_ALWAYS 新建文件,如果文件已存在,则覆盖它。
OPEN_EXISTING 打开文件,如果文件不存在,则函数调用失败。
OPEN_ALWAYS 打开文件,如果文件不存在,则新建文件再打开。
TRUNCATE_EXISTING 打开文件,并清空文件内容。如果文件不存在,则函数调用失败。

ZwCreateFile的CreateDisposition参数的作用与CreateFile的dwCreationDisposition参数是相对应的,它有以下可选值:

CreateDisposition参数 作用
FILE_SUPERSEDE 新建文件,如果文件已存在,则替换文件。
FILE_CREATE 新建文件,如果文件已存在,则函数调用失败。
FILE_OPEN 打开文件,如果文件不存在,则函数调用失败。
FILE_OPEN_IF 打开文件,如果文件不存在,则新建文件。
FILE_OVERWRITE 打开并覆盖文件内容,如果文件不存在,则函数调用失败。
FILE_OVERWRITE_IF 打开文件并覆盖文件内容,如果文件不存在,则新建文件。

※ FILE_SUPERSEDE和FILE_OVERWRITE_IF 的区别是前者是替换文件,函数调用前后对操作系统来说是两个文件,后者是替换文件内容, 函数调用前后对操作系统来说还是同一个文件。

通过设计简单的实验可以得出,CreateFile的dwCreationDisposition参数与ZwCreateFile的CreationDisposition参数对应关系如下所示:

CreateFile ZwCreateFile
OPEN_ALWAYS FILE_OPEN_IF
CREATE_NEW FILE_CREATE
CREATE_ALWAYS FILE_OVERWRITE_IF
OPEN_EXISTING FILE_OPEN
TRUNCATE_EXISTING FILE_OPEN_IF

顺便有个疑问,从上表可知,CreateFile 的dwCreationDisposition参数中没有对应ZwCreateFile的CreationDisposition参数中的FILE_SUPERSEDE和FILE_OVERWIRTE值,并且实验中也没有监测到有软件以FILE_SUPERSEDE或者FILE_OVERWRITE为参数打开文件的情况。这两种参数在哪些场景下会用到呢?除了内核程序可以直接指定参数值之外,其他类型的API参数会映射到这两个参数值吗?

--EOF--

一种安全沙箱模型

Bryan D. Payne等人在论文『An Architecture for Secure Active Monitoring Using Virtualization』中提出了一种基于主动监测潜在攻击的安全系统模型,本文对其主动监测模块进行扩展,提出一种改进后的安全沙箱模型。
Sandbox Mode

图1 安全沙箱模型

如图1所示,安全沙箱模型由三个处理单元模块(Hapi、Fapi、SEapi)和一个制定策略的规则库组成。Hook模块Hapi截获上层应用调用的API流,将其分派给过滤模块Fapi, Fapi对收到的API流进行过滤,将文件访问型API以N(Ie)的形式通知沙箱引擎模块SEapi处理,将其他类型API返回至Hapi,不做处理,直接执行。Ie表示API在各模块间传递时所携带的信息,它具体所指的含义可以是参数信息、函数返回类型等信息。SEapi根据规则库中制定的策略对API进行转换,该过程用T(Ie)表示,T(Ie)执行完毕后SEapi将API请求返回至Hapi,完成API执行。

沙箱引擎模块SEapi中处理API的过程T(Ie)是一个用五元组M=(K,\Sigma,\delta,s,F)表示的确定型有穷自动机(Deterministic Finite Automaton, DFA),其中:

(1) K是一个有穷的状态集合,这里K=\{q_0,q_1\},其中q_0表示接受状态,q_1表示拒绝接受状态。
(2) \Sigma是处理过程中输入符号的集合,该集合定义如下:

\Sigma=(Funcs \times Params)

其中Funcs是函数的集合,Params是参数的集合,\Sigma是函数和参数进行笛卡尔积运算所得的集合。
(3) \delta表示从(K \times \Sigma) \rightarrow K的状态转移函数,它根据规则库中制定的策略决定如何进行状态转移。
(4) s是初始状态: s \ni K。这里s = q_0,表示自动机的初始状态为接受状态。
(5) F是终结状态集合:F \subseteq K。这里F=\{q_0,q_1\},表示无论转移后状态是接受还是拒绝接受,最终自动机都会停止,T(Ie)过程结束。

--EOF--