最近用Java写了一些小工具,又涉及到一个老问题,如何更好更方便地分发?利用现成的安装程序吧,觉得太重。看到github上很多程序直接打包成一个单一的可执行文件,不超过200M,觉得这种形式不错。于是也想利用golang的特性,包装一下自己的Java命令行应用。当然,包装SpringBoot web应用也是可以的。
自从Java诞生以来,其字节码容易被反编译的问题就为程序员所诟病。由此也诞生了不少Java混淆工具和加壳软件。
最关键的一个问题,采用import语句的类,能否被URLClassLoader加载机制替换掉?也就是,一个jar包中,如果我将一部分class移走,这部分class采用自定制的ClassLoader加载,是否可行?如果可行,那我将这部分class通过golang写的server输出,就差不多解决了一半的被破解问题。
经过一番实验,摸索出如下方案:
上述方案称不上绝对安全,但多少增加了些破解难度。
package main
import (
"context"
"embed"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"time"
)
//go:embed cli.jar
//go:embed mylib.jar
var f embed.FS
func ClassServer(w http.ResponseWriter, req *http.Request) {
data, _ := f.ReadFile("mylib.jar")
w.Write(data)
}
func Check(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("ok"))
}
func launch_java() {
for {
time.Sleep(1 * time.Second)
resp, _ := http.Get("http://localhost:18899/check")
bytes, _ := io.ReadAll(resp.Body)
if string(bytes) == "ok" {
cmd := exec.Command("java", "-cp", ".:/tmp/_cli.jar", "com.icool.CLIMain", "http://localhost:18899/jar", "passw0rd")
output, _ := cmd.CombinedOutput()
fmt.Println(string(output))
break
}
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
cli, _ := f.ReadFile("cli.jar")
_ = os.WriteFile("/tmp/_cli.jar", cli, 0755)
go func() {
http.HandleFunc("/jar", ClassServer)
http.HandleFunc("/check", Check)
err := http.ListenAndServe(":18899", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}()
go launch_java()
// Wait for interrupt signal.
<-ctx.Done()
// Restore signal, allowing "force quit".
stop()
}
由于JVM字节码的高语义性,使得期极为容易被分析与阅读,使用动态调试的方式可以很容易分析出其运行逻辑,而动态调试工具的编写并不是一件十分复杂的事情,因此混淆并不是一种可靠的保护方案。
由于JVM附加机制的存在,所有未脱离普通JVM运行的所谓加密代码,都可以使用附加工具轻松读取,因此这是一种最无效的保护方案。
普通的JVM都带有附着自制,用户可以使用jhsdb这类工具,附着到JVM进程,对其内存数据进行查看和分析,并且这些内存数据还是按照源文件中的数据结构被妥善组织好的,这也可以理解为JVM自带的一种后门机制。下面这篇文章介绍了如何使用JVM附着机制读取并保存内存中的类文件信息。https://protector4j.com/articles/cracking-encrypted-java-applications-jhsdb/
除了可以使用JDK自带的jhsdb工具之外,还可以使用阿里巴巴的Arthas对运行中的Java进程进行分析。
虚拟化保护是强度最高的一种代码保护方式,但是由于期对性能的严重影响,因此无法应用到程序中的全部代码,而只能保护关键代码,其他代码仍然有暴露的风险,而以其他部分代码来切入口,就可以获取到虚拟化部分代码的功能信息。
AOT编译配置难度大,编译难度大,编译失败概率高,即使编译成功,代码逻辑也仅是由原来的字节码表示转换为机器代码表示,其本身的运行逻辑仍然存在,并没有进行特别的保护,如果能够了解其本身的编译与运行机制,仍然能够逆向还原出可读性的代码。
代码混淆是最早应用于Java代码保护的方案,也是一个最直接的方案。代码混淆通常有下面四种方法:
代码混淆可以大幅降低反编译代码的可读性,提升静态分析的难度,但是无论如何进行代码混淆,程序的运行逻辑是不会改变的。
JVM字节码上是一种语义很清晰明确,且极为阅读的中间代码,对于被混淆的class文件,即使无法还原成可读的Java源代码,仍然可以在字节码层面进行分析,由于Java字节码的高语义性,这个过程其实还是比较容易的。
下面是一些常见的开源的和商业的混淆工具:
There is an easy-to-read introductory article with extra links on bytecode obfuscation on the OWASP Foundation’s website. Another good introductory article on obfuscation techniques is on the DashO website.
名称 | License | 地址 |
---|---|---|
yGuard | LGPL | http://www.yworks.com/products/yguard |
ProGuard | GPLv2 | https://www.guardsquare.com/en/proguard |
Facebook ProGuard分支 | GPLv2 | https://github.com/facebook/proguard |
DashO | Commercial | https://www.preemptive.com/products/dasho |
Allatori | Commercial | http://www.allatori.com |
Stringer | Commercial | https://jfxstore.com |
Java Antidecompiler | Commercial | http://www.bisguard.com/help/java/ |
Zelix KlassMaster | Commercial | http://www.zelix.com |
先来了解一下Java类加载器的基本常识。三种调用会导致JVM加载一个类: new一个对象、Class.forName()、classLoader.loadClass(),而在文件头import语句只是声明,不会导致类加载。
Bootstrap class loader serves as the parent of all the other ClassLoader instances. This bootstrap class loader is part of the core JVM and is written in native code. 不同平台有不同的实现。
It’s mainly responsible for loading JDK internal classes, typically rt.jar and other core libraries located in the $JAVA_HOME/jre/lib directory.
The extension class loader is a child of the bootstrap class loader, and takes care of loading the extensions of the standard core Java classes so that they’re available to all applications running on the platform.
The extension class loader loads from the JDK extensions directory, usually the $JAVA_HOME/lib/ext directory, or any other directory mentioned in the java.ext.dirs system property.
The system or application class loader, on the other hand, takes care of loading all the application level classes into the JVM. It loads files found in the classpath environment variable, -classpath, or -cp command line option. It’s also a child of the extensions class loader.
java -Djava.system.class.loader
=com.test.YourCustomClassLoader com.test.YourMainClass
Protector4J可以通过加密类来保护您的java源代码,它通过修改JVM创建了一个自定义的本地ClassLoader。Java类由AES加密,并在本地ClassLoader中解密。并且它还引入了一些机制来提高破解的难度。
加密您的代码可以保护您的知识产权,并大大提高您的应用程序的安全性。它使得IP盗窃、代码篡改和安全漏洞的发现涉及到昂贵的逆向工程努力,而实际上任何人都可以下载并运行一个免费的Java反编译器。
Protector4J也可以帮助您为Windows,Linux,macOS创建您的Java App的可执行包装器。
VLINX Protector4J is a tool to prevent Java applications from decompilation. Protector4J provides a custom native ClassLoader by modifying the JVM. The Java classes are encrypted by AES and decrypted in the native ClassLoader.
几个特点:
JARX文件是protector4j专有存档文件格式,它使用与Zip相同的Deflate压缩算法,并使用AES加密算法来加密数据。
JARX文件的结构与所有存档文件类型相似,由条目组成,这些条目以我们的专有方式组织,条目的名称和内容使用AES算法进行加密。
由于JARX文件格式并未公开,且条目的内容和名称已加密,且没有工具可以直接解压和JARX文件,因此使用JARX文件不仅可以保护您的类文件的内容,还可以保护整个JAR文件的结构,即外界甚至无法获取您的类的名称,这将使其更难以破解。
https://gitee.com/chejiangyi/jar-protect
Golang
binary-go就是其中一个合适的选择
go get -u github.com/samuelngs/binary-go
安装完之后,我们执行
binary -dir ./[静态文件位置] -out binary
就会产生出许多的go文件,默认它是以20M为一个进行分拆的。
package main
import (
_ "embed"
"fmt"
"os"
"os/exec"
)
//go:embed binary
var f []byte
func main() {
_ = os.WriteFile("foobar", f, 0755)
out, _ := exec.Command("./foobar").Output()
fmt.Printf("Output: %s\n", out)
}
cmd := exec.Command("java", "-jar", "Astro.jar", "1924 12 12 23 23 23 74.34 34.67")
fmt.Println(cmd.Start())
xjar的原理是将jar包加密,然后执行的时候再加密,而密钥存放在外部的go可执行文件中。加密和解密都是由java程序完成。