HGAME-2026-WEEK1-REVERSE

39次阅读
没有评论

WEEK1-RE

PVZ

打开IDA之后发现这个程序是用java写的,于是直接使用压缩包把应用解包,随后使用Recaf打开

HGAME-2026-WEEK1-REVERSE

找到了其中的关键代码。

发现获取Flag的核心方法是 getFlag()​,它调用了 decryptFlag()。整个解密流程如下:

  1. killCount 派生密钥解密​: decryptWithKillCount 使用僵尸击杀数生成的随机数种子作为密钥,对密文进行异或解密。
  2. 分割与异或​: 将解密后的数组分为两半,分别与 0x66​ (102) 和 0x77 (119) 进行异或。
  3. AES (伪) 解密​: 将合并后的数组与 aesEncryptedKey 进行循环异或。
  4. 字符串转换: 转为 UTF-8 字符串。
  5. 凯撒移位 (Rotation) ​: 根据 "PLANTS_VS_ZOMBIES_2025" 的字符和计算偏移量进行移位。
  6. 替换密码 (Substitution) : 使用硬编码的映射表进行字符替换。
  7. 最终修正: 将字符串开头的 "flag" 替换为 "hgame"。
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class PvZFlagSolver {

    // 密文数据
    static byte[] killCountEncryptedFlag = {0, -8, -6, 6, 31, -39, -104, 114, 86, -23, -35, 28, -122, 56, 29, -126, -29, 94, 23, -29, 46, -126, -4, 45, 20, -57};
    static byte[] aesEncryptedKey = {74, -111, -61, 127, 46, -75, 104, -44, 28, -119, 58, -14, 93, -90, 113, -66};
    
    // 替换表
    static Map<Character, Character> substitutionMap = new HashMap<>();
    static Map<Character, Character> reverseSubstitutionMap = new HashMap<>();

    public static void main(String[] args) {
        initMaps();
        int rotationOffset = getRotationOffset();

        System.out.println("开始爆破 n7 (0-65535)...");

        // 爆破 n7 (中间状态变量)
        for (int n7 = 0; n7 < 65536; n7++) {
            try {
                // 1. 生成密钥
                byte[] key = generateKeyFromN7(n7);
                
                // 2. 第一层解密 (KillCount XOR)
                byte[] step1 = decryptWithKey(killCountEncryptedFlag, key);
                
                // 3. 分割并异或
                int mid = step1.length / 2;
                byte[] part1 = new byte[mid];
                byte[] part2 = new byte[step1.length - mid];
                System.arraycopy(step1, 0, part1, 0, mid);
                System.arraycopy(step1, mid, part2, 0, part2.length);
                
                part1 = xorArray(part1, (byte) 102); // 0x66
                part2 = xorArray(part2, (byte) 119); // 0x77
                
                // 合并
                byte[] step2 = new byte[step1.length];
                System.arraycopy(part1, 0, step2, 0, mid);
                System.arraycopy(part2, 0, step2, mid, part2.length);
                
                // 4. 第二层解密 (Simple AES - 其实就是循环异或)
                byte[] step3 = simpleAesDecrypt(step2, aesEncryptedKey);
                
                // 转字符串
                String rawString = new String(step3, StandardCharsets.UTF_8);
                
                // 5. 凯撒解密
                String rotated = rotateDecrypt(rawString, rotationOffset);
                
                // 6. 替换解密
                String flagCandidate = substitutionDecrypt(rotated);
                
                // 验证格式
                if (flagCandidate.startsWith("flag{") && flagCandidate.endsWith("}")) {
                    System.out.println("-----------------------------------");
                    System.out.println("找到正确的 n7: " + n7);
                    System.out.println("原始 Flag: " + flagCandidate);
                    // 题目最后一步逻辑:替换 flag 为 hgame
                    String finalFlag = flagCandidate.replace("flag", "hgame");
                    System.out.println("最终 Flag: " + finalFlag);
                    System.out.println("-----------------------------------");
                    break;
                }
                
            } catch (Exception e) {
                // 忽略解码错误
            }
        }
    }

    // 根据 n7 生成 16字节密钥 (对应源码 deriveKeyFromKillCount 的后半部分)
    private static byte[] generateKeyFromN7(int n7) {
        byte[] byArray = new byte[16];
        int n8 = n7;
        for (int i = 0; i < 16; ++i) {
            n8 = n8 * 1103515245 + 12345 & Integer.MAX_VALUE;
            byArray[i] = (byte)((n8 >> 16) % 256);
        }
        return byArray;
    }

    // 对应源码 decryptWithKillCount 中的异或逻辑
    private static byte[] decryptWithKey(byte[] input, byte[] key) {
        byte[] output = new byte[input.length];
        for (int i = 0; i < input.length; i++) {
            byte k = key[i % key.length];
            byte modifier = (byte)((i * 13 + 7) % 256);
            output[i] = (byte)(input[i] ^ k ^ modifier);
        }
        return output;
    }

    private static byte[] xorArray(byte[] arr, byte val) {
        byte[] res = new byte[arr.length];
        for (int i = 0; i < arr.length; i++) {
            res[i] = (byte)(arr[i] ^ val);
        }
        return res;
    }

    private static byte[] simpleAesDecrypt(byte[] input, byte[] key) {
        byte[] res = new byte[input.length];
        for (int i = 0; i < input.length; i++) {
            res[i] = (byte)(input[i] ^ key[i % key.length]);
        }
        return res;
    }

    private static int getRotationOffset() {
        String s = "PLANTS_VS_ZOMBIES_2025";
        int sum = 0;
        for (char c : s.toCharArray()) {
            sum += c;
        }
        return sum % 26; // 结果应为 20
    }

    private static String rotateDecrypt(String s, int n) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            if (Character.isUpperCase(c)) {
                int val = (c - 'A' - n + 26) % 26;
                sb.append((char)('A' + val));
            } else if (Character.isLowerCase(c)) {
                int val = (c - 'a' - n + 26) % 26;
                sb.append((char)('a' + val));
            } else {
                sb.append(c);
            }
        }
        return sb.toString();
    }

    private static String substitutionDecrypt(String s) {
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            Character res = reverseSubstitutionMap.get(c);
            sb.append(res != null ? res : c);
        }
        return sb.toString();
    }

    private static void initMaps() {
        // 从源码复制的映射关系
        char[][] pairs = {
            {'A', 'Q'}, {'B', 'W'}, {'C', 'E'}, {'D', 'R'}, {'E', 'T'}, {'F', 'Y'}, {'G', 'U'},
            {'H', 'I'}, {'I', 'O'}, {'J', 'P'}, {'K', 'A'}, {'L', 'S'}, {'M', 'D'}, {'N', 'F'},
            {'O', 'G'}, {'P', 'H'}, {'Q', 'J'}, {'R', 'K'}, {'S', 'L'}, {'T', 'Z'}, {'U', 'X'},
            {'V', 'C'}, {'W', 'V'}, {'X', 'B'}, {'Y', 'N'}, {'Z', 'M'}, {'_', '!'}, {'{', '['}, {'}', ']'}
        };
        
        for (char[] p : pairs) {
            substitutionMap.put(p[0], p[1]);
            // 生成逆向映射用于解密
            reverseSubstitutionMap.put(p[1], p[0]);
        }
    }
}

