English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french
查看: 1|回复: 0

Java字节码增强实际应用在哪些方面?

[复制链接]
查看: 1|回复: 0

Java字节码增强实际应用在哪些方面?

[复制链接]
查看: 1|回复: 0

207

主题

0

回帖

631

积分

高级会员

积分
631
JjgcxGXuMRJA

207

主题

0

回帖

631

积分

高级会员

积分
631
2025-3-3 10:52:57 | 显示全部楼层 |阅读模式
Java字节码增强由于与业务应用耦合性较低,且可任意修改程序代码,所以在许多方面都有应用。也是许多公司产品实现的基础。下面大概分类一下:
1、在可观测和监控方面的应用

如果一个应用的架构服务之间的依赖关系非常简单,我们只需要关注后端服务的运行指标即可掌握整个系统的工作状态,通过监控不同指标就可以很好的解决。可用监控工具,这些监控工具实时监控IO、线程、内存、GC等情况。监控大多会使用JMX来获取流式数据。
随着云原生和微服务的理念不断推广和落地,传统的单体应用开始变得复杂,单体应用的功能被拆分到多个服务中,我们上面的单体应用架构开始变得复杂起来,原来的单体应用可能会被拆分成网关、认证、注册等多个服务,系统的一个请求会流转到多个服务上进行处理,系统中的各个模块之间会相互进行影响。所以性能的排查和诊断变的越来越难。现在假设在云原生和微服务状态下,一个订单请求变慢了,那么我们应该如何查看到底是哪里出现了问题呢?APM应用而生。
在APM(Application Performance Managment,应用性能管理)上有个拓扑图,可以准确识别各种类型的服务,比如Redis、MySQL,SpringBoot等,而且还能给出依赖关系和数据流向等信息。当我们要排查一个请求为什么变慢时,可以拿着这个请求的唯一id去APM的链路追踪上查找到单次请求所依次经过的所有服务,在这些服务上的耗时,落下的相关日志,还有一些请求地址等,这样就能定位出是哪个服务拖慢了速度。链路追踪如下图所示。

现在的问题是,各个服务之间并没有入口和出口的标准实现,我们只能通过字节码增强或过滤器、拦截器等来实现。不过字节码增强相比较来说更好一些,在实际过程也使用的多一些。
在字节码增强的过程中,许多服务的入口和出口需要自己去找,找到以后按链路追踪要求的格式填充相应信息,返回也是,这样才能正确串连起各个服务。 
APM 就是一类典型的基于字节码增强技术实现的性能检测工具,例如 

  • 程序运行时性能指标(CPU、内存等指标)可通过Java.lang.Runtime、java.lang.Management中的方法采集;
  • sql执行耗时情况,可以通过监控 JDBC 的实现函数的耗时情况(在字节码中插入时间统计的代码,并收集),就可以获取系统中的慢sql 等;
  • 服务跟踪,一般是在请求入口,拦截每次服务请求,在请求中加入标识符,记录一次完整的Trace各阶段的执行时间。
2、在剖析工具上的应用

监控和可观测工具是线上使用,而且24小时伴随着业务,所以要特别注意性能。在上面介绍的链路追踪时,只是简单的追踪每个微服务的入口和出口,当发现某个服务出现问题时,才需要根据需求进一步启动更为耗时、且可能严重干扰用户程序的剖析工具。
假设现在发现某个服务节点很慢,但是我们并不知道这个节点服务为什么慢,到底慢在哪里,所以要进一步排查。这时候就会用到剖析工具,有名的剖析工具有GCEasy,MAT、Async-Profiler、Arthas、商用的XRebel、JProfiler,YourKit等。
下面简单介绍几个剖析工具。
2.1 Arthas

Arthas是整个开源Java项目中Star数比较多的之一,也可见其受欢迎的程度。在Arthas项目中提供了 trace、monitor和watch命令,这些命令就是基于ASM通过字节码增强实现的。
(1)trace命令能主动搜索class-pattern/method-pattern 对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。实例如下:

 (2)watch让你能方便的观察到指定函数的调用情况。能观察到的范围为:返回值、抛出异常、入参,通过编写 OGNL 表达式进行对应变量的查看。实例如下:

 (3)monitor对匹配class-pattern/method-pattern/condition-express的类、方法的调用进行监控。实例如下:

更多的实际执行实例:
 
# watch观测执行的查询SQL,-x 3指定对象展开层级[arthas@3368243]$ watch org.apache.ibatis.executor.statement.PreparedStatementHandler parameterize '{target.boundSql.sql,target.boundSql.parameterObject}' -x 3method=org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize location=AtExitts=2021-11-13 14:50:34; [cost=0.071342ms] result=@ArrayList[    @String[select id,log_info,create_time,update_time,add_time from app_log where id=?],    @ParamMap[        @String[id]:@Long[41115],        @String[param1]:@Long[41115],    ],] # watch观测耗时超过200ms的SQL[arthas@3368243]$ watch com.mysql.jdbc.PreparedStatement execute '{target.toString()}' 'target.originalSql.contains("select") && #cost > 200' -x 2Press Q or Ctrl+C to abort.Affect(class count: 3 , method count: 1) cost in 123 ms, listenerId: 25method=com.mysql.jdbc.PreparedStatement.execute location=AtExitts=2021-11-13 14:58:42; [cost=1001.558851ms] result=@ArrayList[    @String[com.mysql.jdbc.PreparedStatement@6283cfe6: select count(*) from app_log],] # trace追踪方法耗时,层层追踪,就可找到耗时根因,--skipJDKMethod false显示jdk方法耗时,默认不显示[arthas@3368243]$ trace com.mysql.jdbc.PreparedStatement execute 'target.originalSql.contains("select") && #cost > 200'  --skipJDKMethod falsePress Q or Ctrl+C to abort.Affect(class count: 3 , method count: 1) cost in 191 ms, listenerId: 26---ts=2021-11-13 15:00:40;thread_name=http-nio-8080-exec-47;id=76;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@5a2d131d    ---[1001.465544ms] com.mysql.jdbc.PreparedStatement:execute()        +---[0.022119ms] com.mysql.jdbc.PreparedStatement:checkClosed() #1274        +---[0.016294ms] com.mysql.jdbc.MySQLConnection:getConnectionMutex() #57        +---[0.017862ms] com.mysql.jdbc.PreparedStatement:checkReadOnlySafeStatement() #1278        +---[0.008996ms] com.mysql.jdbc.PreparedStatement:createStreamingResultSet() #1294        +---[0.010783ms] com.mysql.jdbc.PreparedStatement:clearWarnings() #1296        +---[0.017843ms] com.mysql.jdbc.PreparedStatement:fillSendPacket() #1316        +---[0.008543ms] com.mysql.jdbc.MySQLConnection:getCatalog() #1320        +---[0.009293ms] java.lang.String:equals() #57        +---[0.008824ms] com.mysql.jdbc.MySQLConnection:getCacheResultSetMetadata() #1328        +---[0.009892ms] com.mysql.jdbc.MySQLConnection:useMaxRows() #1354        +---[1001.055229ms] com.mysql.jdbc.PreparedStatement:executeInternal() #1379        +---[0.02076ms] com.mysql.jdbc.ResultSetInternalMethods:reallyResult() #1388        +---[0.011517ms] com.mysql.jdbc.MySQLConnection:getCacheResultSetMetadata() #57        +---[0.00842ms] com.mysql.jdbc.ResultSetInternalMethods:getUpdateID() #1404        ---[0.008112ms] com.mysql.jdbc.ResultSetInternalMethods:reallyResult() #1409 # stack追踪方法调用栈,找到耗时SQL来源[arthas@3368243]$ stack com.mysql.jdbc.PreparedStatement execute 'target.originalSql.contains("select") && #cost > 200'Press Q or Ctrl+C to abort.Affect(class count: 3 , method count: 1) cost in 138 ms, listenerId: 27ts=2021-11-13 15:01:55;thread_name=http-nio-8080-exec-5;id=2d;is_daemon=true;priority=5;TCCL=org.springframework.boot.web.embedded.tomcat.TomcatEmbeddedWebappClassLoader@5a2d131d    @com.mysql.jdbc.PreparedStatement.execute()        at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:493)        at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:63)        at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)        at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)        at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:326)        at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)        at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:136)        at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)        at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)        at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)        at sun.reflect.GeneratedMethodAccessor75.invoke(null:-1)        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)        at java.lang.reflect.Method.invoke(Method.java:498)        at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)        at com.sun.proxy.$Proxy113.selectOne(null:-1)        at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)        at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:83)        at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)        at com.sun.proxy.$Proxy119.selectCost(null:-1)        at com.demo.example.web.controller.TestController.select(TestController.java:57)
 
2.2、XRebel

XRebel 是不间断运行在 web 应用的交互式分析器。可以看到网页上的每一个操作在前端以及服务端、数据库、网络传输都花费多少时间,当发现问题会在浏览器中显示警告信息。是调优程序、追踪性能问题的一大利器。
XRebel可以给出方法级别的调用链路及相关的耗时,和Arthas的trace非常相似,也是通过字节码增强来实现的,不过XRebel只针对Web应用,适配了各种框架和服务,用起来体验比较好。Arthas的trace可能要配合CPU的火焰图找到慢方法来使用了。
当我们开发单体Web应用时,可以不用做任何操作就能获取每次请求调用所经过的详细方法调用栈,而且也能给出单体Web应用依赖的服务(Redis、MySQL等)的耗时情况。如果要给出方法调用栈,那么一次请求所经过的方法非常多,XRebel会自动对不重要的方法忽略,看起来更容易定位问题。
假设APM能定准到某个服务节点响应时间慢,这时候有个类似XRebel的工具给出这个服务某次慢请求整个调用方法栈,并给出方法栈的耗时情况,那么问题就很可能锁定到方法级别的精度了。

可以拦截每个请求,给出每个请求调用的主要方法栈,每个方法的时间耗时和占比。还可以查看IO调用耗时,如果调用了MySQL服务,还能给出执行的SQL语句等信息。
3、在安全方面的应用

3.1、RASP

现在国家越来越重点信息安全,目前有许多公司在做Java生态的信息安全。Java技术栈漏洞目前业已是Web安全领域的主流战场,如果从事这个领域,那么你应该对RASP很熟悉,RASP(Runtime Application Self-Protection,运行时应用程序自我保护)是一种安全防护技术,运行在程序执行期间,使程序能够自我监控和识别有害的输入和行为。
前面介绍的通过Instrumentation API可以向程序注入任何代码,不法分子可以充分利用这一技术。Agent类型的内存马就是通过通过Instrumentation来完成的,动态的修改已加载到内存中的类里的方法,进而注入恶意的代码。部分RASP也会支持Agent类型的内存马检测,我们同样可以利用JavaAgent来动态Attach到虚拟机进行内存马的检测,有些内存马为了防止被检测到,可能还会破坏Attach API,不让内存马检测程序挂载。
不过内存马检测还可以用源代码,就是导出所有已加载类的字节码,还可以进一步反编译为源代码,不过这也是一种安全风险,因为源代码有泄漏的风险。
Log4j 代码注入的问题,带火了一段时间 RASP 工具,RASP工具,不需要用户修改代码重新部署服务,也不需要修改依赖的 log4j 的版本,只需要配置一下 RASP 的策略,就可以防范 Log4j 的注入问题,那么是怎么实现的呢?
当然是 Java 的字节码增强技术!RASP 工具,可以支持我们修改 Log4j 的有问题的函数所在的 class 文件,在函数的开头,配置参数的校验逻辑,如果出现 url,就直接失败返回,这样就不会再有相关的漏洞。
同样,可以在RASP中,防范各类漏洞,比如在 sql 执行时,校验执行的 sql 是否存在问题,执行的系统命令,会不会导致越权访问等。
这样,RASP 就可以帮助我们直接防护相关的漏洞,真正地在可能的问题点加了一个壳,甚至可以防范未知的漏洞。
举一个SQL注入的例子。
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。
例如这段代码就是在前端拿到数据不做任何处理进行查询的功能
SELECT * FROM test.user WHERE username='admin' and password='admin';
假定我们的账号密码均为admin,当我们输入账号:'or 1='1 密码任意填写即可登录。这时的sql命令是
SELECT * FROM test.user WHERE username='' or 1='1' and password='(任意密码)'
因为 or 1='1' ,致使不论密码是否正确,其验证都将通过;
现在我们要检测SQL注入,通常就是通过在SQL查询的语句上进行埋点,如statement.executeQuery方法,将进行查询的SQL语句进行语法分析和词法分析,能够判断用户是否造成了注入。这样即使Rasp的部署的应用使用了常规的查询语句,而未使用预编译来防止SQL注入,也可以进行拦截。其缺点就是,需要独立的语法库进行支持,如果攻击者使用的Payload无法被语法库匹配到,就会造成绕过。同时因为语法/词法分析和匹配也需要资源进行支持,会造成一定的开销。
基于Instrumentation实现的RASP防御原理还是通过在关键函数上埋点,监控其堆栈信息和参数内容,并通过相关检测算法来判断本次请求是否为攻击。
3.1、IAST或者 DAST

