为了防止产品代码泄漏或授权等被破解,想到对源码加密,说是对源码加密,实际是需要对class文件进行加密。如果对class文件加密了,那类加载器如何能解析呢?本文讲解的就是SpringWeb项目加密后如何能在tomcat下面启动运行的。
1、war包加密;
2、tomcat类加载器修改;
3、Spring-asm类加载修改;
1、War包加密
War加密其实很简单,就是把war解压缩、将目录WEB-INF/classess下的所有class文件进行加密,然后再压缩成war包。
解压缩并进行加密
public static List<String> unWarAndEncryptionClass(File srcFile, String destDirPath) throws RuntimeException {
//记录解压出来的所有文件名
List<String> filesName = new ArrayList<>();
long start = System.currentTimeMillis();
// 判断源文件是否存在
if (!srcFile.exists()) {
throw new RuntimeException(srcFile.getPath() + "所指文件不存在");
}
// 开始解压
ZipFile zipFile = null;
try {
zipFile = new ZipFile(srcFile, Charset.forName("GBK"));
Enumeration<?> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) entries.nextElement();
// System.out.println("解压文件:" + entry.getName());
// 如果是文件夹,就创建个文件夹
if (entry.isDirectory()) {
String dirPath = destDirPath + "/" + entry.getName();
File dir = new File(dirPath);
dir.mkdirs();
} else {
//添加进filesName
filesName.add(entry.getName());
// 如果是文件,就先创建一个文件,然后用io流把内容copy过去
File targetFile = new File(destDirPath + "/" + entry.getName());
// 保证这个文件的父文件夹必须要存在
if (!targetFile.getParentFile().exists()) {
targetFile.getParentFile().mkdirs();
}
targetFile.createNewFile();
// 将压缩文件内容写入到这个文件中
InputStream is = zipFile.getInputStream(entry);
FileOutputStream fos = new FileOutputStream(targetFile);
if (targetFile.getName().endsWith(".class") && targetFile.getPath().indexOf("WEB-INF") > -1) {
int data;
while ((data = is.read()) != -1) {
//对每个字节进行加密。
fos.write(data ^ 0xFF);
}
} else {
int len;
byte[] buffer = new byte[BUFFER_SIZE];
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
}
// 关流顺序,先打开的后关闭
fos.close();
is.close();
}
}
long end = System.currentTimeMillis();
System.out.println("解压完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("unzip error from ZipUtils", e);
} finally {
if (zipFile != null) {
try {
zipFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return filesName;
}
再将解压后的文件夹重新压缩为war包,可使用代码也可以手动压缩后修改后缀都可以。
修改tomcat类加载器。
修改类加载器WebappClassLoaderBase
类加载器:org.apache.catalina.loader.WebappClassLoaderBase
我使用的是apache-tomcat-8.5.87-src进行修改;
修改WebappClassLoaderBase.findClassInternal(String Name)。找到以下方法块
byte[] binaryContent = resource.getContent();
if (binaryContent == null) {
// Something went wrong reading the class bytes (and will have
// been logged at debug level).
return null;
}
在以上方法块之后添加以下代码:
try {
DataInputStream filefosStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(binaryContent)));
//当发现加载的class文件内容无法解析为class时,对字节码进行解密。
if (filefosStream.readInt() != Const.JVM_CLASSFILE_MAGIC) {
ByteArrayInputStream ois = new ByteArrayInputStream(binaryContent);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int data;
while ((data = ois.read()) != -1) {
bos.write(data ^ 0xFF);
}
bos.flush();
bos.close();
binaryContent = bos.toByteArray();
}
} catch (IOException e) {
}
ClassParser类加载修改
类:org.apache.tomcat.util.bcel.classfile.ClassParser
修改方法:
public ClassParser(final InputStream inputStream){
this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
}
修改为:
public ClassParser(final InputStream inputStream) {
// this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
//首先判断class文件能否正常解析,如果不能则进行解密
ByteArrayOutputStream boss = new ByteArrayOutputStream();
DataInput dataInputStream = null;
try {
int len;
byte[] buffer = new byte[BUFSIZE];
while ((len = inputStream.read(buffer)) != -1) {
boss.write(buffer,0,len);
}
boss.flush();
boss.close();
byte[] classByte = boss.toByteArray();
dataInputStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(classByte)));
if (dataInputStream.readInt() != Const.JVM_CLASSFILE_MAGIC) {
//不是.class 文件,进行解码
//解码
ByteArrayOutputStream bos = new ByteArrayOutputStream();
BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(classByte));
int data;
while ((data = bis.read()) != -1) {
//进行解密操作
bos.write(data ^ 0xFF);
}
try {
bos.flush();
bos.close();
} catch (IOException e) {
}
dataInputStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(boss.toByteArray())));
}else{
dataInputStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(classByte)));
}
} catch (IOException e) {
}
this.dataInputStream = dataInputStream;
}
修改jsp解析器。
类:org.apache.jasper.compiler.JDTCompiler
jsp文件在解析的时候可能会用到自定义标签,这时class文件会被加载。找到类JDTCompiler 下generateClass(String [] snap)方法。
找到以下代码块,
try (InputStream is = classLoader.getResourceAsStream(resourceName)) {
if (is != null) {
//这里开始读取class内容
byte[] classBytes;
byte[] buf = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length);
int count;
while ((count = is.read(buf, 0, buf.length)) > 0) {
baos.write(buf, 0, count);
}
baos.flush();
classBytes = baos.toByteArray();
char[] fileName = className.toCharArray();
ClassFileReader classFileReader = new ClassFileReader(classBytes, fileName, true);
return new NameEnvironmentAnswer(classFileReader, null);
}
} catch (IOException | ClassFormatException exc) {
log.error(Localizer.getMessage("jsp.error.compilation.dependent", className), exc);
}
修改以上代码块,增加class内容判断,若不能解析则进行解密
try (InputStream is = classLoader.getResourceAsStream(resourceName)) {
if (is != null) {
byte[] classBytes;
byte[] buf = new byte[8192];
ByteArrayOutputStream baos = new ByteArrayOutputStream(buf.length);
int count;
while ((count = is.read(buf, 0, buf.length)) > 0) {
baos.write(buf, 0, count);
}
baos.flush();
classBytes = baos.toByteArray();
//解密开始/
try {
DataInputStream filefosStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(classBytes)));
if (filefosStream.readInt() != Const.JVM_CLASSFILE_MAGIC) {
ByteArrayInputStream ois = new ByteArrayInputStream(classBytes);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int data;
while ((data = ois.read()) != -1) {
bos.write(data ^ 0xFF);
}
bos.flush();
bos.close();
classBytes = bos.toByteArray();
}
} catch (IOException e) {
}
//解密结束/
char[] fileName = className.toCharArray();
ClassFileReader classFileReader = new ClassFileReader(classBytes, fileName, true);
return new NameEnvironmentAnswer(classFileReader, null);
}
} catch (IOException | ClassFormatException exc) {
log.error(Localizer.getMessage("jsp.error.compilation.dependent", className), exc);
}
修改spring-asm包中类加载器
类:org.springframework.asm.ClassReader
找到类文件读取方法,由于没下载到源码,从jar包拉出class文件反编译后修改的,方法名不详,修改后方法块如下:
private static byte[] a(InputStream var0) throws IOException {
if (var0 == null) {
throw new IOException("Class not found");
} else {
byte[] var1 = new byte[var0.available()];
int var2 = 0;
while (true) {
int var3 = var0.read(var1, var2, var1.length - var2);
byte[] var4;
if (var3 == -1) {
if (var2 < var1.length) {
var4 = new byte[var2];
System.arraycopy(var1, 0, var4, 0, var2);
var1 = var4;
}
//判断class文件读取后的内容是否正确,不正确进行解密。
DataInputStream filefosStream = new DataInputStream(new BufferedInputStream(new ByteArrayInputStream(var1)));
if (filefosStream.readInt() != 0xCAFEBABE) {
ByteArrayInputStream ois = new ByteArrayInputStream(var1);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int data;
while ((data = ois.read()) != -1) {
bos.write(data ^ 0xFF);
}
try {
bos.flush();
bos.close();
} catch (IOException e) {
}
var1 = bos.toByteArray();
}
return var1;
}
var2 += var3;
if (var2 == var1.length) {
var4 = new byte[var1.length + 1000];
System.arraycopy(var1, 0, var4, 0, var2);
var1 = var4;
}
}
}
}
至上,我自己项目用到的类加载器修改完毕。你的项目可能会用到其他三方架构或插件可能也会加载类文件。那也需要找到相应类加载器进行修改。
本方所用的方法,只是增加了别人破解代码的成本,别人也知道你修改了类加载器,也能反编译出类加载器里修改的内容,也就找到了class文件加密的方法。
附件:
1、war包加密源码;
2、tomcat修改后的jar包;
3、spring-asm包。(spring-asm.3.1.1.RELEASE.JAR)