Reverse 方向所有题目附件地址:DUTCTF2025-Reverse-Challenges

signin

如果 IDA 给字符串识别成了整数,选中按一下R就能转回来,

Exp:

import struct

s = b""
s += struct.pack("<I", 5526852)[:3]
s += b"CTF{"
s += struct.pack("<I", 7746418)[:3]
s += b"3R53_"
s += struct.pack("<I", 6235185)[:3]
s += b"\\/"     #\\是转义符号
s += b"3ry_"
s += b"f(_)"
s += b"n&ea"
s += struct.pack("<I", 2193717)[:3]
s += struct.pack("<H", 125)[:1]
print(s.decode('ascii'))

# DUTCTF{r3v3R53_1$_\/3ry_f(_)n&ea5y!}

weather?

这题如果会动调应该非常简单,比签到题都好做,smc 部分没做多余的加密,flag 是明文存储的。

纯静态分析的话,先看 IDA,发现无法反编译 sub_1400112B2 函数。

main 函数里一开始有个 sub_140011280 函数,点击跟进发现函数对 .func 段进行了处理,

继续跟进 sub_140011136 函数,

发现是把 .func 和 3 传入了 sub_140011208 函数,继续跟进,

其实就是个简单的 smc,对 .func 段进行了异或,写 IDC 脚本还原即可。

之后按 F5 重新反编译 .func 段函数,

直接看到真 flag:DUTCTF{sunny_and_secret}。至于DUTCTF{perhaps_today_is_Monday}这个是 fake flag,毕竟题目问的是天气,不能回答星期几是吧。

tulip

发现有花指令,

nop 掉,

去花,

加密里还有花,一样去掉,

去完发现是个 tea 加密,

Exp:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#define DELTA 0x114514
void DTea(uint32_t * flag,
    const uint32_t * Key) {
    uint32_t sum = DELTA * 32;
    int i;
    uint32_t v1 = flag[0];
    uint32_t v2 = flag[1];
    for (i = 0; i < 32; i++) {
        v2 -= ((v1 << 4) + Key[2]) ^ (v1 + sum) ^ ((v1 >> 5) + Key[3]);
        v1 -= ((v2 << 4) + Key[0]) ^ (v2 + sum) ^ ((v2 >> 5) + Key[1]);
        sum -= DELTA;
    }
    flag[0] = v1;
    flag[1] = v2;
}
int main() {
    uint32_t Key[4] = {
        0x11223344,
        0x55667788,
        0x99AABBCC,
        0xDDEEFF11
    };
    uint32_t enc[50] = {
        0x329e0eaf,
        0x6a398361,
        0x320b21fa,
        0x2200b7f1,
        0x2e086774,
        0x74eaef36,
        0xe8ef0a23,
        0xafd4ac64,
        0x92f93a03,
        0xb37a9bff,
        0x3ced126c,
        0xf5e00531
    };
    for (int i = 0; i < 6; i++) {
        DTea( & enc[i * 2], Key);
    }
    puts((char * ) enc);
    return 0;
}

// DUTCTF{1c65346e-4377-2384-3c70-962d37b4bc42}

MAZE

线性动态规划。srand 的种子在汇编窗口能看到:

Exp:

import numpy as np

# 模拟C++的rand()函数
class CppRand:
    def __init__(self, seed):
        self.state = seed

    def rand(self):
        self.state = (214013 * self.state + 2531011) & 0xFFFFFFFF
        return (self.state >> 16) & 0x7FFF

# 生成与C++完全一致的迷宫
def mkdata():
    mapp = np.zeros((51, 51), dtype=int)  # 1-based indexing
    rng = CppRand(15532)
    for i in range(1, 51):
        for j in range(1, i + 1):
            mapp[i][j] = rng.rand() % 19492025
    return mapp

mapp = mkdata()

# 动态规划求解
dp = np.zeros((51, 51), dtype=int)
path = [[[] for _ in range(51)] for __ in range(51)]
dp[1][1] = mapp[1][1]

