如何使用 IDEA 遠程 Debug 功能調試 DongTai-agent-java
一、遠程 Debug 原理
首先,Java程序的執行過程分為以下幾個步驟:Java的文件 > 編譯生成的類文件(class文件)> JVM加載類文件 > JVM運行類字節碼文件 > JVM翻譯器翻譯成各個機器認識的不同的機器碼。Java 程序是運行在Java 虛擬機上的,具有良好跨平臺性,是因為Java程序統一以字節碼的形式在JVM中運行,不同平臺的虛擬機都統一使用這種相同的程序存儲格式。因為都是類字節碼文件,只要本地代碼和遠程服務器上的類文件相同,兩個 JVM 通過調試協議進行通信。另外需要注意的是,被調試的服務器需要開啟調試模式,服務器端的代碼和本地代碼必須保持一致,則會造成斷點無法進入的問題。
總結:Java 遠程調試的原理是兩個JVM之間通過debug協議進行通信,然后以達到遠程調試的目的。兩者之間可以通過socket進行通信。
二、編譯打包 DongTai-agent-java
1.Fork DongTai-agent-java[1] 項目到自己的github倉庫

2.將項目 clone 到本地

3.進入 DongTai-agent-java 根目錄,執行打包命令。
& mvn clean package -Dmaven.test.skip=true
注:jdk 版本為1.8。
4.打包結束后項目根目錄下會生成文件夾 release,其目錄結構:
release
├── iast-agent.jar
└── lib
├── dongtai-servlet.jar
├── iast-core.jar
└── iast-inject.jar
5.使用 IDEA 打開要使用 agent 啟動的測試項目(本篇文章以自建測試項目 SpringTest 為例),將這四個 jar 包添加到項目 Libraries 中。

三、IDEA 配置遠程 Debug
1.在 Run/Debug Configurations 中配置遠程 Debug 啟動項
打開Inteliij IDEA,頂部菜單欄選擇Run-> Edit Configurations,進入下圖的運行/調試配置界面。

點擊左上角“+”號,選擇 Remote JVM Debug。分別填寫右側三個紅框中的參數:Name,Host(想要指定的遠程調試端口)。
Host:運行該項目的遠程IP
Port:遠程 IP 的端口
Command:遠程主機在啟動 Java 應用時需要添加的參數

2.配置 springtest 的啟動命令
$ java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -javaagent:/path/to/agent.jar -jar springtest.jar
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005:Remote Debug 配置的 JVM 參數
-javaagent:/path/to/agent.jar:被遠程 Debug 的DongTAi-iast-agent
springtest.jar:將測試項目 springtest 打包,使用該 jar 包啟動項目
三、遠程 Debug
1.在 IDEA 中打斷點
在剛剛引入了四個 jar 包中打斷點,以 iast-core 的transform()方法為例。
transform() 方法會在類文件被加載時調用,在 transform 方法里,我們可以對傳入的二進制字節碼進行改寫或替換,生成新的字節碼數組后返回,JVM 會使用 transform 方法返回的字節碼數據進行類的加載。

2.運行項目 springtest,然后在 IDEA 中點擊 debug
使用上面配置的啟動命令運行 springtest

在 IDEA 中查看到斷點信息,遠程 Debug 成功

