最近在使用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: