前言
2025年,是蓝桥杯引入网络安全(CTF赛道)的第三年,主包也是第一次打蓝桥杯,也是第一次进入到线下总决赛,比赛时间和线上选拔赛时间都是一样的四个小时,四个小时写13道题时间还是很敢的,好在出的题不是很难。(难崩的是我选拔赛交两百的报名费,我入围线下还得交五百的报名费,而且线下还不管中午饭,我真的**********,中间就发三个小面包,都快饿死了……)
逆向分析
encodefile
几乎每年都要出一道RC4的题目,
老规矩,先查壳
无壳,64位,放入IDA里,可以看到没有main函数
那就从字符串中进行入手
根据flag.txt定位到关键代码
v16 = __readfsqword(0x28u);
sub_44BAE0(v14, "flag.txt", 4LL);
sub_44BC50(v12, "enc.dat", 4LL);
if ( (unsigned __int8)sub_44FB80(&v15) || (unsigned __int8)sub_44FB80(&v13) )
{
v1 = sub_46F140(&unk_5DD320, &unk_578019);
sub_46DC30(v1, sub_46EA20);
v2 = 1;
}
else
{
sub_407080(&v7);
sub_404E42(v9);
sub_404DF8(v8, v14);
sub_404E6C(v10, v8[0], v8[1], v9[0], v9[1], &v7);
sub_4070A0(&v7);
sub_407080(v9);
sub_404F50(v11, "key2025lqb", v9);
sub_404605(v10, v11);
sub_475510(v11);
sub_4070A0(v9);
v3 = sub_404DB4(v10);
v4 = sub_405014(v10);
sub_46E890(v12, v4, v3);
v5 = sub_46F140(&unk_5DD440, "鏂囦欢鍔犲瘑瀹屾垚锛岃緭鍑轰负 enc.dat");
sub_46DC30(v5, sub_46EA20);
v2 = 0;
sub_404F08(v10);
}
sub_44C300(v12);
sub_44C440(v14);
result = v2;
if ( v16 != __readfsqword(0x28u) )
sub_52FBA0();
return result;
}
其逻辑就是通过key
加密flag.txt
数据后存储到enc.dat
函数很明显,两轮255次循环,打乱密钥盒,可以识别出是RC4
算法
// 读取FS段寄存器中的值,用于栈保护检测
v15 = __readfsqword(0x28u);
// 初始化S盒:创建一个包含0-255的数组
for ( i = 0; i <= 255; ++i )
v14[i] = i;
// 密钥调度算法(KSA):利用密钥对S盒进行初始置换
v9 = 0;
for ( j = 0; j <= 255; ++j )
{
// 计算当前索引j对应的S盒值与累积值v9的和
v2 = (unsigned __int8)v14[j] + v9;
// 获取密钥长度
v3 = sub_475730(a2);
// 结合密钥字节计算新的累积值v9
v9 = (v2 + *(char *)sub_475A40(a2, j % v3)) % 256;
// 交换S盒中索引j和v9位置的元素
sub_404D3D(&v14[j], &v14[v9]);
}
// 伪随机数生成算法(PRGA)和数据处理:生成密钥流并与数据异或
v12 = 0;
v10 = 0;
for ( k = 0LL; k < sub_404DB4(a1); ++k )
{
// 更新索引i(v12)并确保在0-255范围内
v12 = (v12 + 1) % 256;
// 计算当前S盒值与索引j(v10)的和
v4 = (unsigned __int8)v14[v12] + v10;
// 更新索引j(v10)
v10 = (unsigned __int8)(HIBYTE(v4) + v14[v12] + v10) - HIBYTE(HIDWORD(v4));
// 交换S盒中索引i和j位置的元素
sub_404D3D(&v14[v12], &v14[v10]);
// 生成密钥流字节
v7 = v14[(unsigned __int8)(v14[v12] + v14[v10])];
// 获取数据中的当前字节
v5 = (_BYTE *)sub_404DD8(a1, k);
// 数据与密钥流异或,实现加密/解密
*v5 ^= v7;
}
// 栈保护检测:检查函数执行前后FS段寄存器值是否变化
result = v15 - __readfsqword(0x28u);
// 如果值发生变化,说明栈可能被破坏,调用异常处理函数
if ( result )
sub_52FBA0();
return result;
直接用厨子一把嗦了
rand_pyc
一眼python逆向,命令行反编译一下
用命令:python pyinstxtractor.py rand_pyc_obf.exe
将exe反编译成pyc文件,定位到反编译过后的rand_pyc_obf.pyc文件
继续将pyc文件反编译成可以看的py文件
用命令:uncompyle6 rand_pyc_obf.pyc > 1.py
# uncompyle6 version 3.9.2
# Python bytecode version base 3.8.0 (3413)
# Decompiled from: Python 3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]
# Embedded file name: rand_pyc_obf.py
import sys, random, base64
Ii = input("Please input the flag: ").strip()
if not (Ii.startswith("flag{") and Ii.endswith("}") and len(Ii) == 42):
print("Length incorrect")
sys.exit(-999)
oo0O000ooO = base64.b64encode(Ii.encode()).decode() + "_easyctf"
ii = []
for iiI in oo0O000ooO:
random.seed(ord(iiI))
ii.append(random.randint(1000000, 9999999))
else:
iii111 = [
4417023, 5690625, 9639225, 1327718, 4417023, 5085550, 5752075,
9556690, 5240080, 6431679, 3428007, 3189766, 3438336, 5757818,
3189766, 5690625, 4148389, 2254831, 6292433, 2122126, 5240080,
6431679, 9488271, 2464675, 7216908, 5757818, 3189766, 5690625,
3438336, 6431679, 2360475, 6002055, 5240080, 9040261, 8655414,
9347278, 3438336, 2254831, 2122126, 5135281, 2360475, 9347278,
4417023, 1327718, 3438336, 3448715, 9488271, 5501611, 5240080,
5757818, 9488271, 5501611, 5240080, 9347278, 4148389, 1714134,
9923116, 4267438, 4263793, 5752075, 2464675, 7777627, 6002055,
3485900]
Iio0 = []
for iiI in oo0O000ooO:
random.seed(ord(iiI))
Iio0.append(random.randint(1000000, 9999999))
else:
if Iio0 != iii111:
print("Wrong flag")
sys.exit(-1)
print("Correct!")
# okay decompiling rand_pyc_obf.pyc
代码简单来说就是,检查输入flag的Base64编码拼接"_easyctf"后,每个字符作为随机种子生成的数字是否匹配预设的列表
直接用逆向映射法
import random
import base64
# 随机数列表
target_nums = [
4417023, 5690625, 9639225, 1327718, 4417023, 5085550, 5752075,
9556690, 5240080, 6431679, 3428007, 3189766, 3438336, 5757818,
3189766, 5690625, 4148389, 2254831, 6292433, 2122126, 5240080,
6431679, 9488271, 2464675, 7216908, 5757818, 3189766, 5690625,
3438336, 6431679, 2360475, 6002055, 5240080, 9040261, 8655414,
9347278, 3438336, 2254831, 2122126, 5135281, 2360475, 9347278,
4417023, 1327718, 3438336, 3448715, 9488271, 5501611, 5240080,
5757818, 9488271, 5501611, 5240080, 9347278, 4148389, 1714134,
9923116, 4267438, 4263793, 5752075, 2464675, 7777627, 6002055,
3485900
]
# 生成字符并解码flag
def get_flag():
# 还原拼接字符串(去除末尾8个字符"_easyctf")
encoded_str = ''.join([
chr(c) for num in target_nums
for c in range(128)
if (random.seed(c), random.randint(1000000, 9999999))[1] == num
])[:-8]
# Base64解码得到flag
return base64.b64decode(encoded_str).decode()
print(get_flag())
其中核心思路是利用伪随机数的确定性(相同种子生成相同随机数),反向还原出正确的 flag
总结
今年线下Reverse方向就这两道题,题都不是很难,应该和不让上网搜索和不让用ai有关,出的这些题,都是可以直接手搓出flag的,就是难崩的报名费(真是圈钱杯啊)
真心建议,如果你只是想拿个省二、三就真心不建议报名这个比赛了,不说要先交两百的报名费,就蓝桥杯省二、三的含金量真的就不如不拿,如果想冲一下国一,我非常建议报名,就算这个比赛含金量在低,你拿个国一起码也是有含金量的。