本文首發于先知社群:https://xz.aliyun.com/t/10910
出發點是
Java Agent
記憶體馬的自動分析與清除,實際上其他記憶體馬都可以通過這種方式清除
本文主要的難點主要是以下三個,我會在文中逐個解答
- 如何
出dump
中真正的目前的位元組碼JVM
- 如何解決由于
表達式導緻非法位元組碼無法分析的問題LAMBDA
- 如何對位元組碼進行分析以确定某個類是記憶體馬
基于靜态分析動态,打破規則之道 -- Java King 對本文的評價
背景
對于Java記憶體馬的攻防一直沒有停止,是Java安全領域的重點
回顧
Tomcat
或
Spring
記憶體馬:
Filter
和
Controller
等都需要注冊新的元件
針對于需要注冊新元件的記憶體馬清除起來比較容易:
例如
c0ny1
師傅的
java-memshell-scanner
項目,利用了
Tomcat API
删除添加的元件。優點在于一個簡單的
JSP
檔案即可檢視所有的元件資訊,結合人工審查(類名和
ClassLoader
等資訊)對記憶體馬進行清除,也可以對有風險的Class進行
dump
後反編譯分析
或者
LandGrey
師傅基于Alibaba Arthas編寫的
copagent
項目,分析
JVM
中所有的Class,根據危險注解和類名等資訊
dump
可疑的元件,結合人工反編譯後進行分析
但實戰中,可能并不是以上這種注冊新元件的記憶體馬
例如師傅們常用的冰蠍記憶體馬,是
Java Agent
記憶體馬。以下這段是冰蠍記憶體馬一段代碼,簡單分析後可以發現冰蠍記憶體馬是利用
Java Agent
注入到
javax.servlet.http.HttpServlet
的
service
方法中,這是
JavaEE
的規範,理論上部署在
Tomcat
的都要符合這個規範,簡單來了解這是
Tomcat
處理請求最先且總是經過的地方,在該類加入記憶體馬的邏輯,可以保證穩定觸發

類似的邏輯,可以使用
Java Agent
将記憶體馬注入
org.apache.catalina.core.ApplicationFilterChain
類中,該類位于
Filter
鍊頭部,也就是說經過
Tomcat
的請求都會交經過該類的
doFilter
方法處理,是以在該方法中加入記憶體馬邏輯,也是一種穩定觸發的方式(據說這是老版本冰蠍記憶體馬的方式)
還可以對類似的類進行注入,例如
org.springframework.web.servlet.DispatcherServlet
類,針對于
Spring
架構的底層進行注入。或者一些巧妙的思路,比如注入
Tomcat
自帶的
Filter
之一
org.apache.tomcat.websocket.server.WsFilter
類,這也是
Java Agent
記憶體馬可以做到的
上文簡單地介紹了各種記憶體馬的利用方式與普通記憶體馬的清除,之是以最後介紹
Java Agent
記憶體馬的清除,是因為比較困難。寬位元組安全的師傅提出清除思路:基于javaAgent記憶體馬檢測清除指南
引用文章講到
Java Agent
記憶體馬檢測的難點:
調用
retransformClass
方法的時候參數中的位元組碼并不是調用
redefineClass
後被修改的類的位元組碼。對于冰蠍來講,根本無法擷取被冰蠍修改後的位元組碼。我們自己寫
Java Agent
清除記憶體馬的時候,同樣也是無法擷取到被
redefineClass
修改後的位元組碼,隻能擷取到被
retransformClass
修改後的位元組碼。通過
Javaassist
等
ASM
工具擷取到類的位元組碼,也隻是讀取磁盤上響應類的位元組碼,而不是
JVM
中的位元組碼
寬位元組安全的師傅找到了一種檢測手段:
sa-jdi.jar
借用公衆号師傅的圖檔,這是一個
GUI
工具,可以檢視
JVM
中所有已加載的類。差別在于這裡擷取到的是真正的目前的位元組碼,而不是擷取到原始的,本地的位元組碼,是以是可以檢視被
Java Agent
調用
redefineClass
後被修改的類的位元組碼。進一步可以
dump
下來認為存在風險的類然後反編譯人工稽核
介紹
以上是背景,接下來介紹我做了些什麼,能夠實作怎樣的效果
不難看出,以上記憶體馬清除手段都是半自動結合人工稽核的方式,當檢測出記憶體馬後
是否可以找到一種方式,做到一條龍式服務:
- 檢測(同時支援普通記憶體馬和
記憶體馬的檢測)Java Agent
- 分析(如何确定該類是記憶體馬,僅根據惡意類名和注解等資訊不完善)
- 清除(當确定記憶體馬存在,如何自動地删除記憶體馬并恢複正常業務邏輯)
大緻看來,實作起來似乎不難,然而實際中遇到了很多坑,接下來我會逐個介紹
SA-JDI分析
我嘗試通過
Java Agent
技術來擷取目前的位元組碼,發現如師傅所說拿不到被修改的位元組碼
是以為了可以檢測
Agent
馬需要從
sa-jdi.jar
本身入手,想辦法
dump
得到目前位元組碼(這樣不止可以分析被修改了位元組碼的
Agent
馬也可以分析普通類型的記憶體馬)
注意到其中一個類:
sun.jvm.hotspot.tools.jcore.ClassDump
并通過查資料發現該類功能正是
dump
目前的Class(根據類名也可猜測出)其中的
main
方法提供一個
dump class
的指令行工具
于是我想了一些辦法,用代碼實作了指令行工具的功能,并可以設定一個
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();
複制
上文提到設定一個
Filter
是用于确定需要對哪些類進行
dump
操作(dump過多會導緻性能等問題)
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;
}
}
複制
以上包含了類的黑名單和關鍵字:
- 黑名單:Java Agent記憶體馬通常會Hook的地方,需要
下來進行分析dump
- 關鍵字:類名如果出現
和memshell
等關鍵字認為可能是普通記憶體馬,需要分析shell
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");
}
}
複制
另外如果想在
Maven
項目中加入
JDK/lib
下的依賴,需要特殊配置
<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>
複制
在打包成工具
Jar
包時預設情況下不會加入
system scope
的依賴,是以需要特殊處理
<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>
複制
編寫
assembly.xml
檔案
<!-- 省略部分 -->
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<scope>system</scope>
</dependencySet>
</dependencySets>
複制
接着就可以通過代碼的方式,根據黑名單和關鍵字來确定需要
dump
哪些類然後進行
dump
操作了
我在測試中遇到一個小問題,值得分享:
HttpServlet
是正常可以
dump
的但是
ApplicationFilterChain
類沒有找到。這是因為
SpringBoot
的懶加載問題,需要手動請求下某個接口就可以了
解決非法位元組碼
接下來我遇到了一個比較大的坑,通過
sa-jdi
庫
dump
下來的位元組碼是非法的
在對
ApplicationFilterChain
類分析的時候,會報如下的錯
起初我懷疑是自己用了最新版
ASM
架構:9.2
于是逐漸降級,發現降級到7.0後不再報錯,但
ClassReader
不報錯,在分析時候會報錯
經過對比,發現是以下的情況
不報錯版本
稍微分析了下,發現是
ApplicationFilterChain
類包含了
LAMBDA
不止這個類,不少的類都有可能會包含
LAMBDA
發現通過
sa-jdi
擷取的位元組碼在存在
LAMBDA
的情況下是非法位元組碼,無法進行分析
這時候如果還想進行分析,隻有兩個選擇:
- 自己解析CLASS檔案做分析(本末倒置)
- 改寫ASM源碼使跳過
LAMBDA
根據Java基礎知識可以得知:
LAMBDA
和
INVOKEDYNAMIC
指令相關,于是我改了
ASM
的代碼
(這裡不解釋為什麼這麼改了,是經過多次調試确定的)
org/objectweb/asm/ClassReader#274
bootstrapMethodOffsets = null;
複制
org/objectweb/asm/ClassReader#2456
case Opcodes.INVOKEDYNAMIC:
{
return;
}
複制
改了源碼後,就可以正常對非法位元組碼進行分析了。目前來看沒有什麼大問題,可以正常分析,但不确定這樣的修改是否會存在一些隐患和BUG。總之目前能繼續了
分析位元組碼
分析位元組碼并不需要太深入做,因為大部分可能出現的記憶體馬都是
Runtime.exec
或冰蠍反射調
ClassLoader.defineClass
實作的,針對于這兩種情況做分析,足以應對絕大多數情況
以下代碼是讀取
dump
的位元組碼并針對兩種情況對所有方法分析
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());
}
複制
對于
Runtime.exec
型的分析最為簡單,僅判斷已
dump
的位元組碼中所有方法中是否存在該方法的調用即可(理論上會存在誤報,但黑名單類不可能存在該方法,關鍵字類本身就是可疑的,是以這樣做并無不妥)
@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);
}
複制
但這種情況不适用于冰蠍反射調
ClassLoader.defineClass
代碼不長,但對應的位元組碼較複雜
Method m = ClassLoader.class.getDeclaredMethod("defineClass",
String.class, ByteBuffer.class, ProtectionDomain.class);
m.invoke(null);
複制
對應位元組碼
LDC Ljava/lang/ClassLoader;.class // 重點關注
LDC "defineClass" // 重點關注
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; // 重點關注
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; // 重點關注
POP
複制
這種操作需要多個步驟,并不是簡單的一個
INVOKE
那麼簡單,不特殊處理的話,由于反射和
ClassLoader
相關操作都算是比較常見的,有一定的誤報可能
于是繼續掏出棧幀分析大法,具體不再介紹,之前文章 已有詳細解釋
根據位元組碼,在
defineClass
和
Ljava/lang/ClassLoader;
通過
LDC
指令入棧之前,應該認為這是惡意操作,模拟JVM指令執行後應該在棧頂設定污點
@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);
}
複制
後續主要是對于兩個
INVOKE
進行分析
- 如果
傳入的是上文getDeclaredMethod
處設定的污點,認為方法傳回值也是污點,給棧頂的傳回值設定LDC
标志REFLECTION_METHOD
- 如果
方法中的Method.invoke
被标記了Method
則可以确定這是記憶體馬REFLECTION_METHOD
- 開頭一部分代碼主要是根據方法參數的實際情況對參數在操作數棧中的索引位置進行确定,是一種動态和自動的确認方式,而不是直接根據經驗或者調試寫死索引,算是優雅寫法
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);
}
複制
檢測效果如下:
先寫個記憶體馬注入的
Agent
注入到
HttpServlet
中(關于這個不是文章重點)
然後跑起來我寫的工具
- 其中紅色框内是注入的
記憶體馬,可以分析出Agent
- 發現上面還有兩個記憶體馬結果,這是我模拟的普通記憶體馬,直接寫入到代碼中做測試的
自動修複
接下來是記憶體馬的修複,自行寫一個
Java Agent
即可
暫時隻處理
ApplicationFilterChain
和
HttpServlet
的情況(也是最常見的情況)
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();
}
}
}
}
}
複制
處理的邏輯并不複雜
- 由于ApplicationFilterChain中包含了LAMBDA是以我直接簡化了代碼,變成簡單的一句internalDoFilter(1,2)做修複(慎重選擇,為什麼這樣做我将在總結裡解釋)
- 修改方法的參數需要用1 2這樣表示,不能寫req和resp
- 這裡
的情況稍複雜,其中有兩個HttpServlet
方法,實際上對任何一個進行修改都可以導緻記憶體馬的效果,是以我要做的事情是恢複這兩個方法,而不是隻針對某一個service
- 注意任何非
下的類都需要完整類名java.lang
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];
}
}
複制
當我們寫好了
Agent
後,需要加入自動修複的邏輯
List<Result> results = Analysis.doAnalysis(files);
if (command.repair) {
RepairService.start(results, pid);
}
複制
如果分析出了結果,且使用者選擇了修複功能,才會進入修複邏輯(暫隻修複這兩個最常見的類)
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) {
}
}
}
}
複制
修複的核心代碼:把打包好的
Agent
拿過來,做一下
Atach
和
Load
将位元組碼替換為正常情況即可
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();
}
}
複制
注意使用
VirtualMachine
等
API
需要加入
tools.jar
,由于上文已經配置了打包插件,是以可以直接打入
Jar
包,使用時候
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>
複制
通過以上這些修複手段可以做到的效果:
- 啟動某SpringBoot應用
- 通過
注入記憶體馬,通路後記憶體馬可用Agent
- 通過工具檢測到記憶體馬,嘗試修改,使位元組碼被還原
- 再次通路後記憶體馬失效,不需要重新開機
總結
關于Dump位元組碼
經過我的一些測試,使用
sa-jdi
庫不能保證
dump
所有的位元組碼,會出現莫名其妙的異常,猜測是某些位元組碼不允許被
dump
下來。但測試了常見
Tomcat
和
SpringBoot
等程式,發現基本沒有問題
關于非法位元組碼
隻要是包含
LAMBDA
的位元組碼都是非法位元組碼,無法正常處理,需要用修改源碼後的
ASM
來做。這種方式終究不是完美的辦法,是否存在能夠
dump
下來合法位元組碼的方式呢(經過一些嘗試沒有找到辦法)
關于檢測
可以看到,位元組碼分析的過程比較簡單,尤其是
Runtime.exec
的普通執行指令記憶體馬,很容易繞過,但個人認為這已足夠,因為之前的一些條件已經限制了分析的類是不可能包含
Runtime.exec
的黑名單類,且大多數使用者都是腳本小子,使用免殺型記憶體馬的可能性不大。大多數使用者可能直接用了現成的工具,例如冰蠍型記憶體馬的檢測方式已完成,暫時來看這樣做是足夠的,沒有必要加入各種免殺檢測手段
關于清除
使用Agent恢複位元組碼的修複方式理論上沒有問題。但其中的
ApplicationFilterChain
類的
doFilter
方法中包含了
LAMBDA
和匿名内部類,這兩者都是
Javassist
架構不支援的内容,可以用
ASM
來做,但可能難度較高
另外對于普通型記憶體馬的修複,通過Agent技術隻能覆寫方法體,不可以增加或删除方法。是以理論上可以根據方法的傳回值類型,做傳回
NULL
的處理進行修複
關于拓展
例如代碼中我定義的黑名單和關鍵字,可以根據實戰經驗自行添加新的類,以實作更完善的效果。在清除方面我做了最常見的兩種,可以根據實際情況自行添加更多的邏輯
最後
代碼位址:https://github.com/4ra1n/FindShell