JDK1.5开始引入了Agent机制(即启动java程序时添加“-javaagent”参数,Java Agent机制允许用户在JVM加载class文件的时候先加载自己编写的Agent文件,通过修改JVM传入的字节码来实现注入自定义的代码。采用这种方式时,必须在容器启动时添加jvm参数,所以需要重启Web容器。
JDK1.6新增了attach方式,可以对运行中的java进程附加agent,提供了动态修改运行中已经被加载的类的途径。一般通过VirtualMachine的attach(pid)方法获得VirtualMachine实例,随后可调用loadagent方法将JavaAgent的jar包加载到目标JVM中。
下面一个章节笔者将通过两个demo案例说明JavaAgent技术的两种方式,让读者明白premain和agentmain的具体原理。
1Premain
创建一个sayHello类,写一个say()方法。
public class sayHello {
public String say() {
return "hello,world!";
}
}
创建一个People类,运行say()方法,输出结果为:hello,world!
public class People {
public static void main(String[] args) {
System.out.println(new sayHello().say());
}
}
创建Transformer重写transformer方法,实现修改传入JVM的字节码。笔者这里通过javassist对类字节码进行处理。
package org.example;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Transformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(className);
if (className.endsWith("sayHello")){
try {
final ClassPool classPool = ClassPool.getDefault(); // 创建ClassPool对象
final CtClass ctClass = classPool.get("org.example.sayHello");
CtMethod ctMethod = ctClass.getDeclaredMethod("say"); // 获取成员方法
String methodBody = "return \"hello premain\";";
ctMethod.setBody(methodBody); //替换方法体中所有内容
byte[] bytes = ctClass.toBytecode(); //使用类CtClass,生成类二进制
//调用CtClass对象的detach()方法 CtClass对象从ClassPool移除掉减少内存消耗
ctClass.detach();
return bytes;
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
定义Premain类的premain方法
package org.example;
import java.lang.instrument.Instrumentation;
public class Premain {
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premain agent run!");
inst.addTransformer(new Transformer());
}
}
使用Maven打包成TestPremain-1.0-SNAPSHOT.jar文件,需要如下修改pom.xml文件。把<Premain-class>设置为premain方法所在类。
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestEntries>
<Premain-class>org.example.Premain</Premain-class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
在运行配置中添加vm选项
图1
运行结果如图2所示,修改了say方法。
图2
2Agentmain
同premain也创建一个People类循环打印字符串,代码如下所示。
package org.example;
public class People {
public void sayHello(String name) {
System.out.println(String.format("%s say hello!", name));
}
public static void main(String[] args) throws InterruptedException {
People p = new People();
for (;;){
Thread.sleep(1000);
p.sayHello(Thread.currentThread().getName());
}
}
}
重写transform方法,注入进程后打印输出代理的类,代码如下所示。
package org.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Transform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println(String.format("agent run target class= %s", className));
return classfileBuffer;
}
}
新建Agent类实现agentmain方法,代码如下所示
public class Agent {
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
inst.addTransformer(new Transform(),true);
inst.retransformClasses(Class.forName("org.example.People"));
}
}
将Agent设置为<Agent-Class>并打包成为jar文件。Pom.xml文件如下所示,值得注意的是如果需要修改已经被JVM加载过的类的字节码,那么还需要在MANIFEST.MF中添加Can-Retransform-Classes:true或Can-Redefine-Classes:true。
<Agent-Class>org.example.Agent</Agent-Class>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
创建Attach类注入目标类的进程,代码如下所示。
public class Attach {
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar";
List<VirtualMachineDescriptor> list = VirtualMachine.list(); //获取本机所有运行的Java进程
for (VirtualMachineDescriptor desc :list){
if (desc.displayName().endsWith("People")){
VirtualMachine vm = VirtualMachine.attach(desc.id());
vm.loadAgent(agentPath);
vm.detach();
}
}
}
}
Attach捕获到类进程号如图3所示。
图3
先运行people类在运行attach,运行结果如图4所示。
图4
由上文可知Agentmain可实现最重要的三个类Agent Attach Transform,来分析冰蝎作者之前写的memshell实现原理,项目地址:https://github.com/rebeyond/memShell.gitMemshell中Transform类代码如图5所示。
图5
不同于前面章节的demo,这里除了使用ClassPool.getDefault()还使用ClassClassPath搜索class路径其原理是:ClassPool.getDefault()获取的ClassPool使用JVM的classpath。在Tomcat等Web服务器运行时,服务器会使用多个类加载器作为系统类加载器,这可能导致ClassPool可能无法找到用户的类。这时,ClassPool须添加额外的classpath才能搜索到用户的类。
CtClass cc = cp.get("org.apache.catalina.core.ApplicationFilterChain");
CtMethod m = cc.getDeclaredMethod("internalDoFilter");
m.addLocalVariable("elapsedTime", CtClass.longType);
m.insertBefore(readSource());
如上代码:作者Hook了ApplicationFilterChain中的internalDoFilter方法,然后定义一个long类型的属性,elapsedTime,并通过insertBefore方法将source.txt中内容插入到方法内容的开始处。source.txt是url参数和agent交互的逻辑,如图6所示。
图6
笔者之前用此内存马时发现两个特点:第一是该内存马会自己删除jar包,实现代码如下。
图7
第二点是重启tomcat服务之后内存马还是存在,只有通过jps-l kill掉进程后启动服务才能删除内存马,其原理是使用了ShutdownHook机制。
图8
通过使用Runtime.addShutdownHook(Thread hook)方法注册JVM关闭的勾子,调用writeFiles方法把jar包落地磁盘,再通过Runtime.exec启动java-jar inject.jar。
由于Hook的关键函数ApplicationFilterChain.internalDoFilter是tomcat的方法,导致其他中间件不适用,在冰蝎3.0中的内存马作者更改了Hook点。(源码版本为V3.0 Beta11_t00ls)在agentmain中做了一个判断,如果是Tomcat选择hook javax.servlet.http.HttpServlet中的service方法,如果是weblogic选择hookweblogic.servlet.internal.ServletStubImpl中的execute方法。
代码如图9所示。
图9
在jdk9及以后的版本不允许SelfAttach(即无法attach自身的进程)。修改前面章节Attach demo,将jdk换成9之后的,attach自身的PID会报错提示Can not attach to current VM。代码如下,报错截图如图10所示。
public class Attach {
public static void main(String[] args) throws Exception {
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for(VirtualMachineDescriptor desc : list){
System.out.println("进程ID:" + desc.id() + ",进程名称:" + desc.displayName());
}
Scanner myObj = new Scanner(System.in);
System.out.println("输入要注入的进程:");
String pid = myObj.nextLine();
String agentPath = "F:\\IdeaProjects\\TestAgent\\target\\TestAgent-1.0-SNAPSHOT.jar";
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath);
vm.detach();
}
}
图10
看到Rebeyond师傅在《Java内存攻击技术漫谈》中提出一种方法,绕过allowAttachSelf。首先Debug attch执行流程,如图11所示。可以发现attach的时候会创建一个HotSpotVirtualMachine的父类对象,取键值对jdk.attach.allowAttachSelf的值计算后保存到ALLOW_ATTACH_SELF中,可通过反射修改该属性值。
图11
ALLOW_ATTACH_SELF字段有final修饰符,需要设置setAccessible(true);具体代码如下所示。
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.setAccessible(true);
modifiersField.setInt(field,field.getModifiers()&~Modifier.FINAL);
field.setBoolean(null,true);
修改后会弹出警告信息如图12所示,成功注入结果如图13所示。
图12
图13
回到冰蝎3.0源码中,通过setProperty将jdk.attach.allowAttachSelf设置为true,实现绕过SelfAttach。
System.setProperty("jdk.attach.allowAttachSelf", "true");
本文从permain和agentmain两种实现JavaAgent的原理方法引入到java agent在内存马中的应用,通过分析memshell到冰蝎3.0内存马源码,加深了对agent型内存马Hook的关键函数、持久化方法以及绕过SelfAttach方法等内存马技术点的理解与学习,希望对读者有帮助。
本文作者:安全狗
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/174266.html