├── .gitignore ├── LICENSE ├── README.md ├── jar ├── java-crack-agent.jar └── javassist-3.27.0-GA.jar ├── pom.xml └── src └── main └── java └── crack └── thinking └── JavaCrackAgent.java /.gitignore: -------------------------------------------------------------------------------- 1 | log/ 2 | logs/ 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Derick Setsuna Jin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # java-crack-thinking 2 | 基于 javaagent 对 java 原生类的 方法进行字节码动态修改, 以此引发的一些关于 破解 Java 软件授权验证机制的思考. 3 | 4 | ## 起源 5 | 出于学习和研究的目的, 我尝试对使用一些 字节码反编译工具(JavaDecompiler[http://java-decompiler.github.io/] 或 IDEA 自带的反编译插件, 个人更推荐使用IDEA的这个插件), 查看了一些基于Java开发的需要授权的软件反编译后的代码, 尝试了解这些软件的授权验证机制. 6 | 7 | ## 问路 8 | 在阅读字节码反编译后的源码时, 不可避免的会看到许多令人痛苦的 `var...` 命名的变量, 在根据日志文件不断的翻看相关的 Jar 包后, 你才可能抓到你所要关注的重点类文件, 这些和软件授权验证相关的类文件一般会被开发者掩藏的很深, 并且可能会被加入一些专门混淆视听的垃圾代码进去. 9 | 10 | 我看到过一些将核心验证逻辑类的字节码文件进行 gzip 压缩, 得到字节数组, 并对其进行一些拆分和混淆, 在其它的字节码文件中 以 `byte[]` 的变量形式保存这些字节数组, 在程序启动的时候, 再将这些字节数组计算出来, 最终通过 `URLClassLoader` 的 `defineClass` 方法将这些字节数组转换为字节码加载进 JVM, 不得不为此有所感叹, 真是 `煞费苦心` 不过顺着源码的思路最终我们还是可以看到这些验证的核心代码逻辑, 这里写个简单的例子, 真实情况会远比这个要复杂, 假设下方的代码段就是某个软件的某个 jar 包中 `被雪藏的认证逻辑` . 11 | 12 | ``` java 13 | import java.security.*; 14 | import java.security.spec.X509EncodedKeySpec; 15 | public class Main { 16 | public static void main(String[] args) throws Exception { 17 | // 此处省略很长一串 rsa 的公钥的字节数组定义 ... 18 | byte[] publicKey = new byte[]{0, 0, 0, 0, 0, 0, 0, }; 19 | 20 | Signature signature = Signature.getInstance("MD5withRSA"); 21 | signature.initVerify(KeyFactory.getInstance("RSA") 22 | .generatePublic(new X509EncodedKeySpec(publicKey))); 23 | 24 | // 此处模拟 一些软件中的 核心验证逻辑 25 | signature.update("数据".getBytes()); 26 | if (signature.verify("签名".getBytes())) { 27 | System.out.println("认证成功"); 28 | } else { 29 | System.out.println("认证失败"); 30 | } 31 | } 32 | } 33 | ``` 34 | 35 | ## 思考 36 | 在找到这核心的认证逻辑后就会发现豁然开朗, 剩下的就是要怎样跳过这部分验证逻辑了. 思路无外乎是篡改这部分字节码, 修改RSA公钥, 或者让认证判断前加上 `true || ` 等. 但是鉴于之前说的问题, 这部分字节码并不是直接在 jar 包中的字节码文件, 而是 被各种混淆视听, 压缩分解存放到其它类文件中的, 而且软件中可能还有一些其它地方会对这部分的字节数组进行验证, 所以你可能要把整个 jar 包反编译掉, 进行修改后重新打包, 可能还会遇到一些 Jar 包的数字签名认证问题, 遗漏了一些验证逻辑 等等. 简而言之工作量巨大~ 37 | 38 | 那么有没有一些其它更好的方案呢, 简单便捷, 直指问题的核心? 39 | 40 | 我曾设计实现一套业务监控系统, 其中就有对 Java 程序内部方法执行的链式调用追踪, 做业务监控不可避免的要对项目的代码进行埋点, 多少都会有代码的侵入, 但是如何减少这种侵入造成的影响? 当时在考量了多种方案后, 决定使用 Javaagent 和注解的形式实现这套业务监控系统及相关的埋点SDK, SDK 的核心功能除了监控数据的上送部分外, 便是自定义的一个注解 和 一个 ThreadLocal 级别的监控消息存放实现了, 应用程序将注解标注在需要链路追踪的方法上, 注解中包含一些元信息, 同时 应用程序内部可使用 一些SDK的静态方法直接将业务数据存入监控消息中. 如此埋点即可避免将监控消息传来传去, 对代码进行大量的改动. 应用除了使用SDK埋点外还需在应用启动时制定 `-javaagent` 参数, 使用业务监控系统提供的 javaagent Jar 包, 这个 Jar 包会在程序加载类文件到JVM前对字节码进行一些动态修改, 对标注了指定注解的方法进行字节码前后插桩以便于统计方法的执行耗时, 同时将注解中的元数据也写入到监控消息中, 这部分都是直接修改该方法的字节码实现的, 字节码修改后再被JVM加载, 相比其它使用AOP实现的埋点 `性能更好`, 且`侵入更低`. 41 | 42 | 鉴于上述的工作经验, 我思考使用 `javaagent` 来达到我想要的目的, 在软件启动时加入 javaagent 对字节码进行动态修改, 将部分处理授权验证的方法实现给重写掉, 再让其加载进JVM中, 以达成目的, 但是有时候授权验证的逻辑和一些软件的初始化逻辑是捆绑在一个函数中的, 通常会很长, 操控字节码重写起来就比较费事费时, 所以能否有个更好的切入点, 比如 `java.security.Signature` 的 `verify` 方法, 是否也就直接绕过了软件的授权验证逻辑, 并且不对其现有的执行逻辑做任何的修改 ? 43 | 44 | ## 实践 45 | 想到便去尝试, 为此开源了这个项目, 在这个项目中我尝试直接修改 JDK 的原生类 `java.security.Signature` 的 `verify` 方法, 使得其直接返回 true. 46 | 47 | 因为对 Java JDK 原生类 `java.security.Signature` 的字节码进行了修改, 进而会导致 Java 的 `Jar 包数字签名验证`失败, 再去重新给 Jdk 搞个签名或去除签名 ? `不!` 我们可以直接把 `Jar 包数字签名验证` 这部分的核心方法也给修改掉, 不就完事了? 查看 JDK 源码后发现其是 `javax.crypto.JarVerifier` 类的 `testSignatures` 方法, 重置为空, 搞定~ 48 | 49 | ## 使用 50 | 1. 将 `./jar` 目录下的 jar 包放置到系统的某个目录 如 `/opt` 下. 51 | 2. 在 软件的启动命令开头的`java` 后面插入 `-javaagent:/opt/java-crack-agent.jar` 具体使用请自行查询学习 javaagent 相关知识. 52 | 53 | ## 反思 54 | 在尝试按照 用 `javaagent` 去修改 JDK 的类的字节码的思路看问题的话, 我们如何才能做到更安全的将 Java 软件交付到客户的手中, 并实施一些授权验证的措施? 虚心请教 []~( ̄▽ ̄)~* 烦请留言~ 55 | 56 | 以我目前的见识感觉很难做到, 在代码的混淆上做到极致是个思路, 让人望而却步? 不提供给客户核心的程序, 将程序放着服务器上, 只允许客户通过网络访问使用 ? 有些工具又不可避免的 像IDEA, Java 有 javaagent 类比其它编程语言应该也有一些有异曲同工之妙的东西. 57 | -------------------------------------------------------------------------------- /jar/java-crack-agent.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloSetsuna/java-crack-thinking/0840a819d5365d589b836e34cc3e27b488b32b6d/jar/java-crack-agent.jar -------------------------------------------------------------------------------- /jar/javassist-3.27.0-GA.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloSetsuna/java-crack-thinking/0840a819d5365d589b836e34cc3e27b488b32b6d/jar/javassist-3.27.0-GA.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | org.veda 8 | java-crack-thinking 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | 13 | 1.8 14 | ${java.version} 15 | ${java.version} 16 | 17 | 18 | 19 | 20 | org.javassist 21 | javassist 22 | 3.27.0-GA 23 | 24 | 25 | 26 | 27 | java-crack-agent 28 | 29 | 30 | org.apache.maven.plugins 31 | maven-jar-plugin 32 | 2.3.2 33 | 34 | 35 | 36 | true 37 | 38 | 39 | crack.thinking.JavaCrackAgent 40 | true 41 | true 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/main/java/crack/thinking/JavaCrackAgent.java: -------------------------------------------------------------------------------- 1 | package crack.thinking; 2 | 3 | import javassist.ClassPool; 4 | import javassist.CtClass; 5 | import javassist.CtMethod; 6 | 7 | import java.lang.instrument.ClassFileTransformer; 8 | import java.lang.instrument.IllegalClassFormatException; 9 | import java.lang.instrument.Instrumentation; 10 | import java.security.ProtectionDomain; 11 | 12 | /** 13 | * Java Crack Agent 14 | * 该类尝试修改掉 Java 原生类 java.security.Signature 的 verify 方法的返回值,使其永远返回 true 的一个实现样例 15 | * 基于该思路,理论上可以修改任何 Java 原生类的方法返回值,包括但不仅限于一些加密类,这在绕过一些 Java 开发的软件 16 | * 的安全验证机制上有时会有一些不错的效果,请以研究和学习的目的合法使用该源码。 17 | * 18 | * 声明:该类仅用于研究和学习 javaagent 及 javassist 工具的使用,源码将于我的 github 上开源, 任何人都可以下载和 19 | * 尝试使用,但本人不对其可能造成的任何影响承担任何法律责任。 20 | * 21 | * @author derick.setsuna.jin 2021-01-21 14:40:00 22 | * @version 1.0 23 | **/ 24 | public class JavaCrackAgent { 25 | 26 | 27 | public static void premain(String agentArgs, Instrumentation inst) { 28 | inst.addTransformer(new ClassFileTransformer() { 29 | 30 | // 类加载期间不需考虑并发 31 | private int rewriteId = 0; 32 | 33 | @Override 34 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 35 | // 避免不必要的判断 减少 javaagent 对类加载时间的影响 36 | if (rewriteId > 1) { 37 | return null; 38 | } 39 | // 使用 javassist 工具修改 java 自身的 signature 的返回值, 使其一直返回 true 40 | if ("java/security/Signature".equals(className)) { 41 | try { 42 | final ClassPool classPool = ClassPool.getDefault(); 43 | final CtClass clazz = classPool.get("java.security.Signature"); 44 | CtMethod[] methods = clazz.getDeclaredMethods("verify"); 45 | for (CtMethod method : methods) { 46 | System.out.println("\n [" + rewriteId + "] JavaCrackAgent rewrite java.security.Signature # verify(...)\n"); 47 | method.setBody("{return true;}"); 48 | } 49 | byte[] byteCode = clazz.toBytecode(); 50 | // detach 的意思是将内存中曾经被 javassist 加载过的类移除 51 | // 如果下次有需要在内存中 找不到会 重新走 javassist 加载 52 | clazz.detach(); 53 | rewriteId ++; 54 | return byteCode; 55 | } catch (Exception ex) { 56 | ex.printStackTrace(); 57 | } 58 | } 59 | 60 | // 因上面修改了 java.security.Signature 的字节码, 所以可能会导致 jce 的 JarVerifier 对 jar 包的签名验证过不去 61 | // 最简单粗暴的方案就是将 javax.crypto.JarVerifier 的 建议逻辑函数 testSignatures 的字节码一样给修改掉, ok ~ 62 | else if ("javax/crypto/JarVerifier".equals(className)) { 63 | try { 64 | final ClassPool classPool = ClassPool.getDefault(); 65 | final CtClass clazz = classPool.get("javax.crypto.JarVerifier"); 66 | CtMethod method = clazz.getDeclaredMethod("testSignatures"); 67 | System.out.println("\n [" + rewriteId + "] JavaCrackAgent rewrite javax.crypto.JarVerifier # testSignatures(...)\n"); 68 | method.setBody("{}"); 69 | byte[] byteCode = clazz.toBytecode(); 70 | clazz.detach(); 71 | rewriteId ++; 72 | return byteCode; 73 | } catch (Exception ex) { 74 | ex.printStackTrace(); 75 | } 76 | } 77 | return null; 78 | } 79 | }); 80 | } 81 | } 82 | --------------------------------------------------------------------------------