阴间CTF-re复现

这是一道伪装成web的re题,比赛的时候作为一个re手没被web难到被逻辑分析薄纱了有点难绷()

flag1:

先在索引里把反调试关掉

然后我是先找到的flag2说实话,观察flag2的所在位置,推测出flag1应该在如下的文件里面:

/api/get_userdata?filename=/etc/flag1

然后,执行这个指令(curl "http://7788-7605bdb9-8e37-46d0-99ea-e4851c74c07c.challenge.ctfplus.cn/api/get_userdata?filename=/etc/flag1"),得到flag1:vniq15sdanub

flag2:

观察下面的代码,不难发现游戏逻辑:将最大步数作为分数加到总分上,如果总分超过10000,就输出flag2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
async function getFlag2() {
try {
const token = await VHUIIU();
const response = await fetch('/api/get_flag2?sign='+token.sign+'&t='+token.t);
return await response.json();
} catch (error) {
console.error('error2:', error);
return { error: 'error2' };
}
}

async function checkScore () {
try {
const resp_json = await getFlag2();
if (resp_json.status === 'success') {
alert('flag2: ' + resp_json.flag2);
return true;
} else {
alert(resp_json.flag2);
return false;
}
} catch (error) {
console.error('error2:', error);
}
}

async function bindNextLevelButton() {
const nextLevelButton = document.getElementById('next-level');
nextLevelButton.addEventListener('click', async() => {
const count_str = document.getElementById('count').innerText;
const tryToBeat_str = document.getElementById(computerGuessFieldID).innerText;
const check1 = parseInt(count_str, 10) <= parseInt(tryToBeat_str, 10);
const game = JSON.parse(getGame());
const cells = game.Cells;
const check2 = cells.every(row => row.every(cell => cell === cells[0][0]));
const savedUserdata = await getUserdata();
if (check1 && check2) {
await saveUserdata(savedUserdata.total_score+parseInt(tryToBeat_str, 10), savedUserdata.current_level+1);
const reta = await checkScore();
if (reta) {
alert('Congratulations! You have passed all levels! Another challenge is about to begin...');
const flag3 = prompt('Please input flag3:');
alert(checkFlag3(flag3));
}
}
if (check2 && !check1) {
alert('YOU NEED TO BE LESS THAN MAX STEP TO PASS THE LEVEL');
}
location.reload();
});
}


修改判断准则的那10000失败了,改掉后每次通关时会改回去,但是修改最大步数成功了,这样玩一次游戏就可以完成,得到flag2:3a8b26aee5

(其实说真的,没理解为什么,但是毕竟不是web手,能得到flag就行)

flag3:

终于开始逆向部分了

分析代码容易发现关键函数checkFlag3()隐藏在game.wasm里面不能被查看,把这个文件保存下来,用工具wasm2c转为.c文件再查看

执行如下操作:
.\wasm2c.exe "D:\game.wasm" -o game.c

得到game.js

静态分析+前端调试找关键数据(这部分好难,)

可以最终得到算法逻辑为:先进行一个循环异或,再进行魔改TEA加密

TEA魔改点:魔改为了CBC模式, 链接起来、更改了默认的delta值、32轮中每轮: v0+=后面多加了一个^(v1 + sum), v1+=后面多加了一个^(v0 + sum)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>

// 全局变量
uint32_t data1 = 0x5F797274;
uint32_t data2 = 0x64726168;

// 将字节数组转换为 uint32 数组
void Byte2uint32(uint8_t* key, uint32_t* key1, int key_len) {
for (int i = 0; i < key_len; i += 4) {
key1[i / 4] = ((uint32_t)key[i] << 24) | ((uint32_t)key[i + 1] << 16) | ((uint32_t)key[i + 2] << 8) | (uint32_t)key[i + 3];
}
}

// 更新 data1 和 data2
void update_data1_data2(uint32_t v0, uint32_t v1, uint32_t* key) {
data1 ^= v0;
data2 ^= v1;
v0 = data1;
v1 = data2;
uint32_t delta = 0x6675636b;
uint32_t sum = 0;
for (int i = 0; i < 32; i++) {
sum += delta;
v0 += (((v1 << 4) + key[0]) ^ (v1 + sum) ^ ((v1 >> 5) + key[1]) ^ (v1 + sum));
v1 += (((v0 << 4) + key[2]) ^ (v0 + sum) ^ ((v0 >> 5) + key[3]) ^ (v0 + sum));
}
data1 = v0;
data2 = v1;
}

