JEB破解

大类
Util
技术标签
逆向-Java
开发-HookPatch-Java
开发-协议脚本
优先级
Medium
状态
Maintaining
开始日期
Jul 28, 2021
最后更新
Jan 5, 2022
Public
Public

前言

  1. 核心目标:不泄露我们的License信息给JEB
  1. 需求:自动更新,无需每次重新patch

思路

I. 确认JEB中校验License的部分

看雪上有人持续跟进JEB的破解:https://bbs.pediy.com/thread-268316.htm
由于他破解的是demo+community edition,缺少一部分功能,所以他选择patch java class

1. 找好用的Java Decompiler

  • JEB:不支持JAR
  • JADX:快捷键为0,改名有bug,自动改名垃圾的要死,而且也经常反编译不出来
  • JD-GUI:界面友好点,反编译比JADX还菜
  • 单独IDEA直接反编译JAR:可以反编译,效果比JADX好,但是不能搜索
  • Fern Flower+IDEA:反编译效果好,操作顺手,可以搜索,可以跳转,但是改名还是有bug

2. 尝试进行各种比对

  • Beyond Compare+直接比较jar:文件名中有大小写的区别,Beyond Compare匹配文件名时会忽略大小写
  • fern-flower反编译后在操作:
    • fernflower反编译命令:
    • java -cp '/mnt/c/Program Files/JetBrains/IntelliJ IDEA 2020.2.3/plugins/java-decompiler/lib/java-decompiler.jar' org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -hdc=0 -dgs=1 -rsy=1 -rbr=1 -lit=1 -nls=1 -mpm=60 jebori.jar jebdec/jeb_jar_420ori
    • 输出JAR包配合Beyond Compare:不会错匹,但是会丢文件
    • 输出JAR包wsl2解压后用\\wsl$配合Beyond Compare,效果不错
    • 解压后配合WSL2的WSLg使用Meld同样不错
 

3. 分析所需修改点

notion image
Diff的部分分别为:
  1. 文件自校验部分:AbstractContext.integrity_failed = XXX
    1. notion image
      • 调用Jar中对应JarEntry的java.util.jar.JarEntry.getCertificates方法,检查certificate是否相同
      • (如直接修改不签名,方法返回null,若重签名则返回不一样的证书)
  1. 各种授权校验:AbstractClientContext、Licensing类里面一堆方法
  1. 增加自己的破解信息:Launcher
  1. 解除运行时间限制:mB
    1. notion image
  1. 解除反汇编导出限制:XXXXXExporter里面,把代码补齐
  1. 修复debugger的bug
  1. 解除保存限制:serialization相关
  1. 解除文件格式支持限制:补全parsers代码

II. 判断是否需要直接patch java class

我们可以直接Patch Licensing类,因为:
  1. 对License的解析只有Licensing类的static class constructor里面有
  1. 但是JEB在AbstractClientContext、AbstractContext以及各种小角落中大量调用Licensing类的一堆方法,一旦少patch一个方法就可能造成license泄漏
  1. patch class file可能会触发自校验
所以我想了一下,感觉可以直接做个license generator:
  1. Licensing类实际上调用了jebglobal中的类读取了jeb-license.txt,所以如果换掉jeb-license,就可以整个替换掉license内容
 

III. 编写LicGen

