CTFshow逆向题解:RC4加密算法深度分析与解密实践
本文详细解析CTFshow逆向挑战赛中的RC4加密题`re2`,通过IDA Pro分析S盒初始化、密钥调度算法及密钥流生成过程,并附带Python解密脚本,助你掌握RC4逆向分析技巧。
- 网络安全
- 逆向工程
逆向签到题
直接查看反汇编代码就能看到flag

re2
弄清楚加密过程 找到密钥 ,本题采用RC4加密,可以参考如下流程

将样本放进IDA中分析,在左侧找到_main函数:
按下tab,查看反汇编代码:

进入main_0:
代码如下:

int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-44Ch]
char v5; // [esp+0h] [ebp-44Ch]
char v6; // [esp+0h] [ebp-44Ch]
char v7; // [esp+0h] [ebp-44Ch]
char v8; // [esp+0h] [ebp-44Ch]
char v9; // [esp+0h] [ebp-44Ch]
char v10[4]; // [esp+194h] [ebp-2B8h] BYREF
int v11; // [esp+1A0h] [ebp-2ACh]
FILE *v12; // [esp+1ACh] [ebp-2A0h]
FILE *v13; // [esp+1B8h] [ebp-294h]
_BYTE v14[264]; // [esp+1C4h] [ebp-288h] BYREF
_BYTE v15[264]; // [esp+2CCh] [ebp-180h] BYREF
char Str1[60]; // [esp+3D4h] [ebp-78h] BYREF
char Str[56]; // [esp+410h] [ebp-3Ch] BYREF
__CheckForDebuggerJustMyCode(&unk_40B027);
memset(Str, 0, 50);
memset(Str1, 0, 50);
memset(v15, 0, 0x100u);
memset(v14, 0, 0x100u);
v11 = 1;
do
{
print(asc_406BA0, v4);
print((char *)&byte_406BFC, v5);
print(asc_406C48, v6);
print((char *)&byte_406CA8, v7);
print((char *)&byte_406D0C, v8);
print(a1, v9);
sub_401073("%d", (char)v10);
if ( *(_DWORD *)v10 == 1 )
{
v13 = fopen("flag.txt", "r");
if ( !v13 )
{
print((char *)&byte_406D48, v4);
getchar();
exit(0);
}
v12 = fopen("enflag.txt", "w");
if ( !v12 )
{
print((char *)&byte_406D70, v4);
getchar();
exit(0);
}
print(asc_406D84, v4);
sub_401073("%s", (char)Str);
sub_401069(Str, Str1);
sub_401028(Str, (int)v15, (int)v14, v13, v12);
}
else if ( *(_DWORD *)v10 == 2 )
{
v11 = 0;
}
else
{
print(asc_406D98, v4);
}
}
while ( v11 );
return 0;
}
- v10是我们输入的选项,为1的时候执行以下代码:
- 以只读的方式打开
flag.txt,打开失败则退出 - 以写的方式打开
enflag.txt,打开失败则退出 - 接下来输入一个字符串
Str - 最后执行两个函数
sub_401069,sub_401028
- 分析这两个函数
-
针对
sub_401069:// attributes: thunk int __cdecl sub_401069(char *Str, char *Str1) { return sub_401A70(Str, Str1); }进入sub_401A70:
char __cdecl sub_401A70(char *Str, char *Str1) { char v3; // [esp+0h] [ebp-E4h] signed int i; // [esp+D0h] [ebp-14h] signed int v5; // [esp+DCh] [ebp-8h] __CheckForDebuggerJustMyCode(&unk_40B027); v5 = strlen(Str); for ( i = 0; i < v5; ++i ) Str1[i] += Str[i] ^ 0x1F; if ( !strcmp(Str1, "DH~mqqvqxB^||zll@Jq~jkwpmvez{") ) print((char *)&byte_406B80, v3); else print("Error!\n", v3); return *Str1; }
这里接受两个参数Str,Str1,其中Str是我们输入的参数,Str1是主函数创建的长度为50字节的值为0的数组。程序流程如下:
- 将Str1中的每个字符与
0x1F异或 - 得到的新字符串与”DH~mqqvqxB^||zll@Jq~jkwpmvez{“进行比较
- 如果成功则返回字符串Str1
这里可以先算一下Str1:
cmp_str1 = "DH~mqqvqxB^||ll@Jq~jkwpmvez{" plain_text = "" for decrypted_char in cmp_str1: plain_char = ord(decrypted_char) ^ 0x1f plain_text += chr(plain_char) print(plain_text)
可知
Str1 == [Warnning]Accss_Unauthorized,这也是我们需要输入的字符串。接下来点击X返回主函数:
- 将Str1中的每个字符与
-
针对sub_401028:
接收的参数有
sub_401028(Str, (int)v15, (int)v14, v13, v12),跟进查看
这里的
a2 = v15,a3 = v14,Stream = v13,a5 = v12,继续跟进
继续跟进看到的四个函数
Sub_4010F0:

这里要注意的是a1是上一个函数里的a2也就是主函数的v15,a2是我们输入的字符串([Warnning]Accss_Unauthorized),a3是我们输入的字符串的长度。
Sub_401800:
int __cdecl sub_401800(int a1, int a2, int a3) { int result; // eax int j; // [esp+D0h] [ebp-14h] int i; // [esp+DCh] [ebp-8h] result = __CheckForDebuggerJustMyCode(&unk_40B027); if ( a3 <= 256 ) { for ( i = 0; i < 256; ++i ) { *(_BYTE *)(i + a1) = *(_BYTE *)(a2 + i % a3); result = i + 1; } } if ( a3 > 256 ) { for ( j = 0; j < 256; ++j ) { *(_BYTE *)(j + a1) = *(_BYTE *)(j + a2); result = j + 1; } } return result; }接下来判断我们输入的字符串长度是否大于256 ,如果不足256,则复制a2([Warnning]Accss_Unauthorized)直到长度大于256。如果超过256,则截断。所以这个函数主要是改变了主函数里的v15。
sub_4010c8:


这里的a1是主函数里的v14。只是初始化了一个数组 并把这个数组每个值赋值为索引。
sub_40116D:
接收的参数分别为a3,a2,即主函数里的v14,v15。由于v14之前提到过则记录一下 他是一个索引为值的数组,v15 是第一个函数256位处理的那个

继续跟进得到下面的函数
int __cdecl sub_4018E0(int a1, int a2) { int result; // eax int i; // [esp+D0h] [ebp-2Ch] char v4; // [esp+EBh] [ebp-11h] int v5; // [esp+F4h] [ebp-8h] result = __CheckForDebuggerJustMyCode(&unk_40B027); v5 = 0; for ( i = 0; i < 256; ++i ) { v5 = (*(unsigned __int8 *)(i + a2) + v5 + *(unsigned __int8 *)(i + a1)) % 256; v4 = *(_BYTE *)(i + a1); *(_BYTE *)(i + a1) = *(_BYTE *)(v5 + a1); *(_BYTE *)(v5 + a1) = v4; result = i + 1; } return result; }这里需要把a1认为是v14,a2认为是v15
这里是把两个数组的每一位分别做计算之后通过v5这个变量累加起来。v4就是a1这个数组的第i位,之后将a1这个数组的v5这个位置的值放入v4,将v4放入v5的这个位置。也就是将a1的第i个位置与第v5个位置的值进行置换。
这个函数改变的就是a1 就是主函数的v14,类似于做了个初始置换.
看最后一个函数:
4010EB:
接收的参数为sub_4010EB(a3, Stream, a5);也就是主函数的v14,v13,v12(这两是和flag有关的文件)

跟进:


这里的a1就是v14,stream就是v13,a3就是v12
代码可知:
- 循环读取v13(flag.txt)中的字符,v7很显然代表的是读取字符的位置(在文件中的第几位),v6就是v14文件中对应位置的字符并与v6累加。
- 接下来三行代码是将v14文件中的v6位置的字符与v7位置的字符进行替换。
- 最后将v14文件中的v6位置的字符与v7位置的字符转成数字后累加得到的值作为索引t,拿到v14文件中的第t个字符并与源码中的
i(这个i是原本的flag哈)进行异或之后写入v2(enflag.txt),即加密后的文件
- 综上,整个过程就是RC4的实现方式
- 不难看出我们输入的Str实际就是密钥,密钥为
[Warnning]Accss_Unauthorized - v14对应的就是RC4中的S盒,V15对应的就是T,我们通过输入的密钥不断填充V15。
- 之后就是V14与V15直接的运算包括S的初始置换以及最后密钥流的生成。
- 不难看出我们输入的Str实际就是密钥,密钥为
- 之后编写解密代码 ,我们可以拿到加密后flag的值 ,通过分析也得到了密钥
[Warnning]Accss_Unauthorized之后我们就需要按照RC4的方式同样生成密钥流,最后对密文进行解密。
#
# 根据IDA逆向分析结果编写的RC4解密脚本
#
# ========================== 用户配置区 ==========================
# ❗️ 在这里填入加密时使用的密钥
YOUR_SECRET_KEY = "[Warnning]Access_Unauthorized"
# ❗️ 填入加密后的文件名 (即enflag.txt的路径)
ENCRYPTED_FILE_PATH = "enflag.txt"
# ❗️ 填入保存解密后文件的名字
DECRYPTED_FILE_PATH = "flag_decrypted.txt"
# =============================================================
class RC4:
def __init__(self, key: str):
"""
使用密钥初始化RC4状态,完成密钥调度算法(KSA)。
这个构造函数完整复刻了 sub_401780, sub_401800, 和 sub_4018E0 的功能。
"""
key_bytes = key.encode('ascii') # 将字符串密钥转为字节
key_length = len(key_bytes)
# 对应 sub_401780: 初始化S-box, S[i] = i
self.S = list(range(256))
# 对应 sub_401800: 根据密钥初始化T-vector
T = [key_bytes[i % key_length] for i in range(256)]
# 对应 sub_4018E0: 使用T-vector对S-box进行初始置换
j = 0
for i in range(256):
j = (j + self.S[i] + T[i]) % 256
# 交换S[i]和S[j]
self.S[i], self.S[j] = self.S[j], self.S[i]
print("✅ RC4 状态初始化完成 (KSA complete).")
def crypt(self, data: bytes) -> bytes:
"""
对数据进行加密或解密。
这个方法完整复刻了 sub_4015E0 (PRGA) 的功能。
"""
i = 0
j = 0
output = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + self.S[i]) % 256
# 交换S[i]和S[j]
self.S[i], self.S[j] = self.S[j], self.S[i]
# 计算t并生成密钥流字节k
t = (self.S[i] + self.S[j]) % 256
keystream_byte = self.S[t]
# 将数据字节与密钥流字节进行XOR
output.append(byte ^ keystream_byte)
return bytes(output)
# --- 主执行逻辑 ---
if __name__ == "__main__":
print("--- RC4解密程序启动 ---")
# 检查密钥是否已填写
try:
# 1. 读取加密后的文件内容
print(f"📄 正在读取加密文件: {ENCRYPTED_FILE_PATH}")
with open(ENCRYPTED_FILE_PATH, 'rb') as f:
encrypted_data = f.read()
print(f" 读取了 {len(encrypted_data)} 字节数据。")
# 2. 使用密钥初始化RC4加密器
print(f"🔑 使用密钥 '{YOUR_SECRET_KEY}' 初始化RC4...")
cipher = RC4(YOUR_SECRET_KEY)
# 3. 执行解密 (与加密过程完全相同)
print("⚙️ 正在执行解密操作...")
decrypted_data = cipher.crypt(encrypted_data)
print(" 解密完成。")
# 4. 将解密后的数据写入新文件
print(f"💾 正在将解密结果写入文件: {DECRYPTED_FILE_PATH}")
with open(DECRYPTED_FILE_PATH, 'wb') as f:
f.write(decrypted_data)
print("\n" + "=" * 50)
print(f"🎉 解密成功!结果已保存至 {DECRYPTED_FILE_PATH}")
print("=" * 50)
except FileNotFoundError:
print(f"❌ 错误: 找不到文件 '{ENCRYPTED_FILE_PATH}'。请检查文件名和路径是否正确。")
except Exception as e:
print(f"❌ 发生未知错误: {e}")
Re3
本题比较简单,没有跨函数分析:
int __fastcall main(int argc, const char **argv, const char **envp)
{
size_t v3; // rax
int v5; // [rsp+Ch] [rbp-134h] BYREF
unsigned int i; // [rsp+10h] [rbp-130h]
int v7; // [rsp+14h] [rbp-12Ch]
int v8; // [rsp+18h] [rbp-128h]
int v9; // [rsp+1Ch] [rbp-124h]
int v10; // [rsp+20h] [rbp-120h]
int v11; // [rsp+24h] [rbp-11Ch]
int v12; // [rsp+28h] [rbp-118h]
int v13; // [rsp+2Ch] [rbp-114h]
int v14; // [rsp+30h] [rbp-110h]
int v15; // [rsp+34h] [rbp-10Ch]
unsigned __int64 v16; // [rsp+38h] [rbp-108h]
_DWORD v17[8]; // [rsp+40h] [rbp-100h]
char s[5]; // [rsp+60h] [rbp-E0h] BYREF
char v19[107]; // [rsp+65h] [rbp-DBh] BYREF
char dest[104]; // [rsp+D0h] [rbp-70h] BYREF
unsigned __int64 v21; // [rsp+138h] [rbp-8h]
v21 = __readfsqword(0x28u);
v7 = 80;
v8 = 64227;
v9 = 226312059;
v10 = -1540056586;
v11 = 5;
v12 = 16;
v13 = 3833;
v5 = 0;
puts("plz input the key:");
__isoc99_scanf("%s", s);
v3 = strlen(s); // 判断输入字符的长度
strncpy(dest, v19, v3 - 6);
dest[strlen(s) - 6] = 0;
__isoc99_sscanf(dest, "%x", &v5);
v17[0] = v7;
v17[1] = v8;
v17[2] = v9;
v17[3] = v10;
v17[4] = (v11 << 12) + v12;
v17[5] = v13;
v17[6] = v5;
v16 = 0;
for ( i = 0; i <= 6; ++i )
{
for ( v16 += (unsigned int)v17[i]; v16 > 0xFFFF; v16 = v15 + (unsigned int)(unsigned __int16)v16 )
{
v14 = (unsigned __int16)v16;
v15 = v16 >> 16;
}
}
if ( v16 == 0xFFFF )
puts("OK");
else
puts("Error");
return 0;
}
-
整体流程:
- 初始化变量后,需要输入一个字符串
- 从第六位开始截取这个字符串并copy给dest,注意这里输入的时候是给的s但是由于s的长度只有5而且紧接着s下面就是v19,所以我们输入的字符串超过5位后的字符都会被存在v19
- 将dest中的值以十六进制的形式读给v5
- 初始化v17数组
- 之后进行六次循环
- 将v16加上v17对应位置的值
- 如果超过了ffff那么将v16保存到v14,之后右移16位并与自己累加
- 循环完成之后,如果得到v16恰好是ffff则输出OK。
-
写脚本求输入的字符串,使得v16恰好为ffff
分析算法过程:

根据分析过程编写如下代码:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
def calculate_hex_key():
"""
根据已知的校验和算法和 "%x" 格式,直接计算出最终的key。
"""
# 1. 初始化常量
s_fixed_values = [
0x50, # s[0] = x
0xfae3, # s[1] = c
0xd7d3f7b, # s[2] = v
0xa43499f6, # s[3] = n
0x10 + 5 * 0x1000, # s[4] = m + b * 0x1000
0xef9 # s[5] = h
]
# 2. 计算已知数值的总和
fixed_sum = sum(s_fixed_values)
# 3. 模拟C代码中的校验和折叠算法
def checksum_fold(value):
while value > 0xffff:
value = (value & 0xffff) + (value >> 16)
return value
# 4. 计算部分校验和
partial_checksum = checksum_fold(fixed_sum)
# 5. 根据目标 0xffff 计算出 j 的值
target = 0xffff
j = target - partial_checksum
# 6. 构造最终的key
# 格式为 "%x",所以使用j的十六进制字符串表示
# 末尾附加任意6个字符以满足程序的长度要求
final_key = f"{j:x}" + "A" * 6
print("=" * 40)
print("已确认 sscanf 格式为 '%x'.")
print(f"计算出的 j 的值: 0x{j:x}")
print(f"因此, 正确的 Key 是: {final_key}")
print("=" * 40)
if __name__ == "__main__":
calculate_hex_key()
得到输入的值应该是0x1a9f:

re4
查看main函数

v4是我们需要输入的值,直接跳到sub_1400010E0函数:

这里的a1就是v4(我们输入的字符串)
先要看懂v3 + -26 * (v3 / 26)实际上就是v3%26
由于循环中v5 = a4890572163qwe[v3 + -26 * (v3 / 26)];在不停除以26,则实际上是将v3转换成一个26进制的数,这个数的每一位作为a4890572163qwe字符串的索引取值放进v4这个字符串中,所以我们得先求v4,由于目前不知道我们需要输入的字符串我们接着往下看。
v2代表的是转换为26进制之后的位数
下面的循环:
由于v8和v9都是char类型,所以v8+1就是v9,所以v7 = *(&v8 + v6--);
v7为v3的逆序的每一个字符
查看字符串a4890572163qwe:

接下来看sub_1400011E0:
sub_1400011E0(v7 ^ 7);该函数先将每个字符与7进行异或作为函数的参数
_QWORD *__fastcall sub_1400011E0(char a1)
{
_QWORD *result; // rax
__int64 v3; // rdx
result = malloc(0x10u);
v3 = qword_140004618;
qword_140004618 = (__int64)result;
*(_QWORD *)(v3 + 8) = result;
*(_BYTE *)v3 = a1;
result[1] = 0;
return result;
}
这里就是把与7异或完的字符放进数组里组成完整的字符串。
这里要留意下数组的地址是qword_140004618,接着返回主函数,继续分析下一个函数
void __noreturn sub_140001220()
{
__int64 v0; // r9
int v1; // ecx
__int64 v2; // rdx
char v3; // al
int v4; // r8d
__int64 v5; // r9
char v6; // cl
int v7; // eax
v0 = qword_140004620;
v1 = 0;
v2 = 0;
while ( 1 )
{
v3 = *(_BYTE *)v0;
v4 = v1 + 1;
v5 = *(_QWORD *)(v0 + 8);
if ( v3 != aV4pY59[v2] )
v4 = v1;
qword_140004620 = v5;
if ( !v5 )
break;
v6 = *(_BYTE *)v5;
v7 = v4 + 1;
v0 = *(_QWORD *)(v5 + 8);
if ( v6 != aV4pY59[v2 + 1] )
v7 = v4;
qword_140004620 = v0;
if ( v0 )
{
v2 += 2;
v1 = v7;
if ( v2 < 14 )
continue;
}
goto LABEL_11;
}
v7 = v4;
LABEL_11:
if ( v7 == 14 )
sub_1400012E0();
sub_1400012B0();
}
这里就是从我们刚才的字符串数组开始与字符串aV4pY59进行比较,我们先获得该字符串:

最终就是逐位比较这14位的字符串如果比较成功那么v7刚好等于14则执行12E0函数会给出flag。
接下来编写脚本,获取我们要输入的字符串:
-
首先根据密文/..v4p$$!>Y59-,每一位与7异或回退到进入函数前:
target_ciphertext = "/..v4p$$!>Y59-" decrypted_list = [chr(ord(c) ^ 7) for c in target_ciphertext] decrypted_string = "".join(decrypted_list) print(f"目标密文是: {target_ciphertext}") print(f"解密后的字符串是: {decrypted_string}")
-
根据得到的解密后的字符串())q3w##&9^2>*继续逆推:
- ())q3w##&9^2>*
- 找到())q3w##&9^2>每个字符在原本字符(”)(&^%489$!057@#><:2163qwe”)的索引
- 最后将获取到的索引值转换成十进制
alphabet = ")(*&^%489$!057@#><:2163qwe" base26_string = "())q3w##&9^2>*" final_number = 0 for char in base26_string: # 找到字符在字母表中的位置(索引) digit_value = alphabet.find(char) if digit_value == -1: print(f"错误: 字符 '{char}' 不在字母表中!") break # 核心转换算法 final_number = final_number * 26 + digit_value print(f"最终的密码 (输入数字) 是: {final_number}")
Re5
先直接说结果:

把这里原本的jnz语句改成两个nop即可;
还是分析main函数:

sub_4020B0()是做一些静态初始化
sub_4015BD()函数

这里会比较Str这个字符串是不是1,如果是的话执行下面的函数 不是的话就退出

我们先看一下Str的值,如下图,很显然第二位不是1。

Sub_401520函数这里会调用一个1.dll中的H函数,逐位进行加/解密

最后进行输出
所以 有两种方法:
一个是修改字符串的值,另一个是修改汇编语句
到 CMP 指令之后的那条条件跳转指令,例如 JNZ short loc_XXXXXX (如果不相等就跳转)。
用 NOP 替换指令: NOP (No Operation, 空操作) 是一个CPU指令,它什么也不做,只会让程序执行下一条指令。我们可以用它来“抹掉”不想要的指令。
由于一个jnz指令的长度是两个字节,而NOP指令的长度只有一个字节,必须使用两个1字节的指令去填满原来两字节的空间,最后得到如下

变成


覆盖之后,patch 进文件即可 之后打开终端运行软件命令可以查看到结果


武穆遗书
- 使用die工具检测软件为upx打包方式(加壳),需要去壳

- 使用x64dbg打开软件,先执行F9进入ep,但这里只是壳的ep我们需要找到oep;

- 由于是upx打包,可以看到第一句是pushad,则找到对应popad就能找到源程序的oep
- Ctrl f 搜索popad

- 找到popad之后,早第一个jmp语句,即是源程序的oep,打断点之后执行到oep

- 进入403d2c并选中

- 使用scylla进行脱壳,得到脱壳后的文件

之后使用IDA分析脱壳后的文件,前面做了一些反调试和虚拟机检测的操作我们可以直接nop掉,使用x32dbg。

首先通过IDA得到这四个初始函数的位置,使用x32dbg通过ctrl g跳转到这个地址,将这四个函数全部nop掉:选中cll=>指令点击Space=>改成nop(下面选择剩余字节用nop填充)


在这些nop的地方放置断点,使用f9运行到这里后一步一步调试,接下来是je指令,默认不会跳转,我们需要修改zf标志位改成1跳转:
注意:一定是跳转 不能直接把je指令nop掉除非你把下面的三条指令全部nop掉直接执行跳转的位置


跳转之后,继续执行,直到执行完401390函数,就可以直接看到flag,之后继续执行就是输入和flag相同的字符即可。

Re7
下载文件后是个jar包,先提取jar包中的文件
jar -xf EzJar.jar
得到两个文件,其中一个Ez.java

按照上面的思路,发现flag不对
之后尝试解析这个下载出来的jar包,解析脚本如下:
import zipfile
import os
import sys
import datetime
import argparse
def analyze_jar(filepath):
"""
Analyzes the structure of a JAR/ZIP file, showing offsets and appended data.
"""
if not os.path.exists(filepath):
print(f"错误: 文件不存在 '{filepath}'")
return
print(f"--- 开始分析: {os.path.basename(filepath)} ---")
print(f"文件总大小: {os.path.getsize(filepath):,} 字节")
print("-" * 40)
try:
with zipfile.ZipFile(filepath, 'r') as zf:
print("[+] 文件列表和内部结构:\n")
infolist = zf.infolist()
if not infolist:
print(" -> 此JAR包为空或无法识别内部文件。")
for info in infolist:
# ZipInfo对象包含了丰富的元数据
header_offset = info.header_offset
filename = info.filename
# 计算压缩数据的真实起始偏移量
# 公式: 数据偏移量 = 文件头偏移量 + 30 (固定头大小) + 文件名长度 + 额外字段长度
data_offset = header_offset + 30 + len(filename.encode('utf-8')) + len(info.extra)
print(f" 文件名: {filename}")
print(f" ├─ 修改时间: {datetime.datetime(*info.date_time)}")
print(f" ├─ 文件头偏移量 (Header Offset): {header_offset:<10} (0x{header_offset:08X})")
print(f" ├─ 数据偏移量 (Data Offset): {data_offset:<10} (0x{data_offset:08X})")
print(f" ├─ 压缩后大小: {info.compress_size:,} 字节")
print(f" ├─ 原始大小: {info.file_size:,} 字节")
print(f" └─ CRC校验码: 0x{info.CRC:08X}\n")
except zipfile.BadZipFile as e:
print(f"[!] 严重错误: {e}")
print(" -> 文件不是一个有效的JAR/ZIP文件,或者已经严重损坏。")
except Exception as e:
print(f"[!] 发生未知错误: {e}")
# --- 手动检查文件末尾的附加数据 ---
print("[+] 附加数据检查:")
with open(filepath, 'rb') as f:
# ZIP文件的结尾记录(EOCD)标志是 PK\x05\x06
eocd_signature = b'\x50\x4B\x05\x06'
# 从文件末尾开始搜索,效率更高
f.seek(0, os.SEEK_END)
filesize = f.tell()
# 我们只在文件末尾一小块区域内搜索,以防万一
read_buffer = max(0, filesize - 65536)
f.seek(read_buffer)
data = f.read()
eocd_pos_in_data = data.rfind(eocd_signature)
if eocd_pos_in_data == -1:
print(" -> 未找到标准的ZIP文件结尾记录 (EOCD)。")
print(" 这证实了 'zip END header not found' 错误。文件可能不完整。")
else:
eocd_abs_pos = read_buffer + eocd_pos_in_data
# EOCD记录中,注释长度字段在偏移量20处,占2字节
comment_len_bytes = data[eocd_pos_in_data + 20: eocd_pos_in_data + 22]
comment_len = int.from_bytes(comment_len_bytes, 'little')
# ZIP文件的理论结束位置
expected_end = eocd_abs_pos + 22 + comment_len
print(f" -> 找到文件结尾记录 (EOCD) 在偏移量: {eocd_abs_pos}")
print(f" -> ZIP结构理论结束位置: {expected_end}")
if filesize > expected_end:
appended_size = filesize - expected_end
print(f" -> [!] 警告: 文件末尾发现 {appended_size:,} 字节的附加数据!")
else:
print(" -> 正常: 未发现附加数据。")
print("-" * 40)
print("--- 分析结束 ---")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="分析一个JAR/ZIP文件的内部结构、偏移量和附加数据。")
parser.add_argument("jarfile", help="需要分析的JAR文件路径")
args = parser.parse_args()
analyze_jar(args.jarfile)


可以看到一个异常EzJar.class/这个文件没被解压出来而且大小是6000字节,很显然有问题,可尝试分离jar包:
import zipfile
import argparse
import os
def separate_jar_components_v2(source_jar_path):
OBFUSCATED_SOURCE_NAME = 'EzJar.class/EzJar.java'
OBFUSCATED_BYTECODE_NAME = 'EzJar.class/'
OUTPUT_SOURCE_JAR = 'source_code.jar'
OUTPUT_BYTECODE_JAR = 'compiled_bytecode.jar'
CLEAN_SOURCE_NAME = 'EzJar.java'
CLEAN_BYTECODE_NAME = 'EzJar.class'
# 标准清单文件的内容
# MANIFEST_CONTENT = "Manifest-Version: 1.0\n\n"
MANIFEST_CONTENT = "Manifest-Version: 1.0\nMain-Class: EzJar\n\n"
MANIFEST_PATH = "META-INF/MANIFEST.MF"
if not os.path.exists(source_jar_path):
print(f"错误: 源文件不存在 '{source_jar_path}'")
return
source_data = None
bytecode_data = None
print(f"--- 正在分析 '{source_jar_path}' ---")
try:
with zipfile.ZipFile(source_jar_path, 'r') as zf:
for info in zf.infolist():
if info.filename == OBFUSCATED_SOURCE_NAME:
source_data = zf.read(info)
elif info.filename == OBFUSCATED_BYTECODE_NAME:
bytecode_data = zf.read(info)
# 创建源代码JAR
if source_data:
print(f"正在创建标准源代码包 -> '{OUTPUT_SOURCE_JAR}'...")
with zipfile.ZipFile(OUTPUT_SOURCE_JAR, 'w', compression=zipfile.ZIP_DEFLATED) as zf_source:
# 添加清单文件
zf_source.writestr(MANIFEST_PATH, MANIFEST_CONTENT)
# 添加源代码文件
zf_source.writestr(CLEAN_SOURCE_NAME, source_data)
print(f" -> 成功创建 '{OUTPUT_SOURCE_JAR}'")
# 创建字节码JAR
if bytecode_data:
print(f"正在创建标准字节码包 -> '{OUTPUT_BYTECODE_JAR}'...")
with zipfile.ZipFile(OUTPUT_BYTECODE_JAR, 'w', compression=zipfile.ZIP_DEFLATED) as zf_bytecode:
# 添加清单文件
zf_bytecode.writestr(MANIFEST_PATH, MANIFEST_CONTENT)
# 添加字节码文件
zf_bytecode.writestr(CLEAN_BYTECODE_NAME, bytecode_data)
print(f" -> 成功创建 '{OUTPUT_BYTECODE_JAR}'")
print("\n--- 分离完成 ---")
except Exception as e:
print(f"[!] 处理过程中发生错误: {e}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="将一个混淆的JAR文件分离成两个包含标准清单的、干净的JAR文件。")
parser.add_argument("source_jar", help="原始的、被混淆的JAR文件路径。")
args = parser.parse_args()
separate_jar_components_v2(args.source_jar)

compiled_bytecode.jar是我们分离的jar包,接下来使用jd或者直接在idea反编译这个jar包,可以直接读到伪代码

根据代码逻辑写一个复现脚本,找到密文,密钥之后解密:
import base64
import hashlib
from Crypto.Cipher import DES, Blowfish
from Crypto.Util.Padding import unpad
# 全局变量,用于存储Java代码中的 l 和 I 数组
l = [0] * 18
I = [""] * 17
def lII():
"""复刻Java的lII()方法,计算出l[]数组的值"""
global l
l[0] = (156 ^ 136) & ~(127 ^ 107)
l[1] = 1
l[2] = 3
l[3] = 2
l[4] = 185 ^ 197 ^ 32 ^ 88
l[5] = 1 ^ 28 ^ 24
l[6] = 16 ^ 22
l[7] = 69 ^ 77 ^ 70 ^ 73
l[8] = (106 + 107 - 66 + 32) ^ (33 + 90 - 98 + 162)
l[9] = (154 + 21 - 48 + 38) ^ (78 + 146 - 163 + 111)
l[10] = 29 ^ 23
l[11] = 64 ^ 75
l[12] = 105 ^ 101
l[13] = 2 ^ 106 ^ 101
l[14] = 72 ^ 70
l[15] = 144 ^ 159
l[16] = 160 ^ 176
l[17] = 177 ^ 160
def decrypt_l(b64_str, key_str):
"""复刻Java的l()方法 (XOR解密)"""
decoded_bytes = base64.b64decode(b64_str)
key_bytes = key_str.encode('utf-8')
result = bytearray()
for i in range(len(decoded_bytes)):
result.append(decoded_bytes[i] ^ key_bytes[i % len(key_bytes)])
return result.decode('utf-8')
def decrypt_lI(b64_str, key_str):
"""复刻Java的lI()方法 (Blowfish解密)"""
key = hashlib.md5(key_str.encode('utf-8')).digest()
cipher = Blowfish.new(key, Blowfish.MODE_ECB)
decoded_bytes = base64.b64decode(b64_str)
return unpad(cipher.decrypt(decoded_bytes), Blowfish.block_size).decode('utf-8')
def decrypt_I(b64_str, key_str):
"""复刻Java的I()方法 (DES解密)"""
# Java的Arrays.copyOf(..., 8) 相当于取前8个字节
key = hashlib.md5(key_str.encode('utf-8')).digest()[:8]
cipher = DES.new(key, DES.MODE_ECB)
decoded_bytes = base64.b64decode(b64_str)
return unpad(cipher.decrypt(decoded_bytes), DES.block_size).decode('utf-8')
def ll():
"""复刻Java的ll()方法,解密并填充I[]数组"""
global I, l
I[l[0]] = decrypt_I("AQiA0bYffm9HvMlm7RnEMX/tEQAUj4Xb", "FrlAZ")
I[l[1]] = decrypt_I("hXzZyx8IUHw=", "Esxsh")
I[l[3]] = decrypt_l("ID8PFlEKM1kKHhIkWRUdBjFD", "gVysq")
I[l[2]] = decrypt_I("50fO6ARqllg=", "VZbFF")
I[l[4]] = decrypt_lI("mvXqH+/XIESPZaSG3ZbZlA==", "TuZSw")
I[l[5]] = decrypt_l("JQ0R", "aHBFu")
I[l[6]] = decrypt_I(
"dMKiRQ19iTevvzL7NtVg5+ye5BywL2QaxtVANFLuC5B2/KuC+/5L6BwtCB7zpWK1XBTQr0VWC3Vt/uYEl2xmjskE0dDrCk2C", "dPxYA")
I[l[7]] = decrypt_lI("B/MVYKSzgq8=", "phiUP")
I[l[8]] = decrypt_I("ZtBOhuHeK3Y=", "MfnkQ")
I[l[9]] = decrypt_lI("aPhz+GjGynRlU3Alo00QeQ==", "wWtUj")
I[l[10]] = decrypt_l("BRQFNiRyBwQrNDcUSw==", "RfjXC")
I[l[11]] = decrypt_I("pT10j0lChvyrNwYRFdqBzxqFp1ruTgo9", "hGcuT")
I[l[12]] = decrypt_lI(
"UhBbCDk5yqaWl1uHJyS/OGmtcfVyvOOsk78/1f0MU8U3UfAf1Xf0FWNbpcKes/0HRz9SU/icRJHswW2xWjHrcFzhpsvwzqUl", "eeMoV")
I[l[13]] = decrypt_I("s11BihYBzRBpcX9EF43utw==", "RjiXK")
I[l[14]] = decrypt_I("wk5jH1cyKoA=", "frsxP")
I[l[15]] = decrypt_I("Nfa0rxB8IRArMq2F4iLlLg==", "ulQWJ")
I[l[16]] = decrypt_lI("wLcWNd0Xsbw=", "JgPGn")
def final_crack():
"""执行最终的核心破解逻辑"""
print("[+] 正在复刻Java内部的解密过程...")
# 步骤一: 初始化常量数组 l 和 I
lII()
ll()
print("[+] 已成功解密并重建所有隐藏字符串。")
# 步骤二: 从重建的数组中获取关键信息
# main方法中的密钥是I[l[4]],l[4]的值是4
main_logic_key_str = I[l[4]]
# main方法中的目标密文是I[l[6]],l[6]的值是6
main_logic_ciphertext_b64 = I[l[6]]
print(f" -> 破解出的DES密钥: {main_logic_key_str}")
print(f" -> 破解出的目标密文: {main_logic_ciphertext_b64[:20]}...")
# 步骤三: 执行最终的DES解密
print("[+] 正在执行最终解密...")
key_bytes = main_logic_key_str.encode('utf-8')
ciphertext_bytes = base64.b64decode(main_logic_ciphertext_b64)
cipher = DES.new(key_bytes, DES.MODE_ECB)
decrypted_padded_bytes = cipher.decrypt(ciphertext_bytes)
# 移除填充
flag = unpad(decrypted_padded_bytes, DES.block_size).decode('utf-8')
print("-" * 50)
print(f"[*] 最终Flag: {flag}")
print("-" * 50)
if __name__ == '__main__':
final_crack()

留言讨论
0 条留言
正在加载留言...