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 进行比对,
在这里断住,看内存,
结合代码和调试得知,这页先存的是一堆 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}