最近私下搭建了一下冒险岛的私服来自娱自乐下,从网上获取了冒险岛的源码。然后搭建了服务在游戏的过程中发现了不少BUG,于是就想看下源码反编译后修改下bug。
解压jar后,用反编译工具发现竟然不能反编译,然后有尝试了各种各样的反编译工具结果都失败了。然后打开了启动服务的bat脚本发现命令中有着这样的命令。
一般加了这样的命令等于在加载class之前回去执行这个dll文件进行一系列操作,这个文件名也很直白-解密。那肯定这个jar包里的class就是被加密了。然后打开了class的16进制文件发现,class16进制文件开头的魔改数字应该是ca fe ba be。此时打开发现完全对应不上。下面就开始解密了。
首先我们获取不到加密的方式,想通过加密区写解密的方法自然不现实。没法通过写解密方法去解密文件,那我们应该首先想到jvm加载执行class的过程。既然虚拟机可以正常解析class文件,那么经过虚拟机加载的class一定是解密之后。所以,解密后的文件肯定会在jvm中加载。然后经过我谷歌,一番面向谷歌百度的编程之后,发现了java探针技术,其实这个探针技术和spring的aop有点类似。就是在执行某个class的时候对他进行动态代理。在执行前后插入一些自己想要的方法操作。
具体代码下
import java.io.*;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
public class ClazzDumpCustomAgent implements ClassFileTransformer {
/**
* 导出过滤表达式,此处为类名前缀, 以 -f 参数指定
*/
private String filterStr;
/**
* 导出文件目录根目录, 以 -d 参数指定
*/
private String exportBaseDir = "/tmp/";
/**
* 是否创建多级目录, 以 -r 参数指定
*/
private boolean packageRecursive;
public ClazzDumpCustomAgent(String exportBaseDir, String filterStr) {
this(exportBaseDir, filterStr, false);
}
public ClazzDumpCustomAgent(String exportBaseDir, String filterStr, boolean packageRecursive) {
if(exportBaseDir != null) {
this.exportBaseDir = exportBaseDir;
}
this.packageRecursive = packageRecursive;
this.filterStr = filterStr;
}
/**
* 入口地址
*
* @param agentArgs agent参数
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
String exportDir = null;
String filterStr = null;
boolean recursiveDir = false;
if(agentArgs != null) {
if(agentArgs.contains(";")) {
String[] args = agentArgs.split(";");
for (String param1 : args) {
String[] kv = param1.split("=");
if("-d".equalsIgnoreCase(kv[0])) {
exportDir = kv[1];
}
else if("-f".equalsIgnoreCase(kv[0])) {
filterStr = kv[1];
}
else if("-r".equalsIgnoreCase(kv[0])) {
recursiveDir = true;
}
}
}
else {
filterStr = agentArgs;
}
}
inst.addTransformer(new ClazzDumpCustomAgent(exportDir, filterStr, recursiveDir));
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (needExportClass(className)) {
int lastSeparatorIndex = className.lastIndexOf("/") + 1;
String fileName = className.substring(lastSeparatorIndex) + ".class";
String exportDir = exportBaseDir;
if(packageRecursive) {
exportDir += className.substring(0, lastSeparatorIndex);
}
exportClazzToFile(exportDir, fileName, classfileBuffer); //"D:/server-tool/tmp/bytecode/exported/"
System.out.println(className + " --> EXPORTED");
}
return classfileBuffer;
}
/**
* 检测是否需要进行文件导出
*
* @param className class名,如 com.xx.abc.AooMock
* @return y/n
*/
private boolean needExportClass(String className) {
String str[] = className.split("/");
String packageName = str[0];
List<String> list = new ArrayList<>();
String a = "a";
String b = "b";
String c = "c";
String client = "client";
String configs = "configs";
String constants ="constants";
String d = "d";
String scripts = "scripts";
String server = "server";
String tools = "tools";
String Test = "Test";
list.add(a);
list.add(b);
list.add(c);
list.add(client);
list.add(configs);
list.add(constants);
list.add(d);
list.add(scripts);
list.add(server);
list.add(tools);
list.add(Test);
String callback = "callback";
String com = "com";
String config = "config";
String mode = "mode";
String okhttp3 = "okhttp3";
String okio = "okio";
String org = "org";
String util = "util";
list.add(callback);
list.add(com);
list.add(config);
list.add(okhttp3);
list.add(okio);
list.add(org);
list.add(util);
list.add(mode);
//写文件
SimpleDateFormat sdf2=new SimpleDateFormat("yyyy-MM-dd");
String dateString = sdf2.format(new Date());
File file = new File("D:/file/"+dateString+".txt");
BufferedWriter out = null;
String conent1 = "className is: "+className;
String conent2 = "packageName is: "+packageName;
try {
if(!file.exists()) {
//初次写入
file.createNewFile();
out = new BufferedWriter( new OutputStreamWriter(
new FileOutputStream(file)));
} else {
//追加
out = new BufferedWriter( new OutputStreamWriter(
new FileOutputStream(file, true)));
}
out.write(conent1+" "+conent2+";");
out.write("\r\n");;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(list != null) {
if(list.contains(packageName)) {
System.out.println("packageName is: " + packageName);
return true;
}
else {
return false;
}
}
if (!className.startsWith("java") && !className.startsWith("sun")) {
return true;
}
return false;
}
/**
* 执行文件导出写入
*
* @param dirPath 导出目录
* @param fileName 导出文件名
* @param data 字节流
*/
private void exportClazzToFile(String dirPath, String fileName, byte[] data) {
try {
File dir = new File(dirPath);
if(!dir.isDirectory()) {
dir.mkdirs();
}
File file = new File(dirPath + fileName);
if (!file.exists()) {
System.out.println(dirPath + fileName + " is not exist, creating...");
file.createNewFile();
}
else {
// String os = System.getProperty("os.name"); // 主要针对windows文件不区分大小写问题
// if(os.toLowerCase().startsWith("win")){
// // it's win
// }
try {
int maxLoop = 9999;
int renameSuffixId = 2;
String[] cc = fileName.split("\\.");
do {
Long fileLen = file.length();
byte[] fileContent = new byte[fileLen.intValue()];
FileInputStream in = new FileInputStream(file);
in.read(fileContent);
in.close();
if(!Arrays.equals(fileContent, data)) {
fileName = cc[0] + "_" + renameSuffixId + "." + cc[1];
file = new File(dirPath + fileName);
if (!file.exists()) {
System.out.println("new create file: " + dirPath + fileName);
file.createNewFile();
break;
}
}
else {
break;
}
renameSuffixId++;
maxLoop--;
} while (maxLoop > 0);
}
catch (Exception e) {
System.err.println("exception in read class file..., path: " + dirPath + fileName);
e.printStackTrace();
}
}
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
}
catch (Exception e) {
System.err.println("exception occur while export class.");
e.printStackTrace();
}
}
}
编写以上代码后将代码编译成class文件放在指定的位置然后通过以下指令运行jar包。
jre\bin\java.exe -server -Xms2000m -Xmx2000m -Xss256k -XX:ReservedCodeCacheSize=256m -jar -agentlib:jre/bin/decrypt -javaagent:config/jvmti1.jar=-d=config/Servers/classes;-r config/Server.jar
注意加粗的字体,这一段是关键。
config/jvmti1.jar 代表上面代码打成的jar包。
-d=config/Servers/classes 代表解析后文件地址。
-r 代表循环解析,会按照对应目录生成文件。
然后只要运行过得class文件都会在你指定的目录下被加载出来。
至此这次破解就已经全部完成,当然想要获取所有的文件还是需要慢慢的去执行各个class文件才行。毕竟只有jvm加载过得class才会去被这个代理拦截生成解密后的class文件。