提前声明:由于GitHub copilot是服务端下发的代码补全,因此如果不内置合法账号,则无法通过逆向本地文件获取copilot代码补全能力,本文仅讨论本地插件的改造

1 、前言

最近GitHub copilot过期了,之前也未想过jetbrains插件的逆向。于是乎研究了一下GitHub copilot插件的的激活机制,遗憾的是最后发现GitHub copilot的提示都是云端下发的,本地的修改是不能通过服务端校验

不过也许会有人想对插件进行定制化改造,这时候本篇文章或许可以提供一些有用的参考

2、开发环境

Jadx:https://github.com/skylot/jadx/releases 下载最新版或者爱盘里的版本均可
Idea: 2023.2.3 自行寻找下载
GitHub copilot 下载地址:https://plugins.jetbrains.com/plugin/17718-github-copilot/versions

Javassist :3.29.2-GA  可以新建gradle工程从maven仓库引入依赖

3、分析

3.1 目标

我的目标是将copilot插件的状态mock为登录态

3.2 插件的结构

从插件官网下载插件到本地后,我们可以发现插件是个zip文件,目录结构之这样的

打开lib目录,我们发现都是由jar包组成的,于是乎插件的逆向就简化为对jar的逆向

3.3 定位关键点

由于是jar包的逆向,我们无法用x64DBG、OD这类工具进行调试和修改了,我们可以用Jadx打开jar包

打开后发现,插件没有混淆,通过搜索关键词 signed in 我们可以快速定位到关键类AuthStatusResult

AuthStatusResult中保存了账号信息,包含user和status信息,其内部枚举类Status更为清晰,我们只需要将AuthStatusResult获取status的方法返回枚举类型Status.Ok,将user返回一个非空字符串就能达到 mock登陆状态的目的

    [b]public [b]enum Status {
        Ok("OK"),
        MaybeOk("MaybeOK"),
        NotSignedIn("NotSignedIn"),
        NotAuthorized("NotAuthorized"),
        FailedToGetToken("FailedToGetToken"),
        TokenInvalid("TokenInvalid");
        [b]private [b]final String id;
        Status(String id) {
            [b]this.id = id;
        }
                ...
    }

3.4 修改关键类

我们无法用x64DBG和OD来修改jar包,因此我们需要专业的字节码操作库Javassist来实现字节码的修改,Javassist拥有方法体替换、构造函数替换等一系列能力

我们要做的就是用它把AuthStatusResult改造成我们想要的样子

我们的目标是 修改class文件,并将其打入修改后的jar包

3.4.1 新建工程

Javassist是一个第三方库,我们可以用Idea新建一个gradle 构建的Kotlin工程,调用相关的方法

3.4.2 包装工具类

首先包装一个工具,帮助我们更好的来修改class文件,下面代码的作用就是替换方法体和构造函数.

注意替换代码文本中如果涉及到类,需要类的全名,例如com.github.copilot.lang.agent.commands.AuthStatusResult,如果是内部类,则需要用 外部类全名内部类名称,例如:com.github.copilot.lang.agent.commands.AuthStatusResult内部类名称,例如:com.github.copilot.lang.agent.commands.AuthStatusResultStatus

···java
fun modifyClass(
jarPath: String,
className: String,
patchClass: PatchClass
): PatchedClass {
ClassPool.getDefault().insertClassPath(jarPath)
val classToModify = ClassPool.getDefault().getCtClass(className)
patchClass.patchConstructors.forEach { data ->
val paramsTypes = data.parameterTypes.map {
ClassPool.getDefault().get(it)
}.toTypedArray()
var cont = try {
classToModify.getDeclaredConstructor(paramsTypes)
} catch (_: Exception) {
null
}
if (cont == null) {
cont = CtNewConstructor.make(paramsTypes, null, “{ {data.body} }”, classToModify) classToModify.addConstructor(cont) } else { cont.setBody(“{{data.body} }”, classToModify) classToModify.addConstructor(cont) } else { cont.setBody(“{{data.body} }”)
}

    }
    patchClass.patchMethods.forEach { data ->
        val methodToModify = classToModify.getDeclaredMethod(data.name)
        methodToModify.setBody(data.body)
    }
    val exportClassFolder = File(jarPath).parent + File.separator + "crack"
    classToModify.writeFile(exportClassFolder)
    val path = exportClassFolder + File.separator + className.replace(".", File.separator) + ".class"
    return PatchedClass(className, path)
}

···

