破解 JVM 上的第三方 API

打印 上一主题 下一主题

主题 808|帖子 808|积分 2424

JVM 生态系统已经成熟,提供了很多库,因此您无需重新发明轮子。基本功能(以及不那么基本的功能)只需依靠即可实现。然而,有时依靠项和您的用例略有不一致。
解决此问题的精确方法是创建一个 Pull 请求。但您的截止日期是来日诰日:您必要立纵然其工作!现在是时候破解提供的 API 了。
在本文中,我们将介绍一些替代方法,这些方法允许您让第三方 API 按照其计划者不期望的方式运行。
反射

想象一下,API 的计划遵照开放封闭原则:
   在面向对象编程中,开放封闭原则指出“软件实体(类、模块、函数等)应该对扩睁开放,但对修改关闭”;也就是说,这样的实体可以允许扩展其行为而无需修改其源代码。
  --开放封闭原则
  假设依靠项的公共 API 不适合你的用例。你必要扩展它,但这是不可能的,因为计划不允许这样做 - 故意的。
为相识决这个问题,书中关于 JVM 的最古老的技巧可能是反射。
   反射是 Java 编程语言中的一项功能。它允许正在执行的 Java 程序查抄或“自省”自身,并操纵程序的内部属性。例如,Java 类可以获取其所有成员的名称并表现它们。
  --利用 Java 反射
  在我们的范围内,反射允许您访问不应访问的状态,或调用不应调用的方法。
                    
  1. public class Private {
  2.   private String attribute = "My private attribute";
  3.   private String getAttribute() {
  4.     return attribute;
  5.   }
  6. }
  7. public class ReflectionTest {
  8.   private Private priv;
  9.   @BeforeEach
  10.   protected void setUp() {
  11.     priv = new Private();
  12.   }
  13.   @Test
  14.   public void should_access_private_members() throws Exception {
  15.     var clazz = priv.getClass();
  16.     var field = clazz.getDeclaredField("attribute");                             // 1
  17.     var method = clazz.getDeclaredMethod("getAttribute");                        // 2
  18.     AccessibleObject.setAccessible(new AccessibleObject[]{field, method}, true); // 3
  19.     field.set(priv, "A private attribute whose value has been updated");         // 4
  20.     var value = method.invoke(priv);                                             // 5
  21.     assertThat(value).isEqualTo("A private attribute whose value has been updated");
  22.   }
  23. }
复制代码
     
               

  • 获取对类private的字段的引用Private
  • 获取类private方法的引用Private
  • 允许利用private成员
  • private设置字段的值
  • 调用private方法
然而,反射也有一些局限性:


  • “魔法”发生在 上AccessibleObject.setAccessible。可以通过适当设置的安全管理器在运行时克制这种环境。我承认,在我的职业生活中,我从未见过有人利用安全管理器。
  • 模块系统限制了反射 API 的利用。例如,调用者和目标类必须位于同一个模块中,目标成员必须是public,等等。请注意,许多库不利用模块系统。
  • 假如您直接利用具有私有成员的类,反射是很好的。但假如您必要更改依靠类的行为,反射就毫无用处:假如您的类利用了第三方类,A而该第三方类本身又必要一个类B,您必要更改B。
类路径阴影

一篇很长的文章可以专门介绍 Java 的类加载机制。在这篇文章中,我们将重点介绍类路径。类路径是JVM 将查找以加载先前卸载的类的文件夹和 JAR 的有序列表。
让我们从以下架构开始:


启动该应用程序的最简单命令如下:
  1. java -cp=.:thirdparty.jar Main
复制代码
无论出于什么原因,假设我们必要改变类的行为B。它的计划不允许这样做。
无论这种计划怎样,我们仍然可以通过以下方式对其进行破解:

  • 获取类的源代码B
  • 根据我们的要求进行更改
  • 编译
  • 将编译后的类放在类路径上包含原始类的 JAR之前