HGAME-2026-WEEK1-REVERSE

Signal Storm

看似是只要动态调试就行,但是实际上是异常处理。

HGAME-2026-WEEK1-REVERSE

找到主程序入口。这里很明显能发现三个信号处理函数:

  • SIGSEGV​ (11): sub_1640
  • SIGFPE​ (8): sub_16E0
  • SIGTRAP​ (5): sub_1740

在读取到长度为32的flag之后,进入一个循环

  • dword_0 = 0​:尝试向空地址写入,触发 ​SIGSEGV​。这会跳转到 sub_1640 执行,处理完后跳回。
  • raise(5)​:主动抛出 SIGTRAP。这会跳转到 sub_1740 执行。

那三个信号处理函数其实是在对flag进行变换。

  • sub_1640(核心加密步) ​:
    这是一个典型的 RC4 KSA/PRGA 的变种步骤。

    • v0​ (即 i) 递增。
    • dword_4064​ (即 j​) 更新公式:j = (j + S[i] + Key[i % 21]) % 256
    • 交换 S[i]​ 和 S[j]
    • 注意:这里引用的 aC0lmBe4ore7heS​ 是一个长度为 21 的​动态 Key 数组
  • sub_1740(密钥变换)

    • memmove​ 和赋值操作实现了将 aC0lmBe4ore7heS​ 数组​循环左移 1 位
    • 这意味着每一轮加密使用的 Key 数组都在变化,且每一轮取 Key 的索引也在变化 (i % 21),大大增加了复杂度。

而1780,经过确认则是标准的RC4 KSA过程

其中 key找到发现是:

至此,整个程序的逻辑链条已完全打通:

  1. 准备

    • 使用 Key "C0lm_be4ore_7he_st0rm"​ 对 S-Box 进行标准的 RC4 初始化 (sub_1780)。
  2. 解密循环 (32次)

    • 生成密钥流 (sub_1640):

      • i = (i + 1) % 256
      • 魔改点​:j = (j + S[i] + Key[i % 21]) % 256 (注意:这里再次用到了 Key 数组,且是当前轮次旋转前的 Key)。
      • Swap(S[i], S[j])
      • K = S[(S[i] + S[j]) % 256]
    • 异或​:Plain[n] = Cipher[n] ^ K

    • Key 旋转 (sub_1740):将 Key 数组循环左移 1 位。

import struct

def solve():
    # ---------------------------------------------------------
    # 1. 提取密文 (Little Endian)
    # 来源于 main 函数中的 qword_4088, qword_4090 等
    # ---------------------------------------------------------
    cipher_qwords = [
        0x8260C1C9C8D936E3,
        0x1C4BB2D52511D975,
        0xF11CAF1C716DE64D,
        0x1A5AF67F261CA506
    ]
    
    cipher = b''
    for q in cipher_qwords:
        cipher += struct.pack('<Q', q)
        
    print(f"[*] Cipher Hex: {cipher.hex()}")

    # ---------------------------------------------------------
    # 2. 准备初始数据
    # ---------------------------------------------------------
    # Key 来源于 .data 段
    key_str = "C0lm_be4ore_7he_st0rm" # len = 21
    key = [ord(c) for c in key_str]
    
    # S-Box 初始化 (sub_1780)
    # 步骤 A: 填充 0-255 (SIMD 部分的等价逻辑)
    S = list(range(256))
    
    # 步骤 B: 标准 KSA 打乱
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) % 256
        S[i], S[j] = S[j], S[i]

    # ---------------------------------------------------------
    # 3. 模拟主循环 (sub_1640 和 sub_1740)
    # ---------------------------------------------------------
    # 全局变量初始化
    i_idx = 0
    j_idx = 0
    
    decrypted_flag = []
    
    print("[*] Starting decryption loop...")
    
    for n in range(32):
        # === SIGSEGV: sub_1640 (生成密钥流) ===
        i_idx = (i_idx + 1) % 256
        
        # 魔改点:计算 j 时引入了 Key 数组
        # 使用当前状态的 Key 数组
        key_val = key[i_idx % 21]
        
        j_idx = (j_idx + S[i_idx] + key_val) % 256
        
        # Swap
        S[i_idx], S[j_idx] = S[j_idx], S[i_idx]
        
        # 生成密钥流字节 K
        K = S[(S[i_idx] + S[j_idx]) % 256]
        
        # === 解密当前字节 ===
        # 异或解密
        p = cipher[n] ^ K
        decrypted_flag.append(p)
        
        # === SIGTRAP: sub_1740 (Key 旋转) ===
        # memmove(base, base+1, 20); base[20] = v0;
        # 逻辑:循环左移 1 位
        first_byte = key.pop(0)
        key.append(first_byte)

    # ---------------------------------------------------------
    # 4. 输出结果
    # ---------------------------------------------------------
    flag_bytes = bytes(decrypted_flag)
    print(f"[*] Decrypted Bytes: {flag_bytes}")
    try:
        print(f"[*] FLAG: {flag_bytes.decode()}")
    except:
        print("[!] Decode error, check key or algorithm details.")

if __name__ == "__main__":
    solve()

HGAME-2026-WEEK1-REVERSE

看不懂的华容道

题目给了一个huarongdao.exe 还有一个game.bin,那么大致猜测game.bin 就是用来存放游戏数据的。

HGAME-2026-WEEK1-REVERSE

用IDA打开之后发现了程序的主入口,其中有一个地方引起了我的注意:

HGAME-2026-WEEK1-REVERSE

这里出现了一个 “HuarongDao2026_Salt” 这看起来是某种加密用的盐值

