REVERSE-手忙脚乱
ISCC2026 WriteUp 提交模板
REVERSE-手忙脚乱
解题思路
1.分析main函数

提示输入


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

然后就是调用此函数
算法流程大致如下:
输入: 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

第一轮校验:
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 ? → 进入第二轮 : 重新输入

第二轮校验:
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}

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()

浙公网安备 33010602011771号