如何使用 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
