前言
偶然接触到了这样一个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
上海丨四川丨江苏 | 云南