跟 RASP 功能非常相似,只是 RASP 是进行现网防护的,而 IAST 或者 DAST 是通过发送大量请求,查看我们通过字节码增强,在特定的危险API中,会不会存在没有防护的污点数据或者字符串传递进来。
IAST(Interactive Application Security Testing,交互式应用程序安全测试),通过服务端部署Agent探针,流量代理/VPN或主机系统软件等方式,监控Web应用程序运行时函数执行并与扫描器实时交互,高效、精准的安全漏洞,是一种运行时灰盒安全测试技术。在分析并评估了业界主流的几家IAST产品后,发现IAST一般可以提供Agent插桩检测、流量代理、流量信使等几种漏洞检测模式,其中除Agent插桩模式外,其余几种检测模式与DAST被动漏洞扫描的原理一致,所以本篇文章我们主要分析Agent插桩模式。
Agent插桩的检测模式,一般分为主动和被动模式。JavaAgent可以直接获取到类加载前的字节码,再结合一些字节码修改技术,从而通过修改字节码来增强函数功能。通过这种方式,我们可以获取类、方法的一些信息,比如类名、方法名、参数、返回值等,同时也可以做一些拦截操作。所以,这项技术通常被用于实现调用链监控、日志采集等组件,而在安全方向的应用,IAST和RASP则是典型例子。
在被动模式下,Agent可以动态获取一次请求的调用链、数据流等信息,基于获取的信息,IAST可以做一些基于污点追踪的白盒分析,从而检测该次请求的调用链中是否存在漏洞。如果没有做过滤,并且外部也可以随意控制参数的值,例如SQL注入,最终走到了JDBC上执行,那么就可以确定,当前是存在SQL注入漏洞的。
在主动模式下,Agent会hook一些危险函数,当一次请求触发到这些危险函数后,Agent会将该次请求发送给IAST server端,并使用DAST能力发送攻击payload做主动验证。基于这些特性,IAST非常适合融入到DevOps的测试环节,在业务测试完成正常功能逻辑测试工作的同时,无感知的进行一些安全漏洞的检测。

  • IAST被动检测模式
  • IAST主动检测模式
这里我们主要研究一下IAST被动检测模式的细节,该检测模式也是IAST首推的检测模式。在上文提到,在被动检测模式下,Agent主要用来做Web应用程序的数据采集并传至分析引擎,分析引擎做白盒分析。接下来,我们就实际的去实现一个Agent,对一次请求做调用链数据采集。
4、测试方面的应用