case 0x18u:
        sub_140011212(a1);
        sub_1400110A0(v49, 32LL); // 初始化 MD5 上下文
        sub_140011717(v49);

        // [关键步骤 1]:先处理棋盘数据 (BoardBytes)
        // 循环 20 次,对应 4x5 = 20 个格子的棋子 ID
        for ( j = 0; j < 20; ++j )
          sub_1400116A9(v49, a1 + j + 80 + 160); // update(board[j])

        // [关键步骤 2]:后处理 Salt
        sub_14001148D(v51, "HuarongDao2026_Salt"); // 加载 Salt 字符串
        v52 = v51;
        v53 = (char *)sub_14001103C(v51);
        v54 = sub_14001107D(v52);
        
        // 遍历 Salt 字符串的每个字符并 update
        while ( v53 != (char *)v54 )
        {
          v55 = *v53;
          v60[0] = v55;
          sub_140011AB4(v49, v60); // update(salt_char)
          ++v53;
        }
        
        v62 = sub_14001144C(v49); // Finalize / HeXDigest

也就说明了,当程序跑起来之后,这一串默认输出的“683200ad824e73245bbe16997099688a” 是什么东西的问题。

那么。接下来只需要找到棋盘最初的状态,用BFS把棋盘的胜利状态算出来就行了。(后来发现也可以直接把这个虚拟机的操作的字节码读出来,也就是game.bin来获取)

HGAME-2026-WEEK1-REVERSE

HGAME-2026-WEEK1-REVERSE

HGAME-2026-WEEK1-REVERSE
HGAME-2026-WEEK1-REVERSE

那么接下来,解决这个华容道就直接BFS算法

import collections
import hashlib
import sys
import time

# --- Configurations ---
W, H = 4, 5
TARGET_P0_IDX = 13

SHAPES = {
    0: (2, 2),
    1: (2, 1), 2: (2, 1), 3: (2, 1), 4: (2, 1), 
    5: (1, 2),                                  
    6: (1, 1), 7: (1, 1), 8: (1, 1), 9: (1, 1)
}

INITIAL_POS = (1, 0, 3, 11, 10, 8, 12, 16, 13, 19)

MOVES = {
    'w': -4,
    'a': -1,
    's': 4,
    'd': 1
}
MOVE_ORDER = ['w', 'a', 's', 'd']

MASKS = {}

def get_mask(idx, shape):
    h, w = shape
    r, c = divmod(idx, W)
    if r + h > H or c + w > W:
        return None
    mask = 0
    for dr in range(h):
        for dc in range(w):
            pos = (r + dr) * W + (c + dc)
            mask |= (1 << pos)
    return mask

def init_masks():
    for pid, shape in SHAPES.items():
        MASKS[pid] = {}
        for idx in range(W * H):
            m = get_mask(idx, shape)
            if m is not None:
                MASKS[pid][idx] = m
init_masks()