// CBC TEA 解密
void cbc_tea_decrypt(uint32_t* v0, uint32_t* v1, uint32_t* key) {
uint32_t delta = 0x6675636b;
uint32_t sum = (delta * 32);
for (int i = 0; i < 32; i++) {
*v1 -= (((*v0 << 4) + key[2]) ^ (*v0 + sum) ^ ((*v0 >> 5) + key[3]) ^ (*v0 + sum));
*v0 -= (((*v1 << 4) + key[0]) ^ (*v1 + sum) ^ ((*v1 >> 5) + key[1]) ^ (*v1 + sum));
sum -= delta;
}
*v0 = data1 ^ *v0;
*v1 = data2 ^ *v1;
}

// TEA 解密
void Tea_decrypto(uint8_t* data, uint8_t* key, uint8_t* dec_data, int data_len, int key_len) {
data1 = 0x5F797274;
data2 = 0x64726168;
// 使用动态内存分配
uint32_t* data_uint32 = (uint32_t*)malloc((data_len / 4) * sizeof(uint32_t));
uint32_t* key_uint32 = (uint32_t*)malloc((key_len / 4) * sizeof(uint32_t));

if (data_uint32 == NULL || key_uint32 == NULL) {
fprintf(stderr, "内存分配失败\n");
return;
}

Byte2uint32(data, data_uint32, data_len);
Byte2uint32(key, key_uint32, key_len);

int dec_data_len = 0;
for (int i = 0; i < data_len / 4; i += 2) {
uint32_t temp0 = data_uint32[i];
uint32_t temp1 = data_uint32[i + 1];
cbc_tea_decrypt(&temp0, &temp1, key_uint32);
dec_data[dec_data_len++] = (uint8_t)(temp0 >> 24);
dec_data[dec_data_len++] = (uint8_t)(temp0 >> 16);
dec_data[dec_data_len++] = (uint8_t)(temp0 >> 8);
dec_data[dec_data_len++] = (uint8_t)temp0;
dec_data[dec_data_len++] = (uint8_t)(temp1 >> 24);
dec_data[dec_data_len++] = (uint8_t)(temp1 >> 16);
dec_data[dec_data_len++] = (uint8_t)(temp1 >> 8);
dec_data[dec_data_len++] = (uint8_t)temp1;
update_data1_data2(temp0, temp1, key_uint32);
}

while (dec_data_len > 0 && dec_data[dec_data_len - 1] == 0x00) {
dec_data_len--;
}
dec_data[dec_data_len] = '\0';

// 释放动态分配的内存
free(data_uint32);
free(key_uint32);
}

// 反转数组并异或
void ReverseArraySelfXor0(uint8_t* arr, int arr_len) {
for (int i = arr_len - 1; i >= 0; i--) {
arr[i] ^= arr[(i + 1) % arr_len];
}
}

#define CMP_SIZE 16
int main() {
uint8_t cmp[CMP_SIZE] = { 0x04, 0x0a, 0xf3, 0xbd, 0x68, 0x8c, 0xb7, 0x76, 0x36, 0x25, 0x2d, 0x96, 0xca, 0x62, 0xa3, 0x41 };
uint8_t key_[] = { 113, 49, 119, 100, 53, 53, 54, 113, 119, 49, 53, 54, 54, 51, 53, 49 };
uint8_t dec_data[CMP_SIZE];
Tea_decrypto(cmp, key_, dec_data, sizeof(cmp), sizeof(key_));
ReverseArraySelfXor0(dec_data, strlen((char*)dec_data));
printf("%s\n", dec_data);
return 0;
}

运行得到flag3:oJQjpShVldkteWGV

(哈哈,这是第一篇re博客,五一假期在宾馆居然能来复现,我真是太佩服自己了,虽然不知道拖了多久了


阴间CTF-re复现
http://example.com/2025/05/03/阴间CTF-re复现/
作者
oxygen
发布于
2025年5月3日
许可协议