作者:rebeyond
前言
Java技术栈漏洞目前业已是web安全领域的主流战场,随着IPS、RASP等防御系统的更新迭代,Java攻防交战阵地已经从磁盘升级到了内存里面。在今年7月份上海银针安全沙龙上,我分享了《Java内存攻击技术漫谈》的议题,个人觉得PPT承载的信息比较离散,技术类的内容还是更适合用文章的形式来分享,所以一直想着抽时间写一篇和议题配套的文章,不巧赶上南京的新冠疫情,这篇文章拖了一个多月才有时间写。
allowAttachSelf绕过
Java的instrument是Java内存攻击常用的一种机制,instrument通过attach方法提供了在JVM运行时动态查看、修改Java类的功能,比如通过instrument动态注入内存马。但是在Java9及以后的版本中,默认不允许SelfAttach:
Attach API cannot be used to attach to the current VM by default
The implementation of Attach API has changed in JDK 9 to disallow attaching to the current VM by default. This change should have no impact on tools that use the Attach API to attach to a running VM. It may impact libraries that misuse this API as a way to get at the java.lang.instrument API. The system property jdk.attach.allowAttachSelf may be set on the command line to mitigate any compatibility with this change.
也就是说,系统提供了一个jdk.attach.allowAttachSelf的VM参数,这个参数默认为false,且必须在Java启动时指定才生效。
编写一个demo尝试attach自身PID,提示Can not attach to current VM,如下:
经过分析attch API的执行流程,定位到如下代码:
由上图可见,attach的时候会创建一个HotSpotVirtualMachine的父类,这个类在初始化的时候会去获取VM的启动参数,并把这个参数保存至HotSpotVirtualMachine的ALLOW_ATTACH_SELF属性中,恰好这个属性是个静态属性,所以我们可以通过反射动态修改这个属性的值。构造如下POC:
Class cls=Class.forName("sun.tools.attach.HotSpotVirtualMachine");
Field field=cls.getDeclaredField("ALLOW_ATTACH_SELF");
field.setAccessible(true);
Field modifiersField=Field.class.getDeclaredField("modifiers");
modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);
field.setBoolean(null,true);
由于ALLOW_ATTACH_SELF字段有final修饰符,所以在修改ALLOW_ATTACH_SELF值的同时,也需要把它的final修饰符给去掉(修改的时候,会有告警产提示,不影响最终效果,可以忽略)。修改后,可以成功attach到自身进程,如下图:
这样,我们就成功绕过了allowAttachSelf的限制。
内存马防检测
随着攻防热度的升级,内存马注入现在已经发展成为一个常用的攻击技术。目前业界的内存马主要分为两大类:
- Agent型
利用instrument机制,在不增加新类和新方法的情况下,对现有类的执行逻辑进行修改。JVM层注入,通用性强。 - 非Agent型
通过新增一些Java web组件(如Servlet、Filter、Listener、Controller等)来实现拦截请求,从而注入木马代码,对目标容器环境有较强的依赖性,通用性较弱。
由于内存马技术的火热,内存马的检测也如火如荼,针对内存马的检测,目前业界主要有两种方法: - 基于反射的检测方法
该方法是一种轻量级的检测方法,不需要注入Java进程,主要用于检测非Agent型的内存马,由于非Agent型的内存马会在Java层新增多个类和对象,并且会修改一些已有的数组,因此通过反射的方法即可检测,但是这种方法无法检测Agent型内存马。 - 基于instrument机制的检测方法
该方法是一种通用的重量级检测方法,需要将检测逻辑通过attach API注入Java进程,理论上可以检测出所有类型的内存马。当然instrument不仅能用于内存马检测,java.lang.instrument是Java 1.5引入的一种可以通过修改字节码对Java程序进行监测的一种机制,这种机制广泛应用于各种Java性能检测框架、程序调试框架,如JProfiler、IntelliJ IDE等,当然近几年比较流行的RASP也是基于此类技术。
既然通过instrument机制能检测到Agent型内存马,那我们怎么样才能避免被检测到呢?答案比较简单,也比较粗暴,那就是把instrument机制破坏掉。这也是在冰蝎3.0中内存马防检测机制的实现原理,检测软件无法attach,自然也就无法检测。
首先,我们先分析一下instrument的工作流程,如下图:
1.检测工具作为Client,根据指定的PID,向目标JVM发起attach请求;
2.JVM收到请求后,做一些校验(比如上文提到的jdk.attach.allowAttachSelf的校验),校验通过后,会打开一个IPC通道。
3.接下来Client会封装一个名为AttachOperation的C++对象,发送给Server端;
4.Server端会把Client发过来的AttachOperation对象放入一个队列;
5.Server端另外一个线程会从队列中取出AttachOperation对象并解析,然后执行对应的操作,并把执行结果通过IPC通道返回Client。
由于该套流程的具体实现在不同的操作系统平台上略有差异,因此接下来我分平台来展开。
windows平台
通过分析定位到如下关键代码:
可以看到当var5不等于0的时候,attach会报错,而var5是从var4中读取的,var4是execute的返回值,跟入execute,如下:
可以看到,execute方法又把核心工作交给了方法enqueue,这个方法是一个native方法,如下图:
继续跟入enqueue方法:
可以看到enqueue中封装了一个DataBlock对象,里面有几个关键参数:
strcpy(data.jvmLib, "jvm");
strcpy(data.func1, "JVM_EnqueueOperation");
strcpy(data.func2, "_JVM_EnqueueOperation@20");
以上操作都发生在Client侧,接下来我们转到Server侧,定位到如下代码:
这段代码是把Client发过来的对象进行解包,然后解析里面的指令。经常写Windows shellcode的人应该会看到两个特别熟悉的API:GetModuleHandle、GetProcAddress,这是动态定位DLL中导出函数的常用API。这里的操作就是动态从jvm.dll中动态定位名称为JVM_EnqueueOperation和_JVM_EnqueueOperation@20的两个导出函数,这两个函数就是上文流程图中将AttachOperation对象放入队列的执行函数。
到这里我想大家应该知道接下来该怎么做了,那就是inlineHook。我们只要把jvm.dll中的这两个导出函数给NOP掉,不就可以成功把instrument的流程给破坏掉了么?
静态分析结束了,接下来动态调试Server侧,定位到如下位置:
图中RIP所指即为JVM_EnqueueOperation函数的入口,我们只要让RIP执行到这里直接返回即可:
怎么修改呢?当然是用JNI,核心代码如下
unsigned char buf[]="\xc2\x14\x00"; //32,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOID dst=GetProcAddress(hModule,"_JVM_EnqueueOperation@20");
DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 3, PAGE_EXECUTE_READWRITE, &old)){
WriteProcessMemory(GetCurrentProcess(), dst, buf, 3, NULL);
VirtualProtectEx(GetCurrentProcess(), dst, 3, old, &old);
}
/*unsigned char buf[]="\xc3"; //64,direct return enqueue function
HINSTANCE hModule = LoadLibrary(L"jvm.dll");
//LPVOID dst=GetProcAddress(hModule,"ConnectNamedPipe");
LPVOID dst=GetProcAddress(hModule,"JVM_EnqueueOperation");
//printf("ConnectNamedPipe:%p",dst);
DWORD old;
if (VirtualProtectEx(GetCurrentProcess(),dst, 1, PAGE_EXECUTE_READWRITE, &old)){
WriteProcessMemory(GetCurrentProcess(), dst, buf, 1, NULL);
VirtualProtectEx(GetCurrentProcess(), dst, 1, old, &old);
}*/
注意这里要考虑32位和64位的区别,同时要注意堆栈平衡,否则可能会导致进程crash。到此,我们就实现了Windows平台上的内存马防检测(Anti-Attach)功能,我们尝试用JProfiler连接试一下,可见已经无法attach到目标进程了:
以上即是Windows平台上的内存马防检测功能原理。
Linux平台
在Linux平台,instrument的实现略有不同,通过跟踪整个流程定位到如下代码:
可以看到,在Linux平台上,IPC通信采用的是UNIX Domain Socket,因此想破坏Linux平台下的instrument attach流程还是比较简单的,只要把对应的UNIX Domain Socket文件删掉就可以了。删掉后,我们尝试对目标JVM进行attach,便会提示无法attach:
到此,我们就实现了Linux平台上的内存马防检测(Anti-Attach)功能,当然其他*nix-like的操作系统平台也同样适用于此方法。
最后说一句,内存马防检测,其实可以在上述instrument流程图中的任意一个环节进行破坏,都可以实现Anti-Attach的效果。