前言
偶然接觸到了這樣一個JAVA內存馬,其作者也是冰蝎的作者,項目地址:
https://github.com/rebeyond/memShell
正好最近在接觸JAVA,借此機會學習下大佬的代碼,對自己的編程思路也有了一定的提升。當然筆者只是一個腳本小子,對代碼接觸不深,如果文中出現理解不當或是錯誤的情況,還望各位大佬不吝賜教:)
背景知識
Java Instrumentation
JAVA在SE5版本引入了Java Instrumentation,其包含在java.lang.instrument中。通過Instrumentation,我們可以構建獨立于應用程序的代理端,而通過這個代理我們可以監控JVM狀態以及修改類定義。而在SE6版本中,我們能夠實現注入代碼到運行時的JVM中。
Java Agent
前面提到的代理端也就是Java Agent,Java Agent是依附于JAVA應用程序并能對其字節碼做修改的一項技術,不能獨立運行。加載運行方式有兩種:premain模式和attach模式,前者是在程序運行前加載,后者是在程序運行后加載,本文分析的這個木馬是使用的后者。
項目作者在這篇文章中有個原理demo,感興趣的可以看看。
分析
分析按由外及里、由簡入難的順序進行,即:用戶可見功能模塊、注入模塊、代理模塊、持久化模塊。
用戶可見模塊分析
從項目README文件里可以看到這款木馬有以下功能:
l? 歡迎頁
l? 命令執行
l? 反彈Shell
l? 遠程文件下載
l? 文件操作
l? 本地文件下載
l? 文件上傳
l? 代理
l? 菜刀連接
歡迎頁
當我們獲取目標服務器權限時,將木馬上傳至目標服務器,執行java -jar inject.jar password,即可進行進程注入獲取一個隱藏進程會話。
[+]OK.i find a jvm. [+]memeShell is injected.
此時我們去瀏覽器訪問目標服務器即可調用目標模塊。
需要注意的是,這個木馬和常規木馬不同。常規木馬需要去訪問目標木馬文件來執行命令,而這款木馬訪問任意URL都可以調用木馬執行其模塊。其原理是此木馬向org.apache.catalina.core.ApplicationFilterChain類的internalDoFilter方法注入了自定義代碼,這個JAVA類在HTTP請求調用棧的上方,可以相應我們的任意Request請求,因此我們可以用GET請求也可以用POST請求。
internalDoFilter方法的原型如下:
換個角度來講,這個類就是一個過濾器,而過濾器可以在客戶端的請求訪問后端資源之前,攔截這些請求。也可在服務器的響應發送回客戶端之前,處理這些響應。也就是說用戶的每一個Request請求都會經過過濾器,無論用戶訪問的資源是否存在,如何處理這些請求也是過濾器的核心所在。
命令執行
命令執行模塊的核心就是調用Runtime.getRuntime().exec(cmd)方法來執行任意命令并獲取返回結果。
例如:
反彈Shell
反彈Shell主要依靠的是Socket通信,首先判斷操作系統類型,再進行Socket連接以達到反彈Shell的目的。
例如:
遠程文件下載
遠程文件下載支持HTTPS,下面的代碼片段為了排版美觀我去掉了SSL認證代碼。
文件操作
文件操作包含列文件、刪除文件、查看文件、刪除文件夾,都是一些很基礎的文件操作代碼,因此我們以查看文件為例。
本地文件下載
文件上傳
有普通上傳方式,有Base64編碼上傳方式。
代理
木馬里內嵌了一個reGeorg代理,因此我們可以直接使用這個木馬實現代理轉發,進一步滲透內網。
核心源碼就是根據reGeorg改的,網上也有分析文章,此處不再多贅述。
菜刀連接
菜刀模塊的源碼也是根據這個JSP菜刀源碼改的,但是個人覺得挺有意思——方法命名全是AA、BB、CC這種,不明白原作者是為了混淆還是只是單純的惡作劇。
當然,以上的這些用戶可見功能模塊都建立在一個判斷邏輯里,也就是需要正確輸入我們所指定的密碼。
if (pass_the_world!=null&&pass_the_world.equals(net.rebeyond.memshell.Agent.password))
這些用戶可見模塊除了代理模塊和菜刀模塊都是一些常用的功能代碼,簡單易懂。緊接著我們深入分析其他模塊。
注入模塊分析
注入模塊主要是用來遍歷目標機器上的JVM實例并進行代碼注入。前面提到我們有兩種方式進行注入,premain和attach,這個木馬里用的attach注入方式。
注入模塊在運行的時候,會動態加載一個代理,也就是我們的代理模塊。換句話說,代理模塊會被注入模塊注入到tomcat進程中。
代理模塊分析
在代理端被注入到JVM后,會自動運行agentmain方法。agentmain方法在獲取用戶輸入的參數后,會遍歷獲取當前所有類,如果匹配到我們要注入的目標類,則查看當前的JVM配置是否支持類的重新定義,代碼如下所示。
為避免默認端口號被更改初始化失敗,木馬會先獲取當前工程的端口號,然后對本地的tomcat發起一起請求進行初始化。進行初始化的原因原作者也解釋過,因為我們在運行木馬后會將木馬文件刪除,因此需要在刪除之前沒有將木馬寫入內存。寫入內存的方式有兩種:依次加載需要的類、進行一次模擬訪問,這款木馬選擇的后者。如下代碼所示,會訪問一次本地的tomcat服務,以達到將木馬寫入內存的目的。
在注入進程后,需要刪除自身,但是我注意到刪除的代碼還比較多,原因是操作系統的不同,刪除方式也不同,例如Windows下不能直接刪除一個占用中的文件。因此首先需要判斷操作系統類型,如果是Linux直接刪除即可。
如果是Windows,需要利用unlockFile這個方法來進行刪除,unlockFile方法里使用了一個二進制文件——forceDelete.exe。例如當我們要刪除代理端時,需要先用以下代碼獲取當前JVM進程PID。
public static String getCurrentPid() { RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); return runtimeMXBean.getName().split("@")[0]; }
然后利用命令執行的方式使用這個二進制文件強制刪除占用的文件,并且將自身刪除。
這個強制刪除功能的二進制文件我們利用IDA進行簡單分析,利用IDA打開后,手動調試恢復函數即可看到整個二進制文件代碼邏輯。我這里就沒有去恢復變量名,各位大佬湊合著看。
首先有一個判斷邏輯:
if ( !sub_411440 () || !sub_411590() ) ??????ExitProcess(0);??
跟進sub_411440()函數,根據其代碼邏輯我們發現其實就是Change_access()函數,其代碼邏輯如下:
??BOOL?sub_411440()????{????????HANDLE?v0;?//?eax????????struct?_TOKEN_PRIVILEGES?NewState;?//?[esp+D0h]?[ebp-24h]????????HANDLE?TokenHandle;?//?[esp+E8h]?[ebp-Ch]????????????NewState.PrivilegeCount?=?1;????????v0?=?GetCurrentProcess();????????if?(?!OpenProcessToken(v0,?0x28u,?&TokenHandle)?)????????????return?0;????????LookupPrivilegeValueW(0,?L"SeDebugPrivilege",?(PLUID)NewState.Privileges);?? NewState.Privileges[0].Attributes = 2; return AdjustTokenPrivileges(TokenHandle, 0, &NewState, 0x10u, 0, 0) != 0; }
作用是獲取當前進程信息并修改其權限。
再跟進sub_411590()函數,根據其代碼邏輯推斷是Get_Functions_address()函數
??BOOL?sub_411590()????{?? v0 = GetModuleHandleW(L"ntdll.dll"); dword_41713C = (int)GetProcAddress(v0, "ZwSuspendProcess"); v1 = GetModuleHandleW(L"ntdll.dll"); QueryInformationFille = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))GetProcAddress(v1, "ZwQueryInformationFile"); v2 = GetModuleHandleW(L"ntdll.dll"); QuerySystemInformation = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD))GetProcAddress(v2, "ZwQuerySystemInformation"); v3 = GetModuleHandleW(L"ntdll.dll"); QueryObject = (int (__stdcall *)(_DWORD, _DWORD, _DWORD, _DWORD, _DWORD))GetProcAddress(v3, "ZwQueryObject"); v4 = GetModuleHandleW(L"ntdll.dll"); dword_417138 = (int)GetProcAddress(v4, "ZwResumeProcess"); ??????v5?=?GetModuleHandleW(L"ntdll.dll");??14.??????QueryInformationPrecess?=?(int?(__stdcall?*)(_DWORD,?_DWORD,?_DWORD,?_DWORD,?_DWORD))GetProcAddress(v5,?"ZwQueryInformationProcess");?? return dword_41713C && QuerySystemInformation && QueryObject && dword_417138 && QueryInformationPrecess; ??}??
其作用是加載ntdll.dll模塊并從中獲取ZwSuspendProcess、ZwQueryInformationFile、ZwQuerySystemInformation、ZwQueryObject、ZwResumeProcess、ZwQueryInformationProcess的函數地址。
還有一個sub_411DD0()函數,根據其代碼邏輯推斷是Find_SubStr()函數。因此此時整個二進制文件代碼邏輯就清晰了。其核心代碼如下:
??if?(?QueryInformationFille(TargetHandle,&v13,FileInformation,528,9)?>=?0?)2.??{?? if ( Find_SubStr(FileInformation + 2, L"agent.jar") ) { v4 = GetCurrentProcess(); if ( DuplicateHandle(hSourceProcessHandle, (HANDLE)v8, v4, &TargetHandle, 0, 0, 1u) ) { CloseHandle(TargetHandle); ExitProcess(0); } } ??}??
我們梳理下整個二進制文件的運行邏輯:首先打開agent.jar進程,遍歷該進程的所有句柄信息,通過DuplicateHandle()函數復制句柄到本地進程,關閉文件句柄,此時就能刪除占用中的文件了。
本來DuplicateHandle()函數是用來創建新句柄的,但是我們可以利用這個特性來刪除被占用的文件,巧妙的實現刪除文件的功能。
持久化模塊分析
持久化模塊主要是用于tomcat服務重啟后也能繼續使用這款木馬,也就是說,只要目標機器不重啟,tomcat服務運行起來我們無需進行二次注入也能獲取權限,其核心代碼如下。
主要的核心原理在于addShutdownHook鉤子,JVM關閉的時候,會執行系統中已經設置的所有通過方法addShutdownHook添加的鉤子,當系統執行完這些鉤子后,JVM才會關閉。所以這些鉤子可以在JVM關閉的時候進行內存清理、對象銷毀等操作。當然這些只是一些“正規的操作”,我們可以設置一些“非法操作”,在JVM關閉的時候將我們已經注入內存的代碼寫入到文件,然后再調用startInject方法,startInject方法源碼如下:
再次調用startInject方法后就達到了持久化的目的。
總結
我們再梳理下整個木馬工作流程:
1. ? 獲取到目標服務器權限,將Inject.jar和Agent.jar上傳至服務器。
2. ? 執行java -jar Inject.jar password,開始注入Tomcat進程。
3. ? 注入模塊尋找目標類。
4. ? 將代碼注入到Tomcat進程。
5. ? 成功注入后刪除自身。
6. ? 遇到Tomcat進程重啟,將內存代碼寫入臨時文件。
7. ? 再次注入Tomcat進程達到持久化目的。
注入的核心關鍵在于Servlet過濾器的internalDoFilter方法,因為所有的用戶請求都會通過這個方法。
未來工作
JSP服務器的內存注入,使得JAVA內存馬的通用性得到提高。原作者也提到使得這個內存馬通用性提高的關鍵就在于要尋找到“關鍵類”,Tomcat里使用的是Servlet的過濾器關鍵類,在其他JAVA容器我們也需要找到這樣的一個關鍵類,這也是未來工作的重點。

安洵信息技術有限公司
www.i-soon.net

以實力陪伴客戶成長??使客戶更加強大
400-066-5915
上海丨四川丨江蘇 | 云南