作者:Longofo@知道創(chuàng)宇404實(shí)驗(yàn)室
之前在一個(gè)應(yīng)用中搜索到一個(gè)類,但是在反序列化測(cè)試的時(shí)出錯(cuò),錯(cuò)誤不是class notfound,是其他0xxx這樣的錯(cuò)誤,通過(guò)搜索這個(gè)錯(cuò)誤大概是類沒(méi)有被加載。最近剛好看到了JavaAgent,初步學(xué)習(xí)了下,能進(jìn)行攔截,主要通過(guò)Instrument Agent來(lái)進(jìn)行字節(jié)碼增強(qiáng),可以進(jìn)行字節(jié)碼插樁,bTrace,Arthas?等操作,結(jié)合ASM,javassist,cglib框架能實(shí)現(xiàn)更強(qiáng)大的功能。Java RASP也是基于JavaAgent實(shí)現(xiàn)的。趁熱記錄下JavaAgent基礎(chǔ)概念,以及簡(jiǎn)單使用JavaAgent實(shí)現(xiàn)一個(gè)獲取目標(biāo)進(jìn)程已加載的類的測(cè)試。
JVMTI與Java Instrument
Java平臺(tái)調(diào)試器架構(gòu)(Java Platform Debugger Architecture,JPDA)是一組用于調(diào)試Java代碼的API(摘自維基百科):
JVMTI 提供了一套”代理”程序機(jī)制,可以支持第三方工具程序以代理的方式連接和訪問(wèn) JVM,并利用 JVMTI 提供的豐富的編程接口,完成很多跟 JVM 相關(guān)的功能。JVMTI是基于事件驅(qū)動(dòng)的,JVM每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開(kāi)發(fā)者去擴(kuò)展自己的邏輯。
JVMTIAgent是一個(gè)利用JVMTI暴露出來(lái)的接口提供了代理啟動(dòng)時(shí)加載(agent on load)、代理通過(guò)attach形式加載(agent on attach)和代理卸載(agent on unload)功能的動(dòng)態(tài)庫(kù)。Instrument Agent可以理解為一類JVMTIAgent動(dòng)態(tài)庫(kù),別名是JPLISAgent(Java Programming Language Instrumentation Services Agent),是專門為java語(yǔ)言編寫的插樁服務(wù)提供支持的代理。
Instrumentation接口
以下接口是Java SE 8?API文檔中[1]提供的(不同版本可能接口有變化):
void addTransformer(ClassFileTransformer transformer, boolean canRetransform)//注冊(cè)ClassFileTransformer實(shí)例,注冊(cè)多個(gè)會(huì)按照注冊(cè)順序進(jìn)行調(diào)用。所有的類被加載完畢之后會(huì)調(diào)用ClassFileTransformer實(shí)例,相當(dāng)于它們通過(guò)了redefineClasses方法進(jìn)行重定義。布爾值參數(shù)canRetransform決定這里被重定義的類是否能夠通過(guò)retransformClasses方法進(jìn)行回滾。
void addTransformer(ClassFileTransformer transformer)//相當(dāng)于addTransformer(transformer, false),也就是通過(guò)ClassFileTransformer實(shí)例重定義的類不能進(jìn)行回滾。
boolean removeTransformer(ClassFileTransformer transformer)//移除(反注冊(cè))ClassFileTransformer實(shí)例。
void retransformClasses(Class<?>... classes)//已加載類進(jìn)行重新轉(zhuǎn)換的方法,重新轉(zhuǎn)換的類會(huì)被回調(diào)到ClassFileTransformer的列表中進(jìn)行處理。
void appendToBootstrapClassLoaderSearch(JarFile jarfile)//將某個(gè)jar加入到Bootstrap Classpath里優(yōu)先其他jar被加載。
void appendToSystemClassLoaderSearch(JarFile jarfile)//將某個(gè)jar加入到Classpath里供AppClassloard去加載。
Class[] getAllLoadedClasses()//獲取所有已經(jīng)被加載的類。
Class[] getInitiatedClasses(ClassLoader loader)//獲取所有已經(jīng)被初始化過(guò)了的類。
long getObjectSize(Object objectToSize)//獲取某個(gè)對(duì)象的(字節(jié))大小,注意嵌套對(duì)象或者對(duì)象中的屬性引用需要另外單獨(dú)計(jì)算。
boolean isModifiableClass(Class<?> theClass)//判斷對(duì)應(yīng)類是否被修改過(guò)。
boolean isNativeMethodPrefixSupported()//是否支持設(shè)置native方法的前綴。
boolean isRedefineClassesSupported()//返回當(dāng)前JVM配置是否支持重定義類(修改類的字節(jié)碼)的特性。
boolean isRetransformClassesSupported()//返回當(dāng)前JVM配置是否支持類重新轉(zhuǎn)換的特性。
void redefineClasses(ClassDefinition... definitions)//重定義類,也就是對(duì)已經(jīng)加載的類進(jìn)行重定義,ClassDefinition類型的入?yún)藢?duì)應(yīng)的類型Class<?>對(duì)象和字節(jié)碼文件對(duì)應(yīng)的字節(jié)數(shù)組。
void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)//設(shè)置某些native方法的前綴,主要在找native方法的時(shí)候做規(guī)則匹配。
redefineClasses與redefineClasses:
重新定義功能在Java SE 5中進(jìn)行了介紹,重新轉(zhuǎn)換功能在Java SE 6中進(jìn)行了介紹,一種猜測(cè)是將重新轉(zhuǎn)換作為更通用的功能引入,但是必須保留重新定義以實(shí)現(xiàn)向后兼容,并且重新轉(zhuǎn)換操作也更加方便。
Instrument Agent兩種加載方式
在官方API文檔[1]中提到,有兩種獲取Instrumentation接口實(shí)例的方法 :
premain對(duì)應(yīng)的就是VM啟動(dòng)時(shí)的Instrument Agent加載,即agent on load,agentmain對(duì)應(yīng)的是VM運(yùn)行時(shí)的Instrument Agent加載,即agent on attach。兩種加載形式所加載的Instrument Agent都關(guān)注同一個(gè)JVMTI事件 –?ClassFileLoadHook事件,這個(gè)事件是在讀取字節(jié)碼文件之后回調(diào)時(shí)用,也就是說(shuō)premain和agentmain方式的回調(diào)時(shí)機(jī)都是類文件字節(jié)碼讀取之后(或者說(shuō)是類加載之后),之后對(duì)字節(jié)碼進(jìn)行重定義或重轉(zhuǎn)換,不過(guò)修改的字節(jié)碼也需要滿足一些要求,在最后的局限性有說(shuō)明。
premain與agentmain的區(qū)別:
premain和agentmain兩種方式最終的目的都是為了回調(diào)Instrumentation實(shí)例并激活sun.instrument.InstrumentationImpl#transform()(InstrumentationImpl是Instrumentation的實(shí)現(xiàn)類)從而回調(diào)注冊(cè)到Instrumentation中的ClassFileTransformer實(shí)現(xiàn)字節(jié)碼修改,本質(zhì)功能上沒(méi)有很大區(qū)別。兩者的非本質(zhì)功能的區(qū)別如下:
通過(guò)下面的測(cè)試也能看到它們之間的一些區(qū)別。
premain方式編寫步驟簡(jiǎn)單如下:
1.編寫premain函數(shù),包含下面兩個(gè)方法的其中之一:
java public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);
如果兩個(gè)方法都被實(shí)現(xiàn)了,那么帶Instrumentation參數(shù)的優(yōu)先級(jí)高一些,會(huì)被優(yōu)先調(diào)用。agentArgs是premain函數(shù)得到的程序參數(shù),通過(guò)命令行參數(shù)傳入
2.定義一個(gè) MANIFEST.MF 文件,必須包含 Premain-Class 選項(xiàng),通常也會(huì)加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項(xiàng)
3.將 premain 的類和 MANIFEST.MF 文件打成 jar 包
4.使用參數(shù) -javaagent: jar包路徑啟動(dòng)代理
premain加載過(guò)程如下:
1.創(chuàng)建并初始化 JPLISAgent
2.MANIFEST.MF 文件的參數(shù),并根據(jù)這些參數(shù)來(lái)設(shè)置 JPLISAgent 里的一些內(nèi)容
3.監(jiān)聽(tīng)?VMInit?事件,在 JVM 初始化完成之后做下面的事情:
(1)創(chuàng)建 InstrumentationImpl 對(duì)象 ;
(2)監(jiān)聽(tīng) ClassFileLoadHook 事件 ;
(3)調(diào)用 InstrumentationImpl 的loadClassAndCallPremain方法,在這個(gè)方法里會(huì)去調(diào)用 javaagent 中 MANIFEST.MF 里指定的Premain-Class 類的 premain 方法
下面是一個(gè)簡(jiǎn)單的例子(在JDK1.8.0_181進(jìn)行了測(cè)試):
PreMainAgent
package com.longofo;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class PreMainAgent {
static {
System.out.println("PreMainAgent class static block run...");
}
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("PreMainAgent agentArgs : " + agentArgs);
Class<?>[] cLasses = inst.getAllLoadedClasses();
for (Class<?> cls : cLasses) {
System.out.println("PreMainAgent get loaded class:" + cls.getName());
}
inst.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("PreMainAgent transform Class:" + className);
return classfileBuffer;
}
}
}
MANIFEST.MF:
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.longofo.PreMainAgent
Testmain
package com.longofo;
public class TestMain {
static {
System.out.println("TestMain static block run...");
}
public static void main(String[] args) {
System.out.println("TestMain main start...");
try {
for (int i = 0; i < 100; i++) {
Thread.sleep(3000);
System.out.println("TestMain main running...");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("TestMain main end...");
}
}
將PreMainAgent打包為Jar包(可以直接用idea打包,也可以使用maven插件打包),在idea可以像下面這樣啟動(dòng):
命令行的話可以用形如java -javaagent:PreMainAgent.jar路徑 -jar TestMain/TestMain.jar啟動(dòng)
結(jié)果如下:
PreMainAgent class static block run...
PreMainAgent agentArgs : null
PreMainAgent get loaded class:com.longofo.PreMainAgent
PreMainAgent get loaded class:sun.reflect.DelegatingMethodAccessorImpl
PreMainAgent get loaded class:sun.reflect.NativeMethodAccessorImpl
PreMainAgent get loaded class:sun.instrument.InstrumentationImpl$1
PreMainAgent get loaded class:[Ljava.lang.reflect.Method;
...
...
PreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders
PreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$1
PreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$Cache
PreMainAgent transform Class:sun/nio/cs/ThreadLocalCoders$2
...
...
PreMainAgent transform Class:java/lang/Class$MethodArray
PreMainAgent transform Class:java/net/DualStackPlainSocketImpl
PreMainAgent transform Class:java/lang/Void
TestMain static block run...
TestMain main start...
PreMainAgent transform Class:java/net/Inet6Address
PreMainAgent transform Class:java/net/Inet6Address$Inet6AddressHolder
PreMainAgent transform Class:java/net/SocksSocketImpl$3
...
...
PreMainAgent transform Class:java/util/LinkedHashMap$LinkedKeySet
PreMainAgent transform Class:sun/util/locale/provider/LocaleResources$ResourceReference
TestMain main running...
TestMain main running...
...
...
TestMain main running...
TestMain main end...
PreMainAgent transform Class:java/lang/Shutdown
PreMainAgent transform Class:java/lang/Shutdown$Lock
可以看到在PreMainAgent之前已經(jīng)加載了一些必要的類,即PreMainAgent get loaded class:xxx部分,這些類沒(méi)有經(jīng)過(guò)transform。然后在main之前有一些類經(jīng)過(guò)了transform,在main啟動(dòng)之后還有類經(jīng)過(guò)transform,main結(jié)束之后也還有類經(jīng)過(guò)transform,可以和agentmain的結(jié)果對(duì)比下。
agentmain加載方式
agentmain方式編寫步驟簡(jiǎn)單如下:
1.編寫agentmain函數(shù),包含下面兩個(gè)方法的其中之一:
public static void agentmain(String agentArgs, Instrumentation inst);
public static void agentmain(String agentArgs);
如果兩個(gè)方法都被實(shí)現(xiàn)了,那么帶Instrumentation參數(shù)的優(yōu)先級(jí)高一些,會(huì)被優(yōu)先調(diào)用。agentArgs是premain函數(shù)得到的程序參數(shù),通過(guò)命令行參數(shù)傳入
2.定義一個(gè) MANIFEST.MF 文件,必須包含 Agent-Class 選項(xiàng),通常也會(huì)加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項(xiàng)
3.將 agentmain 的類和 MANIFEST.MF 文件打成 jar 包
4.通過(guò)attach工具直接加載Agent,執(zhí)行attach的程序和需要被代理的程序可以是兩個(gè)完全不同的程序:
// 列出所有VM實(shí)例
List<VirtualMachineDescriptor> list = VirtualMachine.list();
// attach目標(biāo)VM
VirtualMachine.attach(descriptor.id());
// 目標(biāo)VM加載Agent
VirtualMachine#loadAgent("代理Jar路徑","命令參數(shù)");
agentmain方式加載過(guò)程類似:
1.創(chuàng)建并初始化JPLISAgent
2.解析MANIFEST.MF 里的參數(shù),并根據(jù)這些參數(shù)來(lái)設(shè)置 JPLISAgent 里的一些內(nèi)容
3.監(jiān)聽(tīng)?VMInit?事件,在 JVM 初始化完成之后做下面的事情:
(1)創(chuàng)建 InstrumentationImpl 對(duì)象 ;
(2)監(jiān)聽(tīng) ClassFileLoadHook 事件 ;
(3)調(diào)用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在這個(gè)方法里會(huì)去調(diào)用javaagent里 MANIFEST.MF 里指定的Agent-Class類的agentmain方法。
下面是一個(gè)簡(jiǎn)單的例子(在JDK 1.8.0_181上進(jìn)行了測(cè)試):
SufMainAgent
package com.longofo;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class SufMainAgent {
static {
System.out.println("SufMainAgent static block run...");
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("SufMainAgent agentArgs: " + agentArgs);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
for (Class<?> cls : classes) {
System.out.println("SufMainAgent get loaded class: " + cls.getName());
}
instrumentation.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("SufMainAgent transform Class:" + className);
return classfileBuffer;
}
}
}
MANIFEST.MF
Manifest-Version: 1.0
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class: com.longofo.SufMainAgent
TestSufMainAgent
package com.longofo;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class TestSufMainAgent {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
//獲取當(dāng)前系統(tǒng)中所有 運(yùn)行中的 虛擬機(jī)
System.out.println("TestSufMainAgent start...");
String option = args[0];
List<VirtualMachineDescriptor> list = VirtualMachine.list();
if (option.equals("list")) {
for (VirtualMachineDescriptor vmd : list) {
//如果虛擬機(jī)的名稱為 xxx 則 該虛擬機(jī)為目標(biāo)虛擬機(jī),獲取該虛擬機(jī)的 pid
//然后加載 agent.jar 發(fā)送給該虛擬機(jī)
System.out.println(vmd.displayName());
}
} else if (option.equals("attach")) {
String jProcessName = args[1];
String agentPath = args[2];
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().equals(jProcessName)) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentPath);
}
}
}
}
}
Testmain
package com.longofo;
public class TestMain {
static {
System.out.println("TestMain static block run...");
}
public static void main(String[] args) {
System.out.println("TestMain main start...");
try {
for (int i = 0; i < 100; i++) {
Thread.sleep(3000);
System.out.println("TestMain main running...");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("TestMain main end...");
}
}
將SufMainAgent和TestSufMainAgent打包為Jar包(可以直接用idea打包,也可以使用maven插件打包),首先啟動(dòng)Testmain,然后先列下當(dāng)前有哪些Java程序:
attach SufMainAgent到Testmain:
在Testmain中的結(jié)果如下:
TestMain static block run...
TestMain main start...
TestMain main running...
TestMain main running...
TestMain main running...
...
...
SufMainAgent static block run...
SufMainAgent agentArgs: null
SufMainAgent get loaded class: com.longofo.SufMainAgent
SufMainAgent get loaded class: com.longofo.TestMain
SufMainAgent get loaded class: com.intellij.rt.execution.application.AppMainV2$1
SufMainAgent get loaded class: com.intellij.rt.execution.application.AppMainV2
...
...
SufMainAgent get loaded class: java.lang.Throwable
SufMainAgent get loaded class: java.lang.System
...
...
TestMain main running...
TestMain main running...
...
...
TestMain main running...
TestMain main running...
TestMain main end...
SufMainAgent transform Class:java/lang/Shutdown
SufMainAgent transform Class:java/lang/Shutdown$Lock
和前面premain對(duì)比下就能看出,在agentmain中直接getloadedclasses的類數(shù)目比在premain直接getloadedclasses的數(shù)量多,而且premain getloadedclasses的類+premain transform的類和agentmain getloadedclasses基本吻合(只針對(duì)這個(gè)測(cè)試,如果程序中間還有其他通信,可能會(huì)不一樣)。也就是說(shuō)某個(gè)類之前沒(méi)有加載過(guò),那么都會(huì)通過(guò)兩者設(shè)置的transform,這可以從最后的java/lang/Shutdown看出來(lái)。
測(cè)試Weblogic的某個(gè)類是否被加載
這里使用weblogic進(jìn)行測(cè)試,代理方式使用agentmain方式(在jdk1.6.0_29上進(jìn)行了測(cè)試):
WeblogicSufMainAgent
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class WeblogicSufMainAgent {
static {
System.out.println("SufMainAgent static block run...");
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("SufMainAgent agentArgs: " + agentArgs);
Class<?>[] classes = instrumentation.getAllLoadedClasses();
for (Class<?> cls : classes) {
System.out.println("SufMainAgent get loaded class: " + cls.getName());
}
instrumentation.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("SufMainAgent transform Class:" + className);
return classfileBuffer;
}
}
}
WeblogicTestSufMainAgent:
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class WeblogicTestSufMainAgent {
public static void main(String[] args) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
//獲取當(dāng)前系統(tǒng)中所有 運(yùn)行中的 虛擬機(jī)
System.out.println("TestSufMainAgent start...");
String option = args[0];
List<VirtualMachineDescriptor> list = VirtualMachine.list();
if (option.equals("list")) {
for (VirtualMachineDescriptor vmd : list) {
//如果虛擬機(jī)的名稱為 xxx 則 該虛擬機(jī)為目標(biāo)虛擬機(jī),獲取該虛擬機(jī)的 pid
//然后加載 agent.jar 發(fā)送給該虛擬機(jī)
System.out.println(vmd.displayName());
}
} else if (option.equals("attach")) {
String jProcessName = args[1];
String agentPath = args[2];
for (VirtualMachineDescriptor vmd : list) {
if (vmd.displayName().equals(jProcessName)) {
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
virtualMachine.loadAgent(agentPath);
}
}
}
}
}
列出正在運(yùn)行的Java應(yīng)用程序:
進(jìn)行attach:
Weblogic輸出:
假如在進(jìn)行Weblogic t3反序列化利用時(shí),如果某個(gè)類之前沒(méi)有被加載,但是能夠被Weblogic找到,那么利用時(shí)對(duì)應(yīng)的類會(huì)通過(guò)Agent的transform,但是有些類雖然在Weblogic目錄下的某些Jar包中,但是weblogic不會(huì)去加載,需要一些特殊的配置Weblogic才會(huì)去尋找并加載。
Instrumentation局限性
大多數(shù)情況下,使用Instrumentation都是使用其字節(jié)碼插樁的功能,籠統(tǒng)說(shuō)是類重轉(zhuǎn)換的功能,但是有以下的局限性:
實(shí)際中遇到的限制可能不止這些,遇到了再去解決吧。如果想要重新定義一全新類(類名在已加載類中不存在),可以考慮基于類加載器隔離的方式:創(chuàng)建一個(gè)新的自定義類加載器去通過(guò)新的字節(jié)碼去定義一個(gè)全新的類,不過(guò)只能通過(guò)反射調(diào)用該全新類的局限性。
小結(jié)
代碼放到了github上,有興趣的可以去測(cè)試下,注意pom.xml文件中的jdk版本,在切換JDK測(cè)試如果出現(xiàn)錯(cuò)誤,記得修改pom.xml里面的JDK版本。
參考
1.https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html
2.https://paper.seebug.org/513/#0x01-rasp
3.https://paper.seebug.org/1041/#31-java-agent
5.https://www.cnblogs.com/rickiyang/p/11368932.html
6.https://c0d3p1ut0s.github.io/%E4%B8%80%E7%B1%BBPHP-RASP%E7%9A%84%E5%AE%9E%E7%8E%B0/