为了描述方面,本文用PC代表下一条指定的地址,即PC寄存器中的内容,以R(x)表示函数x的返回地址。那么对第一个open,可以用R(main)→R(f1)→PC表示该系统调用发生时的位置,对第二个open,则用R(main)→R(f2)→PC来表示其调用位置。我们将这种表示称为“返回地址链”,尽管在返回地址链中的最后一项并不是函数的返回地址,而是下一条指令。利用返回地址链,可以区分同一个系统调用在不同位置的调用情况。
上述做法还存在一个缺点:得到的每一个地址(包括返回地址和下一指令的内容)都是线性地址(即逻辑地址),它是由段地址和段内偏移量组成。直接使用逻辑地址可能会导致这样一种情况:系统在启动程序时,每一个段的初始地址可能不同,这样就导致了同一个系统调用在不同的运行过程中得到不同的逻辑地址,显然这不能直接用来识别系统调用。根据操作系统的设计可知,每一条指令所在段的起始地址可以不同,但是该指令相对于段起始地址的偏移量是固定的,因此可以使用“段名+段内偏移量” 的方式来代替逻辑地址。为了方便起见,事先给每一个段赋予一个唯一的ID,这样每一个逻辑地址就表示为SegmentID@Offset。
为了能够将一个逻辑地址映射成上述表示,需要事先记录所有段的起始地址和终止地址(或者该段的大小),并保存在段地址映射表中。当遇到一个逻辑地址时,首先判断该逻辑地址在哪一个段中,然后计算出偏移量,这样就可以用该段的ID和偏移量来表示该地址。在段地址映射表中,每一项可简单地表示为:
<StartAddress,EndAddress,SegmentName,SegmentID>
2.L-Call的构造
当发生一次系统调用时,通过访问系统堆栈就可以得到返回地址链,然后转变成与起始地址无关的形式,并用此来表示该系统调用的位置。图3则给出了系统调用位置信息的获取算法。
GetLocation( ):获取系统调用时的位置信息
输入:无
输出:调用时的位置Location
1 pEBP <-- GetCurrentEBP(); // 获取当前帧指针
2 ReturnAddr[] <-- 0; // 清空存储空间
3 i <-- 0;
4 Location <-- NULL;
/* 遍历堆栈 */
5 do
6 ReturnAddr[i++] <-- *(pEBP+4); // 获取返回地址
7 pEBP <-- *(pEBP); // 获取下一帧的帧指针
8 while pEBP ≠ STACK_BOTTOM ; // STACK_BOTTOM表示堆栈底部
9 reverse ReturnAddr[]; // 将ReturnAddr倒序
10 ReturnAddr[i++] <-- GetCurrentEIP(); // 获取EIP内容,即下一指令
11 size <-- i;
/* 将线性地址转变为 “段ID + 偏移量” 方式 */
12 for i = 0 to size-1
13 查找段地址映射表,将每一个地址转变为段ID和偏移量;
14 Location += SegmentID@offset;
15 endfor
16 return Location;
图3 获取系统调用位置信息算法
整个算法分为两个部分,第一部分是遍历堆栈获取返回地址链表,第二部分是将每一个返回地址转变为SegmentID@Offset这种格式。需要注意的是,在第一部分的遍历过程中,是从当前帧遍历到最底层帧,即帧顺序为N,N-1,…1,所以随后有一个倒序操作,最后加入下一指令的内容,即寄存器的内容。第二部分就只需要简单地查找段地址映射表,获取SegmentID和Offset。段地址映射表是在程序刚执行时就建立,每一项的格式如前面所示。这样,得到的位置表示为
L:S1@O1-S2@O2…SN@ON (Si表示段i,Oi表示在该段的偏移量) 最后,将每一个位置的系统调用映射为一个新的事件,用一个唯一ID表示,也就是说不同位置的同一调用映射为不同的事件。例如:
open@L1→e1
open@L2→e2
open@L3→e3
为了描述方便,本文称这个新的事件为L-Call(System Call with Location),比如上面的、和。随后部分将通过分析L-Call来检测程序是否受到攻击。
(责任编辑:adminadmin2008)