在自动化测试中,主要有流量录制和回放、Mock服务、生成单元测试等。
(1)流量录制回放是通过复制线上真实流量(录制)然后在测试环境进行模拟请求(回放)验证代码逻辑正确性。通过采集线上流量在测试环境回放逐一对比每个子调用差异和入口调用结果来发现接口代码是否存在问题。实现就是使用某种方式记录每个请求的入参和返回值,然后在测试环境使用参数重新发起请求,并验证请求结果。
(2)Mock服务,如果写一个微服务,可能依赖的其它服务非常多,联调非常麻烦,根本没办法做单体测试,或者有些服务可能测试环境根本不可用,此时就需要Mock这些依赖的服务了。比如Mock一个发送查询和接收的验证请求,这个请求并不需要真实发到Web服务并返回。目前针对Java的主流Mock工具有Mockito,Spock,PowerMock,JMockit等,然而这些工具存在以下问题:

  • Mockito与Spock均基于动态代理实现,但是不能Mock私有/静态和构造方法,并且使用复杂,学习成本高;
  • PowerMock是基于自定义类加载器进行实现,虽然可以对私有方法进行Mock,但同样存在易用性低的问题,不利于开发人员使用;
  • JMockit基于运行时字节码修改实现,无法针对Java构造方法进行Mock,并且对IDE支持性较低。
综上所述,目前Java开发测试过程中采用的Mock工具,具有无法Mock私有/静态和构造方法,Mock最小级别限于类,使用成本高,动态执行效率低等问题,大大降级项目开发速度与测试效率,因此,亟需一种可以克服上述缺陷的动态单元测试方案。 
(3)生成单元测试,记录线上一部分方法的入参和返回值,依据这些信息生成单元测试,增强代码的健状性
(4)测试覆盖率统计,例如JaCoCo 工具,支持多种维度的覆盖率统计,有分支,行,方法和类等
(5)故障注入(混沌工程),就是以应用为出发点,在各种环境和条件下,给应用系统注入各种可预测的故障,以此来验证应用在面对各种故障发生的时候,它的服务质量和稳定性等能力。
5、热部署

热部署目前有Intellj IDEA的HotSwap,JRebel以及Spring Loaded等
对于Java应用程序,热部署就是程序运行时实现Java类文件更新。要实现程序在运行中进行程序更新,就需要让java虚拟机在检测到Java类文件发生变化时,把原来的类文件卸载,并重新加载新的类文件。总的来说,热部署的本质是让jvm重新加载新的class文件。程序运行时,类加载器只会加载一次Java类文件,切不能卸载,这很明显不符合热部署的需要。但是,因为类加载器是可以进行更换的,所以,我们采取的方式是自定义类加载器,在自定义的类加载器中,重写findClass方法,从而实现热部署。
Spring Loaded热更新原理如下:
1、在应用程序启动时,Spring Loaded 在目标类路径中查找所有的类,并在 ClassPreProcessor 中使用自定义类加载器加载这些类,重新定义后存入 TypeRegistry,用于缓存、变更对比和依赖关系维护。
2、注册一个文件变化监听器 FileChangeListener,当一个类文件被修改后,Spring Loaded 会检测到这个变化,并重新加载该类文件。
3、当一个类被重新加载时,Spring Loaded 会尝试对比类的签名和继承关系没有改变,如果新的类定义与之前的类定义兼容,那么 Spring Loaded 会更新应用程序中的对象引用,以指向新的类定义。
Spring Loaded使用 Java 的 Instrumentation API 在JVM启动时指定Agent ,以完成上述相关的逻辑。
6、类库以及中间件

JDK中的反射和Lambda是通过动态生成类来实现相关功能的,有的是为了提高效率,有的是为了完成某个功能
Java字节码增强可以帮助应用或中间件实现动态代理、Spring AOP、拦截器等

 
 
 

 



 
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

207

主题

0

回帖

631

积分

高级会员

积分
631

QQ|智能设备 | 粤ICP备2024353841号-1

GMT+8, 2025-3-11 03:40 , Processed in 5.014899 second(s), 29 queries .

Powered by 智能设备

©2025

|网站地图