四、通過 Debug 探索 transform 方法
DongTai-agent-java 對應用的每個類進行字節碼插樁:
public byte[] transform(ClassLoader loader, String internalClassName, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] srcByteCodeArray) {
boolean isRunning = EngineManager.isLingzhiRunning();
if (isRunning) {
EngineManager.turnOffLingzhi();
}
StopWatch clock = null;
if (this.logger.isDebugEnabled()) {
clock = new StopWatch();
clock.start();
}
try {
CodeSource codeSource = protectionDomain != null ? protectionDomain.getCodeSource() : null;
if (codeSource != null && internalClassName != null && !internalClassName.startsWith("com/secnium/iast/")) {
this.COMMON_UTILS.scanCodeSource(codeSource);
}
if (ConfigMatcher.isHookPoint(internalClassName, loader)) {
byte[] sourceCodeBak = new byte[srcByteCodeArray.length];
System.arraycopy(srcByteCodeArray, 0, sourceCodeBak, 0, srcByteCodeArray.length);
ClassReader cr = new ClassReader(sourceCodeBak);
int flags = cr.getAccess();
int targetClassLoaderObjectID = ObjectIDs.instance.identity(loader);
String[] interfaces = cr.getInterfaces();
String superName = cr.getSuperName();
String className = cr.getClassName();
this.COMMON_UTILS.setLoader(loader);
this.COMMON_UTILS.saveAncestors(className, superName, interfaces);
HashSet<String> ancestors = this.COMMON_UTILS.getAncestors(className, superName, interfaces);
ClassWriter cw = this.createClassWriter(loader, cr);
ClassVisitor cv = this.PLUGINS.initial(cw, IastContext.build(className, className, ancestors, interfaces, superName, flags, sourceCodeBak, codeSource, loader, this.listenerId, this.namespace, targetClassLoaderObjectID));
if (cv instanceof AbstractClassVisitor) {
cr.accept(cv, 8);
AbstractClassVisitor dumpClassVisitor = (AbstractClassVisitor)cv;
if (dumpClassVisitor.hasTransformed()) {
++this.transformClassCount;
if (this.logger.isDebugEnabled() && null != clock) {
clock.stop();
this.logger.debug("conversion class {} is successful, and it takes {}ms, total {}.", new Object[]{internalClassName, clock.getTime(), this.transformClassCount});
}
byte[] var20 = this.dumpClassIfNecessary(cr.getClassName(), cw.toByteArray(), srcByteCodeArray);
return var20;
}
} else if (this.logger.isDebugEnabled() && null != clock) {
clock.stop();
this.logger.debug("failed to convert the class {}, and it takes {} ms", internalClassName, clock.getTime());
}
}
} catch (Throwable var24) {
ErrorLogReport.sendErrorLog(ThrowableUtils.getStackTrace(var24));
} finally {
if (isRunning) {
EngineManager.turnOnLingzhi();
}
}
return srcByteCodeArray;
}
1.transform 方法參數、返回值的意義
? loader:ClassLoader類對象,類加載器,將class文件加載到jvm虛擬機中去。
? internalClassName:被掃描類的類名
? classBeingRedefined:要重定義的類所對應的 Class 對象
? protectionDomain:定義權限,ProtectionDomain類封裝了域的特征,該域包含一組類,這些類的實例在代表給定的Principal集執行時被授予一組權限
? srcByteCodeArray:被掃描類的原始字節碼
? return:如果從 transform 方法中return null 的話,將會告訴運行時環境我們并沒有對這個類進行變更。如果要修改類的字節的話,需要在 transform 中提供字節碼操縱的邏輯并 return 修改后的字節。
2.DongTai-agent-java 中 transform 方法對每個類做了什么
? this.COMMON_UTILS.scanCodeSource(codeSource) :對每個類所依賴的 jar 包進行掃描,并將信息發送至洞態IAST云端,在云端對這些 jar 包進行掃描(在云端稱為應用組件),將有安全漏洞的組件進行展示并提示該組件的安全版本。
?if (ConfigMatcher.isHookPoint(internalClassName, loader)) :在對類進行 HOOK 前需要判斷該類是否在DongTai-agent-java 自定義的 HOOK 黑名單中,以下類會出現在 HOOK 黑名單:
? agent自身的類
? 已知的框架類、中間件類
? 類名為null
? JDK內部類且不在hook點配置白名單中
? 接口
? 將不在黑名單中的類進行 HOOK,將信息發送至洞態IAST云端
五、總結
遠程 Debug 不僅為研發人員在編寫、調優、測試 DongTai-agent-java 提供方便,也為想要了解 DongTai-agent-java 的同學對其實現原理提供極大地便利,歡迎對該技術感興趣的同學進行嘗試。
?DongTai-agent-java[2]
?DongTai[3]
References
[1] DongTai-agent-java: https://github.com/HXSecurity/DongTai-agent-java
[2] DongTai-agent-java: https://github.com/HXSecurity/DongTai-agent-java
[3] DongTai: https://github.com/HXSecurity/DongTai