【第十六届蓝桥杯网络安全总决赛】Reverse题解WriteUp

前言

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的,就是难崩的报名费(真是圈钱杯啊)

真心建议,如果你只是想拿个省二、三就真心不建议报名这个比赛了,不说要先交两百的报名费,就蓝桥杯省二、三的含金量真的就不如不拿,如果想冲一下国一,我非常建议报名,就算这个比赛含金量在低,你拿个国一起码也是有含金量的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值