最近在使用kafkatool 作为查看Kafka消息的工具,但是只支持text、json、xml等常见格式。若想支持自定义的消息格式,例如protobuf,则需要自己编写插件。辛辛苦苦写好一个插件后,发现需要付费才能使用。向来都是白嫖的我,想到它既然是用Java写的,那么反编译一下,是不是有希望绕过付费呢?
反编译
说干就干,很快地在GitHub上找到了一个反编译工具Luyten。kafkatool的lib目录中有许多jar包,那哪个是主jar包呢?依次用解压工具打开看看,发现ofjar.jar
的包名是com.kafkatool
,看来就是它了。
遂用Luyten打开ofjar.jar
,搜索licensed users
,顺利地找到了检查是否付费的地方,详细流程如下:
查看反编译后的源码,发现在使用自定义消息解码器前,判断了LICENSE是否过期,若判断过期了就显示Feature only available for licensed users
。那么我们只需要hack这个if判断,让它返回true就好了。
定位字节码
在Luyten中转换为字节码查看模式,下图中高亮部分,即是此关键if对应的字节码,我们只需要把ifeq
改成ifneq
就可以绕过付费检查了。
那么ifeq
对应的字节码是啥呢?参考JVM Specs可以发现,ifeq
指令是0x99
,其后是两个字节的分支地址。存放的是若判断为true,下一条指令相对于本条指令的offset。
Execution then proceeds at that offset from the address of the opcode of this if
instruction.
Luyten中显示的是,本条指令地址是19
,跳转的指令是28
,则offset应该是9
,所以完整的ifeq
对应的字节码是0x99 00 09
。若要跳过付费检测,则需要将ifeq
改成ifne
,即将字节码改成0x9a 00 09
即可。
修改字节码
如何修改字节码呢?找一个二进制文件编辑器就OK了。我使用的是Hex Fiend
。首先将ofjar.jar
解压,然后找到DecoderAdapter.class
,打开后,搜索上述的ifeq
字节码ox99 00 09
,刚好只能找到一个。故将其替换为0x9a 00 09
即可。
替换原始Jar
修改DecoderAdapter.class
后,重新打包jar包,zip -r ofjar.jar *
。然后替换kafkatool原始的ofjar.jar
。发现可以看到自定义解码后的消息了!!!
使用javaagent
当然,手动修改字节码还是挺麻烦的,每次发布新版本都需要手动修改。因此可以使用javaagent技术,在JVM加载Class的字节码时,对其内容进行动态修改。JAVA SDK 提供了相应的接口,详见java.lang.instrument。但是这个接口比较底层,仍然需要自己手动将修改后的字节码传递给JVM。为了解决这个问题,可以使用开源库javassist帮忙将修改后的Java代码编译成字节码。
那么我们应该怎么修改相关的函数呢?观察到源代码的36行会调用init()
函数:加载LICENSE,判断是否过期后,将其赋值给this.expired
。之后再对this.expired
进行判断,判断是否有资格加载用户自定义的插件代码。故我们只需要修改init()
函数,不走LICENSE校验流程,直接将this.expired
设置为false
即可。即将init()
函数体修改为{this.expired = Boolean.valueOf(false);}
。
详细代码如下:
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.Instrumentation;
public class KafkaToolAgent {
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {
if (!"com/kafkatool/common/DecoderAdapter".equals(className)) {
return null;
}
try {
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("com.kafkatool.common.DecoderAdapter");
CtMethod convertToAbbr = clazz.getDeclaredMethod("init");
String methodBody = "{this.expired = Boolean.valueOf(false);}";
convertToAbbr.setBody(methodBody);
byte[] byteCode = clazz.toBytecode();
clazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
});
}
}
References: