Skip to content

基于 javaagent 对 java 原生类的 方法进行字节码动态修改, 以此引发的一些关于 绕过 Java 软件授权验证机制的思考

License

Notifications You must be signed in to change notification settings

HelloSetsuna/java-crack-thinking

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

java-crack-thinking

基于 javaagent 对 java 原生类的 方法进行字节码动态修改, 以此引发的一些关于 破解 Java 软件授权验证机制的思考.

起源

出于学习和研究的目的, 我尝试对使用一些 字节码反编译工具(JavaDecompiler[http://java-decompiler.github.io/] 或 IDEA 自带的反编译插件, 个人更推荐使用IDEA的这个插件), 查看了一些基于Java开发的需要授权的软件反编译后的代码, 尝试了解这些软件的授权验证机制.

问路

在阅读字节码反编译后的源码时, 不可避免的会看到许多令人痛苦的 var... 命名的变量, 在根据日志文件不断的翻看相关的 Jar 包后, 你才可能抓到你所要关注的重点类文件, 这些和软件授权验证相关的类文件一般会被开发者掩藏的很深, 并且可能会被加入一些专门混淆视听的垃圾代码进去.

我看到过一些将核心验证逻辑类的字节码文件进行 gzip 压缩, 得到字节数组, 并对其进行一些拆分和混淆, 在其它的字节码文件中 以 byte[] 的变量形式保存这些字节数组, 在程序启动的时候, 再将这些字节数组计算出来, 最终通过 URLClassLoaderdefineClass 方法将这些字节数组转换为字节码加载进 JVM, 不得不为此有所感叹, 真是 煞费苦心 不过顺着源码的思路最终我们还是可以看到这些验证的核心代码逻辑, 这里写个简单的例子, 真实情况会远比这个要复杂, 假设下方的代码段就是某个软件的某个 jar 包中 被雪藏的认证逻辑 .

import java.security.*;
import java.security.spec.X509EncodedKeySpec;
public class Main {
    public static void main(String[] args) throws Exception {
        // 此处省略很长一串 rsa 的公钥的字节数组定义 ... 
        byte[] publicKey = new byte[]{0, 0, 0, 0, 0, 0, 0, };
        
        Signature signature = Signature.getInstance("MD5withRSA");
        signature.initVerify(KeyFactory.getInstance("RSA")
        	.generatePublic(new X509EncodedKeySpec(publicKey)));
        
        // 此处模拟 一些软件中的 核心验证逻辑
        signature.update("数据".getBytes());
        if (signature.verify("签名".getBytes())) {
            System.out.println("认证成功");
        } else {
            System.out.println("认证失败");
        }
    }
}

思考

在找到这核心的认证逻辑后就会发现豁然开朗, 剩下的就是要怎样跳过这部分验证逻辑了. 思路无外乎是篡改这部分字节码, 修改RSA公钥, 或者让认证判断前加上 true || 等. 但是鉴于之前说的问题, 这部分字节码并不是直接在 jar 包中的字节码文件, 而是 被各种混淆视听, 压缩分解存放到其它类文件中的, 而且软件中可能还有一些其它地方会对这部分的字节数组进行验证, 所以你可能要把整个 jar 包反编译掉, 进行修改后重新打包, 可能还会遇到一些 Jar 包的数字签名认证问题, 遗漏了一些验证逻辑 等等. 简而言之工作量巨大~

那么有没有一些其它更好的方案呢, 简单便捷, 直指问题的核心?

我曾设计实现一套业务监控系统, 其中就有对 Java 程序内部方法执行的链式调用追踪, 做业务监控不可避免的要对项目的代码进行埋点, 多少都会有代码的侵入, 但是如何减少这种侵入造成的影响? 当时在考量了多种方案后, 决定使用 Javaagent 和注解的形式实现这套业务监控系统及相关的埋点SDK, SDK 的核心功能除了监控数据的上送部分外, 便是自定义的一个注解 和 一个 ThreadLocal 级别的监控消息存放实现了, 应用程序将注解标注在需要链路追踪的方法上, 注解中包含一些元信息, 同时 应用程序内部可使用 一些SDK的静态方法直接将业务数据存入监控消息中. 如此埋点即可避免将监控消息传来传去, 对代码进行大量的改动. 应用除了使用SDK埋点外还需在应用启动时制定 -javaagent 参数, 使用业务监控系统提供的 javaagent Jar 包, 这个 Jar 包会在程序加载类文件到JVM前对字节码进行一些动态修改, 对标注了指定注解的方法进行字节码前后插桩以便于统计方法的执行耗时, 同时将注解中的元数据也写入到监控消息中, 这部分都是直接修改该方法的字节码实现的, 字节码修改后再被JVM加载, 相比其它使用AOP实现的埋点 性能更好, 且侵入更低.

鉴于上述的工作经验, 我思考使用 javaagent 来达到我想要的目的, 在软件启动时加入 javaagent 对字节码进行动态修改, 将部分处理授权验证的方法实现给重写掉, 再让其加载进JVM中, 以达成目的, 但是有时候授权验证的逻辑和一些软件的初始化逻辑是捆绑在一个函数中的, 通常会很长, 操控字节码重写起来就比较费事费时, 所以能否有个更好的切入点, 比如 java.security.Signatureverify 方法, 是否也就直接绕过了软件的授权验证逻辑, 并且不对其现有的执行逻辑做任何的修改 ?

实践

想到便去尝试, 为此开源了这个项目, 在这个项目中我尝试直接修改 JDK 的原生类 java.security.Signatureverify 方法, 使得其直接返回 true.

因为对 Java JDK 原生类 java.security.Signature 的字节码进行了修改, 进而会导致 Java 的 Jar 包数字签名验证失败, 再去重新给 Jdk 搞个签名或去除签名 ? 不! 我们可以直接把 Jar 包数字签名验证 这部分的核心方法也给修改掉, 不就完事了? 查看 JDK 源码后发现其是 javax.crypto.JarVerifier 类的 testSignatures 方法, 重置为空, 搞定~

使用

  1. ./jar 目录下的 jar 包放置到系统的某个目录 如 /opt 下.
  2. 在 软件的启动命令开头的java 后面插入 -javaagent:/opt/java-crack-agent.jar 具体使用请自行查询学习 javaagent 相关知识.

反思

在尝试按照 用 javaagent 去修改 JDK 的类的字节码的思路看问题的话, 我们如何才能做到更安全的将 Java 软件交付到客户的手中, 并实施一些授权验证的措施? 虚心请教 []( ̄▽ ̄)* 烦请留言~

以我目前的见识感觉很难做到, 在代码的混淆上做到极致是个思路, 让人望而却步? 不提供给客户核心的程序, 将程序放着服务器上, 只允许客户通过网络访问使用 ? 有些工具又不可避免的 像IDEA, Java 有 javaagent 类比其它编程语言应该也有一些有异曲同工之妙的东西.

About

基于 javaagent 对 java 原生类的 方法进行字节码动态修改, 以此引发的一些关于 绕过 Java 软件授权验证机制的思考

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages