Featured image of post PatriotCTF2025逆向、密码WP

PatriotCTF2025逆向、密码WP

包含五道逆向和前两道密码

patriot CTF 2025

re

space pirates

下载附件,得到challenge.c,查看源码,先把有用的部分提取出来,祛除干扰的代码:

#include <stdint.h>
#include <string.h>

// ==========================================================
// 1. 核心常量与数据区
// ==========================================================

// 最终要达到的目标(密文)。
// 我们的任务是找到一个输入,经过处理后变成这串数字。
#define FLAG_LEN 30
const uint8_t TARGET[FLAG_LEN] = {
    0x5A,0x3D,0x5B,0x9C,0x98,0x73,0xAE,0x32,0x25,0x47,
    0x48,0x51,0x6C,0x71,0x3A,0x62,0xB8,0x7B,0x63,0x57,
    0x25,0x89,0x58,0xBF,0x78,0x34,0x98,0x71,0x68,0x59
};

// 加密用的密钥(5字节),循环使用
const uint8_t XOR_KEY[5] = {0x42, 0x73, 0x21, 0x69, 0x37};

// 加法偏移量
const uint8_t MAGIC_ADD = 0x2A; // 十进制 42

// ==========================================================
// 2. 加密核心逻辑 (假设 buffer 里已经是你输入的 Flag)
// ==========================================================
void encrypt(uint8_t *buffer) {
    
    // ------------------------------------------------------
    // 第 1 步:循环异或 (XOR Key)
    // ------------------------------------------------------
    // 这里的核心是 i % 5。
    // buffer[0] ^ KEY[0]
    // buffer[1] ^ KEY[1] ...
    // buffer[5] ^ KEY[0] (循环回去了)
    for (int i = 0; i < FLAG_LEN; i++) {
        buffer[i] ^= XOR_KEY[i % 5];
    }

    // ------------------------------------------------------
    // 第 2 步:相邻交换 (Swap Pairs)
    // ------------------------------------------------------
    // 注意步长是 i += 2。
    // 把 [0]和[1] 换位置,把 [2]和[3] 换位置...
    for (int i = 0; i < FLAG_LEN; i += 2) {
        uint8_t temp = buffer[i];
        buffer[i] = buffer[i + 1];
        buffer[i + 1] = temp;
    }

    // ------------------------------------------------------
    // 第 3 步:加法偏移 (Add Constant)
    // ------------------------------------------------------
    // 每一个字节都加上 0x2A。
    // % 256 意味着如果结果超过 255,就只取后8位(溢出截断)。
    for (int i = 0; i < FLAG_LEN; i++) {
        buffer[i] = (buffer[i] + MAGIC_ADD) % 256;
    }

    // ------------------------------------------------------
    // 第 4 步:位置异或 (XOR Position)
    // ------------------------------------------------------
    // 这一步混淆了位置信息。
    // 第 0 个字节 ^ 0,第 10 个字节 ^ 10。
    for (int i = 0; i < FLAG_LEN; i++) {
        buffer[i] ^= i;
    }
}

// ==========================================================
// 3. 验证逻辑
// ==========================================================
// 如果加密后的 buffer 和 TARGET 完全一样,说明你的输入是对的。
// memcmp(buffer, TARGET, FLAG_LEN) == 0

流程为:

  • 输入: X (未知)

  • Step 1: A = X ^ 0x42 (异或 Key[0])

  • Step 2: B = Swap(A) (假设它被换到了某个位置,或者有什么东西换到了它的位置)

  • Step 3: C = (B + 0x2A) % 256

  • Step 4: D = C ^ 0 (异或它的索引 0)

  • 结果: D 必须等于 TARGET[0] (即 0x5A)

我们的目的就是解出这个方程。

python脚本:

def solve():
    # 1. 准备原始数据 (TARGET)
    TARGET = [
        0x5A, 0x3D, 0x5B, 0x9C, 0x98, 0x73, 0xAE, 0x32, 0x25, 0x47, 
        0x48, 0x51, 0x6C, 0x71, 0x3A, 0x62, 0xB8, 0x7B, 0x63, 0x57, 
        0x25, 0x89, 0x58, 0xBF, 0x78, 0x34, 0x98, 0x71, 0x68, 0x59
    ]

    XOR_KEY = [0x42, 0x73, 0x21, 0x69, 0x37]
    MAGIC_ADD = 0x2A
    FLAG_LEN = 30

    # 将 TARGET 复制到 buffer,开始逆向操作
    buffer = list(TARGET)

    print("[+] 开始逆向解密...")

    # -------------------------------------------------
    # STEP 1: 逆向 [4/4] XOR with Position
    # -------------------------------------------------
    # 正向逻辑:buffer[i] ^= i
    # 逆向逻辑:同上
    for i in range(FLAG_LEN):
        buffer[i] ^= i

    # -------------------------------------------------
    # STEP 2: 逆向 [3/4] Add Magic Constant
    # -------------------------------------------------
    # 正向逻辑:buffer[i] = (buffer[i] + MAGIC_ADD) % 256
    # 逆向逻辑:buffer[i] = (buffer[i] - MAGIC_ADD) % 256
    for i in range(FLAG_LEN):
        buffer[i] = (buffer[i] - MAGIC_ADD) & 0xFF  # & 0xFF 确保限制在 0-255 之间

    # -------------------------------------------------
    # STEP 3: 逆向 [2/4] Swap Adjacent Pairs
    # -------------------------------------------------
    # 正向逻辑:交换 i 和 i+1
    # 逆向逻辑:同上
    for i in range(0, FLAG_LEN, 2):
        temp = buffer[i]
        buffer[i] = buffer[i+1]
        buffer[i+1] = temp

    # -------------------------------------------------
    # STEP 4: 逆向 [1/4] XOR with Rotating Key
    # -------------------------------------------------
    # 正向逻辑:buffer[i] ^= XOR_KEY[i % 5]
    # 逆向逻辑:同上
    for i in range(FLAG_LEN):
        buffer[i] ^= XOR_KEY[i % 5]

    # -------------------------------------------------
    # 输出结果
    # -------------------------------------------------
    flag = ''.join(chr(b) for b in buffer)
    print(f"Flag 结果: {flag}")

if __name__ == "__main__":
    solve()