for i in range(2, 51):
    for j in range(1, i + 1):
        max_val = -1
        best_move = ''

        # 检查三种可能的移动方式
        if j <= i - 1 and dp[i - 1][j] > max_val:
            max_val = dp[i - 1][j]
            best_move = 'C'

        if j - 1 >= 1 and j - 1 <= i - 1 and dp[i - 1][j - 1] > max_val:
            max_val = dp[i - 1][j - 1]
            best_move = 'B'

        if j + 1 <= i - 1 and dp[i - 1][j + 1] > max_val:
            max_val = dp[i - 1][j + 1]
            best_move = 'A'

        dp[i][j] = max_val + mapp[i][j]

        # 记录路径
        if best_move == 'C':
            path[i][j] = path[i - 1][j] + ['C']
        elif best_move == 'B':
            path[i][j] = path[i - 1][j - 1] + ['B']
        elif best_move == 'A':
            path[i][j] = path[i - 1][j + 1] + ['A']

# 找到最大分数
max_score = -1
best_j = -1
for j in range(1, 51):
    if dp[50][j] > max_score:
        max_score = dp[50][j]
        best_j = j

movement = path[50][best_j]
flag = ''.join(movement)
print("Flag:", f"DUTCTF{{{flag}}}")
print("Max score:", max_score)

# DUTCTF{CBBBCBACCCCBCBCBBBACBBACACACCACCCCCBAAACBCAACABBA}

WoAiShiinaMahiru

很基础的 WASM 题,使用命令wasm-objdump -x nikovsdonknimenzhidaoma.wasm查看 Data,

标准 Base64 加密,直接解密得到 flag:DUTCTF{680dc551-2584-800e-b8bf-d3c3934d97e8}

real_WoAiShiinaMahiru

出题人的预期解是把 .wasm 文件转 .o 之后再用 IDA 反编译分析,不过选手反映这题也可以动调。

先安装一个浏览器插件

C/C++ DevTools Support (DWARF) - Chrome 应用商店↗

然后开始打断点调试。

简单分析可知 flag 长度为 44,先输入 44 个 0 试试。监控一下内存变化。

经过反复测试loop $label0是在对加密后的 flag 进行比对,

在这里断住,看内存,

image (15).png

结合代码和调试得知,这页先存的是一堆 0,即为输入原文,然后是原文加密后的数据,然后是比对的密文。

将代码给 AI 分析其实可以看出来这里是一个魔改的 RC4。可以爆破,也可以直接计算出来明文。

可以爆破,把以下代码替换掉原来的 main.js:

import init from './WoAiShiinaMahiru.js';

