Misc

谍影重重 6.0

Challenge

经过我国执法部门的努力,终于在今年十月提取出了张纪星(系杜撰名字,与现实人员无关)被捕前布置的监听设备中的加密信息,据本人供述其曾恢复过我国一份绝密情报。

flag为情报所提及的详细时间和地址的md5值,即flag{md5(x年x月x日x时x分于x地)}。

题目提示:本题依托于架空的时间线,取材自真实历史事件,请关注地点信息。

Solution

qwb2025-1

先看看协议分级,发现全是 UDP 流量

qwb2025-2

先查看第一个包的 payload,发现是以 80 开头的,这使我不禁想起前段时间的 WMCTF2025,参考当时写的 wp:Voice-hacker

先假设它是 RTP 流量,然后用同样的方法解析它的头部 80 80 76 38 99 59 48 23 88 48 19 ee

  • 80: 版本号(V=2)
  • 80: 标记位(M=1),载荷类型 (Payload Type, PT) = 0
  • 76 38: 序列号 (Sequence Number) = 30264
  • 99 59 48 23: 时间戳 (Timestamp) = 2572765219
  • 88 48 19 ee: 同步源标识符 (SSRC) = 0x884819EE

完美对上了,这就是 RTP 协议,右键第一条流量在 Decode As... 把端口 40000 的 UDP 流量解析为 RTP

然后往下滑,发现还有多个端口,把端口 40001 的也解析了

qwb2025-3

不难发现,这个流量文件里并不是所有包的 SSRC 都相同的,这就意味着并不是所有包都属于同一个音频流,需要根据 SSRC 来划分

先用 tshark 把 UDP 包的 Payload 提取出来方便稍后使用脚本处理:

text
tshark -r Data.pcap -T fields -e data > Data.txt

由于里面的流量并非全部属于同一个音频流,因此需要根据 SSRC 划出多段音频,最后将它们按顺序拼接起来合并成同一个音频文件,这里在 Voice-hacker 的脚本的基础上进行修改。

这样简单的处理得到的结果效果并不好,出现了两个问题:音频的音量过小,音频长达 18 小时😰

在听了两三分钟后,发现音频中存在较长的空白片段,随便往后一划也很容易划到没有声音的地方,检查一下前面导出的 Data.txt ,可以发现:

text
8000c6445d9424d7842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8000c6455d942577842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8000c6465d942617842e8e8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

里面存在大量类似这样的片段,因此我们可以先做一个预处理,修改 Data.txt,删除里面所有完全空白的片段:

python
kept_lines_count = 0removed_lines_count = 0 with open('Data.txt', 'r', encoding='utf-8') as infile, open('Data_new.txt', 'w', encoding='utf-8') as outfile:        for line in infile:        stripped_line = line.strip()         if not stripped_line:            removed_lines_count += 1            continue         header_length = 24 # RTP头部的12字节        is_filler_payload = False        if len(stripped_line) > header_length:            payload = stripped_line[header_length:]            if payload and all(char == 'f' for char in payload):                is_filler_payload = True         if is_filler_payload:            removed_lines_count += 1            continue         outfile.write(line)        kept_lines_count += 1

