REVERSE-手忙脚乱

ISCC2026 WriteUp 提交模板

REVERSE-手忙脚乱

解题思路

1.分析main函数

image.png

提示输入

image.png

image.png

自定义流密码解密函数,用于解密 main 中使用的所有嵌入式常量

image.png

然后就是调用此函数
算法流程大致如下:

输入: 24字节明文, 密钥(key_len = 3 或 6)
输出: 24字节密文

1. 矩阵构建 (write row-wise)
   matrix = string[key_len × (nRows+1)]    // nRows = 24 / key_len
   matrix[0][0..key_len-1] = key[0..key_len-1]   // 第0行放密钥
   for row = 1..nRows:
       for col = 0..key_len-1:
           matrix[row][col] = input[row*key_len + col]  // 按行填充

2. 列排序 (stable_sort)
   indices = [0, 1, 2, ..., key_len-1]
   stable_sort(indices, cmp: (a,b) => key[a] < key[b])
   // 按密钥字节升序对列索引排序

3. 按列读取 (read column-wise in sorted order)
   output = ""
   for sorted_col in indices:           // 按排序后的列顺序
       for row = 1..nRows:              // 跳过密钥行
           output += matrix[row][sorted_col]
   return output

image.png

第一轮校验:

decrypt_blob(CT_MASK1)  →  MASK1
decrypt_blob(CT_TARGET) →  TARGET

columnarTransposeEncrypt(input, K3)          →  tmp1   (列换位, key_len=3)
columnarTransposeEncrypt(input, tmp1)         →  tmp2   (列换位, key_len=6)
encryptPart2(tmp2, K6)                        →  result
result ^ MASK1 == TARGET  ?  →  进入第二轮  :  重新输入

image.png

第二轮校验:

decrypt_blob(CT_MASK2)     →  MASK2
decrypt_blob(CT_TARGET_T)  →  TARGET_T

result = encryptChain(input)      // 同第一轮的加密链
data   = (result ^ MASK2) ^ TARGET_T
bits   = bytesToBits(data)        // 192 bits
hdb3_a = hdb3(bits)               // HDB3线路编码
stat_a = getBVStat(hdb3_a)        // 比特违规统计值

// 参考路径:Size 来自于第一轮成功时记录的 memcmp 的 Buf2 长度
ref_data = 24字节数据(来源另算)
ref_bits = bytesToBits(ref_data)
hdb3_b  = hdb3(ref_bits)
stat_b  = getBVStat(hdb3_b)

PASS if stat_a == stat_b

需要注意的是第二轮输入:才是实际的 flag_body

结合本地求解脚本和程序的最终成功路径,可以把最终提交格式确定为:

ISCC{b/$s:T\|tj:'nq }euP^^PS2}

image.png

Exp

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Solver for attachment-62.exe.

This version keeps the same reversing logic but uses a different structure and
renamed parameters throughout.
"""

from __future__ import annotations

import argparse
import struct
from dataclasses import dataclass
from pathlib import Path


SECRET_MAP = {
    "MASK2": (0x140008100, 24, 0x4444),
    "MASK1": (0x140008120, 24, 0x3333),
    "TARGET_T": (0x140008140, 24, 0x2222),
    "TARGET": (0x140008160, 24, 0x1111),
    "K3": (0x140008178, 3, 0x6666),
    "K6": (0x14000817B, 6, 0x5555),
}


@dataclass(frozen=True)
class SectionView:
    label: str
    virtual_start: int
    virtual_span: int
    file_start: int
    file_size: int


def read_word(image_data: bytes, offset_value: int) -> int:
    return struct.unpack_from("<H", image_data, offset_value)[0]


def read_dword(image_data: bytes, offset_value: int) -> int:
    return struct.unpack_from("<I", image_data, offset_value)[0]


def read_qword(image_data: bytes, offset_value: int) -> int:
    return struct.unpack_from("<Q", image_data, offset_value)[0]


class PortableExecutable:
    def __init__(self, file_blob: bytes) -> None:
        self._blob = file_blob
        self._image_base, self._sections = self._parse_headers(file_blob)

    @staticmethod
    def _parse_headers(raw_image: bytes) -> tuple[int, list[SectionView]]:
        if raw_image[:2] != b"MZ":
            raise ValueError("not a PE file")

        pe_header = read_dword(raw_image, 0x3C)
        if raw_image[pe_header : pe_header + 4] != b"PE\0\0":
            raise ValueError("bad PE signature")

        coff_header = pe_header + 4
        section_count = read_word(raw_image, coff_header + 2)
        optional_size = read_word(raw_image, coff_header + 16)
        optional_header = coff_header + 20
        magic_value = read_word(raw_image, optional_header)

        if magic_value == 0x20B:
            base_address = read_qword(raw_image, optional_header + 24)
        elif magic_value == 0x10B:
            base_address = read_dword(raw_image, optional_header + 28)
        else:
            raise ValueError(f"unknown optional header magic: 0x{magic_value:x}")

        views: list[SectionView] = []
        section_table = optional_header + optional_size
        for index_value in range(section_count):
            header_offset = section_table + 40 * index_value
            section_name = (
                raw_image[header_offset : header_offset + 8]
                .split(b"\0", 1)[0]
                .decode("latin-1", "replace")
            )
            virtual_size = read_dword(raw_image, header_offset + 8)
            virtual_addr = read_dword(raw_image, header_offset + 12)
            raw_size = read_dword(raw_image, header_offset + 16)
            raw_addr = read_dword(raw_image, header_offset + 20)
            views.append(
                SectionView(
                    label=section_name,
                    virtual_start=virtual_addr,
                    virtual_span=max(virtual_size, raw_size),
                    file_start=raw_addr,
                    file_size=raw_size,
                )
            )

        return base_address, views

    def slice_at_va(self, virtual_address: int, wanted_size: int) -> bytes:
        relative_address = virtual_address - self._image_base
        for section_info in self._sections:
            start = section_info.virtual_start
            end = start + section_info.virtual_span
            if start <= relative_address < end:
                delta_value = relative_address - start
                if delta_value + wanted_size > section_info.file_size:
                    raise ValueError(
                        f"read crosses section boundary at 0x{virtual_address:x}"
                    )
                begin = section_info.file_start + delta_value
                finish = begin + wanted_size
                return self._blob[begin:finish]
        raise ValueError(f"VA 0x{virtual_address:x} not found")


def rotate_right_8(byte_value: int, shift_count: int) -> int:
    masked_shift = shift_count & 7
    if masked_shift == 0:
        return byte_value & 0xFF
    return ((byte_value >> masked_shift) | ((byte_value << (8 - masked_shift)) & 0xFF)) & 0xFF


def decode_secret(cipher_bytes: bytes, seed_value: int) -> bytes:
    lane_r9 = (seed_value ^ 0xA3B1C2D3) & 0xFFFFFFFF
    lane_r8 = 0
    lane_r10 = 0
    plain_bytes: list[int] = []

    for current_byte in cipher_bytes:
        lane_eax = (lane_r9 + lane_r8) & 0xFFFFFFFF
        lane_r8 = (lane_r8 - 0x61C88647) & 0xFFFFFFFF
        lane_r9 = lane_eax
        lane_r9 = (((lane_r9 << 13) & 0xFFFFFFFF) ^ lane_eax) & 0xFFFFFFFF
        lane_r9 = (lane_r9 ^ (lane_r9 >> 17)) & 0xFFFFFFFF
        lane_r9 = (lane_r9 ^ ((lane_r9 << 5) & 0xFFFFFFFF)) & 0xFFFFFFFF

        lane_eax = (lane_r10 + lane_r9 * 8) & 0xFFFFFFFF
        lane_r10 = (lane_r10 + 0x0B) & 0xFFFFFFFF
        lane_eax = (lane_eax - lane_r9) & 0xFFFFFFFF
        low_rotate = (lane_r9 >> 27) & 0x1F
        lane_al = (lane_eax & 0xFF) ^ current_byte
        lane_al = rotate_right_8(lane_al, low_rotate)
        lane_eax = ((lane_eax & 0xFFFFFF00) | lane_al) & 0xFFFFFFFF
        plain_bytes.append((lane_eax ^ lane_r9) & 0xFF)

    return bytes(plain_bytes)


def xor_pair(left_bytes: bytes, right_bytes: bytes) -> bytes:
    return bytes(left ^ right for left, right in zip(left_bytes, right_bytes))


def build_shuffle_table(key_material: bytes, table_seed: int) -> bytes:
    printable_pool = list(range(0x20, 0x7F))
    mixer_state = table_seed & 0xFFFFFFFF

    for symbol_value in key_material:
        mixer_state = (mixer_state * 0x83 + symbol_value) & 0xFFFFFFFF

    for pool_index in range(0x5E, 0, -1):
        mixer_state = (mixer_state * 0x41C64E6D + 0x3039) & 0xFFFFFFFF
        swap_index = mixer_state % (pool_index + 1)
        printable_pool[pool_index], printable_pool[swap_index] = (
            printable_pool[swap_index],
            printable_pool[pool_index],
        )

    return bytes(printable_pool)


def expand_key_bits(key_material: bytes) -> list[int]:
    return [
        (symbol_value >> bit_index) & 1
        for symbol_value in key_material
        for bit_index in range(7, -1, -1)
    ]


def rank_columns(sort_key: bytes) -> list[int]:
    return sorted(range(len(sort_key)), key=lambda key_index: sort_key[key_index])


def permute_columns(source_text: bytes, sort_key: bytes) -> bytes:
    if len(source_text) % len(sort_key) != 0:
        raise ValueError("columnar length mismatch")
    row_total = len(source_text) // len(sort_key)
    sorted_columns = rank_columns(sort_key)
    return bytes(
        source_text[row_index * len(sort_key) + column_index]
        for column_index in sorted_columns
        for row_index in range(row_total)
    )


def undo_column_permute(encoded_text: bytes, sort_key: bytes) -> bytes:
    if len(encoded_text) % len(sort_key) != 0:
        raise ValueError("inverse columnar length mismatch")
    column_total = len(sort_key)
    row_total = len(encoded_text) // column_total
    sorted_columns = rank_columns(sort_key)
    canvas = [[0] * column_total for _ in range(row_total)]
    cursor = 0
    for column_index in sorted_columns:
        for row_index in range(row_total):
            canvas[row_index][column_index] = encoded_text[cursor]
            cursor += 1
    return bytes(
        canvas[row_index][column_index]
        for row_index in range(row_total)
        for column_index in range(column_total)
    )


def reverse_substitution(
    encoded_char: int,
    bit_flag: int,
    low_table: bytes,
    high_table: bytes,
) -> int:
    active_table = high_table if bit_flag else low_table
    return active_table.index(encoded_char) + 0x20


def unwind_part2(scrambled_text: bytes, key_material: bytes) -> bytes:
    low_table = build_shuffle_table(key_material, 0x1234)
    high_table = build_shuffle_table(key_material, 0x8888)
    bit_stream = expand_key_bits(key_material)
    buffer_state = bytearray(scrambled_text)
    block_width = len(buffer_state)

    for bit_position in range(len(bit_stream) - 1, -1, -1):
        if (bit_position + 1) % block_width == 0:
            buffer_state = bytearray(undo_column_permute(buffer_state, key_material))
        char_offset = bit_position % block_width
        buffer_state[char_offset] = reverse_substitution(
            buffer_state[char_offset],
            bit_stream[bit_position],
            low_table,
            high_table,
        )

    return bytes(buffer_state)


def recover_inputs(sample_path: str) -> tuple[str, str, str]:
    executable_blob = Path(sample_path).read_bytes()
    pe_image = PortableExecutable(executable_blob)
    revealed = {
        secret_name: decode_secret(pe_image.slice_at_va(secret_va, secret_size), secret_seed)
        for secret_name, (secret_va, secret_size, secret_seed) in SECRET_MAP.items()
    }

    key_six = revealed["K6"]
    key_three = revealed["K3"]
    transit_key = permute_columns(key_six, key_three)

    phase_one_target = xor_pair(revealed["MASK1"], revealed["TARGET"])
    phase_one_mid = unwind_part2(phase_one_target, key_six)
    phase_one_text = undo_column_permute(phase_one_mid, transit_key).decode("latin-1")

    phase_two_target = xor_pair(revealed["MASK2"], revealed["TARGET_T"])
    phase_two_mid = unwind_part2(phase_two_target, key_six)
    phase_two_text = undo_column_permute(phase_two_mid, transit_key).decode("latin-1")

    final_flag = f"ISCC{{{phase_two_text}}}"
    return phase_one_text, phase_two_text, final_flag


def main() -> None:
    cli_parser = argparse.ArgumentParser(description="solver for attachment-62.exe")
    cli_parser.add_argument("sample_path", nargs="?", default="attachment-62.exe")
    cli_args = cli_parser.parse_args()

    first_gate, inner_flag, wrapped_flag = recover_inputs(cli_args.sample_path)
    print(f"[+] first input : {first_gate}")
    print(f"[+] flag inner  : {inner_flag}")
    print(f"[+] flag        : {wrapped_flag}")


if __name__ == "__main__":
    main()

posted @ 2026-05-19 16:28  MillionMind  阅读(14)  评论(0)    收藏  举报