当启动与上述相同的命令时,类加载将按以下次序进行:从文件系统加载,然后Main从JAR 加载;将跳过 JAR 中的类加载。BAB
这种方法也有一些局限性:


  • 您必要源代码B- 或者至少必要一种从编译的代码中获取它的方法。
  • 您必要能够B从源代码进行编译。这意味着您必要重新创建所有必要的依靠项B。
这些都是技术要求。至于是否合法则完满是另一个问题,超出了本文的讨论范围。
面向方面编程

与 C++ 相反,Java 语言提供单一继承:一个类可以从单个超类继承。
但在某些环境下,多重继承是必须的。例如,我们渴望在类层次布局中为不同的日志级别提供日志记录方法。有些语言遵照单一继承原则,但为日志记录等横切关注点提供了替代方案:Scala 提供特性,而 Java 和 Kotlin 的接口可以具有属性。
“在过去”,AOP非常流行,用于向不属于同一层次布局的类添加横切功能。
   在计算领域,面向方面编程 (AOP) 是一种编程范式,旨在通太过离横切关注点来提高模块化程度。它通过在不修改代码本身的环境下向现有代码添加额生手为(发起)来实现这一点,而是通过“切入点”规范单独指定要修改的代码,例如“当函数名称以‘set’开头时记录所有函数调用”。这允许将不属于业务逻辑核心的行为(例如日志记录)添加到程序中,而不会使代码核心与功能混杂在一起。AOP 构成了面向方面软件开辟的底子。
  --面向方面编程
  在 Java 中,AspectJ是首选的 AOP 库。它依靠于以下GitHub:


  • 连接点定义程序执行过程中某个明白定义的点,例如方法的执行
  • 切入点在程序流中挑选出特定的连接点,例如执行任何用@Loggable
  • 发起将切入点(挑选连接点)和代码主体(在每个连接点上运行)结合在一起
这里有两个类:一个代表公共 API,并将其实现委托给另一个。
                    
  1. public class Public {
  2.   private final Private priv;
  3.   public Public() {
  4.     this.priv = new Private();
  5.   }
  6.   public String entryPoint() {
  7.     return priv.implementation();
  8.   }
  9. }
  10. final class Private {
  11.   final String implementation() {
  12.     return "Private internal implementation";
  13.   }
  14. }
复制代码
     
                想象一下,我们必要改变私有实现。
                    
  1. public aspect Hack {
  2.   pointcut privateImplementation(): execution(String Private.implementation()); // 1
  3.   String around(): privateImplementation() {                                    // 2
  4.     return "Hacked private implementation!";
  5.   }
  6. }
复制代码
     
               

  • 截取执行的切入点Private.implementation()
  • 包装上述执行并将原始方法体更换为自己的方法体的发起
AspectJ 提供了不同的实现:

  • 编译时:字节码在构建期间更新
  • 后编译时间:字节码在构建后立即更新。它不但允许更新项目类,还允许更新依靠的 JAR。
  • 加载时:字节码在运行时加载类时更新
您可以像这样在 Maven 中设置第一个选项:
                    
  1. <build>
  2.   <plugins>
  3.     <plugin>
  4.       <artifactId>maven-surefire-plugin</artifactId>
  5.       <version>2.22.2</version>
  6.     </plugin>
  7.     <plugin>
  8.       <groupId>com.nickwongdev</groupId>
  9.       <artifactId>aspectj-maven-plugin</artifactId>
  10.       <version>1.12.6</version>
  11.       <configuration>
  12.         <complianceLevel>${java.version}</complianceLevel>
  13.         <source>${java.version}</source>
  14.         <target>${java.version}</target>
  15.         <encoding>${project.encoding}</encoding>
  16.       </configuration>
  17.       <executions>
  18.         <execution>
  19.           <goals>
  20.             <goal>compile</goal>
  21.           </goals>
  22.         </execution>
  23.       </executions>
  24.     </plugin>
  25.   </plugins>
  26. </build>
  27. <dependencies>
  28.   <dependency>
  29.     <groupId>org.aspectj</groupId>
  30.     <artifactId>aspectjrt</artifactId>
  31.     <version>1.9.5</version>
  32.   </dependency>
  33. </dependencies>
