如何使用 IDEA 远程 Debug 功能调试 DongTai-agent-java
一、远程 Debug 原理
首先,Java程序的执行过程分为以下几个步骤:Java的文件 > 编译生成的类文件(class文件)> JVM加载类文件 > JVM运行类字节码文件 > JVM翻译器翻译成各个机器认识的不同的机器码。Java 程序是运行在Java 虚拟机上的,具有良好跨平台性,是因为Java程序统一以字节码的形式在JVM中运行,不同平台的虚拟机都统一使用这种相同的程序存储格式。因为都是类字节码文件,只要本地代码和远程服务器上的类文件相同,两个 JVM 通过调试协议进行通信。另外需要注意的是,被调试的服务器需要开启调试模式,服务器端的代码和本地代码必须保持一致,则会造成断点无法进入的问题。
总结:Java 远程调试的原理是两个JVM之间通过debug协议进行通信,然后以达到远程调试的目的。两者之间可以通过socket进行通信。
二、编译打包 DongTai-agent-java
Fork DongTai-agent-java 项目到自己的github仓库

将项目 clone 到本地

进入 DongTai-agent-java 根目录,执行打包命令。
& mvn clean package -Dmaven.test.skip=true
注:jdk 版本为1.8。
打包结束后项目根目录下会生成文件夹 release,其目录结构:
release
├── iast-agent.jar
└── lib
├── dongtai-servlet.jar
├── iast-core.jar
└── iast-inject.jar
使用 IDEA 打开要使用 agent 启动的测试项目(本篇文章以自建测试项目 SpringTest 为例),将这四个 jar 包添加到项目 Libraries 中。

三、IDEA 配置远程 Debug
在 Run/Debug Configurations 中配置远程 Debug 启动项
打开Inteliij IDEA,顶部菜单栏选择Run-> Edit Configurations,进入下图的运行/调试配置界面。

点击左上角“+”号,选择 Remote JVM Debug。分别填写右侧三个红框中的参数:Name,Host(想要指定的远程调试端口)。
Host:运行该项目的远程IP
Port:远程 IP 的端口
Command:远程主机在启动 Java 应用时需要添加的参数

配置 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
在 IDEA 中打断点
运行项目 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;
}
- transform 方法参数、返回值的意义
- loader:ClassLoader类对象,类加载器,将class文件加载到jvm虚拟机中去。
- internalClassName:被扫描类的类名
- classBeingRedefined:要重定义的类所对应的 Class 对象
- protectionDomain:定义权限,ProtectionDomain类封装了域的特征,该域包含一组类,这些类的实例在代表给定的Principal集执行时被授予一组权限
- srcByteCodeArray:被扫描类的原始字节码
- return:如果从 transform 方法中return null 的话,将会告诉运行时环境我们并没有对这个类进行变更。如果要修改类的字节的话,需要在 transform 中提供字节码操纵的逻辑并 return 修改后的字节。
- 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 的同学对其实现原理提供极大地便利,欢迎对该技术感兴趣的同学进行尝试。