得到flag:PCTF{0x_M4rks_tH3_sp0t_M4t3ys}

space pirates 2

下载附件得到main.rs,查看源代码,祛除干扰项,得到:

// ==========================================================
// 1. 核心数据区
// ==========================================================

// 最终目标 (Target),长度是 32 字节 (上次是 30)
const TARGET: [u8; 32] = [
    0x15, 0x5A, 0xAC, 0xF6, 0x36, 0x22, 0x3B, 0x52, 
    0x6C, 0x4F, 0x90, 0xD9, 0x35, 0x63, 0xF8, 0x0E, 
    0x02, 0x33, 0xB0, 0xF1, 0xB7, 0x69, 0x42, 0x67, 
    0x25, 0xEA, 0x96, 0x63, 0x1B, 0xA7, 0x03, 0x0B
];

// 新的 XOR 密钥 (5字节)
const XOR_KEY: [u8; 5] = [0x7E, 0x33, 0x91, 0x4C, 0xA5];

// 【新机制】旋转模式数组 (7个数字)
// 用来决定每个字节向左旋转多少位
const ROTATION_PATTERN: [u32; 7] = [1, 3, 5, 7, 2, 4, 6];

// 魔法常数 (这次是做减法用的)
const MAGIC_SUB: u8 = 0x5D;

// ==========================================================
// 2. 加密核心流水线
// ==========================================================
// 假设 buffer 里装的是你输入的 Flag
fn encrypt_logic(buffer: &mut [u8]) {

    // ------------------------------------------------------
    // 第 1 步:量子纠缠 V2 (XOR Key)
    // ------------------------------------------------------
    // 逻辑不变:还是循环使用 5 个 Key 进行异或
    for (i, byte) in buffer.iter_mut().enumerate() {
        *byte ^= XOR_KEY[i % 5];
    }

    // ------------------------------------------------------
    // 第 2 步:星际旋转 (Rotate Left) 【难点】
    // ------------------------------------------------------
    // 这里的 "Rotate Left" 不是普通的移位,而是“循环移位”。
    // 比如二进制 10000001,左旋1位变成 00000011 (最左边的1跑到了最右边)
    // 旋转的位数由 ROTATION_PATTERN[i % 7] 决定
    for (i, byte) in buffer.iter_mut().enumerate() {
        let rotation = ROTATION_PATTERN[i % 7];
        *byte = rotate_left(*byte, rotation); 
    }

    // ------------------------------------------------------
    // 第 3 步:空间转置 (Swap Pairs)
    // ------------------------------------------------------
    // 逻辑不变:相邻两个字节互换位置
    // (0,1) 互换, (2,3) 互换...
    for i in (0..buffer.len()).step_by(2) {
        buffer.swap(i, i + 1);
    }

    // ------------------------------------------------------
    // 第 4 步:重力偏移 V2 (Subtraction) 【注意!】
    // ------------------------------------------------------
    // 这次是减法!每个字节减去 MAGIC_SUB (0x5D)。
    // 同样要注意 mod 256 (在 Rust 里叫 wrapping_sub,防止负数崩溃)
    for byte in buffer.iter_mut() {
        *byte = byte.wrapping_sub(MAGIC_SUB);
    }

    // ------------------------------------------------------
    // 第 5 步:时间逆转 (Reverse Chunks) 【新机制】
    // ------------------------------------------------------
    // 它把 32 个字节切成了几块,每块 5 个字节。
    // 然后把这 5 个字节的顺序完全颠倒。
    // 例子:输入 [A, B, C, D, E] -> 变成 [E, D, C, B, A]
    const CHUNK_SIZE: usize = 5;
    for chunk_start in (0..buffer.len()).step_by(CHUNK_SIZE) {
        // 处理每一块的翻转
        buffer[chunk_start..chunk_end].reverse();
    }

    // ------------------------------------------------------
    // 第 6 步:坐标校准 V2 (XOR Position Squared)
    // ------------------------------------------------------
    // 这次不是异或 i,而是异或 (i * i)。
    // 同样要模 256 保证它是个单字节数字。
    for (i, byte) in buffer.iter_mut().enumerate() {
        let position_squared = ((i * i) % 256) as u8;
        *byte ^= position_squared;
    }
}

从后往前推:

我们要从 TARGET 出发,倒着走回 Flag:

  1. 逆·Step 6: 异或 $(i \times i) % 256$。

  2. 逆·Step 5: 再次每 5 个一组进行翻转 (Reverse)

  3. 逆·Step 4: 加上 (Add) MAGIC_SUB (记得模 256)。

  4. 逆·Step 3: 再次交换 (Swap) 相邻字节。

  5. 逆·Step 2: 向右旋转 (Rotate Right)。旋转次数查表 ROTATION_PATTERN

  6. 逆·Step 1: 异或 XOR_KEY

python脚本:

def solve_level2():
    # ==========================================
    # 1. 提取题目中的常量
    # ==========================================
    TARGET = [
        0x15, 0x5A, 0xAC, 0xF6, 0x36, 0x22, 0x3B, 0x52, 0x6C, 0x4F, 
        0x90, 0xD9, 0x35, 0x63, 0xF8, 0x0E, 0x02, 0x33, 0xB0, 0xF1, 
        0xB7, 0x69, 0x42, 0x67, 0x25, 0xEA, 0x96, 0x63, 0x1B, 0xA7, 
        0x03, 0x0B
    ]

    XOR_KEY = [0x7E, 0x33, 0x91, 0x4C, 0xA5]
    ROTATION_PATTERN = [1, 3, 5, 7, 2, 4, 6]
    MAGIC_SUB = 0x5D

    # 复制 TARGET 到 buffer
    buffer = list(TARGET)
    length = len(buffer)

    print("[+] 开始逆向 Level 2 解密...")

    # ==========================================
    # 2. 执行逆向操作 (步骤 6 -> 1)
    # ==========================================

    # ------------------------------------------
    # STEP 6 (Inverse): Coordinate Calibration
    # 正向: XOR (i*i)%256
    # 逆向: 同上
    # ------------------------------------------
    for i in range(length):
        pos_sq = (i * i) % 256
        buffer[i] ^= pos_sq

    # ------------------------------------------
    # STEP 5 (Inverse): Temporal Inversion
    # 正向: 每5个字节一组翻转
    # 逆向: 再次翻转即可复原
    # ------------------------------------------
    CHUNK_SIZE = 5
    for i in range(0, length, CHUNK_SIZE):
        # Python 切片翻转技巧
        chunk_end = min(i + CHUNK_SIZE, length)
        buffer[i:chunk_end] = buffer[i:chunk_end][::-1]

    # ------------------------------------------
    # STEP 4 (Inverse): Gravitational Shift
    # 正向: 减去 MAGIC_SUB (0x5D)
    # 逆向: 加上 MAGIC_SUB (注意 % 256)
    # ------------------------------------------
    for i in range(length):
        buffer[i] = (buffer[i] + MAGIC_SUB) & 0xFF

    # ------------------------------------------
    # STEP 3 (Inverse): Spatial Transposition
    # 正向: 交换相邻对
    # 逆向: 再次交换
    # ------------------------------------------
    for i in range(0, length, 2):
        if i + 1 < length:
            buffer[i], buffer[i+1] = buffer[i+1], buffer[i]

    # ------------------------------------------
    # STEP 2 (Inverse): Stellar Rotation
    # 正向: Rotate Left by pattern
    # 逆向: Rotate Right by pattern
    # ------------------------------------------
    # 定义循环右移函数 (8位)
    def ror(val, n):
        n = n % 8
        return ((val >> n) | (val << (8 - n))) & 0xFF

    for i in range(length):
        rotation = ROTATION_PATTERN[i % 7]
        buffer[i] = ror(buffer[i], rotation)

    # ------------------------------------------
    # STEP 1 (Inverse): Quantum Cipher
    # 正向: XOR Key
    # 逆向: 同上
    # ------------------------------------------
    for i in range(length):
        buffer[i] ^= XOR_KEY[i % 5]

    # ==========================================
    # 3. 输出结果
    # ==========================================
    try:
        flag = ''.join(chr(b) for b in buffer)
        print(f"\n[SUCCESS] Flag: {flag}")
    except ValueError:
        print("解码失败,含有不可打印字符。")

if __name__ == "__main__":
    solve_level2()

space pirates 3

下载附件,是go语言的,同样先祛除无关代码,理清逻辑:

package main

// ==========================================================
// 1. 核心数据区
// ==========================================================

// 最终目标 (Target),长度是 30 字节
// 我们的任务:输入 Flag -> 经过 6 步处理 -> 变成这串数字
var target = [30]byte{
    0x60, 0x6D, 0x5D, 0x97, 0x2C, 0x04, 0xAF, 0x7C, 0xE2, 0x9E, 
    0x77, 0x85, 0xD1, 0x0F, 0x1D, 0x17, 0xD4, 0x30, 0xB7, 0x48, 
    0xDC, 0x48, 0x36, 0xC1, 0xCA, 0x28, 0xE1, 0x37, 0x58, 0x0F,
}

// 海盗王的 XOR 密钥 (注意:这次是 7 个字节!)
// 之前分别是 5 个,这次用 7 是为了增加循环的不规律性
var xorKey = [7]byte{0xC7, 0x2E, 0x89, 0x51, 0xB4, 0x6D, 0x1F}

// 旋转模式数组 (8个数字)
// 决定每个字节向左旋转多少位
var rotationPattern = [8]uint{7, 5, 3, 1, 6, 4, 2, 0}

// 减法常数 (这次是 0x93)
const magicSub byte = 0x93

// 分块大小 (这次变成了 6 !)
const chunkSize = 6

// ==========================================================
// 2. 辅助函数:左旋转 (Rotate Left)
// ==========================================================
// Go 语言里手动实现的循环移位。
// 比如二进制 10000001 左旋1位 -> 00000011
func rotateLeft(b byte, n uint) byte {
    n = n % 8 
    // 核心位运算原理:
    // (b << n) 把左边部分移过去,(b >> (8-n)) 把溢出的部分从右边补回来
    return (b << n) | (b >> (8 - n))
}

// ==========================================================
// 3. 加密核心流水线 (假设 buffer 是你的输入)
// ==========================================================
func processVault(buffer []byte) {

    // ------------------------------------------------------
    // 第 1 步:终极量子纠缠 (XOR Key)
    // ------------------------------------------------------
    // 逻辑:buffer[i] ^ key[i % 7]
    // 用 7 字节的 Key 循环异或
    for i := range buffer {
        buffer[i] ^= xorKey[i%len(xorKey)]
    }

    // ------------------------------------------------------
    // 第 2 步:恒星旋转 V2 (Rotate Left)
    // ------------------------------------------------------
    // 逻辑:每个字节向左旋转 rotationPattern[i % 8] 位
    // 这是一个循环查表的过程
    for i := range buffer {
        rotation := rotationPattern[i%len(rotationPattern)]
        buffer[i] = rotateLeft(buffer[i], rotation)
    }

    // ------------------------------------------------------
    // 第 3 步:空间转置 (Swap Pairs)
    // ------------------------------------------------------
    // 逻辑:相邻互换。[0]<->[1], [2]<->[3]
    // i += 2 是步长
    for i := 0; i < len(buffer)-1; i += 2 {
        buffer[i], buffer[i+1] = buffer[i+1], buffer[i]
    }

    // ------------------------------------------------------
    // 第 4 步:重力偏移 V3 (Subtraction)
    // ------------------------------------------------------
    // 逻辑:减法。 buffer[i] = buffer[i] - 0x93
    // Go 的 byte 类型自动处理溢出 (2 - 5 = 253),不需要像 Python 那样手动 % 256
    for i := range buffer {
        buffer[i] -= magicSub
    }

    // ------------------------------------------------------
    // 第 5 步:时间逆转 V2 (Reverse Chunks of 6) 【注意变化】
    // ------------------------------------------------------
    // 逻辑:每 6 个字节一组,进行倒序翻转。
    // 因为 Flag 长度是 30,正好切成 5 块 (30 / 6 = 5),没有余数。
    // Chunk 1: [0-5] -> [5,4,3,2,1,0]
    for chunkStart := 0; chunkStart < len(buffer); chunkStart += chunkSize {
        chunkEnd := chunkStart + chunkSize
        // 这里是标准的 Go 语言切片翻转写法
        for i, j := chunkStart, chunkEnd-1; i < j; i, j = i+1, j-1 {
            buffer[i], buffer[j] = buffer[j], buffer[i]
        }
    }

    // ------------------------------------------------------
    // 第 6 步:坐标校准 V3 (Complex XOR) 【难点升级】
    // ------------------------------------------------------
    // 逻辑:buffer[i] ^= (i * i + i) % 256
    // 这次异或的值是 (位置平方 + 位置)
    for i := range buffer {
        positionValue := ((i * i) + i) % 256
        buffer[i] ^= byte(positionValue)
    }
}