复制代码
     
                AOP 总体上和 AspectJ 尤其代表了核心选项。它们实际上没有任何限制,尽管我必须承认我没有查抄它怎样与 Java 模块一起工作。
但是,Codehaus 的官方 AspectJ Maven 插件仅支持 JDK 8 及以下版本,因为自 2018 年以来没有人更新过。有人在GitHub上 fork 了支持更高版本的代码。该 fork 可以支持 JDK 13 及以下版本,以及 AspectJ 库 1.9.5 及以下版本。
Java 代理

当您想要破解时,AOP 提供了高级抽象。但是假如您想以细粒度的方式更改代码,那么除了更改字节码本身之外别无他法。有趣的是,JVM 为我们提供了一种在加载类时更改字节码的标准机制。
您可能已经在您的职业生活中遇到过该功能:它们被称为 Java 代理。Java 代理可以在启动 JVM 时在命令行上静态设置,也可以在之后动态附加到已经运行的 JVM。有关 Java 代理的更多信息,请查看此帖子(“Java 代理快速入门”部分)。
这是一个简单 Java 代理的代码:
                    
  1. public class Agent {
  2.     public static void premain(                      // 1
  3.             String args,                             // 2
  4.             Instrumentation instrumentation){        // 3
  5.         var transformer = new HackTransformer();
  6.         instrumentation.addTransformer(transformer); // 4
  7.     }
  8. }
复制代码
     
               

  • premain是静态设置的 Java 代理的入口点,就像main常规应用程序一样
  • 我们也会有争论,就像main
  • Instrumentation是“魔法”类
  • 设置一个可以在 JVM 加载字节码之前更改字节码的转换器
Java 代理在字节码级别工作。代理为您提供根据 JVM 规范(更准确地说,根据类文件格式)存储类定义的字节数组。必须更改字节数组中的字节并不是一件有趣的事情。好消息是其他人之前也有过这种需求。因此,生态系统提供了可提供更高级别抽象的现成库。
在以下代码片段中,转换器利用Javassist:
                    
  1. public class HackTransformer implements ClassFileTransformer {
  2.   @Override
  3.   public byte[] transform(ClassLoader loader,
  4.               String name,
  5.               Class<?> clazz,
  6.               ProtectionDomain domain,
  7.               byte[] bytes) {                                            // 1
  8.     if ("ch/frankel/blog/agent/Private".equals(name)) {
  9.       var pool = ClassPool.getDefault();                                 // 2
  10.       try {
  11.         var cc = pool.get("ch.frankel.blog.agent.Private");              // 3
  12.         var method = cc.getDeclaredMethod("implementation");             // 4
  13.         method.setBody("{ return "Agent-hacked implementation!"; }");  // 5
  14.         bytes = cc.toBytecode();                                         // 6
  15.       } catch (NotFoundException | CannotCompileException | IOException e) {
  16.         e.printStackTrace();
  17.       }
  18.     }
  19.     return bytes;                                                        // 7
  20.   }
  21. }
复制代码
     
               

  • 类的字节数组
  • Javassist API 的入口点
  • 从池中获取类
  • 从类中获取方法
  • 通过设置新的方法来更换方法主体
  • 用更新后的字节数组更换原始字节数组
  • 返回更新后的字节数组,供 JVM 加载
结论

在这篇文章中,我们列出了四种不同的方法来破解第三方库的行为:反射、类路径阴影、面向方面编程和 Java 代理。
有了这些,你应该能够解决遇到的任何问题。只需记住,库和 JVM 的计划都是有原因的:防止你犯错误。
你可以忽略这些护栏,但我发起你将这些黑客攻击保持在原位的时间应尽可能短,不要再长了。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

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

本版积分规则

曂沅仴駦

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表