JEB License的校验类可以通过Licensing类定位到,结构为一层CRC、一层RC4、一层莫名其妙的随机数据+checksum、和最外面一层RC4
from io import BytesIO import struct import time from datetime import datetime from zlib import crc32 from Crypto.Cipher import ARC4 as RC4 RC4.key_size = range(1,257) class BIO(BytesIO): def writeInt(o, x): return o.write(struct.pack("<I", x)) def writeLong(o, x): return o.write(struct.pack("<Q", x)) def writeUTF(o, x): return o.write(struct.pack(">H", len(x)) + x) def genJEBLic(userId, userName, email, organization, licId, userCount, buildkey, licTime, licValidity, unk1=0xBA, unk2=0x3F, genTime=int(time.time() * 1000)): licInfo = BIO() licInfo.writeLong(genTime) licInfo.writeUTF(userName) licInfo.writeUTF(organization) licInfo.writeUTF(email) licInfo.writeInt(userId) licInfo.writeLong(licId) licInfo.writeInt(userCount) licInfo.writeInt(licTime) licInfo.writeInt(licValidity) licInfo.writeUTF(buildkey) licInfo.writeInt(unk1) licInfo.writeInt(unk2) licInfoData = licInfo.getvalue() licWrap1 = BIO() licWrap1.writeInt(1) licWrap1.writeInt(crc32(licInfoData) & 0xffffffff) licWrap1.write(licInfoData) licWrap1Data = licWrap1.getvalue() wrap1Len = len(licWrap1Data) wrap1Key = 0x11223344 rc4KeyWrap1 = struct.pack("<II", wrap1Len, wrap1Key) + ''.join((chr(c & 0xff) for c in [69, 103, -94, -103, 95, -125, -15, 16])) licWrap1Enc = RC4.new(rc4KeyWrap1).encrypt(licWrap1Data) licWrap2 = BIO() licWrap2.writeInt(1) licWrap2.write(257 * '\x00') licWrap2.writeInt(wrap1Len) licWrap2.writeInt(wrap1Key) licWrap2.write(licWrap1Enc) #return licWrap2.getvalue() licWrap2Data = licWrap2.getvalue() licWrap3 = licWrap2Data.ljust(4095, '\x00') licWrap3 += chr(sum([ord(c) for c in licWrap3]) & 0xff) licWrap3Enc = RC4.new("java").encrypt(licWrap3) return '\n'.join([ "# JEB License File", "", "User ID: %d" % userId, "Name: %s" % userName, "Email: %s" % email, "Organization: %s" % organization, "", "License ID: %d" % licId, "Build: %s, %d user" % (buildkey, userCount), "Period: %s to %s" % ( datetime.fromtimestamp(licTime).strftime("%Y-%m-%d"), datetime.fromtimestamp(licTime + licValidity * 24 * 60 * 60).strftime("%Y-%m-%d") ), "", "Validation: %s" % licWrap3Enc.encode('hex') ]) #licInfo.writeInt(1) #o.writeInt(1) print genJEBLic(1234567890, "Misty", "jeb@misty.moe", "Misty Moe", 123456789012345678, 1, "jeb-pro", time.time() - 24 * 60 * 60, 365 * 12)
 

IV. 调试JEB

新开一个IDEA,把jeb.jar导入进来,然后加一个JAR application的调试配置,在IDEA直接反编译的源码里下断点即可
notion image
 

V. 自动更新