async function main() {
    const wasm = await init();
    const { check_flag, alloc, memory } = wasm;

    function readWasmMemory(offset, length) {
        const memview = new Uint8Array(memory.buffer);
        // 使用 slice 创建一个副本,避免原始 buffer 被意外修改影响结果
        const data = memview.slice(offset, offset + length);
        return data;
    }

    async function attemptFlag(flag) {
        const encoded = new TextEncoder().encode(flag);
        const ptr = alloc(encoded.length);
        new Uint8Array(memory.buffer, ptr, encoded.length).set(encoded);

        // 调用 check_flag,注意:这里我们没有使用它的返回值,假设逻辑依赖于内存比较
        check_flag(ptr, encoded.length); // 忽略返回值 result

        // 定义偏移量和长度 (与原代码保持一致)
        const encryptedOffset = ptr + 0x30;
        const compareOffset = ptr + 0x60;
        const length = 44; // 假设 flag 长度和比较长度都是 44

        // 从 WASM 内存读取数据
        // !! 注意:读取操作应该在 check_flag 执行后进行
        const encryptedData = readWasmMemory(encryptedOffset, length);
        const compareData = readWasmMemory(compareOffset, length);

        // 注意:WASM 的内存管理,理论上 alloc 的内存在使用后可能需要 dealloc,
        // 但在这个爆破场景下,每次调用 alloc 可能问题不大,除非内存泄漏严重。
        // 如果 WASM 提供了 free/dealloc 函数,最好在读取完数据后调用。

        // 返回读取到的数据,注意原代码返回的 result 被忽略了
        return { encryptedData, compareData };
    }

    async function bruteForceFlag() {
        const flagLength = 44; // 假设 flag 长度为 44
        let currentFlag = "";
        // 确保字符集包含 flag 可能的所有字符
        const possibleChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}-"; // 扩展字符集以防万一
        document.getElementById("result").textContent = "开始自动爆破...";

        for (let i = 0; i < flagLength; i++) {
            let foundChar = null;
            for (const char of possibleChars) {
                // 构造测试 flag: 当前已知部分 + 测试字符 + 填充 (使用 'A' 或其他字符)
                const testFlag = currentFlag + char + "A".repeat(flagLength - currentFlag.length - 1);
                const { encryptedData, compareData } = await attemptFlag(testFlag);

                // --- 修改后的比较逻辑 ---
                let match = true;
                // 只比较到当前尝试的字符位置 i (包含 i)
                // 同时检查索引是否越界
                if (i < encryptedData.length && i < compareData.length) {
                    for (let j = 0; j <= i; j++) {
                        if (encryptedData[j] !== compareData[j]) {
                            match = false;
                            break;
                        }
                    }
                } else {
                    console.error(`错误:尝试比较的索引 ${i} 超出了获取数据的长度 (${encryptedData.length}, ${compareData.length})`);
                    match = false; // 无法比较,认为不匹配
                }
                // --- 比较逻辑结束 ---

                if (match) {
                    currentFlag += char;
                    foundChar = char;
                    console.log(`找到第 ${i + 1} 位: ${char}, 当前 Flag: ${currentFlag}`);
                    document.getElementById("result").textContent = `正在爆破... (${currentFlag.length}/${flagLength}) ${currentFlag}`;
                    break; // 找到当前位置的字符,跳出内层循环
                }
            } // 内层循环结束 (尝试所有字符)

            if (!foundChar) {
                console.log(`爆破失败,在位置 ${i} 无法找到匹配的字符。`);
                document.getElementById("result").textContent = `❌ 爆破失败,在位置 ${i} 无法找到匹配字符。当前进度: ${currentFlag}`;
                return; // 终止爆破
            }
        } // 外层循环结束 (遍历所有位置)

        if (currentFlag.length === flagLength) {
            console.log("🎉 爆破成功!Flag 为:", currentFlag);
            // 最后用完整的 flag 验证一次 (可选,但建议)
            const finalAttempt = await attemptFlag(currentFlag);
            let finalMatch = true;
            if (flagLength <= finalAttempt.encryptedData.length && flagLength <= finalAttempt.compareData.length) {
                 for (let j = 0; j < flagLength; j++) {
                    if (finalAttempt.encryptedData[j] !== finalAttempt.compareData[j]) {
                        finalMatch = false;
                        break;
                    }
                 }
            } else {
                finalMatch = false; // 长度不足
            }

            if (finalMatch) {
                 document.getElementById("result").textContent = `🎉 爆破成功!Flag 为: ${currentFlag}`;
            } else {
                 document.getElementById("result").textContent = `🤔 爆破完成,但最终验证失败?Flag 可能为: ${currentFlag}`;
                 console.warn("爆破声称成功,但最终验证时内存比较不完全匹配。");
            }

        } else {
            // 理论上如果中间没有 return,这里不应该执行
            document.getElementById("result").textContent = "❌ 爆破未完成,但循环结束了?";
        }
    }

    document.getElementById("verifyButton").addEventListener("click", async () => {
        const verifyButton = document.getElementById("verifyButton");
        const resultDiv = document.getElementById("result"); // 获取显示结果的元素

        verifyButton.disabled = true;
        verifyButton.textContent = "爆破中...";
        resultDiv.textContent = "初始化 Wasm 并准备爆破..."; // 初始提示

        try {
            await bruteForceFlag(); // 执行爆破
        } catch (error) {
            console.error("爆破过程中发生错误:", error);
            resultDiv.textContent = `❌ 爆破过程中发生错误: ${error.message}`;
        } finally {
            // 无论成功、失败或出错,都重新启用按钮
            verifyButton.disabled = false;
            verifyButton.textContent = "开始验证";
        }
    });
}

