提前声明:由于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