def solve_valid():
    start_node = tuple(INITIAL_POS)
    # Store path: state -> (parent, move)
    # To save memory, maybe just store parent? Or rely on reconstructing later?
    # To be fast and get the path, I'll store (parent, move). 
    # Visited: state -> (parent_state, move_from_parent)
    visited = {start_node: (None, None)}
    queue = collections.deque([start_node])
    
    steps = 0
    
    while queue:
        current = queue.popleft()
        
        if current[0] == TARGET_P0_IDX:
            # Found solution!
            print(f"Solution found!")
            path = []
            curr = current
            while curr != start_node:
                parent, move = visited[curr]
                path.append(move)
                curr = parent
            return path[::-1], current

        # Calculate full mask
        full_mask = 0
        current_masks = []
        for i in range(10):
            m = MASKS[i][current[i]]
            current_masks.append(m)
            full_mask |= m
            
        # Expand
        for pid in range(10):
            p_mask = current_masks[pid]
            current_pos = current[pid]
            board_without_p = full_mask ^ p_mask
            
            for move in MOVE_ORDER:
                delta = MOVES[move]
                new_pos = current_pos + delta
                
                # --- VALIDITY CHECKS ---
                # Check 1: Boundary check handled by get_mask (returns None if OOB)
                # Check 2: Row Wrap check for Left/Right
                if move == 'a' or move == 'd':
                    if (current_pos // W) != (new_pos // W):
                        continue # Invalid wrap-around
                
                new_p_mask = MASKS[pid].get(new_pos)
                
                if new_p_mask is not None:
                    if (new_p_mask & board_without_p) == 0:
                        new_state = current[:pid] + (new_pos,) + current[pid+1:]
                        
                        if new_state not in visited:
                            visited[new_state] = (current, f"{pid}{move}")
                            queue.append(new_state)
        
        steps += 1
        if steps % 50000 == 0:
            print(f"Steps: {steps}, Queue: {len(queue)}")
            sys.stdout.flush()

if __name__ == "__main__":
    path, final_state = solve_valid()
    print(f"Path Length: {len(path)}")
    print("Sequence:")
    print(" ".join(path))
    
    # Also calc hash of final state
    salt = "HuarongDao2026_Salt"
    board_bytes = [255] * 20
    for pid, pos in enumerate(final_state):
        h, w = SHAPES[pid]
        r, c = divmod(pos, W)
        for dr in range(h):
            for dc in range(w):
                idx = (r + dr) * W + (c + dc)
                board_bytes[idx] = pid
                
    m = hashlib.md5()
    m.update(bytes(board_bytes))
    m.update(salt.encode('utf-8'))
    digest = m.hexdigest()
    digest_bytes = bytes.fromhex(digest)
    reversed_digest = digest_bytes[::-1].hex()
    print(f"Final Hash: {reversed_digest}")

HGAME-2026-WEEK1-REVERSE

NonceSense

题目给了三个文件:

  1. Client.exe:用户态的客户端程序。
  2. GateDriver.sys:内核驱动程序。
  3. Drv_blob.bin:被加密后的数据文件。

任务很明确:分析加密流程,解密 Drv_blob.bin 获取 Flag。

将 Client.exe 拖入 IDA。定位到 main 函数,逻辑看起来比较清晰:

  1. 输入处理:程序读取用户输入。
  2. 预处理 (VM混淆):

输入数据并没有直接发送给驱动,而是先经过了一个 do-while 循环。

    // IDA 伪代码片段
    do {
        switch ( v12 ) {
            case 0: ...
            case 1: ... // XOR
            case 6: ... // ROL (循环左移)
        }
        // ...读取字节码...
    } while ( v12 \<\= 6 );

这里看起来像是一个微型的 VM (虚拟机) 混淆。它读取一段硬编码的字节码 (unk_1400043F3),根据操作码对输入数据的每一个字节进行变换。

通过仔细分析这几个 case 和字节码,我还原了加密(混淆)逻辑:

  • v24 寄存器实际上就是当前处理的字节。
  • 它对每个字节 input[i] 进行了以下操作:
  1. x ^= (i * 13 + 195) & 0xFF (基于索引的线性变换)
  2. x = ROL(x, (i * 3 + 1) & 7) (循环左移)
  3. x ^= 0x5A (固定异或)
  4. 驱动交互:

处理完数据后,Client 调用了 DeviceIoControl 与驱动通信。

  • IOCTL 0x222000:获取 Nonce (虽然获取了,但在 VM 逻辑里似乎没用到)。
  • IOCTL 0x222004:将处理后的数据发送给驱动进行加密,结果写出到 Drv_blob.bin。

接着分析内核驱动 GateDriver.sys。在 DriverEntry 中找到分发例程,定位到处理 DeviceIoControl 的函数 sub_1400010C0。

1. 算法识别

在驱动中漫游时,注意到了一个加密函数 sub_1400016B0。

查看其引用的数据,发现了一个非常眼熟的数组:

0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B…

这正是 AES 的 S-Box!这说明驱动内部使用了 AES 加密。看循环结构,是对每个 16 字节块独立加密,应该是 ECB 模式。

2. 密钥生成 (HKDF)

AES 的密钥从哪来?跟踪调用链,发现密钥是在 sub_1400019B8 中生成的。

这个函数引用了一系列 SHA-256 的常数 (0x6A09E667, 0xBB67AE85…)。

逻辑大概是:

Key = HMAC_SHA256(Key=PRK, Data=KeyMaterial + "\x01")

这非常像 HKDF (HMAC-based Extract-and-Expand Key Derivation Function) 的扩展阶段。

3. 关键材料

继续追踪,发现参与密钥生成的有两个关键部分:

  1. Key Material: 一串解密后的硬编码字符串,我在内存中找到了它:"VIDAR_HGAME_D3C_A3S_K2_build2026"。
  2. Nonce: 一个 16 字节的随机数。

Nonce 在哪?

最初我试图分析驱动中生成 Nonce 的函数 sub_140001668,发现它使用了 KeQueryPerformanceCounter 作为种子。

我想过爆破种子(Seed 是 32 位的),写了个 OpenMP 的 C 脚本跑了一遍,结果一无所获。这让我意识到方向可能错了。

突破:重新审视密文

既然爆破行不通,我回头看了一眼 Drv_blob.bin。

文件大小是 80 字节。

AES 的块大小是 16 字节,80 字节正好是 5 个块。

通常 flag 长度都在 30-60 字节左右,加上 Padding 可能是 64 字节(4 块)。

多出来的 16 字节是什么?

在密码学协议设计中,为了解密方能解密,通常会将 IV (或 Nonce) 明文放在密文的最前面!

虽然驱动代码看起来有些绕,但我猜测:

Drv_blob.bin = [Nonce (16 bytes)] + [AES_Ciphertext (64 bytes)]

验证猜想的时刻到了。编写了如下 Python 脚本:

  1. 从 Drv_blob.bin 读入数据。
  2. 切分:前 16 字节视为 Nonce,后面视为密文。
  3. 计算密钥:模拟驱动的 HKDF 逻辑,用提取的 Nonce 和硬编码字符串生成 AES Key。
  4. AES 解密:用生成的 Key 解密后半部分数据。
  5. 去混淆 (Client VM 逆向):将 AES 解密后的数据,按照 Client 中 VM 的逻辑逆向操作(ROR, XOR)。
import struct
import hashlib
import hmac
from Crypto.Cipher import AES

# 1. 载入密文
# 来自 Drv_blob.bin
CIPHERTEXT_FULL = bytes.fromhex("8a8fa4ab7254811ef7a28a47394b66bf24c7cf462bf5f7e04e07abdf170e1ea6ec8df713af3a574b381d396a9ea1eb0260b8aa8a646d9d26d2454c79459510dc532317b2f1488960220a9104ee31772d")

# 2. 准备 Key Material (从驱动中提取并解密后的字符串)
# 原始内存数据经过简单的 ROR 解密后得到这个字符串
KEY_MAT_STR = b"VIDAR_HGAME_D3C_A3S_K2_build2026"

def ror8(x, n):
    n = n % 8
    return ((x >> n) | (x << (8 - n))) & 0xFF

# 3. 逆向 Client 的 VM 混淆逻辑
def deobfuscate_byte(y, i):
    # 正向逻辑:
    # x ^= (i * 13 + 195)
    # x = ROL(x, shift)
    # x ^= 0x5A
    
    # 逆向逻辑:
    y = y ^ 0x5A
    shift = (i * 3 + 1) & 7
    # ROL 的逆操作是 ROR
    y = ror8(y, shift) 
    y = y ^ ((i * 13 + 195) & 0xFF)
    return y

# 4. 模拟驱动的 HKDF 密钥派生
def hkdf_derive(nonce):
    # Step 1: Extract (虽然 salt 是 0,但这里 nonce 充当了 IKM)
    prk = hmac.new(b'\x00'*32, nonce, hashlib.sha256).digest()
    # Step 2: Expand
    okm = hmac.new(prk, KEY_MAT_STR + b'\x01', hashlib.sha256).digest()
    return okm[:16] # 取前16字节做 AES-128 Key

if __name__ == '__main__':
    # 猜想:前 16 字节是 Nonce
    nonce = CIPHERTEXT_FULL[:16]
    cipher_data = CIPHERTEXT_FULL[16:]
    
    print(f"Nonce: {nonce.hex()}")
    
    # 计算 AES 密钥
    key = hkdf_derive(nonce)
    print(f"Derived Key: {key.hex()}")
    
    # AES 解密
    cipher = AES.new(key, AES.MODE_ECB)
    decrypted = cipher.decrypt(cipher_data)
    
    # VM 去混淆
    plain = bytearray(len(decrypted))
    for i in range(len(decrypted)):
        plain[i] = deobfuscate_byte(decrypted[i], i)
        
    print(f"Flag: {plain.decode('utf-8', errors='ignore')}")

运行脚本,成功得到 Flag:

Nonce: 8a8fa4ab7254811ef7a28a47394b66bf

Derived Key: …

Flag: hgame{n0w_y9u_2_a_n0nces3nser_9f3a1c0e7b2d4a8c1e3f5a7b}

正文完
 0
评论(没有评论)