【网络安全】Agent内存马的自动分析与查杀
前言
出發(fā)點是Java Agent內存馬的自動分析與查殺,實際上其他內存馬都可以通過這種方式查殺
本文主要的難點主要是以下三個,我會在文中逐個解答
背景
對于Java內存馬的攻防一直沒有停止,是Java安全領域的重點
回顧Tomcat或Spring內存馬:Filter和Controller等都需要注冊新的組件
針對于需要注冊新組件的內存馬查殺起來比較容易:
例如c0ny1師傅的java-memshell-scanner項目,利用了Tomcat API刪除添加的組件。優(yōu)點在于一個簡單的JSP文件即可查看所有的組件信息,結合人工審查(類名和ClassLoader等信息)對內存馬進行查殺,也可以對有風險的Class進行dump后反編譯分析
或者LandGrey師傅基于Alibaba Arthas編寫的copagent項目,分析JVM中所有的Class,根據(jù)危險注解和類名等信息dump可疑的組件,結合人工反編譯后進行分析
但實戰(zhàn)中,可能并不是以上這種注冊新組件的內存馬
例如師傅們常用的冰蝎內存馬,是Java Agent內存馬。以下這段是冰蝎內存馬一段代碼,簡單分析后可以發(fā)現(xiàn)冰蝎內存馬是利用Java Agent注入到javax.servlet.http.HttpServlet的service方法中,這是JavaEE的規(guī)范,理論上部署在Tomcat的都要符合這個規(guī)范,簡單來理解這是Tomcat處理請求最先且總是經(jīng)過的地方,在該類加入內存馬的邏輯,可以保證穩(wěn)定觸發(fā)
類似的邏輯,可以使用Java Agent將內存馬注入org.apache.catalina.core.ApplicationFilterChain類中,該類位于Filter鏈頭部,也就是說經(jīng)過Tomcat的請求都會交經(jīng)過該類的doFilter方法處理,所以在該方法中加入內存馬邏輯,也是一種穩(wěn)定觸發(fā)的方式(據(jù)說這是老版本冰蝎內存馬的方式)
還可以對類似的類進行注入,例如org.springframework.web.servlet.DispatcherServlet類,針對于Spring框架的底層進行注入。或者一些巧妙的思路,比如注入Tomcat自帶的Filter之一org.apache.tomcat.websocket.server.WsFilter類,這也是Java Agent內存馬可以做到的
上文簡單地介紹了各種內存馬的利用方式與普通內存馬的查殺,之所以最后介紹Java Agent內存馬的查殺,是因為比較困難。寬字節(jié)安全的師傅提出查殺思路:基于javaAgent內存馬檢測查殺指南
引用文章講到Java Agent內存馬檢測的難點:
調用retransformClass方法的時候參數(shù)中的字節(jié)碼并不是調用redefineClass后被修改的類的字節(jié)碼。對于冰蝎來講,根本無法獲取被冰蝎修改后的字節(jié)碼。我們自己寫Java Agent清除內存馬的時候,同樣也是無法獲取到被redefineClass修改后的字節(jié)碼,只能獲取到被retransformClass修改后的字節(jié)碼。通過Javaassist等ASM工具獲取到類的字節(jié)碼,也只是讀取磁盤上響應類的字節(jié)碼,而不是JVM中的字節(jié)碼
寬字節(jié)安全的師傅找到了一種檢測手段:sa-jdi.jar
借用公眾號師傅的圖片,這是一個GUI工具,可以查看JVM中所有已加載的類。區(qū)別在于這里獲取到的是真正的當前的字節(jié)碼,而不是獲取到原始的,本地的字節(jié)碼,所以是可以查看被Java Agent調用redefineClass后被修改的類的字節(jié)碼。進一步可以dump下來認為存在風險的類然后反編譯人工審核
介紹
以上是背景,接下來介紹我做了些什么,能夠實現(xiàn)怎樣的效果
不難看出,以上內存馬查殺手段都是半自動結合人工審核的方式,當檢測出內存馬后
是否可以找到一種方式,做到一條龍式服務:
- 檢測(同時支持普通內存馬和Java Agent內存馬的檢測)
- 分析(如何確定該類是內存馬,僅根據(jù)惡意類名和注解等信息不完善)
- 查殺(當確定內存馬存在,如何自動地刪除內存馬并恢復正常業(yè)務邏輯)
大致看來,實現(xiàn)起來似乎不難,然而實際中遇到了很多坑,接下來我會逐個介紹
【→>所有資源關注我,查看“資料”獲取<←】
1、網(wǎng)絡安全學習路線
2、電子書籍(白帽子)
3、安全大廠內部視頻
4、100份src文檔
5、常見安全面試題
6、ctf大賽經(jīng)典題目解析
7、全套工具包
8、應急響應筆記
SA-JDI分析
我嘗試通過Java Agent技術來獲取當前的字節(jié)碼,發(fā)現(xiàn)如師傅所說拿不到被修改的字節(jié)碼
所以為了可以檢測Agent馬需要從sa-jdi.jar本身入手,想辦法dump得到當前字節(jié)碼(這樣不止可以分析被修改了字節(jié)碼的Agent馬也可以分析普通類型的內存馬)
注意到其中一個類:sun.jvm.hotspot.tools.jcore.ClassDump并通過查資料發(fā)現(xiàn)該類功能正是dump當前的Class(根據(jù)類名也可猜測出)其中的main方法提供一個dump class的命令行工具
于是我想了一些辦法,用代碼實現(xiàn)了命令行工具的功能,并可以設置一個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 {@Overridepublic boolean canInclude ( InstanceKlass instanceKlass ) {String klassName = instanceKlass . getName (). asString ();// 在黑名單中的類需要dumpif ( blackList . contains ( klassName )) {return true ;}// 包含了關鍵字的類也需要dumpfor ( String k : Constant . keyword ) {if ( klassName . contains ( k )) {return true ;}}return false ;} }以上包含了類的黑名單和關鍵字:
- 黑名單:Java Agent內存馬通常會Hook的地方,需要dump下來進行分析
- 關鍵字:類名如果出現(xiàn)memshell和shell等關鍵字認為可能是普通內存馬,需要分析
另外如果想在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>接著就可以通過代碼的方式,根據(jù)黑名單和關鍵字來確定需要dump哪些類然后進行dump操作了
我在測試中遇到一個小問題,值得分享:HttpServlet是正常可以dump的但是ApplicationFilterChain類沒有找到。這是因為SpringBoot的懶加載問題,需要手動請求下某個接口就可以了
解決非法字節(jié)碼
接下來我遇到了一個比較大的坑,通過sa-jdi庫dump下來的字節(jié)碼是非法的
在對ApplicationFilterChain類分析的時候,會報如下的錯
起初我懷疑是自己用了最新版ASM框架:9.2
于是逐漸降級,發(fā)現(xiàn)降級到7.0后不再報錯,但ClassReader不報錯,在分析時候會報錯
經(jīng)過對比,發(fā)現(xiàn)是以下的情況
不報錯版本
稍微分析了下,發(fā)現(xiàn)是ApplicationFilterChain類包含了LAMBDA
不止這個類,不少的類都有可能會包含LAMBDA
發(fā)現(xiàn)通過sa-jdi獲取的字節(jié)碼在存在LAMBDA的情況下是非法字節(jié)碼,無法進行分析
這時候如果還想進行分析,只有兩個選擇:
- 自己解析CLASS文件做分析(本末倒置)
- 改寫ASM源碼使跳過LAMBDA
根據(jù)Java基礎知識可以得知:LAMBDA和INVOKEDYNAMIC指令相關,于是我改了ASM的代碼
(這里不解釋為什么這么改了,是經(jīng)過多次調試確定的)
org/objectweb/asm/ClassReader#274
bootstrapMethodOffsets = null ;org/objectweb/asm/ClassReader#2456
case Opcodes . INVOKEDYNAMIC :{return ;}改了源碼后,就可以正常對非法字節(jié)碼進行分析了。目前來看沒有什么大問題,可以正常分析,但不確定這樣的修改是否會存在一些隱患和BUG。總之目前能繼續(xù)了
分析字節(jié)碼
分析字節(jié)碼并不需要太深入做,因為大部分可能出現(xiàn)的內存馬都是Runtime.exec或冰蝎反射調ClassLoader.defineClass實現(xiàn)的,針對于這兩種情況做分析,足以應對絕大多數(shù)情況
以下代碼是讀取dump的字節(jié)碼并針對兩種情況對所有方法分析
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 analysiscr = new ClassReader ( bytes );cv = new ShellClassVisitor ( api , results );cr . accept ( cv , parsingOptions );// classloader defineClass analysiscr = 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的字節(jié)碼中所有方法中是否存在該方法的調用即可(理論上會存在誤報,但黑名單類不可能存在該方法,關鍵字類本身就是可疑的,所以這樣做并無不妥)
@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
代碼不長,但對應的字節(jié)碼較復雜
Method m = ClassLoader . class . getDeclaredMethod ( "defineClass" , String . class , ByteBuffer . class , ProtectionDomain . class ); m . invoke ( null );對應字節(jié)碼
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相關操作都算是比較常見的,有一定的誤報可能
于是繼續(xù)掏出棧幀分析大法,具體不再介紹,之前文章 已有詳細解釋
根據(jù)字節(jié)碼,在defineClass和Ljava/lang/ClassLoader;通過LDC指令入棧之前,應該認為這是惡意操作,模擬JVM指令執(zhí)行后應該在棧頂設置污點
@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 ); }后續(xù)主要是對于兩個INVOKE進行分析
- 如果getDeclaredMethod傳入的是上文LDC處設置的污點,認為方法返回值也是污點,給棧頂?shù)姆祷刂翟O置REFLECTION_METHOD標志
- 如果Method.invoke方法中的Method被標記了REFLECTION_METHOD則可以確定這是內存馬
- 開頭一部分代碼主要是根據(jù)方法參數(shù)的實際情況對參數(shù)在操作數(shù)棧中的索引位置進行確定,是一種動態(tài)和自動的確認方式,而不是直接根據(jù)經(jīng)驗或者調試寫死索引,算是優(yōu)雅寫法
檢測效果如下:
先寫個內存馬注入的Agent注入到HttpServlet中(關于這個不是文章重點)
然后跑起來我寫的工具
- 其中紅色框內是注入的Agent內存馬,可以分析出
- 發(fā)現(xiàn)上面還有兩個內存馬結果,這是我模擬的普通內存馬,直接寫入到代碼中做測試的
自動修復
接下來是內存馬的修復,自行寫一個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)做修復(慎重選擇,為什么這樣做我將在總結里解釋)
- 修改方法的參數(shù)需要用$1 $2這樣表示,不能寫req和resp
- 這里HttpServlet的情況稍復雜,其中有兩個service方法,實際上對任何一個進行修改都可以導致內存馬的效果,所以我要做的事情是恢復這兩個方法,而不是只針對某一個
- 注意任何非java.lang下的類都需要完整類名
當我們寫好了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將字節(jié)碼替換為正常情況即可
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,由于上文已經(jīng)配置了打包插件,所以可以直接打入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注入內存馬,訪問后內存馬可用
- 通過工具檢測到內存馬,嘗試修改,使字節(jié)碼被還原
- 再次訪問后內存馬失效,不需要重啟
總結
關于Dump字節(jié)碼
經(jīng)過我的一些測試,使用sa-jdi庫不能保證dump所有的字節(jié)碼,會出現(xiàn)莫名其妙的異常,猜測是某些字節(jié)碼不允許被dump下來。但測試了常見Tomcat和SpringBoot等程序,發(fā)現(xiàn)基本沒有問題
關于非法字節(jié)碼
只要是包含LAMBDA的字節(jié)碼都是非法字節(jié)碼,無法正常處理,需要用修改源碼后的ASM來做。這種方式終究不是完美的辦法,是否存在能夠dump下來合法字節(jié)碼的方式呢(經(jīng)過一些嘗試沒有找到辦法)
關于檢測
可以看到,字節(jié)碼分析的過程比較簡單,尤其是Runtime.exec的普通執(zhí)行命令內存馬,很容易繞過,但個人認為這已足夠,因為之前的一些條件已經(jīng)限制了分析的類是不可能包含Runtime.exec的黑名單類,且大多數(shù)用戶都是腳本小子,使用免殺型內存馬的可能性不大。大多數(shù)用戶可能直接用了現(xiàn)成的工具,例如冰蝎型內存馬的檢測方式已完成,暫時來看這樣做是足夠的,沒有必要加入各種免殺檢測手段
關于查殺
使用Agent恢復字節(jié)碼的修復方式理論上沒有問題。但其中的ApplicationFilterChain類的doFilter方法中包含了LAMBDA和匿名內部類,這兩者都是Javassist框架不支持的內容,可以用ASM來做,但可能難度較高
另外對于普通型內存馬的修復,通過Agent技術只能覆蓋方法體,不可以增加或刪除方法。所以理論上可以根據(jù)方法的返回值類型,做返回NULL的處理進行修復
關于拓展
例如代碼中我定義的黑名單和關鍵字,可以根據(jù)實戰(zhàn)經(jīng)驗自行添加新的類,以實現(xiàn)更完善的效果。在查殺方面我做了最常見的兩種,可以根據(jù)實際情況自行添加更多的邏輯
總結
以上是生活随笔為你收集整理的【网络安全】Agent内存马的自动分析与查杀的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【web安全】你的open_basedi
- 下一篇: 一次历史漏洞分析与复现的全部过程