更新邮件里面拿到的zip里jeb.jar是加密的,需要通过jebi.jar安装器来解密
加密的方法是RC4,我们直接RC4解密后再用一个固定密钥加密回去就可以
顺手写了个辅助脚本,传入更新包下载url、解密密钥后,就可以自动patch并上传google drive
#!/bin/bash set -e if [ $# -eq 0 ]; then ORIZIP=$(cd raw && ls -t -1 *.zip | head -1) elif [ $# -eq 2 ]; then echo "- [+] Parsing URL.." DOWNURL=$1 DOWNPASS=$2 url=$(curl --silent -sI "$DOWNURL" | grep Location | tr -d '\r') ORIZIP="${url##*/}" echo $DOWNPASS > raw/$ORIZIP.pwd echo "- [+] Downloading package $ORIZIP.." curl -Lo "raw/$ORIZIP" "$DOWNURL" else echo "- [!] Wrong arguments" exit 1 fi echo "- [+] Uploading raw zip..." rclone -vP --config=./rclone.conf copy raw --include "${ORIZIP}*" misty-secret-drive:jeb-vul337/jeb-vul337-store echo "- [+] Going to process $ORIZIP" ORIPWD=$(cat raw/${ORIZIP}.pwd) NEWZIP=$(echo ${ORIZIP} | sed -r 's/jeb-(.*)-(.*)-(.*)-(.*)\.zip/jeb-\1-\2-patched.zip/') FIXEDPWD="JEBPRO" TMPDIR=/tmp/jebpatch echo "- [+] Creating temp dir" rm -rf $TMPDIR mkdir $TMPDIR echo "- [+] Unzipping jeb.jar" unzip -p "raw/$ORIZIP" "bin/app/jeb.jar.encrypted" > $TMPDIR/jeb.jar.encrypted echo "- [+] Decrypting jeb.jar" python -c "from Crypto.Cipher import ARC4 as RC4; o = open('$TMPDIR/jeb.jar.patched', 'wb'); f = open('$TMPDIR/jeb.jar.encrypted', 'rb'); o.write(f.read(32)); o.write( RC4.new('$FIXEDPWD').encrypt(RC4.new('$ORIPWD').decrypt(f.read())))" echo "- [+] Preparing patch" cp raw/$ORIZIP release/$NEWZIP mkdir -p $TMPDIR/update-root/bin/app mv $TMPDIR/jeb.jar.patched $TMPDIR/update-root/bin/app/jeb.jar.encrypted cp jeb-license.txt $TMPDIR/update-root/bin/app/jeb-license.txt echo "- [+] Repacking zip" #7za a release/$NEWZIP $TMPDIR/update-root/ PNEWZIP="$(realpath release/$NEWZIP)" (cd $TMPDIR/update-root && zip -r $PNEWZIP .) echo "- [+] Cleaning up" rm -rf $TMPDIR echo "- [+] Uploading patched zip..." rclone -vP --config=./rclone.conf copy "release/${NEWZIP}" misty-secret-drive:jeb-vul337/jeb-vul337-patch
 

VI. 自动请求更新

用邮件请求更新需要检测邮箱,而我们用的教育邮箱,收信会相对复杂一些。
JEB实际上提供了直接请求到更新包的接口,位于BasePinger的ping方法中,可以请求服务器进行/checkupdate,获取更新包的url、版本、checksum,并进行自动更新。
我们的逆向过程分为两步,第一步是静态逆向请求的内容与格式,第二部分则是抓包并查看具体字段的内容。
  • 静态逆向更新请求格式

    过程记录:

    Day1

    从看雪4.20的demo入手,可以看到他主要是在改少的东西,具体暗桩在哪里还需要仔细看
    notion image
    根据初步的比对,这几个文件高度可疑
     
    然后我们还得仔细审核一下jeb的联网请求
     

    Day2

    beyond compare似乎会忽略case来比对文件,因此利用了fernflower把整个文件夹在wsl2下面反编译了
    java -cp '/mnt/c/Program Files/JetBrains/IntelliJ IDEA 2020.2.3/plugins/java-decompiler/lib/java-decompiler.jar' org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler -hdc=0 -dgs=1 -rsy=1 -rbr=1 -lit=1 -nls=1 -mpm=60 jebori.jar jebdec/jeb_jar_420ori
    比对后发现这些是关键修改:
    notion image
    确定修改点:
    破解License相关:
    方案:爆破
    • JebNet.getSupposedlyGoodEpoch
    • com.pnfsoftware.jeb.client.Licensing.getExpirationTimestamp
    • com.pnfsoftware.jeb.client.Licensing
      • build_type (canUseCoreAPI)
      • license_id
    • jebglobal.XXX.retrieveLicenseKey
    方案:keygen
    • 屏蔽telemetry
      • com.pnfsoftware.jeb.client.telemetry.StandardTelemetryEndpoint.dump
    • 屏蔽更新
      • com.pnfsoftware.jeb.client.AbstractClientContext.shouldCheckUpdates
    • 过自校验
      • java.util.jar.JarEntry.getCertificates
     

    Day3

    逆了一圈,发现并没有啥暗桩,直接逆一下JEB的License生成就ok了
    notion image