还是一样的配方,还是熟悉的味道。我们从 Step 6 开始倒推:

  1. 逆·Step 6: 异或 (i * i + i) % 256

  2. 逆·Step 5:6 个一组,再次翻转 (Reverse)

  3. 逆·Step 4: 加法 (Add) 0x93 (注意 mod 256)。

    • (正向是减,逆向就是加)
  4. 逆·Step 3: 交换 (Swap) 相邻字节。

  5. 逆·Step 2: 向右旋转 (Rotate Right)

    • 旋转位数查表 rotationPattern[i % 8]
  6. 逆·Step 1: 异或 xorKey[i % 7]

python脚本

def solve_level3():
    # ==========================================
    # 1. 提取题目中的常量 (Go语言版)
    # ==========================================
    TARGET = [
        0x60, 0x6D, 0x5D, 0x97, 0x2C, 0x04, 0xAF, 0x7C, 0xE2, 0x9E, 
        0x77, 0x85, 0xD1, 0x0F, 0x1D, 0x17, 0xD4, 0x30, 0xB7, 0x48, 
        0xDC, 0x48, 0x36, 0xC1, 0xCA, 0x28, 0xE1, 0x37, 0x58, 0x0F
    ]

    # 注意:XOR Key 长度为 7
    XOR_KEY = [0xC7, 0x2E, 0x89, 0x51, 0xB4, 0x6D, 0x1F]

    # 注意:Rotation Pattern 长度为 8
    ROTATION_PATTERN = [7, 5, 3, 1, 6, 4, 2, 0]

    MAGIC_SUB = 0x93
    CHUNK_SIZE = 6  # 注意:Chunk Size 变成了 6

    # 复制 TARGET 到 buffer
    buffer = list(TARGET)
    length = len(buffer)

    print("[+] 开始逆向 Level 3 (Go版) 解密...")

    # ==========================================
    # 2. 执行逆向操作 (步骤 6 -> 1)
    # ==========================================

    # ------------------------------------------
    # STEP 6 (Inverse): Coordinate Calibration V3
    # 正向: XOR ((i*i + i) % 256)
    # 逆向: 同上
    # ------------------------------------------
    for i in range(length):
        val = ((i * i) + i) % 256
        buffer[i] ^= val

    # ------------------------------------------
    # STEP 5 (Inverse): Temporal Inversion V2
    # 正向: 每 6 个字节一组翻转
    # 逆向: 再次翻转即可复原
    # ------------------------------------------
    for i in range(0, length, CHUNK_SIZE):
        chunk_end = min(i + CHUNK_SIZE, length)
        # Python 切片翻转
        buffer[i:chunk_end] = buffer[i:chunk_end][::-1]

    # ------------------------------------------
    # STEP 4 (Inverse): Gravitational Shift V3
    # 正向: 减去 MAGIC_SUB (0x93)
    # 逆向: 加上 MAGIC_SUB (注意 % 256)
    # ------------------------------------------
    for i in range(length):
        buffer[i] = (buffer[i] + MAGIC_SUB) & 0xFF

    # ------------------------------------------
    # STEP 3 (Inverse): Spatial Transposition
    # 正向: 交换相邻对
    # 逆向: 再次交换
    # ------------------------------------------
    for i in range(0, length, 2):
        if i + 1 < length:
            buffer[i], buffer[i+1] = buffer[i+1], buffer[i]

    # ------------------------------------------
    # STEP 2 (Inverse): Stellar Rotation V2
    # 正向: Rotate Left
    # 逆向: Rotate Right
    # ------------------------------------------
    # 定义 8位循环右移函数
    def ror(val, n):
        n = n % 8
        return ((val >> n) | (val << (8 - n))) & 0xFF

    for i in range(length):
        # 注意:Pattern 长度是 8
        rotation = ROTATION_PATTERN[i % 8]
        buffer[i] = ror(buffer[i], rotation)

    # ------------------------------------------
    # STEP 1 (Inverse): Ultimate Quantum Cipher
    # 正向: XOR Key
    # 逆向: 同上
    # ------------------------------------------
    for i in range(length):
        # 注意:Key 长度是 7
        buffer[i] ^= XOR_KEY[i % 7]

    # ==========================================
    # 3. 输出结果
    # ==========================================
    try:
        flag = ''.join(chr(b) for b in buffer)
        print(f"\n[SUCCESS] Flag: {flag}")
    except ValueError:
        print("解码失败,含有不可打印字符。")

if __name__ == "__main__":
    solve_level3()

Are You Pylingual?

