CDM258's BOX

L3HCTF个人wp

CTF  ·  

## LearnRag

在此感谢谷歌Colab和哈机密ai的大力赞助


flag有些乱 最后团队里大伙一起拼好的。
所谓拼好题就是xxxxx后面不能说了,exp和之前某次比赛差不多,图中就是所有的exp了。

L3HCTF{wowthisisembedding}

TemporalParadox

根据f5报错nop掉一些东西。可以很轻松地找到关键主逻辑 本题tag:时间戳爆破 rand seed 相关知识点23年春秋杯考过了 不过感觉本题是想科普什么md5加盐之类的东西,没懂。 然后动调改跳转把salt搞出来 salt貌似是个什么函数生成 没管( 写个脚本爆破一下 依旧感谢 开虚拟机linux跑一下

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <openssl/md5.h>

#define TMIN   1751990400u
#define TMAX   1752052051u
#define TARGET "8a2fc1e9e2830c37f8a7f51572a640aa"

static uint32_t rng_state;

/* ------------- 线性反馈伪随机,与原算法等价 ------------- */
static inline uint32_t prng_next(void)
{
    uint32_t x = rng_state;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    rng_state = x;
    return x & 0x7fffffffU;
}

/* ------------- 16-byte MD5 → 32-byte lower-hex ------------- */
static void md5_to_hex(const unsigned char *in, char *out)
{
    static const char lut[] = "0123456789abcdef";
    for (int i = 0; i < 16; ++i) {
        uint8_t v = in[i];
        out[i * 2]     = lut[v >> 4];
        out[i * 2 + 1] = lut[v & 0x0f];
    }
    out[32] = '\\\\0';
}

/* ------------- 主程序 ------------- */
int main(void)
{
    unsigned char digest[16];
    char hex[33];
    char query[256];

    for (uint32_t t = TMIN; t <= TMAX; ++t) {

        /* 初始化 PRNG */
        rng_state = t;

        uint32_t a = 0, b = 0, x = 0, y = 0;
        uint32_t loop = prng_next();

        /* 每轮消耗 4 个随机数,完全对齐原逻辑 */
        while (loop--) {
            a = prng_next();
            b = prng_next();
            x = prng_next();
            y = prng_next();
        }
        uint32_t r = prng_next();

        int qlen = snprintf(query, sizeof(query),
            "salt=tlkyeueq7fej8vtzitt26yl24kswrgm5"
            "&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u",
            t, r, a, b, x, y);

        MD5((unsigned char *)query, qlen, digest);
        md5_to_hex(digest, hex);

        if (strcmp(hex, TARGET) == 0) {
            puts(">>> MATCH FOUND! <<<");
            puts(query);
            return 0;
        }
    }

    puts("扫描完毕,未命中目标。");
    return 0;
}

## obfuscate

手动过掉一些反调试 然后跟着调试尝试寻找输入 然后发现一个类似key的东西 WelcometoL3HCTF!,但是函数中key做了复杂的计算 但不涉及输入 同时可以看到% 那么大概率可以猜到这是个KSA结构。那么很显然我们可以直接提取S盒就好了。 s盒:

sbox = [0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F,
        0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51,
        0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936,
        0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE,
        0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A,
        0x913FA548]

跟踪sbox找到加密程序,但是不是RC4,讨论时感觉像RC5/6(把这个信息喂给哈机密ai),哈机密ai梭哈了一下,出来了(赛后队友告诉我是 RC5)

1️⃣ 单块测试
明文: b'09876543'
明文(hex): 3039383736353433
密文(hex): 78f2ca6e7e5c0762
解密结果: bytearray(b'09876543')
解密(hex): 3039383736353433
解密成功: True

2️⃣ 已知密文解密
密文(hex): 1BBBA1F2E97C87218A37FD0A941A81BC401EE3AA732ED83F84B87142CC358B39
解密结果: b'5fd277be39046905ef6348ba89131922'
解密(hex): 3566643237376265333930343639303565663633343862613839313331393232
ASCII解析: '5fd277be39046905ef6348ba89131922'

3️⃣ 长文本测试
原文: b'Hello, this is a test message for Feistel cipher!'
加密后(hex): cbeb26cd874fd2c37e5ded66b52197bbcf2836d3660add7522c77367881ad305148edfe7bf21913a3028888faaf90ff691f1ab74f81d9481
解密后: b'Hello, this is a test message for Feistel cipher!'
长文本解密成功: True

🔬 S盒分析
S盒长度: 26
S盒前8个值: ['0x122f2c9c', '0xe3bccae7', '0xd0ffc0f2', '0xd9a12544', '0x8a27992f', '0x55b1b935', '0x9110b161', '0x92811564']
唯一值数量: 26/26
平均值: 2366628618 (0x8d0fe309)

超高校级密码冠军小课堂

以下是哈机密ai的exp

def a32small(byte_array):
    """字节数组转32位小端整数数组"""
    return [int.from_bytes(byte_array[i:i + 4], byteorder='little', signed=False) for i in range(0, len(byte_array), 4)]

def b32small(dword_array):
    """32位整数数组转字节数组"""
    byte_list = bytearray()
    for dword in dword_array:
        byte_list.extend(dword.to_bytes(4, byteorder='little'))
    return byte_list

# S盒 - 密钥材料
sbox = [0x122F2C9C, 0xE3BCCAE7, 0xD0FFC0F2, 0xD9A12544, 0x8A27992F,
        0x55B1B935, 0x9110B161, 0x92811564, 0x5CE9B359, 0x77C79A51,
        0x4265527A, 0x8AB57C4B, 0x11529FA4, 0x9D9F63FF, 0xA970B936,
        0xC8EABA0D, 0x9A0EB4AA, 0xB0BC6E7F, 0x9784B100, 0x70DCD3AE,
        0x6057A44E, 0x89187658, 0xE00098A8, 0x45773540, 0xF9374F1A,
        0x913FA548]

def ROL32(x, shift):
    """32位循环左移"""
    shift = shift & 31  # 确保shift在0-31范围内
    return ((x << shift) | (x >> (32 - shift))) & 0xFFFFFFFF

def ROR32(x, shift):
    """32位循环右移"""
    shift = shift & 31  # 确保shift在0-31范围内
    return ((x >> shift) | (x << (32 - shift))) & 0xFFFFFFFF

def feistel_encrypt(plaintext_bytes):
    """
    Feistel加密函数
    输入: 8字节明文
    输出: 8字节密文
    """
    # 转换为32位整数对
    m = a32small(plaintext_bytes)

    # 初始化: 加上S盒的前两个值
    num = [(m[0] + sbox[0]) & 0xFFFFFFFF, (m[1] + sbox[1]) & 0xFFFFFFFF]

    # 12轮Feistel变换
    for i in range(1, 13):
        # 第一步: 变换左半部分
        num[0] = (sbox[2 * i] + ROL32(num[0] ^ num[1], num[1] & 31)) & 0xFFFFFFFF

        # 第二步: 变换右半部分
        num[1] = (sbox[2 * i + 1] + ROL32(num[0] ^ num[1], num[0] & 31)) & 0xFFFFFFFF

        # 第三步: 异或交换
        num[0] = (num[0] ^ num[1]) & 0xFFFFFFFF

    # 转换回字节数组
    return b32small(num)

def feistel_decrypt(ciphertext_bytes):
    """
    Feistel解密函数
    输入: 8字节密文
    输出: 8字节明文
    """
    # 转换为32位整数对
    num = a32small(ciphertext_bytes)

    # 12轮逆向Feistel变换
    for i in range(12, 0, -1):
        # 逆向第三步: 异或交换的逆操作
        num[0] = (num[0] ^ num[1]) & 0xFFFFFFFF

        # 逆向第二步: 右半部分的逆变换
        # num[1] = sbox[2*i+1] + ROL32(num[0] ^ num[1], num[0] & 31)
        # 所以: ROL32(num[0] ^ num[1], num[0] & 31) = num[1] - sbox[2*i+1]
        # 因此: num[0] ^ num[1] = ROR32(num[1] - sbox[2*i+1], num[0] & 31)
        temp = (num[1] - sbox[2 * i + 1]) & 0xFFFFFFFF
        num[1] = (ROR32(temp, num[0] & 31) ^ num[0]) & 0xFFFFFFFF

        # 逆向第一步: 左半部分的逆变换
        temp = (num[0] - sbox[2 * i]) & 0xFFFFFFFF
        num[0] = (ROR32(temp, num[1] & 31) ^ num[1]) & 0xFFFFFFFF

    # 逆向初始化: 减去S盒的前两个值
    num = [(num[0] - sbox[0]) & 0xFFFFFFFF, (num[1] - sbox[1]) & 0xFFFFFFFF]

    # 转换回字节数组
    return b32small(num)

def encrypt_multiple_blocks(plaintext_bytes):
    """
    加密多个8字节块
    """
    if len(plaintext_bytes) % 8 != 0:
        # 如果不是8的倍数,进行PKCS7填充
        padding_len = 8 - (len(plaintext_bytes) % 8)
        plaintext_bytes += bytes([padding_len] * padding_len)

    result = bytearray()
    for i in range(0, len(plaintext_bytes), 8):
        block = plaintext_bytes[i:i + 8]
        encrypted_block = feistel_encrypt(block)
        result.extend(encrypted_block)

    return bytes(result)

def decrypt_multiple_blocks(ciphertext_bytes):
    """
    解密多个8字节块
    """
    result = bytearray()
    for i in range(0, len(ciphertext_bytes), 8):
        block = ciphertext_bytes[i:i + 8]
        decrypted_block = feistel_decrypt(block)
        result.extend(decrypted_block)

    # 移除PKCS7填充
    if result:
        padding_len = result[-1]
        if padding_len <= 8 and all(b == padding_len for b in result[-padding_len:]):
            result = result[:-padding_len]

    return bytes(result)

def main():
    """主函数 - 演示加密解密过程"""
    print("🔐 Feistel密码 完整EXP")
    print("=" * 50)

    # 测试1: 单个8字节块
    print("\\\\n1️⃣ 单块测试")
    test_input = b"09876543"
    print(f"明文: {test_input}")
    print(f"明文(hex): {test_input.hex()}")

    # 加密
    encrypted = feistel_encrypt(test_input)
    print(f"密文(hex): {encrypted.hex()}")

    # 解密
    decrypted = feistel_decrypt(encrypted)
    print(f"解密结果: {decrypted}")
    print(f"解密(hex): {decrypted.hex()}")
    print(f"解密成功: {test_input == decrypted}")

    # 测试2: 给定的密文解密
    print("\\\\n2️⃣ 已知密文解密")
    cipher_hex = "1BBBA1F2E97C87218A37FD0A941A81BC401EE3AA732ED83F84B87142CC358B39"
    cipher_bytes = bytearray.fromhex(cipher_hex)
    print(f"密文(hex): {cipher_hex}")

    # 解密多个块
    decrypted_result = decrypt_multiple_blocks(cipher_bytes)
    print(f"解密结果: {decrypted_result}")
    print(f"解密(hex): {decrypted_result.hex()}")

    # 尝试解析为ASCII
    try:
        ascii_result = decrypted_result.decode('ascii', errors='ignore')
        print(f"ASCII解析: '{ascii_result}'")
    except:
        print("无法解析为ASCII")

    # 测试3: 长文本加密解密
    print("\\\\n3️⃣ 长文本测试")
    long_text = b"Hello, this is a test message for Feistel cipher!"
    print(f"原文: {long_text}")

    # 加密
    encrypted_long = encrypt_multiple_blocks(long_text)
    print(f"加密后(hex): {encrypted_long.hex()}")

    # 解密
    decrypted_long = decrypt_multiple_blocks(encrypted_long)
    print(f"解密后: {decrypted_long}")
    print(f"长文本解密成功: {long_text == decrypted_long}")

def crack_known_plaintext(known_pairs):
    """
    已知明文攻击 - 如果有明文密文对,可以验证S盒
    """
    print("\\\\n🎯 已知明文攻击验证")
    for plaintext, ciphertext in known_pairs:
        calculated_cipher = feistel_encrypt(plaintext)
        match = calculated_cipher == ciphertext
        print(f"明文: {plaintext.hex()}")
        print(f"期望密文: {ciphertext.hex()}")
        print(f"计算密文: {calculated_cipher.hex()}")
        print(f"匹配: {match}")
        print("-" * 30)

def analyze_sbox():
    """分析S盒特性"""
    print("\\\\n🔬 S盒分析")
    print(f"S盒长度: {len(sbox)}")
    print(f"S盒前8个值: {[hex(x) for x in sbox[:8]]}")

    # 检查重复值
    unique_count = len(set(sbox))
    print(f"唯一值数量: {unique_count}/{len(sbox)}")

    # 检查值的分布
    avg_val = sum(sbox) / len(sbox)
    print(f"平均值: {avg_val:.0f} (0x{int(avg_val):08x})")

if __name__ == "__main__":
    main()
    analyze_sbox()

    # 如果有已知的明文密文对,可以用这个函数验证
    # known_pairs = [(b"testdata", bytearray.fromhex("..."))]
    # crack_known_plaintext(known_pairs)

## SNAKE

游戏题 同样加了很多反调试 在此感谢xxx赞助的超绝IDA9.1和xxx赞助的超绝反调试插件
讲个笑话 有人一开始反调试加多了跑不起来,以为要用titanhide。
队友用厉害的方法剖析了这道题,但是因为时间问题所以这道题的wp依旧由本菜鸡撰写一个偷鸡解法。
![](_v_images/20250714182947632_9882.png)
通过调试和静态分析找到主要流程 将食物部分的逻辑改掉即可
2个jz改jnz

好的,我们来分析这段来自CTF逆向工程(Reverse Engineering)挑战的伪代码。这看起来像是用IDA Pro之类的工具从一个用Go语言编写的贪吃蛇游戏中反编译出来的C伪代码。

代码的核心逻辑是,flag并不是在满足某个特定条件(比如分数达到100)时才显示,而是在每次吃到食物时,都会被“解密”一次。 你需要正确地进行游戏,才能在最后得到完全解密的flag。

下面我为你分步解析关键部分:

1. 关键数据结构
首先,要理解几个重要的变量,它们很可能是指向游戏核心数据结构的指针:

v11 (或 v79): 指向一个主游戏状态的结构体。这个结构体里包含了游戏板的尺寸、加密密钥、加密的flag等信息。
v25 (或 v80): 指向代表蛇身体的结构体(在Go中通常是“切片”或slice)。这个结构体里包含了蛇的坐标、长度,以及我们最关心的分数。
2. Score(分数)的关键项
我们来定位分数是如何增加的。请看这部分代码:

复制
// ...
v51 = *(_QWORD **)(v11 + 8);
if ( *v51 == v49 && v51[1] == n3 )
{
  ++*(_QWORD *)(v25 + 32); // <--- 分数增加在这里!
  // ...
}
// ...
分析:

if ( *v51 == v49 && v51[1] == n3 ): 这是一个条件判断。v51 指向的是蛇头的坐标,而 v49 和 n3 是食物的坐标。这个 if 语句的作用就是判断蛇头是否碰到了食物。
++*(_QWORD *)(v25 + 32);: 如果蛇吃到了食物,这行代码就会执行。它将 v25 指针偏移 32 字节位置的值加一。
结论: 分数(Score)就存储在 v25 所指向的蛇结构体偏移 32 字节的位置。每次吃到食物,这个值就会 +1。

笑点1 patch思路是从这里来的 笑点2 flag恰好是100分显示
然后玩通关,截图找不到了 写wp的时候又玩了一次

 

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注