使用Java代理和Byte Buddy为JVM构建调试工具。
Java和JVM通常更广泛地用于所有地方的服务,但是通常很难调试和手动测试,尤其是在复杂的微服务体系结构中。
HTTP请求和响应是这些服务之间以及与它们的外部API进行交互的核心,但是它们通常也是不可见和不可访问的。在手动测试和原型制作过程中,很难检查所有发出的请求,在运行中的系统中模拟异常响应和错误,或者模拟依赖关系。
在过去的几周中,我建立了一个Java代理,可以完全自动地做到这一点。它可以在启动或稍后附加时抓住任何JVM中所有HTTP和HTTPS请求的控制权,以将它们重定向到代理,并信任该代理解密所有HTTPS,从而允许所有JVM流量的MitM。零代码更改或需要手动配置。
这意味着您可以选择任何JVM进程-您自己的本地运行的服务,Gradle,Intellij,以及您喜欢的任何东西-并在2秒钟内检查,断点和模拟其所有HTTP(S)请求。
在这篇文章中,我要带你通过这怎么可能,这样你就可以了解一些JVM的秘密力量的细节,学习如何将原始字节码为自己,并建立在对例子和源代码,这背后构建自己的调试和检测工具。
如果您只是想立即尝试, 请下载HTTP Toolkit。
如果您想知道这在现实中是如何实现的,以及如何编写能做到这一点的代码,请继续阅读:
这里发生了什么?
在某些方面,拦截所有HTTP(S)应该很容易:JVM具有标准的HTTP代理和SSL上下文配置设置(例如-Dhttp.proxy和-Djavax.net.ssl.trustStore),因此您可以尝试通过在启动时设置这些选项从外部进行配置。
不幸的是,这是行不通的。大多数现代库默认情况下都会忽略这些设置,而是选择提供自己的默认值和配置界面。即使该库没有,许多应用程序也会显式定义自己的连接和TLS配置。通常,这通常很方便且明智,但是在以后要开始调试和手动测试HTTP交互时非常不便。
无需在启动时设置没人使用的配置值,我们可以使用Java代理强制捕获HTTP。Java代理允许我们从外部连接到JVM进程,运行我们自己的代码并重写现有的字节码。
当我们的代理连接到JVM时(无论是在启动之前加载所有内容,还是在以后加载),我们都会与内置软件包中使用的特定类和一长串流行的外部库进行匹配,以查找从TLS配置状态到连接池的所有内容逻辑,并且我们在整个过程中进行了一些小的更改。这使我们可以更改默认值,忽略自定义设置,重新创建现有连接以及重新配置要由我们的HTTPS拦截代理拦截的所有HTTP(S)。
这真是太酷了!从JVM进程外部,我们可以使用它可靠地重写任意字节码,以更改代码库中所有HTTP的工作方式,并自己控制整个事情。它是针对类固醇的面向方面的编程,并且非常容易实现。
让我们来谈谈细节。
什么是Java代理?
Java代理是一种特殊类型的JAR文件,可以附加到其他JVM进程,并且JVM赋予它额外的权力来转换和检测字节码。
它们已被JVM工具广泛使用,从使用New Relic进行应用程序监视到使用PiTest进行突变测试,应有尽有。
尽管有名称,但它们并非仅Java。它们适用于在JVM上运行的任何东西。
有两种使用Java代理的方法。您可以在启动时将其附加,如下所示:
代理可以在其JAR清单中具有两个单独的入口点来管理此入口点:一个用于启动时的附件,另一个用于以后的附件。还有一些JAR清单属性可以选择转换字节码。为gradle构建的JAR进行配置如下所示:
最后,您有一个实现这些方法的代理类。像这样:
这测试类我们在这里给我们提供了像addTransformer和redefineClasses方法,我们可以用它来读取并覆盖在虚拟机的任何类别的原始字节码。
HTTP Toolkit包括从以上所有内容构建的代理JAR,该代理JAR允许它附加到任何JVM应用程序,在该应用程序内运行代码(在可能的情况下使用常规API设置默认值和配置值)以及转换和挂钩所有HTTP的内部结构相关的类,我们关心。
但是,代理程序的设置只是第一步:这使我们几乎可以完全改变目标应用程序正在执行的操作,但是确定如何转换类很复杂,转换存在一些限制,并且不处理原始字节码简单…
您如何转换原始字节码?
简而言之:使用Byte Buddy。
这是一个复杂的库,可以使用字节码执行许多强大的功能,包括在运行时动态生成子类和接口实现(例如,对于模拟框架),手动更改类和方法以及通过模板自动转换字节码。
在代理工具(例如HTTP Toolkit)的情况下,我们对模板方法感兴趣,因为Java代理有一个局限性:重新加载已经加载的类时,新定义必须与相同的类模式匹配。这意味着我们可以将新逻辑添加到现有方法主体中,但是不能在现有类上创建新方法或字段,也不能更改现有方法签名。
为了解决这个问题,Byte Buddy的内置“建议”系统定义了方法转换模板,它可以应用于我们,同时确保该模式永远不会以任何其他方式更改。
首先,我们需要设置Byte Buddy。此配置似乎很好地工作:
然后,我们定义一个Advice类,它将转换我们的目标。咨询类如下所示:
这表示“在目标方法主体的末尾,插入额外的逻辑,该逻辑将返回值替换为[我们的代理值]”。
此处的代码有效地注入到方法主体的末尾(由于Advice.OnMethodExit),并且可以在方法参数(例如@ Advice.Return)上使用注释,以将该模板方法中的变量链接到方法参数,字段值, “ this”,或在现有方法主体中返回值。
要将这些结合在一起,我们必须告诉Byte Buddy何时应用此建议,如下所示:
Byte Buddy使用此流利的API来构建从类型匹配器(如此处的“命名”)到类型转换器的映射,然后构建将特定建议模板应用于与某些模式匹配的方法的转换(例如hasMethodName(“ getProxy”))。
上面的代码实际上是我们用来拦截OkHttp的实际实现逻辑:对于所有OkHttpClient实例,即使是在附加时已实例化的实例,我们也会覆盖getProxy(),因此无论其先前的配置如何,它始终返回我们的代理配置。这样可以确保来自所有OkHttp客户端的所有新连接都转到我们的代理。
不过,这只是一个简单情况的一部分(完整的OkHttp逻辑在此处),并且对所有HTTP进行的操作要复杂得多……
哪些转换允许您捕获所有HTTPS?
通过以上内容,我们可以构建可以附加到JVM目标的Java代理,并轻松地任意转换方法主体。
有用的拦截HTTP(S)仍然需要我们找到我们关心的方法主体,并研究如何对其进行转换。
实际上,要转换任何目标库以拦截HTTPS,需要执行三个步骤:
重定向新连接以通过HTTP Toolkit代理服务器
在HTTPS连接设置期间信任HTTP Toolkit证书
附加到已运行的应用程序时,使用任何打开的非代理连接重置/停止
我不会针对每个受支持的库的每个版本进行详细的实现(如果您有兴趣,请随时浏览完整的源代码),但让我们来看几个说明性示例。
其中一些逻辑是用Kotlin编写的,并且在上面使用了一些帮助程序,但是,如果您已阅读以上内容,并且了解Java,则将获得要点:
拦截Apache HttpClient:
Apache HttpClient是其HttpComponents项目的一部分,该项目是古老的Commons HttpClient库的继承者。
它以各种各样的形式存在了很长时间,已经被广泛使用,幸运的是,它很容易被拦截。
例如,对于v5,所有传出流量都通过HttpRoutePlanner接口的实现来运行,该接口决定应将请求发送到何处。
我们只需要更改该接口的所有实现的返回值即可:
仅此一项,我们就将所有流量重定向到其他地方。
同时,重置所有SSL连接需要先创建SSL套接字才能更改SSL配置。
令人高兴的是,上面的“ HttpRoutePlanner”方法甚至不需要重置连接:请求路由不再与现有的打开连接匹配,因此请求立即停止使用这些连接,而开始使用我们的代理,而现有连接则无害暂停。
拦截Java的内置ProxySelector:
让我们尝试一些更困难的事情:我们可以重写内置的Java类吗?我们可以。
当我们的代理首次附加时,它将使用普通的公共API更改默认的ProxySelector,以便使用Java的默认代理选择器的任何代码自动使用我们的代理,而无需进行任何转换。
但是,不幸的是,某些应用程序手动管理代理选择器,这可能导致HTTP无法被拦截。
为了解决这个问题,我们在代理设置过程中使用普通的ProxySelector.setDefault()API设置了代理选择器,然后我们转换了内置类以完全禁用该设置器,因此其他人都无法更改它。
看起来像这样:
转换内置类确实有一些警告,例如,您需要在设置Byte Buddy的过程中设置.ignore(none()(请参见上面的示例),并且您不能在建议中引用任何非内置类型对于这样的简单更改,这不是什么大问题。
拦截Spring WebClient HTTP:
好的,最后一个例子,让我们看一个更复杂的情况。Spring的WebClient如何工作?
Spring WebClient是一个相对较新的客户端-它是作为Spring 5的一部分发布的一个响应式客户端,默认情况下在Reactor-Netty的顶部提供了Spring集成的API(但也可以配置为使用其他引擎)。
我怀疑绝大多数用户都使用默认的Reactor-Netty引擎,如果不使用默认引擎,那么他们将使用已经被我们的另一种配置拦截的引擎。这意味着我们只需要拦截Reactor-Netty,我们将捕获所有准备调试的Spring WebClient通信。
非常有帮助的是,Reactor Netty将我们关心的所有状态(代理和SSL上下文)都存储在一个位置:HttpClientConfig类。我们需要以某种方式为所有实例重置该内部状态,但是在公共API中并没有方便地公开它。
但是,更有用的是,在每个请求期间克隆了他们的HttpClient类,将配置传递给请求的客户端,这为我们提供了一个完美的钩子,可以在每次请求之前获取并修改配置。
看起来像这样:
好的,虽然我完全期望,尽管阅读本文的人中有一半可能会着迷,但另一半会感到恐惧。
在这里,我们深深地陷入了图书馆内部的深渊,并且毫不pent悔。
这确实有一些警告:库更改很可能会破坏这一点,或者某些转换可能会导致副作用。我不建议您在生产中进行此操作,而无需进行更仔细的转换和测试,但是对于本地开发和测试而言,风险很低,这就像一个魅力。
在实践中,我怀疑脆弱性问题会很小。我们正在转换的代码是连接设置的低级内部,它很少更改。这里对各种目标的回购表示不满,这表明在大多数情况下,此逻辑自v1以来几乎没有改变,或者每5年左右仅略微改变一次,并且在有变化时更新此逻辑并不是一项艰巨的任务。此外,尽管新的库也将问世,但它们中的大多数库都是在这些现有引擎的基础上构建的,因此我们可以免费为它们提供支持!
参考: 《2020最新Java基础精讲视频教程和学习路线!》
原文链接:https://blog.csdn.net/weixin_...