// 调用 main 函数启动逻辑
main().catch(err => {
    console.error("初始化或执行 main 函数时出错:", err);
    // 可以在页面上显示错误信息
    const resultDiv = document.getElementById("result");
    if (resultDiv) {
        resultDiv.textContent = `❌ 初始化失败: ${err.message}`;
    }
});

可以直接出 flag。

也可以动调,把输入 44 个 0 的加密弄下来记为 enc1,对比密文弄下来记为 ans。其实 flag 就是 enc1 每位异或 0x30(0的ASCII)然后异或 ans 的每位:

enc1 = [92, 167, 143, 28, 191, 33, 18, 99, 99, 76, 192, 9, 249, 152, 157, 240, 225, 216, 101, 233, 100, 60, 232, 110, 152, 187, 16, 137, 51, 167, 6, 54, 216, 4, 25, 93, 145, 166, 123, 216, 101, 116, 184, 166]
ans = [40, 194, 235, 111, 219, 87, 89, 101, 107, 76, 146, 13, 255, 202, 152, 237, 227, 139, 48, 186, 121, 52, 232, 110, 205, 166, 25, 136, 49, 175, 27, 52, 217, 1, 24, 8, 195, 162, 127, 139, 54, 34, 191, 235]
for i in range(len(ans)):
    print(chr(ans[i]^enc1[i]^0x30), end="")
    
 # DUTCTF{680b46b5-2cec-800e-9128-2151eb44ccf7}

Cangjie

华为仓颉语言逆向初探-先知社区

本题零解,直接放main.cj源码供选手研究:

package chall_cj

import net.http.*
import std.time.*
import std.sync.*
import std.log.LogLevel
import encoding.base64.*

// 1. 构建 Server 实例
let server = ServerBuilder()
    .addr("127.0.0.1")
    .port(8080)
    .build()

func encrypt(data: String): String{
    let enc :Array<Byte> = [0xd5,0xde,0xd0,0xdc,0xe7,0xea,0xf4,0xf5,0xed,0xea,0xfe,0xd4,0xee,0xf8,0xfa,0xd0,0xd3,0xf0,0xfc,0xf4,0xfe,0xfc,0xf3,0xc8,0xf1,0xea,0xc5,0xfa,0xc3,0xe5,0xe6,0xe7,0xff,0xe8,0xfd,0x97,0xc3,0xc0,0xd2];
    let src = data.toArray()
    for (i in 0..enc.size) {
        if((src[i] ^ UInt8(0x80+i)) != enc[i]){
            return "wrong";
        }
    }
    return "right";

}

func startServer(): Unit {
    // 2. 注册请求处理逻辑
    server.distributor.register("/", {httpContext =>
        httpContext.responseBuilder.body(encrypt(httpContext.request.url.queryForm.get("A1_cE").getOrThrow()))
    })
    server.logger.level = OFF
    // 3. 启动服务
    server.serve()
}

// func startClient(): Unit {
//     // 1. 构建 client 实例
//     let client = ClientBuilder().build()
//     // 2. 发送 request
//     let response = client.get("http://127.0.0.1:${server.port}/?A1_cE=U_R_correct_but_Cangjie_is_a_xxx_I_4get")
//     // 3. 读取response body
//     let buffer = Array<Byte>(32, item: 0)
//     let length = response.body.read(buffer)
//     println(String.fromUtf8(buffer[..length]))
//     // 4. 关闭连接
//     client.close()
// }

main () {
        // 启动服务端
    let server: Future<Unit> = spawn {
        startServer()
    }
        
        // 阻塞当前运行的线程后再让客户端访问服务端,如无需要可去掉
    // sleep(Duration.second)
    // startClient()

        // 服务端运行中
    server.get()
    
}

DUTCTF{U_R_correct_but_Cangjie_is_a_xxx_I_4get}