
3.4 修改指令指针
攻击者要想在x86-32架构上成功地执行任意代码,必须利用某种方式修改执令指针,使其指向外壳代码。指令指针寄存器(eip)存储了将要执行的下一条指令在当前代码段内的偏移量。
eip寄存器不能被软件直接访问。它在顺序执行代码时由一个指令边界步进到下一条指令,也可以由控制转移指令(例如jmp、jcc、call和ret等)、中断以及异常间接修改[Intel 2004]。
以call指令为例,它首先将返回信息存储于栈中,然后将控制权转移到由目标操作数指定的被调用函数处。目标操作数指定了被调用函数中的第一条指令的地址。该操作数可以是一个立即数(immediate value)、一个通用寄存器或一个内存位置。
例3.4展示了一个程序,其中使用函数指针funcPtr调用一个函数。第6行声明了一个函数指针,该指针所指函数为静态函数,后者接受一个常量字符串参数。在第7行中,该函数指针被赋值为good_function()的地址,因此当第8行调用funcPtr时,实际上调用的是good_function()。作为对比,第9行用静态方式调用了一次good_function()。
例3.4 使用函数指针的示例程序
01 void good_function(const char *str) { 02 printf("%s", str); 03 } 04 05 int main(void) { 06 static void (*funcPtr)(const char *str); 07 funcPtr = &good_function; 08 (void)(*funcPtr)("hi "); 09 good_function("there!\n"); 10 return 0; 11 }
例3.5展示了例3.4中两次调用good_function()的反汇编代码。第一个调用(使用函数指针的形式)发生于0x0042417F处,该地址的机器码为ff 15 00 84 47 00。在x86-32架构中call指令的形式有多种,本例中,ff操作码(参见图3.1)与一个ModR/M(15)结合使用,以指示一个绝对地址的间接调用。
图3.1 x86-32的call(调用)指令
最后4个字节包含了被调用函数的地址(这里存在一级间接性)。可以在例3.5中的dword ptr[funcPtr(478400h)]调用中看到这个地址。这个地址中所保存的good_function()的实际地址为0x00422479。
例3.5 函数指针反汇编
(void)(*funcPtr)("hi "); 00424178 mov esi, esp 0042417A push offset string "hi" (46802Ch) 0042417F call dword ptr [funcPtr (478400h)] 00424185 add esp, 4 00424188 cmp esi, esp good_function("there!\n"); 0042418F push offset string "there!\n" (468020h) 00424194 call good_function (422479h) 00424199 add esp, 4
对good_function()的第二个调用是个静态调用,它发生于0x00424194。该位置的机器码是e8 e0 e2 ff ff。在本例中,调用指令用e8操作码表示。这种形式的调用指令表示一个近调用相对下一条指令的偏移量。偏移量是一个负数,表示good_funciton()出现在更低的地址处。
这些对good_function()的调用提供了一些可被攻击和不可被攻击的调用指令例子。静态调用使用一个立即数作为相对偏移量,由于该偏移量处于code段中,因此无法被覆写。而通过函数指针的调用使用一个间接引用,被引用的地址(通常在data或stack段中)则可以被覆写。这些间接的函数引用与无法在编译期间决定的函数调用可以被利用,从而使程序的控制权转移到任意代码。对于目标是将程序控制权转移到攻击者提供的代码中的任意内存写技术,参见下文描述。