Data.txt 从原来的 1.04 GB 缩小成了 195 MB(看来掺水挺严重

下面是完善后的导出脚本:

python
import waveimport structimport osfrom collections import defaultdict # --- G.711 μ-law to 16-bit Linear PCM Decoder ---# 这是一个标准的查找表,用于将8位的μ-law字节解码为16位的线性采样值_ULAW_DECODE_TABLE = [    -32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,    -23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,    -15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,    -11900, -11388, -10876, -10364,  -9852,  -9340,  -8828,  -8316,    -7932,  -7676,  -7420,  -7164,  -6908,  -6652,  -6396,  -6140,    -5884,  -5628,  -5372,  -5116,  -4860,  -4604,  -4348,  -4092,    -3900,  -3772,  -3644,  -3516,  -3388,  -3260,  -3132,  -3004,    -2876,  -2748,  -2620,  -2492,  -2364,  -2236,  -2108,  -1980,    -1884,  -1820,  -1756,  -1692,  -1628,  -1564,  -1500,  -1436,    -1372,  -1308,  -1244,  -1180,  -1116,  -1052,   -988,   -924,    -876,   -844,   -812,   -780,   -748,   -716,   -684,   -652,    -620,   -588,   -556,   -524,   -492,   -460,   -428,   -396,    -372,   -356,   -340,   -324,   -308,   -292,   -276,   -260,    -244,   -228,   -212,   -196,   -180,   -164,   -148,   -132,    -120,   -112,   -104,    -96,    -88,    -80,    -72,    -64,    -56,    -48,    -40,    -32,    -24,    -16,     -8,      0,    32124,  31100,  30076,  29052,  28028,  27004,  25980,  24956,    23932,  22908,  21884,  20860,  19836,  18812,  17788,  16764,    15996,  15484,  14972,  14460,  13948,  13436,  12924,  12412,    11900,  11388,  10876,  10364,   9852,   9340,   8828,   8316,    7932,   7676,   7420,   7164,   6908,   6652,   6396,   6140,    5884,   5628,   5372,   5116,   4860,   4604,   4348,   4092,    3900,   3772,   3644,   3516,   3388,   3260,   3132,   3004,    2876,   2748,   2620,   2492,   2364,   2236,   2108,   1980,    1884,   1820,   1756,   1692,   1628,   1564,   1500,   1436,    1372,   1308,   1244,   1180,   1116,   1052,    988,    924,    876,    844,    812,    780,    748,    716,    684,    652,    620,    588,    556,    524,    492,    460,    428,    396,    372,    356,    340,    324,    308,    292,    276,    260,    244,    228,    212,    196,    180,    164,    148,    132,    120,    112,    104,     96,     88,     80,     72,     64,    -56,    -48,    -40,    -32,    -24,    -16,     -8,      0] def decode_ulaw_to_pcm16(ulaw_data):    """将一整段 G.711 u-law 字节数据解码为 16-bit 线性 PCM 字节数据"""    pcm_frames = []    for ulaw_byte in ulaw_data:        # 从查找表中获取对应的16位PCM值        pcm_sample = _ULAW_DECODE_TABLE[ulaw_byte]        # 将16位整数打包成2个字节(小端序)        pcm_frames.append(struct.pack('<h', pcm_sample))    return b''.join(pcm_frames) def amplify_and_clip_pcm16(pcm_data, factor):    """    放大16位PCM数据的音量,并进行削波处理。    :param pcm_data: 16位PCM字节数据    :param factor: 放大系数 (例如 2.0 表示放大2倍)    :return: 放大后的16位PCM字节数据    """    # 将字节数据解包成16位整数列表    samples = struct.unpack(f'<{len(pcm_data) // 2}h', pcm_data)        amplified_samples = []    for sample in samples:        amplified_sample = int(sample * factor)        # 削波处理: 确保值在16位有符号整数范围内        if amplified_sample > 32767:            amplified_sample = 32767        elif amplified_sample < -32768:            amplified_sample = -32768        amplified_samples.append(amplified_sample)            # 将处理后的整数列表打包回字节    return struct.pack(f'<{len(amplified_samples)}h', *amplified_samples) def write_wav(filename, pcm_data, channels, sampwidth, framerate):    """一个辅助函数,用于将PCM数据写入WAV文件"""    try:        with wave.open(filename, "wb") as wav_file:            wav_file.setnchannels(channels)            wav_file.setsampwidth(sampwidth)            wav_file.setframerate(framerate)            wav_file.writeframes(pcm_data)    except Exception as e:        print(f"写入文件 {filename} 时出错: {e}") def main():    input_filename = "Data_new.txt"    output_dir = "output"    combined_filename = "combined_audio.wav"    amplification_factor = 50.0  # 音量放大系数        # WAV文件标准参数    CHANNELS = 1    SAMPWIDTH = 2  # 16-bit -> 2 bytes    FRAMERATE = 8000     # --- 准备工作 ---    # 创建输出目录    os.makedirs(output_dir, exist_ok=True)     with open(input_filename, "r") as f:        lines = [line.strip() for line in f if line.strip()]     # --- 步骤 1: 根据SSRC对RTP包进行分组 ---    streams = defaultdict(list)    stream_order = []     for line in lines:        if len(line) < 24: continue        ssrc = line[16:24]        payload_hex = line[24:]        if not payload_hex: continue                if ssrc not in streams:            stream_order.append(ssrc)        streams[ssrc].append(bytes.fromhex(payload_hex))     if not streams:        print("未在文件中找到有效的RTP数据包。")        return     print(f"处理完成,共找到 {len(stream_order)} 个不同的音频流。")    print("SSRC 出现顺序:", stream_order)     # --- 步骤 2: 逐个处理流,导出片段并准备合并 ---    final_pcm_data_list = []    total_samples_processed = 0     for ssrc in stream_order:        # 拼接属于同一个流的所有payload        raw_ulaw_data = b''.join(streams[ssrc])        if not raw_ulaw_data:            print(f"SSRC {ssrc} 没有有效的音频数据,已跳过。")            continue         # 解码为PCM        pcm_data = decode_ulaw_to_pcm16(raw_ulaw_data)                # 放大音量        amplified_pcm_data = amplify_and_clip_pcm16(pcm_data, amplification_factor)         # 计算片段的开始时间        start_time_seconds = total_samples_processed / FRAMERATE                # 创建片段文件名并导出        segment_filename = f"{start_time_seconds:.3f}s.wav"        segment_filepath = os.path.join(output_dir, segment_filename)        print(f"正在导出片段: {segment_filepath}")        write_wav(segment_filepath, amplified_pcm_data, CHANNELS, SAMPWIDTH, FRAMERATE)         # 为合并做准备        final_pcm_data_list.append(amplified_pcm_data)                # 更新已处理的总采样数        num_samples_in_segment = len(amplified_pcm_data) // SAMPWIDTH        total_samples_processed += num_samples_in_segment     # --- 步骤 3: 合并所有片段并导出最终文件 ---    if final_pcm_data_list:        combined_pcm_data = b''.join(final_pcm_data_list)        combined_filepath = os.path.join(output_dir, combined_filename)                print(f"\n正在导出合并后的文件: {combined_filepath}")        write_wav(combined_filepath, combined_pcm_data, CHANNELS, SAMPWIDTH, FRAMERATE)                print("\n所有任务完成")    else:        print("\n没有可处理的音频数据") if __name__ == "__main__":    main()

用语音识别模型(我用的是 FunASR)识别导出的 combined_audio.wav 得到字幕文件

粗略看了下没啥信息,是由多条听起来有点逻辑的语句直接拼接起来的,然而实际上并没有任何意义

因为附件还给了个 Secret.7z 压缩包,联系到题目描述不难想到“监听设备中的加密信息”指的是这些录音,那么“绝密情报”就是 Secret.7z 了,进而可以推测音频中隐藏着 Secret.7z 的解压密码

看了下字幕文件只有 14207 行,索性直接一股脑丢给 Gemini 问问看有没有比较突兀的地方

qwb2025-4

回到字幕文件找到这一段

qwb2025-5

定位到这个片段发现确实是在念数字,人工识别得到:

text
651466314514271616614214660701456661601411451426071146666014214371656514214470

尝试使用这串数字作为密码解压缩包失败了,观察发现字符的范围是 0-7,因此推测这里要八进制转字符

用动态规划算法切成若干个长度为 2 或 3 的八进制段寻找可能的解:

python
s = "651466314514271616614214660701456661601411451426071146666014214371656514214470" from functools import lru_cache @lru_cache(None)def dp(i):    if i == len(s):        return [[]]  # 分割到末尾返回空解    res = []    for l in (2, 3):  # 尝试2或3位八进制段        if i + l <= len(s):            part = s[i:i+l]            val = int(part, 8)  # 按八进制解析            if 32 <= val <= 126:  # 判断是否可打印                for tail in dp(i + l):                    res.append([val] + tail)    return res solutions = dp(0)if solutions:    for vals in solutions:        decoded = "".join(chr(v) for v in vals)        print(decoded)

发现有且仅有一个解:5f3eb916bf08e610aeb09f60bc955bd8

这个解就是压缩包的解压密码,解开压缩包后得到 绝密录音.mp3

绝密录音.mp3 内存储了一段对话(其中A是普通话,B是粤语):

text
A:表兄,近日可好?上回托您带的廿四旦秋茶,家母嘱咐务必在辰时正过三刻前送到,切记用金丝锦盒装妥,此处潮气重,莫让干货受了霉,若赶得及时可赶得菊花开前便可让铺子开张。B:一切安好,我会按照要求准备好秋茶,我该送到何地?A:送至双鲤湖西岸南山茶铺,放右边第二个橱柜,莫放错。B:我已知悉,你在那边可还安好?A:一切安好,希望你我二人早日相见。B:指日可待,茶叶送到了,但是晚了时日,茶铺看来只能另寻良辰吉日了。你在那边千万保重!
  • “廿四”指的是 24日

  • “辰时”指的是上午 7~9时,“辰时正”指的是 8时

  • “三刻”指的是 45分

  • 地点是对话中提到的 双鲤湖西岸南山茶铺

双鲤湖位于福建省金门,结合这些信息可以找到1949年10月24日发起的金门战役,因此年份是 1949年

连起来就是 1949年10月24日8时45分于双鲤湖西岸南山茶铺

FLAG

text
flag{2a97dec80254cdb5c526376d0c683bdd}

The_Interrogation_Room

Challenge

Reminder:
- Complete all rounds to get the flag (or a gift).
- Any invalid token terminates the session.
- Spaces must be added on both sides of ’(’ and ’)’.

Solution

本题的核心是一个逻辑推理挑战,我们需要在25轮游戏中的每一轮都成功推断出服务器在后台生成的8个未知的布尔秘密值(S0S7),挑战规则如下:

  • 查询机会:每轮有 17 次提问机会
  • 查询方式:提问是通过发送一个由白名单内操作符(['==','(',')','S0','S1','S2','S3','S4','S5','S6','S7','0','1','and','or'])组成的逻辑表达式
  • 核心障碍:在17个回答中,服务器会精确地说谎 2 次(即返回与真实计算结果相反的布尔值)
  • 目标:利用这17个可能包含错误的回答反推出唯一正确的 8 个秘密值

这个问题本质上是一个纠错码问题,我们需要设计一个信息冗余的查询系统,使得即便信息在传输过程中出现了2个比特的错误也依然能够恢复出原始的8比特信息。

为了尽可能地提高成功率,我们要设计一个能够消除绝大多数歧义性的查询策略。

  • == 操作符的特性:在布尔逻辑中,A == B 等价于 XNOR(异或非)。当链式使用时(如 S0 == S1 == S2),它会检查参与运算的变量中值为 True 的个数是奇数还是偶数,这种校验方式比 orand 提供了更强的数学约束。

因此我们可以构建一个基于奇偶校验的编码系统,使用 == 操作符来实现奇偶校验,利用全部17次查询来构建一个强大的校验矩阵。

设计查询集(17个问题)

设计如下查询组合以最大化信息获取和冗余度:

  1. 8 个直接查询
    直接查询 S0, S1, …, S7
    这为我们提供了含有最多2个错误的原始数据。

  2. 9 个奇偶校验查询
    设计 9 个不同的互相重叠的秘密子集并对它们进行 == 链式查询,这些查询充当了纠错码中的“校验位”,用于精确定位错误。

    text
    # 例如:S0 == S1 == S2 == S3S4 == S5 == S6 == S7S0 == S2 == S4 == S6  # 偶数位... (以及其他精心挑选的组合)

    这个查询集确保其最小汉明距离足够大,足以纠正2个比特的错误。

解码与暴力破解

在获得17个回答后采取以下步骤进行解码:

  1. 遍历所有可能性:由于秘密总共只有 8 位,所以只存在 2^8 = 256 种可能的组合,可以进行暴力破解。

  2. 验证每个候选解:对 256 种可能的秘密组合,执行以下验证:
    a. 假设候选解为真:假设当前遍历到的组合就是囚犯心中的真实秘密。
    b. 计算理想答案:基于这个假设计算出我们设计的 17 个查询的全部正确答案。
    c. 比较并计算差异:将这 17 个理想答案与服务器返回的 17 个回答逐一比较,计算出它们之间有多少个不一致(即汉明距离)。
    d. 寻找匹配:根据题目规则,真实的秘密组合所产生的理想答案,与服务器的回答之间的汉明距离必须精确等于2。

处理极少数的歧义情况

实验证明,存在极小概率的情况会导致找到不止一个满足条件的候选解,处理方案如下:

  • 如果只找到一个解,那么它就是正确答案。
  • 如果找到多个解,脚本会记录一个警告,并猜测第一个解作为答案提交。
  • 如果猜测错误,服务器会断开连接。此时要重新运行脚本,重跑几次总有一次能成功通过 25 轮。
python
from pwn import *from hashlib import sha256import stringimport itertoolsfrom functools import reduce # --- Config ---context.log_level = 'info'HOST = '39.106.45.147'PORT = 39009 # --- PoW Solver ---def solve_pow(p):    p.recvuntil(b'sha256(XXXX+')    suffix = p.recvuntil(b')', drop=True).decode()    p.recvuntil(b'== ')    target_hash = p.recvline().strip().decode()        log.info(f"Solving PoW: sha256(XXXX+{suffix}) == {target_hash}")        for prefix in itertools.product(string.ascii_letters + string.digits, repeat=4):        prefix_str = "".join(prefix)        guess = (prefix_str + suffix).encode()        if sha256(guess).hexdigest() == target_hash:            log.success(f"PoW solved! XXXX = {prefix_str}")            p.sendlineafter(b'Give me XXXX: ', prefix_str.encode())            return        log.error("PoW failed!")    exit(1) # --- Helper for Parity Calculation ---def calculate_parity(booleans):    if not booleans: return True    return reduce(lambda a, b: a == b, booleans) # --- Main Logic for a Single Round ---def solve_round(p):    log.info("Starting new round with Parity Code query set...")        questions = []    # 1. 8 direct queries    for i in range(8):        questions.append(f"S{i}")        # 2. 9 parity check queries for maximum error correction capability    parity_indices = [        [0, 1, 2, 3], [4, 5, 6, 7], [0, 1, 4, 5],         [2, 3, 6, 7], [0, 2, 4, 6], [1, 3, 5, 7],        [0, 3, 5], [1, 2, 6], [0, 4, 7]    ]    for indices in parity_indices:        questions.append(" == ".join([f"S{i}" for i in indices]))     responses = []    for i, q in enumerate(questions):        # On the first question of a round, check the received preamble for the gift        received_data = p.sendlineafter(b"Ask your question:", q.encode(), timeout=5)                if i == 0 and b"Here is a gift for you:" in received_data:            for line in received_data.strip().split(b'\n'):                if b"Here is a gift for you:" in line:                    log.warning(f"GIFT RECEIVED: {line.decode()}")         p.recvuntil(b"Prisoner's response: ")        res = p.recvline().strip().decode().replace("!", "")        responses.append(res == 'True')     possible_solutions = []    for i in range(256):        candidate_secrets = [(i >> j) & 1 == 1 for j in range(8)]                true_results = candidate_secrets[:]        for indices in parity_indices:            vals_for_parity = [candidate_secrets[k] for k in indices]            parity_val = calculate_parity(vals_for_parity)            true_results.append(parity_val)         distance = sum(1 for j in range(17) if responses[j] != true_results[j])                if distance == 2:            possible_solutions.append(candidate_secrets)     # Handle the rare but real cases of ambiguity    if len(possible_solutions) >= 1:        if len(possible_solutions) > 1:            log.warning(f"AMBIGUITY DETECTED: Found {len(possible_solutions)} solutions. Guessing the first one.")        else:            log.success(f"Found unique solution: {possible_solutions[0]}")        solution = possible_solutions[0]    else: # len == 0        log.error("FATAL: Found NO possible solutions. The logic is flawed or the server is inconsistent.")        p.interactive()        exit(1)            solution_str = " ".join(map(str, map(int, solution)))    p.sendlineafter(b"Now reveal the true secrets (1 for true, 0 for false):", solution_str.encode())  # --- Main Connection Handler ---def main():    try:        p = remote(HOST, PORT)        solve_pow(p)                for i in range(25):            log.info(f"--- Starting Round {i+1}/25 ---")            solve_round(p)                        # Check for success or failure to prevent hanging on a closed socket            response = p.recvline(timeout=3)            if b"laughs triumphantly" in response:                log.error("Round failed, likely due to an unlucky ambiguous case.")                log.error("The server has closed the connection. Please restart the script.")                return         log.success("All rounds completed! Receiving flag...")        p.interactive()    except EOFError:        log.error("Connection closed unexpectedly. This can happen after a failed round.")        log.warning("Please re-run the script.")  if __name__ == "__main__":    main()
text
[x] Opening connection to 39.106.45.147 on port 39009[x] Opening connection to 39.106.45.147 on port 39009: Trying 39.106.45.147[+] Opening connection to 39.106.45.147 on port 39009: Done[*] Solving PoW: sha256(XXXX+gkNcEHeSaUjh8lbR) == 18cc96fea5027c239eb6e8374633cd6986b661039506d41d3fd917319d955b33[+] PoW solved! XXXX = s4zb...[*] --- Starting Round 11/25 ---[*] Starting new round with Parity Code query set...[!] GIFT RECEIVED: Here is a gift for you: NDM0MTUyMmQzMTM1ZTdhYTgxZTU4N2JiZTZhZGE1ZTY5ZWFhMmRlNzgzYmRlNzgxYWJlNTljYjBlNWI4YTYyZDM2NDg1MjQ4NTQ0ZTQzMzA0MjM0Mzk0YzRmNDg1NjQzMzMzMDUzMzgzNw[+] Found unique solution: [True, True, True, True, False, True, True, False]...[+] All rounds completed! Receiving flag...[*] Switching to interactive modeThe prisoner scowls as you expose his lies. 'Very well, ask your next round of questions then.'The prisoner slumps in defeat: 'Alright, you win! I'll tell you everything.' He confesses all his secrets and reveals the hidden location of flag{42b7aa34-00c7-4c4a-88c2-c91e9ee9b315}' As he signs the confession, you notice a coded message hidden in his handwriting that leads you to the ultimate prize.[*] Interrupted[*] Closed connection to 39.106.45.147 port 39009

FLAG

text
flag{42b7aa34-00c7-4c4a-88c2-c91e9ee9b315}

Personal Vault

Challenge

My friend created a vault for each process, unfortunately we haven’t contacted for years, and this vault thing crashed my pc when I tried checking other’s secret? Please help me with this

附件下载 提取码(GAME)

Solution

非预期

qwb2025-6

FLAG

text
flag{personal_vault_seems_a_little_volatile_innit}

Reverse

butterfly

Challenge

(空)

Solution

入口链路与主函数定位

  • 入口链路:start → __startup_libc_wrapper(0x4041B0) → cpu_feature_init_ifunc(0x403200) → call_main_trampoline(0x4021F0) → main(0x4018D0)
  • 通过字符串与调用关系可见 main 打印 Usage/Encoding/Encoded size/%s.key 等信息,确认其为核心逻辑。

main@0x4018D0(核心流程)

c
// 参数检查if (argc != 3) {  printf("Usage: %s <input_file> <output_file>\n", argv[0]);  printf("Example: %s plaintext.txt encoded.dat\n", argv[0]);  return 1;} in = fopen(argv[1], "rb");              // sub_405540fseek(in, 0, SEEK_END);                  // sub_407480(..., 2)n = ftell_like(in);                      // sub_405640fseek(in, 0, SEEK_SET);                  // sub_407480(..., 0) buf = malloc(n + 8);                     // sub_412620read_n = fread_like(buf, 1, n, in);      // sub_41CC80fclose_like(in);                         // sub_405180 if (read_n != n) error("File read failed"); // 关键:MMX 块变换(逐 8 字节)mmx_loop(buf, n, key8 = first8bytes_of_key_material); // 写出编码结果ok = write_file(argv[2], buf, n);        // sub_401CA0if (ok) {  // 生成 key 文件名并写出 32 字节 key 材料  snprintf("%s.key", argv[2]);           // sub_4777A0  write_file(key_path, key_material_32, 32); // sub_401CA0} free(buf);                               // sub_412CF0return 0;

MMX 编码循环的关键指令(0x401A49—0x401A73):

asm
movq mm0, [rax]        ; 加载 8 字节数据块movq mm1, [rsp+var_138]; 加载 8 字节 key(由 "MMXEncode2024" 派生/缓存)pxor mm0, mm1          ; x ^= key8movq mm2, mm0psllw mm2, 8           ; mm2 = x << 8(以 16-bit lane 为单位)psrlw mm0, 8           ; mm0 = x >> 8(以 16-bit lane 为单位)por  mm0, mm2          ; x = swap16(x)(每 16 位内交换高低字节)movq mm2, mm0psllq mm0, 1           ; x = rol1(x)(64 位整体左移1位)psrlq mm2, 3Fh         ; 取原最低位做循环por  mm0, mm2          ; 合成循环左移paddb mm0, mm1         ; x += key8(按字节求和 mod 256)movq [rax], mm0        ; 写回

结论(每个 8 字节块 x → y):

  • y = add8( rol1( swap16( x XOR key8 ) ), key8 )
  • 尾余(<8 字节)未进入 MMX 循环,保持原样。

sub_401CA0(写文件封装)

c
int write_file(const char* path, const void* data, size_t n) {  f = fopen(path, "wb");             // sub_405540  if (!f) { log("Error: Cannot create file %s"); return 0; }  written = fwrite_like(data, 1, n, f); // sub_440140(..., f)  fclose_like(f);                    // sub_405180  if (written != n) { log("Error: File write failed"); return 0; }  return 1;}

功能:安全写文件并校验长度;失败路径打印错误。

sub_405540(fopen 包装)

伪代码要点:

  • sub_412620 分配 FILE 结构;sub_40BF90 初始化;sub_407830 做额外设置。
  • sub_407E10(v3, a1, “rb”/“wb”, 1) 实际为 _IO_new_file_fopen 路径。
  • 成功则返回已初始化的文件对象指针;失败则清理并返回 0。

功能:glibc _IO 层封装的 fopen-like。

sub_405640(获取长度/定位配合)

  • 在互斥/线程本地存储保护下调用 sub_4057E0(a1, 0, 1, 0) 等,配合 fseek/ftell 语义。
  • 返回“当前位置或大小”,与 main 的两次 sub_407480(…2/0) 配合可得文件长度。

功能:获取“文件长度”或“当前位置”的封装。

sub_407480(文件定位封装)

  • 在锁保护下更新流的 owner/tid 与递归计数。
  • 最终调用 sub_4057E0(a1, a2, n2, 3),n2=2/0 分别对应 SEEK_END/SEEK_SET。
    功能:fseek/rewind 等价封装。

sub_41CC80(读取封装)

c
// 溢出与区间检查if (n != 0 && a3 != 0 && (n*a3 overflows || n7_6 < n*a3)) sub_41CB80(...); // 读前加锁/进入 tcache/arena 体系// ...nread_total = sub_40B820(stream, buf, n*a3, ...); // 实质 fread// 读后解锁/递归计数回退return nread_total == n*a3 ? n : nread_total / a3;

功能:带线程/arena 管理的 fread 等价封装(静态 glibc 影子实现)。

python
import sysfrom pathlib import Path def ror64_1_le(block8: bytes) -> bytes:    # rotate-right by 1 bit on the 64-bit little-endian value    v = int.from_bytes(block8, 'little')    v = ((v >> 1) | ((v & 1) << 63)) & ((1 << 64) - 1)    return v.to_bytes(8, 'little') def swap_each_16bit_bytes(b: bytes) -> bytes:    # swap bytes within each 16-bit lane: [b0 b1][b2 b3]... -> [b1 b0][b3 b2]...    ba = bytearray(b)    for i in range(0, len(ba), 2):        if i + 1 < len(ba):            ba[i], ba[i+1] = ba[i+1], ba[i]    return bytes(ba) def per_byte_sub(a: bytes, key8: bytes) -> bytes:    return bytes(((a[i] - key8[i % 8]) & 0xFF) for i in range(len(a))) def per_byte_xor(a: bytes, key8: bytes) -> bytes:    return bytes((a[i] ^ key8[i % 8]) for i in range(len(a))) def decrypt_block(block8: bytes, key8: bytes) -> bytes:    # Encryption: c = paddb( rol64( swap16( x ^ key )), key )    # Decryption: x = ( swap16( ror64( c - key )) ) ^ key    t1 = per_byte_sub(block8, key8)    t2 = ror64_1_le(t1)    t3 = swap_each_16bit_bytes(t2)    plain = per_byte_xor(t3, key8)    return plain def load_key8(key_path: Path) -> bytes:    # Try to read first 8 bytes from key file, fallback to b"MMXEncod"    fallback = b"MMXEncode2024"[:8]    try:        data = key_path.read_bytes()        if len(data) >= 8:            return data[:8]    except Exception:        pass    return fallback def main():    enc_path = Path("encode.dat")    key_path = Path("encode.dat.key")    if not enc_path.exists():        print("encode.dat not found", file=sys.stderr)        sys.exit(1)     key8 = load_key8(key_path)     enc = enc_path.read_bytes()    out = bytearray()    n = len(enc)    # process full 8-byte blocks    full_blocks = (n // 8)    for i in range(full_blocks):        blk = enc[i*8:(i+1)*8]        out.extend(decrypt_block(blk, key8))    # tail bytes remain unchanged (encrypter仅对满8字节块处理)    tail = enc[full_blocks*8:]    if tail:        out.extend(tail)     # print to stdout (binary-safe)    sys.stdout.buffer.write(bytes(out)) if __name__ == "__main__":    main()

FLAG

text
flag{butter_fly_mmx_encode_7778167}