引言
相信大家對 IAST(Interactive Application Security Testing,交互式應(yīng)用程序安全測試) 和 RASP(Runtime application self-protection,運行時應(yīng)用自我保護)這兩款產(chǎn)品已經(jīng)不陌生了,那究竟是什么神仙技術(shù)能衍生出這么牛皮的安全產(chǎn)品?
帶著疑問,我開始了這一次”修行“。

0x01 簡介
在Java SE 5及后續(xù)版本中,開發(fā)者可以構(gòu)建一個獨立于應(yīng)用程序的代理程序(Agent),用來監(jiān)測和協(xié)助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。
利用這一特性衍生出了 IAST(Interactive Application Security Testing,交互式應(yīng)用程序安全測試) 和 RASP(Runtime application self-protection,運行時應(yīng)用自我保護)等相關(guān)安全產(chǎn)品。
-
問題:
- Java Agent 如何調(diào)試呢?
- Java Agent 實現(xiàn)原理是什么?
0x02 加載方式
在官方API文檔中提到,Java Agent 有兩種加載方式:
(1)premain, 當以指示代理類的方式啟動JVM時。 在這種情況下, Instrumentation實例被傳遞給代理類的premain方法。(-javaagent 啟動)
(2)agentmain, 當JVM在JVM啟動后的某個時間提供啟動代理的機制時。 在這種情況下, Instrumentation實例將傳遞給代理代碼的agentmain方法。(利用attach api,動態(tài)啟動)
premain 與 agentmain 的區(qū)別:
運行模式不同:
premain 相當于在main前類加載時進行字節(jié)碼修改,而agentmain則是main后在類調(diào)用前通過重新轉(zhuǎn)換類完成字節(jié)碼修改。
部署方式不同:
由于加載方式不同,所以premain只能在程序啟動時指定Agent文件進行部署,而agentmain需要通過Attach API在程序運行后根據(jù)進程ID動態(tài)注入agent到j(luò)vm中。
0x03 編譯構(gòu)建
(1)那如何構(gòu)建一個Java Agent 呢?創(chuàng)建一個新項目并新建一個MyAgent類:
public class MyAgent {
/**
* premain jvm 參數(shù)形式啟動,運行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premainAgent Start");
}
/**
* agentmain 動態(tài) attach 方式啟動,運行此方法
*
* @param agentArgs
* @param inst
*/
public static void agentmain(String agentArgs, Instrumentation inst){
System.out.println("agentmainAgent Start");
}
}
(2)利用maven插件,將代碼打包為jar包,通常有兩種方式:
a. Pom 指定配置
在 pom.xml 文件中,添加如下配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.bug1024.MyAgent</Premain-Class>
<Agent-Class>com.bug1024.MyAgent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
在配置的打包參數(shù)中,通過manifestEntries的方式添加屬性到MANIFEST.MF文件中,解釋下里面的幾個參數(shù):
- Premain-Class:包含
premain方法的類,需要配置為類的全路徑 - Agent-Class:包含
agentmain方法的類,需要配置為類的全路徑 - Can-Redefine-Classes:為
true時表示能夠重新定義Class - Can-Retransform-Classes:為
true時表示能夠重新轉(zhuǎn)換Class,實現(xiàn)字節(jié)碼替換 - Can-Set-Native-Method-Prefix:為
true時表示能夠設(shè)置native方法的前綴
配置完成后使用mvn命令打包:
mvn clean package
打包完成后生成AgentTest-1.0-SNAPSHOT.jar文件,解壓jar文件我們可以看到生成的MANIFEST.MF文件:
Manifest-Version: 1.0
Premain-Class: com.bug1024.MyAgent
Built-By: 07
Agent-Class: com.bug1024.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_261
此時,第一種打包方式就完成了。
b. MANIFEST.MF 配置文件
通過配置文件MANIFEST.MF打包的方式也比較常見,操作如下:
1. 在資源目錄(resources)下,新建目錄`META-INF`
2. 在`META-INF`目錄下,新建文件`MANIFEST.MF`
文件內(nèi)容可以直接復制我們上述內(nèi)容,然后在pom.xml配置,做對應(yīng)的修改,如下:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestFile>
src/main/resources/META-INF/MANIFEST.MF
</manifestFile>
<!-- <manifestEntries>-->
<!-- <Premain-Class>com.bug1024.MyAgent</Premain-Class>-->
<!-- <Agent-Class>com.bug1024.MyAgent</Agent-Class>-->
<!-- <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!-- <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!-- <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>-->
<!-- </manifestEntries>-->
</archive>
</configuration>
</plugin>
</plugins>
</build>
同樣通過mvn clean package打包即可。
(3)Agent 打包完成后,接下來開始對兩種加載方式進行調(diào)試。
a. premain jvm 參數(shù)形式啟動 ,新建一個demo運行HelloWorld程序,代碼示例如下:
public class Demo {
public void say() throws InterruptedException {
for (int i = 0; i < 7; i++) {
Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時并循環(huán)
System.out.println("Hello World");
}
}
public static void main(String[] args) throws InterruptedException {
Demo d = new Demo();
d.say();
}
}
程序運行結(jié)果:
Hello World
Hello World
Hello World
...
添加jvm參數(shù)啟動:
-javaagent:/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar
程序運行結(jié)果:
premainAgent Start
Hello World
注意此時我們上面MyAgent中的premain方法已經(jīng)執(zhí)行,并輸出了premainAgent Start 表示我們第一種加載方式執(zhí)行成功。
b. agentmain 動態(tài) attach 方式啟動
在上文也有提到過agentmain需要通過Attach API在程序運行后根據(jù)進程ID動態(tài)注入agent到j(luò)vm中,我們利用VirtualMachine的attach方法連接目標虛擬機,代碼如下:
public class AttachMain {
public void attachAgent() throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
List<VirtualMachineDescriptor> vm_list = VirtualMachine.list();
for(VirtualMachineDescriptor v : vm_list){ // 遍歷程序列表
if (v.displayName().equals("com.bug1024.Demo")){ // 判斷我們要注入的程序
VirtualMachine vm = VirtualMachine.attach(v.id()); // 獲取目標程序進程id并根據(jù)進程id連接目標程序
vm.loadAgent("/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar"); //加載Agent
}
}
}
public static void main(String[] args) throws AgentLoadException, IOException, AttachNotSupportedException, AgentInitializationException {
AttachMain attach = new AttachMain();
attach.attachAgent();
}
}
運行我們的Demo測試類后運行AttachMain輸出如下結(jié)果:
premainAgent Start
Hello World
Hello World
Hello World
agentmainAgent Start
Hello World
...
注意此時我們上面MyAgent中的agentmain方法已經(jīng)執(zhí)行,并輸出了agentmainAgent Start 表示我們第二種加載方式也執(zhí)行成功。
(4)小結(jié)
上述內(nèi)容描述了JavaAgent打包調(diào)試全過程,兩種agent加載方式:
| 加載方式 | 說明 | 操作說明 |
|---|---|---|
| premain() | agent以jvm方式加載時調(diào)用,在目標應(yīng)用啟動時指定agent | -javaagent:/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar |
| agentmain() | agent以attach方式運行時調(diào)用,在目標程序啟動后,通過attach api 注入agent | VirtualMachine vm = VirtualMachine.attach(v.id()); // 獲取目標程序進程id并根據(jù)進程id連接目標程序 |
| vm.loadAgent("/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar"); //加載Agent |
兩種打包方式:
- 在pom.xml中指定配置
- 在配置文件
META-INF/MANIFEST.MF中配置
那JavaAgent實現(xiàn)原理是什么?安全產(chǎn)品是如何利用的呢?繼續(xù)往下探索。
0x04 Instrumentation & ASM 實現(xiàn)簡單AOP
Java Agent 是通過使用Instrumentation構(gòu)建出來的一個獨立于應(yīng)用程序的代理程序,用來監(jiān)測和協(xié)助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。
Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。
在 Java SE 6 里面,instrumentation 包被賦予了更強大的功能:啟動后的 instrument、本地代碼(native code)instrument,以及動態(tài)改變 classpath 等等。這些改變,意味著 Java 具有了更強的動態(tài)控制、解釋能力,它使得 Java 語言變得更加靈活多變。
java.lang.instrument包結(jié)構(gòu)【官方API】:
- ClassFileTransformer 接口
// 轉(zhuǎn)換類文件的代理接口,我們可以在獲取到Instrumentation對象后通過addTransformer方法添加自定義類文件轉(zhuǎn)換器。
public interface ClassFileTransformer {
/**
* 類文件轉(zhuǎn)換方法,重寫transform方法可獲取到待加載的類相關(guān)信息
*
* @param loader 定義要轉(zhuǎn)換的類加載器;如果是引導加載器,則為 null
* @param className 類名,如:java/lang/Runtime
* @param classBeingRedefined 如果是被重定義或重轉(zhuǎn)換觸發(fā),則為重定義或重轉(zhuǎn)換的類;如果是類加載,則為 null
* @param protectionDomain 要定義或重定義的類的保護域
* @param classfileBuffer 類文件格式的輸入字節(jié)緩沖區(qū)(不得修改)
* @return 返回一個通過ASM修改后添加了防御代碼的字節(jié)碼byte數(shù)組。
*/
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
}
- Instrumentation 接口
/**
* 注冊一個Transformer,從此之后的類加載都會被Transformer攔截。
* Transformer可以直接對類的字節(jié)碼byte[]進行修改
*/
void addTransformer(ClassFileTransformer transformer);
/**
* 對JVM已經(jīng)加載的類重新觸發(fā)類加載。使用的就是上面注冊的Transformer。
* retransformation可以修改方法體,但是不能變更方法簽名、增加和刪除方法/類的成員屬性
*/
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
/**
* 獲取一個對象的大小
*/
long getObjectSize(Object objectToSize);
/**
* 將一個jar加入到bootstrap classloader的 classpath里
*/
void appendToBootstrapClassLoaderSearch(JarFile jarfile);
/**
* 獲取當前被JVM加載的所有類對象
*/
Class[] getAllLoadedClasses();
以上是幾個比較常用的方法,其他可詳細看官方API文檔;addTransformer 方法配置后,后續(xù)的類加載會被Transformer攔截。對于已經(jīng)加載過的類,可以執(zhí)行retransformClasses來重新觸發(fā)Transformer攔截。
類加載的字節(jié)碼被修改后,除非再次被retransform,否則不會恢復。
結(jié)合上面的描述,我們要通過ASM實現(xiàn)簡單AOP操作,肯定是要利用Transformer對類進行攔截后操作,代碼示例如下:
/**
* premain jvm 參數(shù)形式啟動,運行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst){
System.out.println("premainAgent Start");
// Class<?>[] classes = inst.getAllLoadedClasses();
// for (Class<?> cls : classes){
// System.out.println("premainAgent get loaded class : " + cls.getName());
// }
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("premainAgent get loaded class : " + className);
return classfileBuffer;
}
});
}
運行結(jié)果:
remainAgent Start
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$1
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$Cache
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$2
premainAgent get loaded class : sun/misc/URLClassPath$JarLoader$2
premainAgent get loaded class : java/util/jar/Attributes
premainAgent get loaded class : java/util/jar/Manifest$FastInputStream
premainAgent get loaded class : java/util/jar/Attributes$Name
premainAgent get loaded class : sun/misc/ASCIICaseInsensitiveComparator
premainAgent get loaded class : com/intellij/rt/execution/application/AppMainV2$Agent
premainAgent get loaded class : com/intellij/rt/execution/application/AppMainV2
premainAgent get loaded class : java/lang/NoSuchMethodException
premainAgent get loaded class : java/lang/reflect/InvocationTargetException
...
獲取加載的類之后,怎么實現(xiàn)簡單AOP操作呢?我們需要修改字節(jié)碼來實現(xiàn),常用的字節(jié)碼修改工具主要有ASM、Javassist和byte buddy,下面主要已ASM框架來實現(xiàn)需求。
ASM 簡介:
ASM 是一個 Java 字節(jié)碼操控框架。它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。
ASM 實現(xiàn)簡單AOP:
由于 ASM 是直接對class文件的字節(jié)碼進行操作,因此,要修改class文件內(nèi)容時,也要注入相應(yīng)的java字節(jié)碼。所以,在注入字節(jié)碼之前,我們還需要了解下class文件的結(jié)構(gòu),JVM指令等知識。(還沒搞明白就不展開寫了,直接貼代碼)
為了方便測試,在Demo測試類新增了exp()方法:
public void say() throws InterruptedException {
for (int i = 0; i < 2; i++) {
Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時循環(huán)
String say_str = "普通方法---say()---不操作";
System.out.println(say_str);
}
}
public void exp() throws InterruptedException {
for (int i = 0; i < 2; i++) {
Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時循環(huán)
String exp_str = "目標方法---exp()---攔截并退出";
System.out.println(exp_str);
}
}
目標是當程序運行到exp()方法時做出相應(yīng)操作,下面以premain加載方式為例進行調(diào)試;
/**
* premain jvm 參數(shù)形式啟動,運行此方法
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("premainAgent Start");
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
if (className.equals("com.bug1024.Demo")) {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if ("exp".equals(name)) {
return new MethodVisitor(Opcodes.ASM5, mv) {
@Override
public void visitCode() {
// 文章下面單獨展示
}
};
}
return mv;
}
};
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
classfileBuffer = classWriter.toByteArray();
}
return classfileBuffer;
}
});
}
同樣利用Transformer進行攔截,正常情況將Transform抽出來單獨寫邏輯比較好,這里為了方便采用的流式寫法。
整個流程如下:
- 根據(jù)
className來判斷當前agent攔截的類是否要hook,如果是進入ASM修改流程; - 使用ASM提供的
ClassReader類對字節(jié)碼進行讀取&遍歷,然后新建一個ClassWriter對ClassReader讀取的字節(jié)碼進行拼接;- 在
ClassVisitor中調(diào)用visitMethod方法訪問hook類中的每個方法,根據(jù)方法名判斷是否為需要hook的方法,如果是,則調(diào)用visitCode方法實現(xiàn)后續(xù)邏輯,在目標方法exp()前插入輸出代碼并輸出:“目標方法即將運行”;
- 在
@Override
public void visitCode() {
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC,
Type.getInternalName(System.class),
"out",
Type.getDescriptor(PrintStream.class));
mv.visitLdcInsn("目標方法即將運行");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
Type.getInternalName(PrintStream.class), //"java/io/PrintStream"
"println",
"(Ljava/lang/String;)V",//方法描述符
false);
mv.visitEnd();
super.visitCode();
}
實現(xiàn)效果如下:
premainAgent Start
普通方法---say()---不操作
普通方法---say()---不操作
目標方法即將運行
目標方法---exp()---攔截并退出
目標方法---exp()---攔截并退出
當然我們也可以在目標方法運行前插入其他方法的調(diào)用,代碼示例:
// 新建在目標法發(fā)前要執(zhí)行的方法
public static void Test() {
System.out.println("攔截目標方法并退出程序");
System.exit(0);
}
// 修改visitCode()方法,添加調(diào)用Test()方法的邏輯
@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(Opcodes.INVOKESTATIC,MyAgent.class.getName().replace(".","/"),"Test","()V",false);
mv.visitEnd();
super.visitCode();
}
運行結(jié)果如下:
premainAgent Start
普通方法---say()---不操作
普通方法---say()---不操作
攔截目標方法并退出程序
如上述結(jié)果,當程序執(zhí)行到目標方法exp()時直接退出不在往下進行,這樣一次簡單的AOP就實現(xiàn)了。
0x05 小結(jié)
在這次學習過程中發(fā)現(xiàn)不光是IAST 還是 RASP 中使用這種技術(shù),其實在安全防御實踐中很多場景都可以嘗試使用AOP技術(shù)去解決,如:日志審計、權(quán)限控制等等。(下圖為螞蟻集團的安全切面防御體系 - 安全切面:安全防御的平行空間)
當然,如果我們要在Java中實現(xiàn)這些技術(shù),還是要好好了解下class文件結(jié)構(gòu)、jvm命令、ASM等相關(guān)知識。

參考鏈接:
https://www.bilibili.com/video/av841675771/
https://my.oschina.net/ta8210/blog/162796
http://www.itdecent.cn/p/a85e8f83fa14
http://www.itdecent.cn/p/abd1b1b8d3f3
https://mp.weixin.qq.com/s/qZDvset94O2_2G-NTIvQUg
https://paper.seebug.org/1041/