下载附件,得到一个pyc文件和一个输出结果的txt文件,pyc文件是python的编译文件,我们先反编译pyc文件为py文件(https://pylingual.io/),得到:

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: pylinguese.py
# Bytecode version: 3.12.0rc2 (3531)
# Source timestamp: 2025-09-06 18:41:22 UTC (1757184082)

import pyfiglet
file = open('flag.txt', 'r')
flag = file.read()
font = 'slant'
words = 'MASONCC IS THE BEST CLUB EVER'
flag_track = 0
art = list(pyfiglet.figlet_format(words, font=font))
i = len(art) % 10
for ind in range(len(art)):
    if ind == i and flag_track < len(flag):
        art[ind] = flag[flag_track]
        i += 28
        flag_track += 1
art_str = ''.join(art)
first_val = 5
second_val = 6
first_half = art_str[:len(art_str) // 2]
second_half = art_str[len(art_str) // 2:]
first = [~ord(char) ^ first_val for char in first_half]
second = [~ord(char) ^ second_val for char in second_half]
output = second + first
print(output)

把这段代码的逻辑拆解开。这是一个非常经典的隐写术 + 简单的加密

1. 准备载体 (ASCII Art)

words = 'MASONCC IS THE BEST CLUB EVER'
art = list(pyfiglet.figlet_format(words, font='slant'))
  • 程序生成了一段 ASCII 艺术字(就是用字符拼成的图案)。

  • art 是一个巨大的字符列表。

2. 藏入 Flag (Steganography)

i = len(art) % 10  # 起始位置
for ind in range(len(art)):
    if ind == i ...:
        art[ind] = flag[flag_track] # 替换原有字符
        i += 28                     # 每隔 28 个字符藏一个
  • Flag 的字符被替换进了艺术字里。

  • 藏匿规律:

    • 起点: 总长度 % 10

    • 步长: 每次跳 28 步。

3. 加密与打乱 (XOR + Swap)

# 切成两半
first_half = art_str[:len(art_str) // 2]
second_half = art_str[len(art_str) // 2:]

# 分别异或不同的数字
first = [ord(char) ^ 5 for char in first_half]
second = [ord(char) ^ 6 for char in second_half]

# 重点:顺序反了!先放后半截,再放前半截
output = second + first
print(output)
  • 切分: 把长长的字符串切成两半。

  • 加密: 前半段异或 5,后半段异或 6。

  • 交换: 输出的时候,把屁股放在了头前面(second + first)。

python 脚本:

# 1. 填入 output.txt 的数据 (从你之前的消息中获取)
data = [-90, -42, -39, -42, -39, -39, -39, -42, -39, -42, -39, -42, -39, -42, -39, -90, -90, -39, -48, -13, -52, -39, -42, -39, -42, -39, -42, -39, -42, -90, -42, -39, -42, -39, -90, -90, -42, -39, -39, -39, -39, -42, -39, -90, -90, -39, -39, -42, -105, -90, -90, -42, -39, -39, -91, -90, -90, -39, -91, -39, -42, -39, -42, -39, -39, -39, -39, -42, -39, -42, -39, -39, -39, -42, -39, -42, -34, -39, -39, -42, -39, -42, -39, -42, -39, -42, -39, -90, -90, -39, -39, -123, -13, -39, -42, -39, -42, -39, -42, -39, -90, -90, -39, -39, -115, -39, -42, -90, -90, -90, -39, -39, -39, -42, -39, -42, -90, -42, -39, -42, -39, -42, -90, -90, -90, -39, -90, -90, -90, -42, -39, -42, -90, -39, -42, -39, -39, -39, -39, -42, -39, -42, -90, -90, -90, -42, -39, -42, -90, -90, -90, -42, -39, -42, -90, -42, -39, -42, -39, -42, -68, -42, -39, -42, -39, -13, -42, -90, -42, -39, -42, -90, -42, -39, -42, -90, -42, -90, -90, -90, -90, -90, -42, -39, -39, -42, -90, -90, -105, -90, -90, -42, -90, -90, -90, -90, -90, -42, -42, -90, -90, -90, -90, -42, -42, -90, -42, -39, -39, -39, -39, -39, -91, -90, -90, -90, -102, -42, -90, -90, -90, -90, -90, -42, -91, -90, -90, -90, -90, -42, -90, -90, -90, -90, -90, -42, -39, -39, -13, -39, -39, -39, -39, -39, -85, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -128, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -119, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -13, -39, -39, -39, -39, -90, -90, -90, -90, -90, -90, -90, -39, -39, -39, -39, -90, -115, -90, -90, -90, -90, -90, -90, -90, -90, -90, -90, -39, -13, -39, -39, -39, -42, -39, -90, -90, -90, -90, -42, -39, -123, -39, -39, -42, -56, -42, -39, -90, -90, -90, -90, -42, -39, -90, -90, -39, -91, -13, -39, -39, -42, -39, -90, -90, -42, -39, -39, -123, -39, -123, -39, -42, -106, -42, -39, -90, -90, -42, -39, -42, -39, -42, -90, -42, -39, -42, -13, -39, -42, -39, -42, -90, -90, -90, -39, -39, -123, -39, -123, -42, -73, -42, -39, -42, -90, -90, -90, -42, -39, -90, -43, -39, -90, -42, -39, -13, -42, -90, -90, -90, -90, -90, -42, -39, -39, -123, -90, -90, -124, -42, -90, -90, -90, -90, -90, -42, -90, -42, -39, -123, -90, -123, -39, -39, -13, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -39, -13, -38, -38, -118, -38, -91, -91, -38, -38, -91, -91, -91, -91, -91, -91, -38, -38, -38, -91, -91, -91, -91, -91, -38, -91, -91, -91, -91, -38, -38, -91, -103, -38, -38, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -38, -38, -38, -91, -91, -91, -91, -91, -91, -91, -91, -114, -16, -38, -38, -38, -43, -38, -38, -122, -43, -38, -38, -43, -38, -38, -38, -122, -38, -43, -38, -91, -91, -91, -43, -43, -38, -91, -91, -100, -90, -43, -38, -122, -38, -43, -38, -43, -38, -91, -91, -91, -91, -43, -38, -91, -91, -91, -91, -43, -38, -38, -43, -38, -38, -91, -43, -127, -91, -91, -91, -43, -16, -38, -38, -43, -38, -43, -122, -91, -43, -38, -43, -38, -43, -122, -38, -122, -38, -90, -91, -91, -38, -90, -43, -107, -43, -38, -43, -38, -43, -38, -38, -122, -43, -38, -43, -38, -43, -38, -38, -38, -43, -38, -43, -38, -38, -38, -38, -38, -38, -38, -43, -104, -43, -38, -90, -91, -91, -38, -90, -38, -16, -38, -43, -38, -43, -38, -38, -43, -38, -43, -38, -91, -91, -91, -38, -122, -91, -91, -91, -68, -38, -43, -38, -43, -91, -43, -38, -43, -38, -43, -122, -38, -38, -43, -38, -43, -91, -91, -91, -43, -38, -43, -91, -91, -91, -38, -38, -113, -91, -43, -38, -43, -38, -91, -91, -91, -43, -38, -43, -38, -16, -43, -91, -43, -38, -38, -43, -91, -43, -91, -43, -38, -38, -122, -91, -119, -91, -91, -91, -91, -43, -90, -91, -91, -91, -91, -43, -91, -43, -38, -122, -91, -43, -90, -91, -91, -91, -91, -43, -90, -91, -91, -91, -103, -43, -38, -38, -43, -91, -91, -91, -43, -43, -91, -91, -91, -91, -43, -38, -38, -16, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -50, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -114, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -38, -16, -38, -38, -91, -91, -91, -91, -109, -91, -91, -91, -38, -38, -91, -91, -91, -91, -91, -91, -91, -91, -38, -38, -38, -91, -91, -91, -91, -38, -38, -91, -91, -91, -91, -91, -54, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -91, -38, -38, -38, -91, -91, -91, -91, -91, -91, -91, -91, -38, -38, -38, -38, -91, -108, -38, -38, -91, -91, -91, -91, -91, -91, -38, -16, -38, -43, -91, -38, -38, -91, -91, -43, -38, -43, -38, -43, -38, -43, -38, -91, -91, -91, -91, -43, -38, -38, -43, -38, -91, -91, -38, -45, -43, -38, -91, -91, -91, -91, -43, -38, -91, -91, -91, -43, -91, -38, -38, -91, -91, -109, -38, -38, -43, -38, -91, -91, -91]

def decrypt_val(val, key):
    # 原逻辑: y = ~ord(char) ^ key
    # 逆逻辑: ord(char) = ~(y ^ key)
    return chr(~(val ^ key))

# 2. 拆分数据
# 原代码 output = second + first
# second 是 ASCII 后半段 (用 key 6 加密)
# first 是 ASCII 前半段 (用 key 5 加密)
mid = len(data) // 2
second_part_encrypted = data[:mid]  # 这对应原代码的 variable 'second'
first_part_encrypted = data[mid:]   # 这对应原代码的 variable 'first'

# 3. 解密每一半
# first_half (原图前半部分) 被放在了列表的后面,用的 key 5
first_half_str = "".join([decrypt_val(x, 5) for x in first_part_encrypted])

# second_half (原图后半部分) 被放在了列表的前面,用的 key 6
second_half_str = "".join([decrypt_val(x, 6) for x in second_part_encrypted])

# 4. 拼回完整的 ASCII 艺术字
full_art = first_half_str + second_half_str

print("=== Decrypted ASCII Art ===")
print(full_art)
print("===========================\n")

# 5. 提取 Flag
# 原逻辑: 
# i = len(art) % 10
# 每次 i += 28
# art[i] = flag_char

print("Extracted Flag:")
flag_chars = []
i = len(full_art) % 10
while i < len(full_art):
    flag_chars.append(full_art[i])
    i += 28

print("".join(flag_chars))

Vorpal Masters

下载附件,得到一个license文件,file查看类型:

 file license   
license: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6f5355f7386afc8a69ef3a8c448622b4d46aea62, for GNU/Linux 3.2.0, not stripped

license是64位的elf可执行文件,拖入IDA查看反汇编后的main函数和womp_womp函数(这个函数没啥意义):

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+8h] [rbp-18h] BYREF
  char s1[11]; // [rsp+Ch] [rbp-14h] BYREF
  _BYTE v6[5]; // [rsp+17h] [rbp-9h] BYREF
  int v7; // [rsp+1Ch] [rbp-4h]

  puts("Welcome to {insert game here}\nPlease enter the license key from the 3rd page of the booklet.");
  v7 = __isoc99_scanf("%4s-%d-%10s", v6, &v4, s1);
  if ( v7 != 3 )
  {
    puts("Please enter you key in the format xxxx-xxxx-xxxx");
    exit(0);
  }
  if ( v6[0] != 67 || v6[2] != 67 || v6[3] != 73 || v6[1] != 65 )
    womp_womp();
  if ( v4 < -5000 || v4 > 10000 || (v4 + 22) % 1738 != 6 * (2 * v4 % 2000) + 9 )
    womp_womp();
  if ( strcmp(s1, "PatriotCTF") )
    womp_womp();
  return puts("Lisence key registered, you may play the game now!");
}
void __noreturn womp_womp()
{
  puts("Sorry, that is not a valid license key, please try again.");
  exit(0);
}

分析main函数:

// 主函数入口
int __fastcall main(int argc, const char **argv, const char **envp)
{
  // ================= 定义变量区域 =================
  // 这些 [rsp+...] 是反编译工具告诉你变量在内存栈上的位置,不用太在意
  // 我们只关注类型:
  
  int v4;          // 这是一个整数,用来存注册码中间的那串数字
  char s1[11];     // 这是一个字符数组(最大11字节),用来存注册码最后一段字符串
  _BYTE v6[5];     // 这是一个5字节的数组,用来存注册码第一段字符串
  int v7;          // 这是一个整数,用来存 scanf 函数的返回值(成功读入了几个?)

  // ================= 打印欢迎信息 =================
  puts("Welcome to {insert game here}\nPlease enter the license key from the 3rd page of the booklet.");
  
  // ================= 核心输入逻辑 =================
  // scanf 是从键盘读取输入。
  // 格式字符串 "%4s-%d-%10s" 揭示了注册码的格式:
  // 1. %4s : 读入 4 个字符的字符串 -> 存入 v6
  // 2. -   : 必须有一个减号分隔
  // 3. %d  : 读入一个整数 -> 存入 v4 (注意用了 &v4,因为是整数)
  // 4. -   : 必须有一个减号分隔
  // 5. %10s: 读入 10 个字符的字符串 -> 存入 s1
  v7 = __isoc99_scanf("%4s-%d-%10s", v6, &v4, s1);

  // ================= 格式检查 =================
  // scanf 的返回值 v7 是成功读取的变量个数。
  // 如果没读够 3 个部分(比如你没输中间的数字,或者没输横杠),这里就不等于 3
  if ( v7 != 3 )
  {
    puts("Please enter you key in the format xxxx-xxxx-xxxx");
    exit(0); // 直接退出程序
  }

  // ================= 第一段检查 (v6) =================
  // 这里检查第一段那 4 个字符是不是特定的字母。
  // 它是通过 ASCII 码来对比的:
  // 67 = 'C', 65 = 'A', 73 = 'I'
  // 注意逻辑:如果是“或 (||)”的关系,只要有一个对不上,就报错。
  // 所以这一段必须满足:v6[0]='C', v6[1]='A', v6[2]='C', v6[3]='I'
  if ( v6[0] != 67 || v6[2] != 67 || v6[3] != 73 || v6[1] != 65 )
    womp_womp(); // 失败!打印错误信息并退出

  // ================= 第二段检查 (v4) =================
  // 这里检查中间那个数字 v4。
  // 1. 范围检查:必须大于等于 -5000 且 小于等于 10000
  // 2. 数学方程检查:(v4 + 22) % 1738 必须等于 6 * (2 * v4 % 2000) + 9
  // 只要这三个条件有任何一个不满足(用了 ||),就失败。
  if ( v4 < -5000 || v4 > 10000 || (v4 + 22) % 1738 != 6 * (2 * v4 % 2000) + 9 )
    womp_womp(); // 失败!

  // ================= 第三段检查 (s1) =================
  // strcmp 是字符串比较函数。
  // 如果 s1 和 "PatriotCTF" 相等,strcmp 返回 0。
  // C语言里,0 代表 False。if(0) 就不会进去。
  // 如果 s1 不相等,strcmp 返回非 0 值,if 条件成立,就会执行 womp_womp。
  // 结论:第三段必须是 "PatriotCTF"
  if ( strcmp(s1, "PatriotCTF") )
    womp_womp(); // 失败!

  // ================= 成功! =================
  // 如果上面所有的 womp_womp 都没执行,就会走到这里。
  return puts("Lisence key registered, you may play the game now!");
}

python脚本:

# 模拟 C 语言的取模行为 (主要针对负数)
def c_mod(a, b):
    return int(a - int(a / b) * b)

def solve():
    # 遍历范围 -5000 到 10000
    for v4 in range(-5000, 10001):
        # 计算等式左边: (v4 + 22) % 1738
        lhs = c_mod((v4 + 22), 1738)

        # 计算等式右边: 6 * (2 * v4 % 2000) + 9
        # 注意优先级: 2 * v4 先计算,然后对 2000 取模
        rhs = 6 * c_mod((2 * v4), 2000) + 9

        if lhs == rhs:
            print(f"找到 Key 的第二部分 v4: {v4}")
            return

if __name__ == "__main__":
    solve()

得到flag:CACI{CACI-2025-PatriotCTF}

cry

password palooza

我们先分析一下题目给的信息:

  1. Hash 类型:提供的 Hash 3a52fc83037bd2cb81c5a04e49c048a232 位的十六进制字符。这通常是 MD5 加密的标志。

  2. 密码结构:题目说密码是“Leaked Password” + “Two Random Digits”。

    • 这意味着密码格式是:[单词] + [00-99]

    • 例如:password01, sunshine99, admin12

  3. 字典来源:题目提到的 “Popular password breach” 在 CTF 中通常指代 rockyou.txt(这是世界上最著名的泄露密码字典)。

在kali下载wordlists

sudo apt install wordlists
sudo gzip -d /usr/share/wordlists/rockyou.txt.gz

python脚本:

import hashlib
import os
import sys

# 目标哈希
TARGET_HASH = "3a52fc83037bd2cb81c5a04e49c048a2"

# 字典路径 (Kali 默认路径)
DICT_PATH = "/usr/share/wordlists/rockyou.txt"

def crack():
    print(f"[*] 目标 Hash: {TARGET_HASH}")
    print(f"[*] 正在尝试加载字典: {DICT_PATH}")

    if not os.path.exists(DICT_PATH):
        print(f"[-] 错误: 找不到文件 {DICT_PATH}")
        print("[-] 请先执行: sudo gzip -d /usr/share/wordlists/rockyou.txt.gz")
        return

    try:
        # RockYou 包含非 UTF-8 字符,必须用 latin-1 读取或 ignore 错误
        with open(DICT_PATH, "r", encoding="latin-1") as f:
            count = 0
            print("[*] 开始爆破 (模式: 字典词 + 2位数字)...")

            for line in f:
                word = line.strip()
                count += 1

                # 每处理 100万个词提示一次进度
                if count % 1000000 == 0:
                    print(f"    已扫描 {count} 个基础词汇...")

                # 题目要求:基础密码 + 2位随机数字 (00-99)
                for i in range(100):
                    suffix = f"{i:02d}" # 生成 00, 01 ... 99
                    candidate = word + suffix

                    # 计算 MD5
                    digest = hashlib.md5(candidate.encode()).hexdigest()

                    if digest == TARGET_HASH:
                        print("\\n" + "="*50)
                        print(f"SUCCESS! 破解成功!")
                        print(f"Hash: {TARGET_HASH}")
                        print(f"Password: {candidate}")
                        print(f"Flag: pctf{{{candidate}}}")
                        print("="*50)
                        return

        print("[-] 字典跑完了,未找到密码。")

    except Exception as e:
        print(f"[-] 发生错误: {e}")

if __name__ == "__main__":
    crack()

flag:pctf{mr.krabbs57}

Cipher from Hell

下载附件,一个py文件,还有一个data文件(file查看),分析py代码:

import math, sys

# 1. 获取输入
# 这里的 input 是原本的 Flag 字符串
inp = input("Enter your flag... ").encode()

# 2. 字节转整数 (Base-256 -> Integer)
# 把整个 Flag 字符串当成一个巨大的十进制整数 s
# 例如 "ab" (0x6162) 会变成整数 24930
s = int.from_bytes(inp, 'big') # Python 3 默认这里应该是指明字节序,虽然原题没写,默认大端

# 3. 定义加密字典 (Substitution Matrix)
# 这是一个 3x3 的矩阵,里面的数字刚好是 0-8 (九进制的一位数)
# 这个表的作用是:把两个三进制位 (0,1,2) 映射成一个九进制位 (0-8)
o = (
    (6, 0, 7),  # 左位是0时,右位分别为0,1,2对应的结果
    (8, 2, 1),  # 左位是1时...
    (5, 4, 3)   # 左位是2时...
)

# 4. 计算三进制的“长度”
# math.log(s, 3) 计算 s 如果转成三进制,最高位是多少次幂
# 例如 s=26 (三进制 222),log(26,3) ≈ 2.96,floor 后 c=2。代表最高位是 3^2
c = math.floor(math.log(s, 3))

# 5. 长度检查
# 程序逻辑是“首尾配对”,所以三进制的位数必须是偶数。
# 如果最高次幂 c 是偶数 (比如 c=2,位数是 3^2, 3^1, 3^0 共3位),那就是奇数个位,没法两两配对。
# 所以 c 必须是奇数 (代表位数是偶数)。
if not c % 2:
    sys.stderr.write("Error: flag length needs to be even (hint: but in what base system?)!\n")
    sys.exit(1)

ss = 0

# 6. 核心加密循环 (剥洋葱逻辑)
# 只要 c > -1,说明还有三进制位没处理完
while c > -1:
    # ------------------------------------------------
    # 步骤 A: 腾位置 (Base-9 Shift)
    # ss 是最终的密文。每次循环乘以 9,相当于在九进制下左移一位。
    # 这说明:最先处理的数据,会被推到 ss 的最高位。
    ss *= 9
    
    # ------------------------------------------------
    # 步骤 B: 取头取尾查表 (Extract & Substitute)
    # s // 3**c : 获取 s 在三进制下的【最高位】(最左边的数字)
    # s % 3     : 获取 s 在三进制下的【最低位】(最右边的数字)
    # o[高位][低位] : 查表,把这一对数字变成一个 0-8 的数
    ss += o[s//3**c][s%3]
    
    # ------------------------------------------------
    # 步骤 C: 剥皮 (Update s)
    # 现在要去掉已经处理过的头和尾,准备处理下一层
    
    # 1. 去掉最高位:s 减去 (最高位数值 * 它的权重)
    s -= s//3**c * 3**c
    
    # 2. 去掉最低位:s 整除 3 (相当于三进制右移一位,把个位切掉)
    s //= 3
    
    # ------------------------------------------------
    # 步骤 D: 更新计数器
    # 因为我们去掉了最高位(1位)和最低位(1位),总共少了2位
    # 所以最高次幂 c 减小 2
    c -= 2

# 7. 输出密文
# ss 是个巨大的整数 (九进制逻辑拼成的)。
# math.ceil(...) 计算把 ss 存成字节需要多大的空间。
# 最后把 ss 转回 bytes 并写入文件。
open("encrypted", 'wb').write(ss.to_bytes(math.ceil(math.log(ss, 256)), byteorder='big'))

解密步骤如下:

第一步:准备“反向字典”

原题给了一个表(矩阵 o),规则是:给你两个数(左, 右) -> 变成一个数。 需要先把这个表反过来:看到一个数 -> 知道它原本是哪两个数(左, 右)

  • 比如看到 6,你就得知道它是 (0, 0) 变来的。

第二步:把文件变成数字串

  1. 读取 encrypted 文件。

  2. 把它转成一个巨大的整数。

  3. 把这个大整数拆成一串 0 到 8 之间的小数字(九进制拆分)。

    • 关键点:你要保证拆出来的顺序是 [最外层, 次外层, … 最内层]

第三步:从两头往中间“填空” (核心步骤)

现在你有一串数字(比如 [A, B, C...]),你要还原原来的 Flag(三进制数组)。 准备一个足够长的空数组 [ _ , _ , _ , _ , _ , _ ]

  1. 拿第一个数字 A(对应最外层):

    • 查反向表,A 变成了 (左1, 右1)

    • 左1 填到数组最左边

    • 右1 填到数组最右边

    • 数组变成:[左1, _ , _ , _ , _ , 右1]

  2. 拿第二个数字 B(对应次外层):

    • 查反向表,B 变成了 (左2, 右2)

    • 左2 填到数组次左边

    • 右2 填到数组次右边

    • 数组变成:[左1, 左2, _ , _ , 右2, 右1]

  3. 重复这个过程,直到中间的空都被填满。

第四步:转换回人话

现在你的数组里已经填满了三进制数字 [0, 1, 2, 2, 1...]

  1. 把这个三进制数组算回十进制整数。

  2. 把整数转成字符(ASCII)。

  3. Flag 到手!

python脚本:

import math
import sys

def solve():
    print("[*] Reading encrypted file...")
    try:
        with open("encrypted", "rb") as f:
            data = f.read()
    except FileNotFoundError:
        print("[-] Error: 'encrypted' file not found in the current directory.")
        return

    # 1. 将二进制数据转回整数 ss
    ss = int.from_bytes(data, byteorder='big')

    # 2. 建立反向查找表
    # 原表 o: (头, 尾) -> 值
    o = (
        (6, 0, 7),
        (8, 2, 1),
        (5, 4, 3)
    )

    # 反向表 inv_o: 值 -> (头, 尾)
    inv_o = {}
    for r in range(3):
        for c in range(3):
            val = o[r][c]
            inv_o[val] = (r, c)

    # 3. 将 ss 解析为 9进制数字列表
    # ss 是通过 ss * 9 + new_val 生成的,所以我们通过取余和整除来还原
    base9_digits = []
    temp_ss = ss

    if temp_ss == 0:
        base9_digits = [0]
    else:
        while temp_ss > 0:
            base9_digits.append(temp_ss % 9)
            temp_ss //= 9
        # 因为也就是取余得到的是最后加入的(最内层),但加密是先加的最外层(变成高位)
        # 所以我们需要翻转一下,让高位(最外层)在列表前面
        base9_digits.reverse()

    # 4. 重组 3进制 (Trits)
    heads = []
    tails = []

    for digit in base9_digits:
        # 每一个 9进制位 对应 一对(头, 尾)
        h, t = inv_o[digit]
        heads.append(h)
        tails.append(t)

    # 原始的 3进制序列结构是: [头1, 头2, ... 头N, 尾N, ... 尾2, 尾1]
    # 所以 tails 需要翻转拼接到 heads 后面
    full_trits = heads + tails[::-1]

    # 5. 将 3进制列表 转回 整数 s
    s_recovered = 0
    for trit in full_trits:
        s_recovered = s_recovered * 3 + trit

    # 6. 将整数 s 转回 Bytes (Flag)
    # 计算需要的字节数: (bit_length + 7) // 8
    byte_len = (s_recovered.bit_length() + 7) // 8
    flag = s_recovered.to_bytes(byte_len, byteorder='big')

    print(f"[+] Decrypted Flag: {flag.decode(errors='ignore')}")

if __name__ == "__main__":
    solve()

flag:pctf{a_l3ss_cr4zy_tr1tw1s3_op3r4ti0n_f37d4b}

This blog has been running for
Published 11 posts · Total 48.46k words