3.4.3 替换代码

调用工具类替换方法体

 fun mockLoginStatus(jarPath: String): PatchedClass {
        val className = "com.github.copilot.lang.agent.commands.AuthStatusResult"
        return ByteCodeAssist.modifyClass(
            jarPath = jarPath,
            className = className,
            patchClass = PatchClass(
                patchMethods = listOf(
                    PatchMethod("setStatus", "this.status = ${makeClassName(className, "Status")}.Ok;"),
                    PatchMethod(
                        "setUser"
                    ),
                    PatchMethod("setErrorMessage"),
                    PatchMethod("getErrorMessage", "return null;"),
                    PatchMethod("isSignedIn", "return true;"),
                    PatchMethod("isUnauthorized", "return false;"),
                    PatchMethod("getStatus", "return ${makeClassName(className, "Status")}.Ok;"),
                    PatchMethod(
                        "getUser", """
                        return "yize";
                    """.trimIndent()
                    ),
                    PatchMethod("isError", "return false;"),
                    PatchMethod(
                        "forFailedToGetToken", """
                        return new com.github.copilot.lang.agent.commands.AuthStatusResult(${
                            makeClassName(
                                className,
                                "Status"
                            )
                        }.Ok, "yize", null);
                    """.trimIndent()
                    ),
                    PatchMethod(
                        "forError", """
                        return new com.github.copilot.lang.agent.commands.AuthStatusResult(${
                            makeClassName(
                                className,
                                "Status"
                            )
                        }.Ok, "yize", null);
                    """.trimIndent()
                    )
                ),
                patchConstructors = listOf(
                    PatchConstructor(
                        parameterTypes = listOf(
                            makeClassName(className, "Status"),
                            String::class.java.name,
                            String::class.java.name
                        ),
                        body = """
                            this.user = "yize";
                            this.status = ${makeClassName(className, "Status")}.Ok;
                            this.errorMessage = null;
                        """.trimIndent()
                    ),
                    PatchConstructor(
                        body = """
                            this.user = "yize";
                            this.status = ${makeClassName(className, "Status")}.Ok;
                            this.errorMessage = null;
                        """.trimIndent()
                    )
                )
            )
        )
    }

3.4.4 将修改后的class文件打包进修改版的jar包

    fun packageClassToJar(originalJarPath: String, patchedList: List<PatchedClass>): String? {
        val newJarPath = "$originalJarPath.crack.jar"
        try {
            val entrySet = patchedList.map { it.entryName }.toSet()
            // Create a new JAR file
            val newJar = JarOutputStream(File(newJarPath).outputStream())

            // Copy entries from the original JAR to the new JAR
            ZipInputStream(File(originalJarPath).inputStream()).use { originalZip ->
                while (true) {
                    val entry = originalZip.nextEntry ?: break
                    if (!entrySet.contains(entry.name)) {
                        newJar.putNextEntry(ZipEntry(entry.name))
                        newJar.write(originalZip.readBytes())
                        newJar.closeEntry()
                    }

                }
            }

            // Add the new class to the new JAR
            patchedList.forEach { data ->
                newJar.putNextEntry(ZipEntry(data.entryName)) // 文件路径名,例如com/github/copilot/lang/agent/commands/AuthStatusResult.class
                File(data.classFilePath).inputStream().use { input ->
                    newJar.write(input.readBytes())
                }
            }

            newJar.closeEntry()
            // Close the new JAR file
            newJar.close()
        } catch (e: Exception) {
            return null
        }
        return newJarPath
    }

我们填一些必要信息,测试一下

然后用jadx打开我们修改后的jar文件,可以看到已经修改成功

我们再用修改后的jar包替换原有的core.jar,在Idea中选择从本地导入插件,重启后看idea底部的GitHub copliot图标已变为登陆状态

但是我们仍然无法拥有GitHub copilot的代码补全能力,因为代码补全完全是服务端通过jsonrpc下发的,除非有能通过服务端校验的正式会员信息。可以通过使用Javaassist修改,内置正确的账号信息来实现有实际代码补全能力的插件

4 结束

问 :为什么要给Javaassist封装一层调用接口呢?
答:实现自动化修改的打包,假如jar文件中有很多类需要修改,不封装的话一个一个的手动改太麻烦了

完整版工程代码已开源到GitHub,不止可以用于copilot插件,也可根据实际情况适当修改,用于其它Java程序。

https://github.com/bestyize/ByteMate

扫码免费获取资源: