preface
The starting point is automatic analysis and killing of Java Agent memory horses. In fact, other memory horses can be searched in this way
The main difficulties in this paper are the following three, and I will answer them one by one in this paper
- How to
dump
Out of theJVM
In theThe realCurrent bytecode- How to solve the problem
LAMBDA
Expression resultIllegal bytecodeProblems that cannot be analyzed- How do you analyze bytecode to determine if a class is a memory horse
background
The attack and defense of Java memory horse has not stopped, which is the focus of the Java security field
Review Tomcat or Spring memory horses: New components such as Filter and Controller need to be registered
Memory marchings are easier for those that need to register new components:
For example, master C0NY1’s Java-Memshell-scanner project uses the Tomcat API to remove added components. The advantage is that a simple JSP file can view all the component information, combined with manual review (Class name and ClassLoader information, etc.) to kill the memory horse, also can dump the risky classes after decompilation analysis
Or LandGrey, based on the CopAgent project written by Alibaba Arthas, analyzes all classes in the JVM, dumps suspicious components based on hazardous annotations and Class names, and analyzes them with manual decompilation
But in real life, it may not be the memory horse that registers the new component above
For example, the common ice Scorpion memory horse is Java Agent memory horse. This is ice, scorpion memory a piece of code, a simple analysis of ice were the memory can be found after the horse is the use of Java Agent injected into the javax.mail. Servlet. HTTP. HttpServlet service method, this is the specification, JavaEE This is the first and always the place where Tomcat processes requests. Adding memory horse logic to this class can ensure stable triggering
Similar logic, you can use the Java Agent injected memory horse org. Apache. Catalina. Core. ApplicationFilterChain class, the class is located in the Filter chain head, That is to say, all requests that go through Tomcat will be processed by doFilter method of this class, so adding memory horse logic to this method is also a way of stable triggering (it is said that this is the way of the old version of Ice Scorpion memory horse).
Can also similar classes for injection, such as org. Springframework. Web. Servlet. The DispatcherServlet class, on the bottom of the Spring framework for injection. Or some clever ideas, such as injection Tomcat’s own org. One of the Filter. The apache Tomcat. Websocket. Server WsFilter class, this is also a Java Agent memory mark to do it
The above briefly introduces the utilization of various memory horses and common memory horse search, the reason why the last introduction of Java Agent memory horse search, because it is difficult. The master of wide-byte security proposed the idea of checking and killing: based on javaAgent memory horse detection and killing guide
The article refers to the difficulties of Java Agent memory horse detection:
The bytecode in the argument to the retransformClass method is not the bytecode of the class that was modified after the redefineClass call. In the case of Ice Scorpion, there is no way to get the bytecode modified by ice Scorpion. When we write the Java Agent to clear the memory horse, we cannot obtain the bytecode modified by redefineClass, but can only obtain the bytecode modified by retransformClass. The bytecodes of the class obtained through ASM tools such as Javaassist are only read from the bytecodes of the response class on disk, not the bytecodes in the JVM
Wide-byte security gurus have found a way to detect it: sa-jdi.jar
This is a GUI tool that allows you to view all loaded classes in the JVM. The difference here is that you get the actual current bytecode, not the original, local bytecode, so you can see the bytecode of the class that was modified after the Java Agent called redefineClass. Further, you can dump the classes considered risky and decompile them for manual audit
introduce
So that’s the background, what did I do, what did I achieve
It is not difficult to see that the above means of memory check and kill are semi-automatic combined with manual audit, when the detection of memory horse
Can we find a way to do a one-stop service:
- Detection (both support ordinary memory horse and
Java Agent
Memory horse detection)- Analysis (how to determine that the class is a memory horse, based only on information such as malicious class names and annotations is incomplete)
- Kill (how to automatically remove memory horses and restore normal business logic when it is confirmed that memory horses exist)
In general, it doesn’t seem difficult to implement, but in practice there are a lot of pitfalls, which I’ll cover one by one
1, Network security learning route 2, electronic books (white hat) 3, security factory internal video 4, 100 SRC documents 5, common security comprehensive questions 6, CTF competition classic topic analysis 7, the complete kit 8, emergency response notes
SA – JDI analysis
I tried to obtain the current bytecode through Java Agent technology, but found that I could not get the modified bytecode as the teacher said
Therefore, in order to detect Agent horses, we need to start from sa-jdi.jar itself and try to dump the current bytecode (so that not only Agent horses with modified bytecode can be analyzed, but also common types of memory horses).
Noticed that one of the Class: sun JVM. Hotspot. View jcore. ClassDump and found that the function is through checking data dump the current Class (according to the name of the Class also can guess) one of the main method to provide the dump Class command line tools
So I thought of some methods, with the code to achieve the function of the command line tool, and can set a Filter
ClassDump classDump = new ClassDump (); // my filter classDump . setClassFilter ( filter ); classDump . setOutputDirectory ( "out" ); // protected start method Class <? > toolClass = Class . forName ( "sun.jvm.hotspot.tools.Tool" ); Method method = toolClass . getDeclaredMethod ( "start" , String []. class ); method . setAccessible ( true ); // jvm pid String [] params = new String []{ String . valueOf ( pid )}; try { method . invoke ( classDump , ( Object ) params ); } catch ( Exception ignored ) { logger . error ( "unknown error" ); return ; } logger . info ( "dump class finish" ); // detach Field field = toolClass . getDeclaredField ( "agent" ); field . setAccessible ( true ); HotSpotAgent agent = ( HotSpotAgent ) field . get ( classDump ); agent . detach ();Copy the code
The Filter mentioned above is used to determine which classes need to be dumped (too much dumping can cause performance problems).
public class NameFilter implements ClassFilter { @Override public boolean canInclude ( InstanceKlass instanceKlass ) { String klassName = instanceKlass . getName (). asString (); Dump if (blacklist. contains (klassName)) {return true; } // Dump for (String k: constant.keyword) {if (klassname.contains (k)) {return true; } } return false ; }}Copy the code
The above contains the blacklist and keywords of the class:
- Blacklist: Java Agent memory horse will usually Hook where needed
dump
Come down and analyze it - Keyword: class name if present
memshell
andshell
Such keywords that may be ordinary memory horse, need to be analyzed
public class Constant {
// BLACKLIST (Analysis Target)
// CLASS_NAME#METHOD_NAME
public static List < String > blackList = new ArrayList <>();
// SHELL KEYWORD
public static List < String > keyword = new ArrayList <>();
static {
blackList . add ( "javax/servlet/http/HttpServlet#service" );
blackList . add ( "org/apache/catalina/core/ApplicationFilterChain#doFilter" );
blackList . add ( "org/springframework/web/servlet/DispatcherServlet#doService" );
blackList . add ( "org/apache/tomcat/websocket/server/WsFilter#doFilter" );
keyword . add ( "shell" );
keyword . add ( "memshell" );
keyword . add ( "agentshell" );
keyword . add ( "exploit" );
keyword . add ( "payload" );
keyword . add ( "rebeyond" );
keyword . add ( "metasploit" );
}
}
Copy the code
In addition, if you want to add JDK/lib dependencies to your Maven project, special configuration is required
<dependency>
<groupId> sun.jvm.hotspot </groupId>
<artifactId> sa-jdi </artifactId>
<version> jdk-8 </version>
<scope> system </scope>
<systemPath> ${env.JAVA_HOME}/lib/sa-jdi.jar </systemPath>
</dependency>
Copy the code
System Scope dependencies are not included by default when packaged as tool JARS, so special handling is required
<artifactId> maven-assembly-plugin </artifactId>
<configuration>
<appendAssemblyId> false </appendAssemblyId>
<descriptors>
<descriptor> assembly.xml </descriptor>
</descriptors>
<archive>
<manifest>
<mainClass> org.sec.Main </mainClass>
</manifest>
</archive>
</configuration>
Copy the code
Write the assembly.xml file
<! < <outputDirectory> / </outputDirectory> <unpack> true </unpack> <scope> system </scope> </dependencySet> </dependencySets>Copy the code
You can then use code to determine which classes to dump based on the blacklist and keywords
I encountered a minor problem in my testing that is worth sharing: The HttpServlet was properly dump-able but the ApplicationFilterChain class was not found. This is due to lazy loading of SpringBoot, requiring a manual request for an interface
Resolve illegal bytecode
Then I ran into a big pit where bytecodes dumped through the SA-jDI library were illegal
The following error is reported when analyzing the ApplicationFilterChain class
At first I suspected that I was using the latest ASM framework: 9.2
When ClassReader does not report an error, it does not report an error when ClassReader is demoted to 7.0
After comparison, it is found that the following situation
No error version is reported
After a bit of analysis, it turns out that the ApplicationFilterChain class contains LAMBDA
This class is not the only one that could contain a LAMBDA
It was found that the bytecode obtained through SA-JDI was illegal bytecode in the presence of LAMBDA and could not be analyzed
If you still want to analyze at this point, there are only two options:
- Parse CLASS files by yourself (put the cart before the horse)
- Rewrite ASM source code to skip
LAMBDA
Based on Java basics, LAMBDA is related to the INVOKEDYNAMIC instruction, so I changed the ASM code
(Here does not explain why it is changed, it is determined after many debugging)
org/objectweb/asm/ClassReader#274
bootstrapMethodOffsets = null ;
Copy the code
org/objectweb/asm/ClassReader#2456
case Opcodes . INVOKEDYNAMIC :
{
return ;
}
Copy the code
After changing the source code, you can normally analyze the illegal bytecode. At present, there is no major problem and it can be analyzed normally, but it is not sure whether there are some hidden dangers and bugs in such modification. Anyway, we can continue for now
Analysis bytecode
Analysis of bytecode doesn’t need to go too far, as most of the possible memory drives are implemented by Runtime.exec or classLoader.defineclass, and analysis for both cases is sufficient for the vast majority of cases
The following code reads the bytecode of dump and analyzes all methods for both cases
List < Result > results = new ArrayList <>();
int api = Opcodes . ASM9 ;
int parsingOptions = ClassReader . SKIP_DEBUG | ClassReader . SKIP_FRAMES ;
for ( String fileName : files ) {
byte [] bytes = Files . readAllBytes ( Paths . get ( fileName ));
if ( bytes . length == 0 ) {
continue ;
}
ClassReader cr ;
ClassVisitor cv ;
try {
// runtime exec analysis
cr = new ClassReader ( bytes );
cv = new ShellClassVisitor ( api , results );
cr . accept ( cv , parsingOptions );
// classloader defineClass analysis
cr = new ClassReader ( bytes );
cv = new DefineClassVisitor ( api , results );
cr . accept ( cv , parsingOptions );
} catch ( Exception ignored ) {
}
}
for ( Result r : results ) {
logger . info ( r . getKey () + " -> " + r . getTypeWord ());
}
Copy the code
For runtime. exec type analysis, it is easiest to determine if the method exists in all the methods in the dumped bytecode (theoretically there will be false positives, but this method cannot exist in the blacklist class, the keyword class itself is suspect, so there is nothing wrong with doing this).
@Override public void visitMethodInsn ( int opcode , String owner , String name , String descriptor , boolean isInterface ) { boolean runtimeCondition = owner . equals ( "java/lang/Runtime" ) && name . equals ( "exec" ) && descriptor . equals ( "(Ljava/lang/String;) Ljava/lang/Process;" ); if ( runtimeCondition ) { Result result = new Result (); result . setKey ( this . owner ); result . setType ( Result . RUNTIME_EXEC_TIME ); results . add ( result ); } super . visitMethodInsn ( opcode , owner , name , descriptor , isInterface ); }Copy the code
But this case does not apply to the Ice Scorpion reflex call classLoader.defineclass
The code is not long, but the corresponding bytecode is complex
Method m = ClassLoader . class . getDeclaredMethod ( "defineClass" ,
String . class , ByteBuffer . class , ProtectionDomain . class );
m . invoke ( null );
Copy the code
Corresponding bytecode
LDC Ljava/lang/ClassLoader; .class // focus on LDC "defineClass" // focus on ICONST_3 ANEWARRAY Java /lang/ class DUP ICONST_0 LDC Ljava/lang/String; .class AASTORE DUP ICONST_1 LDC Ljava/nio/ByteBuffer; .class AASTORE DUP ICONST_2 LDC Ljava/security/ProtectionDomain; .class AASTORE INVOKEVIRTUAL java/lang/Class.getDeclaredMethod (Ljava/lang/String; [Ljava/lang/Class;)Ljava/lang/reflect/Method; // Focus on ASTORE 1 L1 LINENUMBER 11 L1 ALOAD 1 ACONST_NULL ICONST_0 ANEWARRAY Java /lang/Object INVOKEVIRTUAL Java/lang/reflect/Method. Invoke (Ljava/lang/Object; [Ljava/lang/Object) Ljava/lang/Object; / / focus on POPCopy the code
This operation requires multiple steps and is not as simple as a single INVOKE. Without special treatment, reflection and classloader-related operations are relatively common, so there is a certain possibility of false positives
So continue to take out stack frame analysis method, specific no longer introduced, before the article has detailed explanation
DefineClass and Ljava/lang/ClassLoader according to bytecode; This should be considered malicious until an LDC instruction is pushed, and a blot should be placed at the top of the stack after an emulated JVM instruction is executed
@Override
public void visitLdcInsn ( Object value ) {
if ( value instanceof String ) {
if ( value . equals ( "defineClass" )) {
super . visitLdcInsn ( value );
this . operandStack . set ( 0 , "LDC_STRING" );
return ;
}
} else {
if ( value . equals ( Type . getType ( "Ljava/lang/ClassLoader;" ))) {
super . visitLdcInsn ( value );
this . operandStack . set ( 0 , "LDC_CL" );
return ;
}
}
super . visitLdcInsn ( value );
}
Copy the code
The following analysis focuses on two Invokes
- if
getDeclaredMethod
The incoming is aboveLDC
The method return value is also a stain and is set to the return value at the top of the stackREFLECTION_METHOD
mark - if
Method.invoke
In the methodMethod
Marked theREFLECTION_METHOD
It can be determined that this is a memory horse - The first part of the code is mainly based on the actual situation of the method parameters in the operand stack index position to determine, is a dynamic and automatic way to confirm, rather than directly based on experience or debugging write dead index, is an elegant way to write
public void visitMethodInsn ( int opcode , String owner , String name , String descriptor , boolean isInterface ) { Type [] argTypes = Type . getArgumentTypes ( descriptor ); if ( opcode ! = Opcodes . INVOKESTATIC ) { Type [] extendedArgTypes = new Type [ argTypes . length + 1 ]; System . arraycopy ( argTypes , 0 , extendedArgTypes , 1 , argTypes . length ); extendedArgTypes [ 0 ] = Type . getObjectType ( owner ); argTypes = extendedArgTypes ; } boolean reflectionMethod = owner . equals ( "java/lang/Class" ) && opcode == Opcodes . INVOKEVIRTUAL && name . equals ( "getDeclaredMethod" ); boolean methodInvoke = owner . equals ( "java/lang/reflect/Method" ) && opcode == Opcodes . INVOKEVIRTUAL && name . equals ( "invoke" ); if ( reflectionMethod ) { int targetIndex = 0 ; for ( int i = 0 ; i < argTypes . length ; i ++) { if ( argTypes [ i ]. getClassName (). equals ( "java.lang.String" )) { targetIndex = i ; break ; } } if ( operandStack . get ( argTypes . length - targetIndex - 1 ). contains ( "LDC_STRING" )) { super . visitMethodInsn ( opcode , owner , name , descriptor , isInterface ); operandStack . set ( TOP , "REFLECTION_METHOD" ); return ; } } if ( methodInvoke ) { int targetIndex = 0 ; for ( int i = 0 ; i < argTypes . length ; i ++) { if ( argTypes [ i ]. getClassName (). equals ( "java.lang.reflect.Method" )) { targetIndex = i ; break ; } } if ( operandStack . get ( argTypes . length - targetIndex - 1 ). contains ( "REFLECTION_METHOD" )) { super . visitMethodInsn ( opcode , owner , name , descriptor , isInterface ); Result result = new Result (); result . setKey ( owner ); result . setType ( Result . CLASSLOADER_DEFINE ); results . add ( result ); return ; } } super . visitMethodInsn ( opcode , owner , name , descriptor , isInterface ); }Copy the code
The detection results are as follows:
Write a memory horse Agent to inject into the HttpServlet (this is not the focus of the article)
Then ran up the tool I wrote
- Where the red box inside is injected
Agent
Memory horse, can be analyzed - There are also two memory horse results, this is my simulation of ordinary memory horse, directly written into the code to do the test
Auto repair
Next is the memory horse repair, write a Java Agent can be
Only the ApplicationFilterChain and HttpServlet cases (which are the most common) are handled for now
public class RepairAgent { public static void agentmain ( String agentArgs , Instrumentation ins ) { ClassFileTransformer transformer = new RepairTransformer (); ins . addTransformer ( transformer , true ); Class <? >[] classes = ins . getAllLoadedClasses (); for ( Class <? > clas : classes ) { if ( clas . getName (). equals ( "org.apache.catalina.core.ApplicationFilterChain" ) || clas . getName (). equals ( "javax.servlet.http.HttpServlet" )) { try { ins . retransformClasses ( clas ); } catch ( Exception e ) { e . printStackTrace (); } } } } }Copy the code
The logic of processing is not complicated
- Due to the
ApplicationFilterChain
Included in theLAMBDA
So I just simplified the code to a simple sentenceinternalDoFilter($1,$2)
Make fixes (choose carefully, I’ll explain why in the summary) - To modify the parameters of the method
$1, $2
I can’t write it like thisreq
andresp
- here
HttpServlet
The situation is slightly more complicated, and there are two of themservice
Method, in fact any modification can cause memory horse effect, so what I want to do is restore both methods, not just one - Pay attention to any non
java.lang
All classes below need full class names
public class RepairTransformer implements ClassFileTransformer { @Override public byte [] transform ( ClassLoader loader , String className , Class <? > classBeingRedefined , ProtectionDomain protectionDomain , byte [] classfileBuffer ) { className = className . replace ( "/" , "." ); ClassPool pool = ClassPool . getDefault (); if ( className . equals ( "org.apache.catalina.core.ApplicationFilterChain" )) { try { CtClass c = pool . getCtClass ( className ); CtMethod m = c . getDeclaredMethod ( "doFilter" ); m . setBody ( "{internalDoFilter($1,$2); } "); byte [] bytes = c . toBytecode (); c . detach (); return bytes ; } catch ( Exception e ) { e . printStackTrace (); } } if ( className . equals ( "javax.servlet.http.HttpServlet" )) { try { CtClass c = pool . getCtClass ( className ); CtClass [] params = new CtClass []{ pool . getCtClass ( "javax.servlet.ServletRequest" ), pool . getCtClass ( "javax.servlet.ServletResponse" ), }; CtMethod m = c . getDeclaredMethod ( "service" , params ); m . setBody ( "{" + " javax.servlet.http.HttpServletRequest request; \n" + " javax.servlet.http.HttpServletResponse response; \n" + "\n" + " try {\n" + " request = (javax.servlet.http.HttpServletRequest) $1; \n" + " response = (javax.servlet.http.HttpServletResponse) $2; \n" + " } catch (ClassCastException e) {\n" + " throw new javax.servlet.ServletException(lStrings.getString("http.non_http")); \n" + " }\n" + " service(request, response);" + "} "); CtClass [] paramsProtected = new CtClass []{ pool . getCtClass ( "javax.servlet.http.HttpServletRequest" ), pool . getCtClass ( "javax.servlet.http.HttpServletResponse" ), }; CtMethod mProtected = c . getDeclaredMethod ( "service" , paramsProtected ); mProtected . setBody ( "{" + "String method = $1.getMethod(); \n" + "\n" + " if (method.equals(METHOD_GET)) {\n" + " long lastModified = getLastModified($1); \n" + " if (lastModified == -1) {\n" + " doGet($1, $2); \n" + " } else {\n" + " long ifModifiedSince; \n" + " try {\n" + " ifModifiedSince = $1.getDateHeader(HEADER_IFMODSINCE); \n" + " } catch (IllegalArgumentException iae) {\n" + " ifModifiedSince = -1; \n" + " }\n" + " if (ifModifiedSince < (lastModified / 1000 * 1000)) {\n" + " maybeSetLastModified($2, lastModified); \n" + " doGet($1, $2); \n" + " } else {\n" + " $2.setStatus(javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED); \n" + " }\n" + " }\n" + "\n" + " } else if (method.equals(METHOD_HEAD)) {\n" + " long lastModified = getLastModified($1); \n" + " maybeSetLastModified($2, lastModified); \n" + " doHead($1, $2); \n" + "\n" + " } else if (method.equals(METHOD_POST)) {\n" + " doPost($1, $2); \n" + "\n" + " } else if (method.equals(METHOD_PUT)) {\n" + " doPut($1, $2); \n" + "\n" + " } else if (method.equals(METHOD_DELETE)) {\n" + " doDelete($1, $2); \n" + "\n" + " } else if (method.equals(METHOD_OPTIONS)) {\n" + " doOptions($1, $2); \n" + "\n" + " } else if (method.equals(METHOD_TRACE)) {\n" + " doTrace($1, $2); \n" + "\n" + " } else {\n" + " String errMsg = lStrings.getString("http.method_not_implemented"); \n" + " Object[] errArgs = new Object[1]; \n" + " errArgs[0] = method; \n" + " errMsg = java.text.MessageFormat.format(errMsg, errArgs); \n" + "\n" + " $2.sendError(javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); \n" + " }" + "}" ); byte [] bytes = c . toBytecode (); c . detach (); return bytes ; } catch ( Exception e ) { e . printStackTrace (); } } return new byte [ 0 ]; }}Copy the code
After we have written the Agent, we need to add automatic repair logic
List < Result > results = Analysis . doAnalysis ( files );
if ( command . repair ) {
RepairService . start ( results , pid );
}
Copy the code
If the analysis results are found and the user selects the repair function, the repair logic will be entered (only the two most common classes will be repaired for now)
public static void start ( List < Result > resultList , int pid ) {
logger . info ( "try repair agent memshell" );
for ( Result result : resultList ) {
String className = result . getKey (). replace ( "/" , "." );
if ( className . equals ( "org.apache.catalina.core.ApplicationFilterChain" ) ||
className . equals ( "javax/servlet/http/HttpServlet" )) {
try {
start ( pid );
return ;
} catch ( Exception ignored ) {
}
}
}
}
Copy the code
Fix the core code: take the packaged Agent, do Atach and Load to replace the bytecode as normal
public static void start ( int pid ) { try { String agent = Paths . get ( "RepairAgent.jar" ). toAbsolutePath (). toString (); VirtualMachine vm = VirtualMachine . attach ( String . valueOf ( pid )); logger . info ( "load agent..." ); vm . loadAgent ( agent ); logger . info ( "repair..." ); vm . detach (); logger . info ( "detach agent..." ); } catch ( Exception e ) { e . printStackTrace (); }}Copy the code
Jar –pid 000 — java-jar xxx.jar –pid 000 — java-jar xxx.jar — PID 000
<dependency>
<groupId> com.sun.tools </groupId>
<artifactId> tools </artifactId>
<version> jdk-8 </version>
<scope> system </scope>
<systemPath> ${env.JAVA_HOME}/lib/tools.jar </systemPath>
</dependency>
Copy the code
The effect that can be achieved through the above repair means:
- Start a SpringBoot application
- through
Agent
Inject memory horse, memory horse is available after access - Memory horse detected by tool, try to modify, so that the bytecode is restored
- The memory horse becomes invalid after the second access. No restart is required
conclusion
About Dump bytecode
In some of my tests, using the SA-JDI library does not guarantee that all bytecodes will be dumped. However, common Tomcat and SpringBoot programs have been tested and found basically no problems
About illegal bytecode
Any bytecode containing LAMBDA is illegal bytecode and cannot be processed normally. You need to use ASM after modifying the source code to do so. This method is not perfect after all, is there a way to dump the legal bytecode (after some attempts failed to find a way)?
About the test
As can be seen, the bytecode analysis process is relatively simple, especially runtime. exec’s ordinary execution command memory, which is easy to bypass, but I think this is enough, because some previous conditions have limited the analysis of the class cannot contain Runtime.exec’s blacklist class, and most users are script boys. The possibility of using a no-kill memory horse is unlikely. Most users may directly use off-the-shelf tools, such as the ice Scorpion memory horse detection method has been completed, for the time being, it is enough to do so, there is no need to add a variety of no-kill detection methods
About killing
The fix using Agent to recover bytecode is theoretically fine. However, the ApplicationFilterChain class has LAMBDA and anonymous inner classes in its doFilter method, both of which are not supported by the Javassist framework and can be done with ASM, though it may be more difficult
In addition, for the repair of ordinary memory horse, Agent technology can only cover method body, can not add or delete method. Therefore, it is theoretically possible to fix the method by returning NULL based on its return value type
About expanding
For example, I defined the blacklist and keywords in the code, you can add new classes according to the actual experience, in order to achieve a better effect. I’ve done two of the most common ones for killing, and can add more logic as needed