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:
-
逆·Step 6: 异或 $(i \times i) % 256$。
-
逆·Step 5: 再次每 5 个一组进行翻转 (Reverse)。
-
逆·Step 4: 加上 (Add)
MAGIC_SUB(记得模 256)。 -
逆·Step 3: 再次交换 (Swap) 相邻字节。
-
逆·Step 2: 向右旋转 (Rotate Right)。旋转次数查表
ROTATION_PATTERN。 -
逆·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 开始倒推:
-
逆·Step 6: 异或
(i * i + i) % 256。 -
逆·Step 5: 每 6 个一组,再次翻转 (Reverse)。
-
逆·Step 4: 加法 (Add)
0x93(注意 mod 256)。- (正向是减,逆向就是加)
-
逆·Step 3: 交换 (Swap) 相邻字节。
-
逆·Step 2: 向右旋转 (Rotate Right)。
- 旋转位数查表
rotationPattern[i % 8]。
- 旋转位数查表
-
逆·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
我们先分析一下题目给的信息:
-
Hash 类型:提供的 Hash
3a52fc83037bd2cb81c5a04e49c048a2是 32 位的十六进制字符。这通常是 MD5 加密的标志。 -
密码结构:题目说密码是“Leaked Password” + “Two Random Digits”。
-
这意味着密码格式是:
[单词] + [00-99]。 -
例如:
password01,sunshine99,admin12。
-
-
字典来源:题目提到的 “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)变来的。
第二步:把文件变成数字串
-
读取
encrypted文件。 -
把它转成一个巨大的整数。
-
把这个大整数拆成一串 0 到 8 之间的小数字(九进制拆分)。
- 关键点:你要保证拆出来的顺序是 [最外层, 次外层, … 最内层]。
第三步:从两头往中间“填空” (核心步骤)
现在你有一串数字(比如 [A, B, C...]),你要还原原来的 Flag(三进制数组)。
准备一个足够长的空数组 [ _ , _ , _ , _ , _ , _ ]。
-
拿第一个数字 A(对应最外层):
-
查反向表,A 变成了
(左1, 右1)。 -
把
左1填到数组最左边。 -
把
右1填到数组最右边。 -
数组变成:
[左1, _ , _ , _ , _ , 右1]。
-
-
拿第二个数字 B(对应次外层):
-
查反向表,B 变成了
(左2, 右2)。 -
把
左2填到数组次左边。 -
把
右2填到数组次右边。 -
数组变成:
[左1, 左2, _ , _ , 右2, 右1]。
-
-
重复这个过程,直到中间的空都被填满。
第四步:转换回人话
现在你的数组里已经填满了三进制数字 [0, 1, 2, 2, 1...]。
-
把这个三进制数组算回十进制整数。
-
把整数转成字符(ASCII)。
-
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}