ctfshow:REVERSE红包六

reverseyulabu2026-10-12 13 次阅读

REVERSE 红包六

之前那个武穆遗书真的给我上强度了,折磨死我了,接下来看看这个……woc怎么是jar,这玩意不是mc模组吗,这个咋逆向啊,完全没思路啊

ok,工具下好了:JD-GUI

直接先用JD-GUI打开看一眼,有一串疑似flag的base64编码,但是解码后啥也不是,根据注释的hint,这题应该没这么简单,而题面提示说jar是个压缩包,那我搜一下吧:

好吧,那我解压看看,看到的东西跟我用JD-GUI看到的东西差不多,但是发现这个文件大小是不是不太对,为啥解压后文件还更小了,本来是4KB变成1KB了,这期神了,是不是有什么隐藏的东西

应该要用我的01edtior看一眼藏了啥,但是我没看过jar,不知道看不看得懂……果然看不懂,那就从这里开始分点(

这道题我是一点思路没有的,所以全程是跟着别人的题解复现的

0x00 找到隐藏class

我们才学完ELF和PE没多久,现在又要碰新文件了:.jar,我等把题单刷得差不读了就写一篇汇总好了。

这个东西本质是个压缩包,我们可以用7-zip来解压出文件(原本这个工具我以为只有misc才会用到)

得到:

复制代码
EZjar.class
META-INF

解压出来这个包变小了,说明肯定还有东西没解压出来,这个时候我们需要学习下压缩包的文件结构了

压缩包有不同种格式,但是jar本质上是一个zip压缩包,所以我们这里看zip的结构

好的学成归来了

这里我们可以看到目录区有两个Ezjar.class,但是解压出来只有一个,可能其中一个被隐藏了,又图中我们可以找到,两个文件分别对应的位置是:

  • 0x0269
  • 0x003D

但是我根据这两个位置定位到的文件都没啥问题啊,为啥解压出来只有一个?是因为有第二个被解压出来把第一个覆盖掉了吗?检查一下

问下狗屁通,原来如此,这里的手法比较……额,高明?看样子我们之前的理解稍微有些偏差,这道题目是把真实的压缩数据条目伪装成目录条目,也就是说,实际上我们想要的数据被隐藏在EzJar.class/,而这个条目被解压器认为是目录,所以没有按照普通文件的方式暴露

但是他不是一个简单的目录!他实际是有数据的!所以我们有两个办法:

  • 改二进制文件(麻烦)
  • 写脚本手工切出压缩数据,然后解压出来

显然后者更方便,毕竟现在都熟悉文件结构。ok啊,目标明确,写写写:

根据entry得到数据的位置和长度

python 复制代码
import zlib
f = open('EzJar.jar', 'rb')
f.seek(659) # 0x0293
data = f.read(3248)
f.close()
inflator = zlib.decompressobj(-zlib.MAX_WBITS) # 初始化解压器
x = inflator.decompress(data)
f = open('EzJar.class', 'wb')
f.write(x)
f.close()

最后写出来和别人题解差不多,这里提一嘴,这个-zlib.MAX_WBITS实际是-15,在初始化解压器中填这个值,代表用裸 DEFLATE的方式解压,这是我们在Local File Header可以看到的(文件中填的是8,代表裸 DEFLATE),还有其他的比如:

  • 15:为zlib格式
  • 31:为gzip格式

最后得到一个.class文件

0x01 读.class

感觉要开始简单了,毕竟麻烦的二进制已经解决了,不过我解出来的结果好像和大佬题解的不是很一样,可能是jd版本不同导致的,试下能不能做:

有很多注释,删掉先

这个时候转到我的vscode进一步处理,好样的看得我想吐血,呜呜,一点点解读吧

我们先来看main函数:

java 复制代码
public static void main(String[] paramArrayOfString) throws Exception {
  JOptionPane.showMessageDialog(null, I[l[0]], I[l[1]], l[2]);
  System.out.print(I[l[3]]);
  Scanner scanner = new Scanner(System.in);
  String str1 = scanner.next();
  Cipher cipher = Cipher.getInstance(I[l[2]]);
  cipher.init(l[1], new SecretKeySpec(I[l[4]].getBytes(), I[l[5]]));
  String str2 = new String(Base64.getEncoder().encode(cipher.doFinal(str1.getBytes())));
  if (lIl(I[l[6]].equals(str2))) {
    JOptionPane.showMessageDialog(null, I[l[7]]);
    System.out.print(I[l[8]]);
    "".length();
    if (null != null)
      return; 
  } else {
    JOptionPane.showMessageDialog(null, I[l[9]]);
    System.out.print(I[l[10]]);
  } 
}

虽然我不是很懂java,但是也能感受到这玩意麻烦到爆炸(此时我还在等待我的jdk下载好),创建一个scanner又创建一个cipher的。

这段main函数大概意思就是输入一个字符串,然后经过一系列加密,最后判断是否相同,至于输出什么现在还不是很看得出来,继续看下一段:

java 复制代码
private static void lII() {
  l = new int[18];
  l[0] = (0x9C ^ 0x88) & (0x7F ^ 0x6B ^ 0xFFFFFFFF); // 0x14 & 0xFFFFFFFB = 0
  l[1] = " ".length(); // 1
  l[2] = "   ".length(); // 3
  l[3] = "  ".length(); // 2
  l[4] = 0xB9 ^ 0xC5 ^ 0x20 ^ 0x58; // 4
  l[5] = " ".length() ^ 0x1C ^ 0x18; // 5
  l[6] = 0x10 ^ 0x16; // 6
  l[7] = 0x45 ^ 0x4D ^ 0x46 ^ 0x49; // 7
  l[8] = 106 + 107 - 66 + 32 ^ 33 + 90 - 98 + 162; // 8 注意这里是先加减后xor
  l[9] = 154 + 21 - 48 + 38 ^ 78 + 146 - 163 + 111; // 9
  l[10] = 0x1D ^ 0x17; // 10
  l[11] = 0x40 ^ 0x4B; // 11
  l[12] = 0x69 ^ 0x65; // 12
  l[13] = "  ".length() ^ 0x6A ^ 0x65; // 13
  l[14] = 0x48 ^ 0x46; // 14
  l[15] = 0x90 ^ 0x9F; // 15
  l[16] = 0xA0 ^ 0xB0; // 16
  l[17] = 0xB1 ^ 0xA0; // 17
}

这里我们给他一个个算一遍就是了,妈的写这么复杂给谁看,直接粘贴到python的print直接算就好了(

有啥用,额,通篇看下来,这个东西最大的作用就是提供了这个l数组,全都翻译过来就没问题了!

这里又设置了两个神秘对象(原本有注释被我删了),不知何用意,出题人是真怕我读懂了啊。

java 复制代码
private static String lI(String lllllllllIlIlII, long lllllllllIlIIll) {
}

private static String I(float lllllllllIIIlll, String lllllllllIIlIII) {
}

下面就用上了,本质就是……?怎么吧字符串当float和long啥意思

java 复制代码
private static void ll() {
  I = new String[l[17]];
  I[l[0]] = I("AQiA0bYffm9HvMlm7RnEMX/tEQAUj4Xb", "FrlAZ");
  I[l[1]] = I("hXzZyx8IUHw=", "Esxsh");
  I[l[3]] = l("ID8PFlEKM1kKHhIkWRUdBjFD", "gVysq");
  I[l[2]] = I("50fO6ARqllg=", "VZbFF");
  I[l[4]] = lI("mvXqH+/XIESPZaSG3ZbZlA==", "TuZSw");
  I[l[5]] = l("JQ0R", "aHBFu");
  I[l[6]] = I("dMKiRQ19iTevvzL7NtVg5+ye5BywL2QaxtVANFLuC5B2/KuC+/5L6BwtCB7zpWK1XBTQr0VWC3Vt/uYEl2xmjskE0dDrCk2C", "dPxYA");
  I[l[7]] = lI("B/MVYKSzgq8=", "phiUP");
  I[l[8]] = I("ZtBOhuHeK3Y=", "MfnkQ");
  I[l[9]] = lI("aPhz+GjGynRlU3Alo00QeQ==", "wWtUj");
  I[l[10]] = l("BRQFNiRyBwQrNDcUSw==", "RfjXC");
  I[l[11]] = I("pT10j0lChvyrNwYRFdqBzxqFp1ruTgo9", "hGcuT");
  I[l[12]] = lI("UhBbCDk5yqaWl1uHJyS/OGmtcfVyvOOsk78/1f0MU8U3UfAf1Xf0FWNbpcKes/0HRz9SU/icRJHswW2xWjHrcFzhpsvwzqUl", "eeMoV");
  I[l[13]] = I("s11BihYBzRBpcX9EF43utw==", "RjiXK");
  I[l[14]] = I("wk5jH1cyKoA=", "frsxP");
  I[l[15]] = I("Nfa0rxB8IRArMq2F4iLlLg==", "ulQWJ");
  I[l[16]] = lI("wLcWNd0Xsbw=", "JgPGn");
}

不管了,反正能看懂,我看看你后面搞什么活

java 复制代码
static {
  enc = I[l[14]];
  WA = I[l[15]];
  AC = I[l[16]];
}

private static boolean llI(byte lllllllllIIIIIl, float lllllllllIIIIII) {
  return (SYNTHETIC_LOCAL_VARIABLE_0 < SYNTHETIC_LOCAL_VARIABLE_1);
}

private static String l(boolean llllllllllIIllI, String llllllllllIlIlI) {

}

private static boolean lIl(long llllllllIlllllI) {
  return (SYNTHETIC_LOCAL_VARIABLE_0 != null);
}

static {
  lII();
  ll();
}

ok啊没活了,虽然下面这些东西有点神秘,但是我大概能看懂,但是我不会写java,于是决定使用python脚本来复现这个过程并逆向出flag

麻烦了,发现这个I和lI是关键的的东西,但是我的反汇编没有内容啊,难道注释暗藏玄机?问了狗屁通告诉我是JVM字节码,不是哥们刚学完汇编就给我来这个?

气笑了,换CFR反汇编就正常出来了,jd真是废物啊

把之前全是注释的地方补上:

java 复制代码
private static String lI(String lllllllllIlIllI, String lllllllllIlIlIl) {
    try {
        SecretKeySpec lllllllllIllIIl = new SecretKeySpec(MessageDigest.getInstance("MD5").digest(lllllllllIlIlIl.getBytes(StandardCharsets.UTF_8)), "Blowfish");
        Cipher lllllllllIllIII = Cipher.getInstance("Blowfish");
        lllllllllIllIII.init(l[3], lllllllllIllIIl);
        return new String(lllllllllIllIII.doFinal(Base64.getDecoder().decode(lllllllllIlIllI.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
    }
    catch (Exception lllllllllIlIlll) {
        lllllllllIlIlll.printStackTrace();
        return null;
    }
}
java 复制代码
private static String I(String lllllllllIIlIIl, String lllllllllIIIllI) {
    try {
        SecretKeySpec lllllllllIIllII = new SecretKeySpec(Arrays.copyOf(MessageDigest.getInstance("MD5").digest(lllllllllIIIllI.getBytes(StandardCharsets.UTF_8)), l[8]), "DES");
        Cipher lllllllllIIlIll = Cipher.getInstance("DES");
        lllllllllIIlIll.init(l[3], lllllllllIIllII);
        return new String(lllllllllIIlIll.doFinal(Base64.getDecoder().decode(lllllllllIIlIIl.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
    }
    catch (Exception lllllllllIIlIlI) {
        lllllllllIIlIlI.printStackTrace();
        return null;
    }
}
java 复制代码
private static String l(String llllllllllIlIll, String llllllllllIIlIl) {
    llllllllllIlIll = new String(Base64.getDecoder().decode(llllllllllIlIll.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
    StringBuilder llllllllllIlIIl = new StringBuilder();
    char[] llllllllllIlIII = llllllllllIIlIl.toCharArray();
    int llllllllllIIlll = l[0];
    char[] llllllllllIIIIl = llllllllllIlIll.toCharArray();
    int llllllllllIIIII = llllllllllIIIIl.length;
    int lllllllllIlllll = l[0];
    while (EzJar.llI(lllllllllIlllll, llllllllllIIIII)) {
        char llllllllllIllII = llllllllllIIIIl[lllllllllIlllll];
        llllllllllIlIIl.append((char)(llllllllllIllII ^ llllllllllIlIII[llllllllllIIlll % llllllllllIlIII.length]));
        "".length();
        ++llllllllllIIlll;
        ++lllllllllIlllll;
        "".length();
        if (-" ".length() <= "   ".length()) continue;
        return null;
    }
    return String.valueOf(llllllllllIlIIl);
}

虽然还是差点意思,但起码是能看懂的java伪代码了,继续美美地读我们的main

首先看到I[l[2]]被用来作为参数传入Cipher加密器,那我们要看看这个参数具体返回什么,我们可以看到这个位置用的

java 复制代码
I("50fO6ARqllg=", "VZbFF")

再看看I里面是怎么写的,SecretKeySpec是java中用于指定密钥的类,新建一个这个类相当于建了个密钥,可以直接用来初始化密码器

java 复制代码
SecretKeySpec key = new SecretKeySpec(Arrays.copyOf(MessageDigest.getInstance("MD5").digest(a2.getBytes(StandardCharsets.UTF_8)), 8), "DES");

我改了下参数,这样好看些,大概就是把a2转成UTF-8字节,然后计算MD5,取前八个字节,最后生成的是一个DES密钥,DES的密钥长度64位,有效长度56位

java 复制代码
Cipher c1 = Cipher.getInstance("Blowfish");
c1.init(2, key);
return new String(c1.doFinal(Base64.getDecoder().decode(a1.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

这里用了个Blowfish的密码机,密钥还是那个密钥,但是这里有个比较重要的地方就是,这个密码机init的第一个参数:

复制代码
1 = ENCRYPT_MODE  加密
2 = DECRYPT_MODE  解密

所以这是个解密机,后续用这个c1把a1先搞成UTF8字节,然后用base64解码,最后用blowfish解密,后续的return String会把UTF8字节转成字符串(我去了好复杂),最后搞半天这个字符串用来初始化……md怎么是DES

脚本如下:

python 复制代码
import hashlib
import base64
from Crypto.Cipher import DES
def i(s, key):
    key = hashlib.md5(key.encode('utf-8')).digest()
    key = key[:8]  # Use the first 8 bytes of the MD5 hash as the DES key
    cipher = DES.new(key, DES.MODE_ECB)
    s = base64.b64decode(s)
    return cipher.decrypt(s)

if __name__ == "__main__":
    s = "50fO6ARqllg="
    key = "VZbFF"
    print(i(s, key))

最后搞定这个i

复制代码
Cipher cipher = Cipher.getInstance(I[l[2]]);

这还只是main的第一步,我们继续看;

java 复制代码
cipher.init(1, new SecretKeySpec(I[l[4]].getBytes(), I[l[5]]));

这是一个加密,用的还是新密钥,继续看吧:

java 复制代码
lI("mvXqH+/XIESPZaSG3ZbZlA==", "TuZSw");

找到这个,我们看看lI函数……感觉长得好像怎么回事,无非就是转utf8字节,然后MD5计算一下,变成一份Blowfish密钥,然后用这个密钥解密,a1又是utf8然后base64解码,blowfish解码,得到一个新的字符串,写个脚本看看是啥……

python 复制代码
import hashlib
import base64
from Crypto.Cipher import Blowfish


def li(s, key):
    key = hashlib.md5(key.encode('utf-8')).digest()
    s = base64.b64decode(s)
    cipher = Blowfish.new(key, Blowfish.MODE_ECB)
    text = cipher.decrypt(s)
    return text.decode('utf-8')

if __name__ == "__main__":
    s = "mvXqH+/XIESPZaSG3ZbZlA=="
    key = "TuZSw"
    print(li(s, key))

不错不错,跑出一个密钥:Ctf3r_me

接下来……

这个字符串转字节,额,再用I[l[5]]算法,什么鬼,再解密,原内容如下:

java 复制代码
l("JQ0R", "aHBFu");

这个l函数好像稍微有些变化了,替换一波参数,也许会好理解点:

java 复制代码
private static String l(String a1, String a2) {
    a1 = new String(Base64.getDecoder().decode(a1.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
    StringBuilder s = new StringBuilder();
    char[] ch1 = a2.toCharArray();
    int num1 = 0;
    char[] ch2 = a1.toCharArray();
    int len2 = ch2.length;
    int index = 0;
    while (EzJar.llI(index, len2)) {
        char c1 = ch2[index];
        s.append((char)(c1 ^ ch1[num1 % ch1.length]));
        "".length();
        ++num1;
        ++index;
        "".length();
        if (-" ".length() <= "   ".length()) continue;
        return null;
    }
    return String.valueOf(s);
}

这里lli是一个大小比较,index小于len2就返回真,就是个检查长度而已,还有一些花指令,我服了,瘦身一下:

java 复制代码
private static String l(String a1, String a2) {
    a1 = new String(Base64.getDecoder().decode(a1.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
    StringBuilder s = new StringBuilder();
    char[] ch1 = a2.toCharArray();
    int num1 = 0;
    char[] ch2 = a1.toCharArray();
    int len2 = ch2.length;
    int index = 0;
    while (EzJar.llI(index, len2)) {
        char c1 = ch2[index];
        s.append((char)(c1 ^ ch1[num1 % ch1.length]));
        ++num1;
        ++index;
    }
    return String.valueOf(s);
}

到这里感觉不难理解了,a2就是一个被循环用的密钥,解密这个a1,最后返回结果应该是个算法类型……累了(

这个感觉写个脚本解比较好,我只能说直接复原java的是个高手

py 复制代码
import base64
def l(s, key):
    s = base64.b64decode(s).decode()
    result = ""
    for i in range(len(s)):
        result += chr(ord(s[i]) ^ ord(key[i % len(key)]))
    return result

if __name__ == "__main__":
    s = "JQ0R"
    key = "aHBFu"
    print(l(s, key))

气死我了,最后弄出来是个DES,气死我了!

不管了,弄到密钥了,继续下一步:

java 复制代码
String input = new String(Base64.getEncoder().encode(cipher.doFinal(s.getBytes())));

最后这里倒是简单,一个DES的加密机,把输入加密一下

最后我们还原出整个main函数加密的机制了,好耶

大概就是创建一个以Ctf3r_me为密钥的DES加密器,加密出来的字符串base64一下跟I[l[6]]比较,最后得到flag

先把I[l[6]]解出来吧:

java 复制代码
I("dMKiRQ19iTevvzL7NtVg5+ye5BywL2QaxtVANFLuC5B2/KuC+/5L6BwtCB7zpWK1XBTQr0VWC3Vt/uYEl2xmjskE0dDrCk2C", "dPxYA");

找到了,不过这次这个字符串真长啊,这个函数应该挺简单的,就一个MD5,终于有个脚本能复用了,太棒了

最后得到这样一坨:

复制代码
Dg/TZuRXF4+UwSZ8Dpwgw8+VOoHVl1YlPL1QRVhroCy4ptnKEcdC05iXcpLyDnuR

也就是说,只要我们输入的东西,经过一系列加密后,等于这个字符串就好了,ok,开始逆向!

脚本如下,因为DES是对称加密,所以写起来反而最简单:

python 复制代码
import base64
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad

ciphertext = "Dg/TZuRXF4+UwSZ8Dpwgw8+VOoHVl1YlPL1QRVhroCy4ptnKEcdC05iXcpLyDnuR"
key = b"Ctf3r_me"

data = base64.b64decode(ciphertext)

cipher = DES.new(key, DES.MODE_ECB)

plaintext = cipher.decrypt(data)

plaintext = unpad(plaintext, DES.block_size)

print(plaintext.decode("utf-8"))

我真是个小猪,得到flag:

ctfshow{eb5e2541-58e6-4f8f-ae6f-5c7a6f920d73}

评论

正在加载评论区…