ctfshow:REVERSE红包六
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}
评论