SekaiCTF 2025 Reverse WP

总结来说,这次XCTF的逆向题质量都非常高(除了lua),每一题都非常有质量,非常推荐做。

Miku-music-machine

考点:控制流保护、SMC、迷宫

main核心代码如下,要求输入50长度字符串,异或上全局的一个数组,然后每个字节进行四步计算,&3来决定执行不同计算,然后Call每轮计算的v8值下标的函数,播放对应音乐。

v7,v8初始值22,最后判断v7是否等于418。

alt text

alt text

可以看到函数列表里面是给dwMsg赋值,用于播放不同的midi音乐。

alt text

看对v7、v8的操作猜测是类似迷宫的移动,导出函数列表,发现一共是441个函数,正好是21**2,说明是21x21的地图,上面的+=21和-=21也就对应的下移和上移操作,++和–对应右移和左移。

alt text

随便输入SEKAI{}格式五十长度字符串进行调试,发现程序跑到一半就异常退出,调试发现是这边Call函数列表的函数时候出错了,这边可以看到这个是控制流保护下才有的Call指令。

alt text

断点该处控制流保护Call,调试发现正常的一些函数就会经过上两个jmp rax跳转到对应函数,部分函数则不符合条件,会到最底下的jmp,然后触发异常。

alt text

这边直接使用Patch代码,遍历所有函数,输出可执行的函数下标。

Call的部分代码直接该成,使用r15来遍历441个函数。

1
2
3
4
5
6
7
8
9
10
.text:00007FF7290F4890 loc_7FF7290F4890:
.text:00007FF7290F4890 xor r15, r15
.text:00007FF7290F4893 loc_7FF7290F4893:
.text:00007FF7290F4893 mov rbx, r15
.text:00007FF7290F4896 mov rax, rva off_7FF729163040[r12+rbx*8]
.text:00007FF7290F489E call cs:__guard_xfg_dispatch_icall_fptr
.text:00007FF7290F48A4 inc r15
.text:00007FF7290F48A7 cmp r15, 1BAh
.text:00007FF7290F48AE nop
.text:00007FF7290F48AF jle short loc_7FF7290F4893

然后顶上r12赋值的地方之后直接jmp到这段代码头

1
2
.text:00007FF7290F4839 lea     r12, unk_7FF7290F0000
.text:00007FF7290F4840 jmp short loc_7FF7290F4890

调试运行,在Call内部将最底下jmp改成ret,然后给两个jmp rax设置条件断点输出r15。

1
2
auto r15 = GetRegValue("r15");
msg("%d,",r15);

运行输出结果,以下就是可以Call的函数下标。

1
22,24,25,26,28,29,30,31,32,33,34,35,36,37,38,39,40,43,47,55,57,59,61,64,65,66,67,68,69,70,71,72,74,75,76,78,80,82,87,93,97,99,106,107,108,110,112,114,115,116,117,118,120,121,122,123,124,131,133,139,143,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,164,165,166,169,179,187,190,192,194,196,197,198,199,200,201,202,204,206,208,213,215,217,219,223,225,227,229,232,234,235,236,237,238,240,241,242,244,246,247,248,249,250,253,259,267,271,274,275,276,277,278,279,280,281,282,283,284,285,286,288,290,291,292,295,299,301,303,305,307,309,311,313,316,318,319,320,322,324,326,328,330,332,334,339,341,345,349,351,355,358,359,360,362,364,365,366,367,368,370,372,373,374,376,379,381,383,387,393,395,397,400,402,404,405,406,408,409,410,412,413,414,416,418,

上面得知该地图是21*21,而且这边可以看出可以Call的函数下标第一个刚好是v7初始值22,最后418刚好是代码最后对v7的校验值。

所以可以知道上面下标就是地图可走路的下标,而其他的则是墙壁,所以Call对应墙壁的函数才会触发异常,是利用控制流保护函数名单来做的校验。

使用代码输出该地图,可以看到确实是标准的一个地图,起点22,终点418。

alt text

然而从地图看,通路仅有一条,并且路程就44步,不符合题目要求输入的50长度字符串要求的200步,且在运行的时候发现有些Call还是会异常。

可以在异常的函数中发现这边有个int,而其他正常函数是没有的,查看这段代码交叉调用,可以看到有另一个函数对他这边做了xor处理。

实际调试发现只要先调用了底下这个Call对上面的做xor处理,上面的函数就可以正常调用。

alt text

alt text

使用IDC脚本遍历出所有带的Xor函数。

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
#include <idc.idc>

static main()
{
auto seg;
// 遍历所有段
for (seg = get_first_seg(); seg != BADADDR; seg = get_next_seg(seg))
{
// 获取段名称
auto seg_name = get_segm_name(seg);
msg(seg_name + "\n");
if (seg_name == ".text")
{
// 从当前段头地址开始遍历
auto current_addr = seg;
// 结束地址
auto end_addr = current_addr + 0x140004730;

// 遍历所有函数
auto cur_func = seg;
auto seg_end = get_segm_end(seg);
while (current_addr != BADADDR && current_addr < 0x140004730)
{
auto insn_name = print_insn_mnem(current_addr);
// 获取操作数1
auto op = print_operand(current_addr, 0);
// 获取操作数2
auto op2 = print_operand(current_addr, 1);

if (insn_name == "xor")
{
auto func_name = get_func_name(cur_func);
msg(func_name + " %X :" + insn_name + " " + op + " " + op2 + "\n", current_addr);
}
current_addr = next_head(current_addr, end_addr);
}
}
}
}

可以整理出以下对应Xor以及被Xor函数的表。

1
2
3
4
5
6
7
8
{
{0x14000157E, 0x1400014F0},
{0x14000297E, 0x1400023D0},
{0x14000365E, 0x140001410},
{0x140003F3E, 0x140002710},
{0x14000421E, 0x140002110},
{0x14000443E, 0x140001790}
};

使用代码绘制出这8个函数下标处在地图上,然后大写字母对应Xor函数,小写字母对应被Xor函数。

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
#include <iostream>
#include <windows.h>

DWORD64 functionList[441] =
{
//... 自行导出
};

int main()
{
int Road[]{22, 24, 25, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 43, 47, 55, 57, 59, 61, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 78, 80, 82, 87, 93, 97, 99, 106, 107, 108, 110, 112, 114, 115, 116, 117, 118, 120, 121, 122, 123, 124, 131, 133, 139, 143, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 164, 165, 166, 169, 179, 187, 190, 192, 194, 196, 197, 198, 199, 200, 201, 202, 204, 206, 208, 213, 215, 217, 219, 223, 225, 227, 229, 232, 234, 235, 236, 237, 238, 240, 241, 242, 244, 246, 247, 248, 249, 250, 253, 259, 267, 271, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 288, 290, 291, 292, 295, 299, 301, 303, 305, 307, 309, 311, 313, 316, 318, 319, 320, 322, 324, 326, 328, 330, 332, 334, 339, 341, 345, 349, 351, 355, 358, 359, 360, 362, 364, 365, 366, 367, 368, 370, 372, 373, 374, 376, 379, 381, 383, 387, 393, 395, 397, 400, 402, 404, 405, 406, 408, 409, 410, 412, 413, 414, 416, 418};

unsigned char XorList[] =
{
0x09, 0x40, 0x11, 0xE4, 0x1C, 0x81, 0x92, 0xDB, 0x0B, 0x75,
0x26, 0x6A, 0x2F, 0x7F, 0xDD, 0xD2, 0x52, 0x21, 0x76, 0x9F,
0xDF, 0x8E, 0x8F, 0xCD, 0x9F, 0x84, 0x61, 0x3F, 0x6D, 0x7A,
0x87, 0x1E, 0x21, 0x99, 0xC7, 0x65, 0xDC, 0xC8, 0x4A, 0x22,
0x7D, 0x28, 0x64, 0x69, 0xDC, 0x20, 0x34, 0xED, 0xFB, 0xD7};

DWORD64 SpecialPointMap[][2]{{0x14000157E, 0x1400014F0},
{0x14000297E, 0x1400023D0},
{0x14000365E, 0x140001410},
{0x140003F3E, 0x140002710},
{0x14000421E, 0x140002110},
{0x14000443E, 0x140001790}};

int SpecialPointIndexMap[6][2]{};

for (int i = 0; i < 6; i++)
{
auto CurrentP1 = SpecialPointMap[i][0] - 14;

auto CurrentP2 = SpecialPointMap[i][1];

int index1{}, index2{};
for (int j = 0; j < 441; j++)
{
if (functionList[j] == CurrentP1)
{
index1 = j;
}
if (functionList[j] == CurrentP2)
{
index2 = j;
}
}
SpecialPointIndexMap[i][0] = index1;
SpecialPointIndexMap[i][1] = index2;
}

int MazeMap[21 * 21]{};

int StartPos = 22, EndPos = 418;

int Pos = 22;

for (int i = 0; i < 21 * 21; i++)
{
MazeMap[i] = '*';
}

for (int i = 0; i < 199; i++)
{
MazeMap[Road[i]] = '_';
}

MazeMap[StartPos] = 'S';

MazeMap[EndPos] = 'E';

for (int i = 0; i < 6; i++)
{
auto index1 = SpecialPointIndexMap[i][0];
auto index2 = SpecialPointIndexMap[i][1];

MazeMap[index1] = 'A' + i;
MazeMap[index2] = 'a' + i;
}

for (int i = 0; i < 21; i++)
{
for (int j = 0; j < 21; j++)
{
printf("%c ", MazeMap[i * 21 + j]);
}
printf("\n");
}
return 0;
}

得到以下地图,看地图就可以知道Xor的意义,也就是得先走到Xor函数位置,触发函数对小写字母处的函数进行Xor,也就是所谓的“解锁”,让小写字母位置函数可以Call,也就是可以通过。

综上需要走到大写字母处去解锁对应小写字母处位置,才能通过那格。

alt text

我是直接写了个交互程序,自己按最短路走了一遍迷宫,发现刚好是200步,就可以将路径数据转为输入字节异或后的数据,再异或即可得到Flag。

交互代码:

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#include <iostream>
#include <windows.h>
#include <string>
#include <vector>

// Map function list
DWORD64 functionList[] =
{
//... 自行导出
};

int main()
{
// SafeRoad
int Road[]{22, 24, 25, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 43, 47, 55, 57, 59, 61, 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 75, 76, 78, 80, 82, 87, 93, 97, 99, 106, 107, 108, 110, 112, 114, 115, 116, 117, 118, 120, 121, 122, 123, 124, 131, 133, 139, 143, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 164, 165, 166, 169, 179, 187, 190, 192, 194, 196, 197, 198, 199, 200, 201, 202, 204, 206, 208, 213, 215, 217, 219, 223, 225, 227, 229, 232, 234, 235, 236, 237, 238, 240, 241, 242, 244, 246, 247, 248, 249, 250, 253, 259, 267, 271, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 288, 290, 291, 292, 295, 299, 301, 303, 305, 307, 309, 311, 313, 316, 318, 319, 320, 322, 324, 326, 328, 330, 332, 334, 339, 341, 345, 349, 351, 355, 358, 359, 360, 362, 364, 365, 366, 367, 368, 370, 372, 373, 374, 376, 379, 381, 383, 387, 393, 395, 397, 400, 402, 404, 405, 406, 408, 409, 410, 412, 413, 414, 416, 418};

unsigned char XorList[] =
{
0x09, 0x40, 0x11, 0xE4, 0x1C, 0x81, 0x92, 0xDB, 0x0B, 0x75,
0x26, 0x6A, 0x2F, 0x7F, 0xDD, 0xD2, 0x52, 0x21, 0x76, 0x9F,
0xDF, 0x8E, 0x8F, 0xCD, 0x9F, 0x84, 0x61, 0x3F, 0x6D, 0x7A,
0x87, 0x1E, 0x21, 0x99, 0xC7, 0x65, 0xDC, 0xC8, 0x4A, 0x22,
0x7D, 0x28, 0x64, 0x69, 0xDC, 0x20, 0x34, 0xED, 0xFB, 0xD7};

// unlock point to locked point (function)
DWORD64 LockPointMap[][2]{{0x14000157E, 0x1400014F0},
{0x14000297E, 0x1400023D0},
{0x14000365E, 0x140001410},
{0x140003F3E, 0x140002710},
{0x14000421E, 0x140002110},
{0x14000443E, 0x140001790}};

// unlock point to locked point (index)
int LockPointIndexMap[6][2]{};

for (int i = 0; i < 6; i++)
{
auto CurrentP1 = LockPointMap[i][0] - 14;

auto CurrentP2 = LockPointMap[i][1];

int index1{}, index2{};
for (int j = 0; j < 441; j++)
{
if (functionList[j] == CurrentP1)
{
index1 = j;
}
if (functionList[j] == CurrentP2)
{
index2 = j;
}
}
LockPointIndexMap[i][0] = index1;
LockPointIndexMap[i][1] = index2;
}

int Pos = 22;

std::vector<std::string> Path;

bool CanClean = true;
while (true)
{
int MazeMap[21 * 21]{};

int StartPos = 22, EndPos = 418;

for (int i = 0; i < 21 * 21; i++)
{
// Wall
MazeMap[i] = '*';
}

for (int i = 0; i < 199; i++)
{
// SafeRoad
MazeMap[Road[i]] = '_';
}

// StartPos
MazeMap[StartPos] = 'S';

// EndPos
MazeMap[EndPos] = 'E';

for (int i = 0; i < 6; i++)
{
auto index1 = LockPointIndexMap[i][0];
auto index2 = LockPointIndexMap[i][1];

// unlock point
MazeMap[index1] = 'A' + i;
// locked point
MazeMap[index2] = 'a' + i;
}

// print out result
if (GetAsyncKeyState(VK_HOME))
{
printf("Size:%d\n", Path.size());
for (auto p_ : Path)
{
printf("%.2s", p_.c_str());
}
printf("\n");
system("pause");
Sleep(200);
}

// up
if (GetAsyncKeyState(VK_UP))
{
Path.push_back("00");
Pos -= 21;
CanClean = true;
Sleep(200);
}
// down
if (GetAsyncKeyState(VK_DOWN))
{
Path.push_back("10");
Pos += 21;
CanClean = true;
Sleep(200);
}
// left
if (GetAsyncKeyState(VK_LEFT))
{
Path.push_back("11");
Pos--;
CanClean = true;
Sleep(200);
}
// right
if (GetAsyncKeyState(VK_RIGHT))
{
Path.push_back("01");
Pos++;
CanClean = true;
Sleep(200);
}
// clean and output
if (CanClean)
{
system("cls");
CanClean = false;
// print out map
for (int i = 0; i < 21; i++)
{
for (int j = 0; j < 21; j++)
{
if (i * 21 + j == Pos)
{
// current point
printf("P ");
}
else
{
printf("%c ", MazeMap[i * 21 + j]);
}
}
printf("\n");
}
}

Sleep(100);
}

return 0;
}

走完所有路程到终点,按Home输出路程大小以及路程01数据。

alt text

解密代码:

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
#include <iostream>
#include <windows.h>
#include <string>

int main()
{
std::string Path = "1010010101010000101001010101101001010101101011111010111111111010111111010101000001010101000001010000000000000101101010100101101001011000111100001111000000001111101010101010111110101111111110101010111110101010100000000000010100000000010101010000111111111101010101011010111111111010101001011010101001011111000000001111000000000101010100000101000000000000010110101010010110100101101010101010101010101010";
unsigned char XorList[] =
{
0x09, 0x40, 0x11, 0xE4, 0x1C, 0x81, 0x92, 0xDB, 0x0B, 0x75,
0x26, 0x6A, 0x2F, 0x7F, 0xDD, 0xD2, 0x52, 0x21, 0x76, 0x9F,
0xDF, 0x8E, 0x8F, 0xCD, 0x9F, 0x84, 0x61, 0x3F, 0x6D, 0x7A,
0x87, 0x1E, 0x21, 0x99, 0xC7, 0x65, 0xDC, 0xC8, 0x4A, 0x22,
0x7D, 0x28, 0x64, 0x69, 0xDC, 0x20, 0x34, 0xED, 0xFB, 0xD7};
for (int i = 0; i < Path.size(); i += 8)
{
int c = 0;
for (int j = 0; j < 8; j += 2)
{
auto b = std::stoi(Path.substr(i + j, 2), nullptr, 2);
b <<= 2 * (j / 2);
c += b;
}
printf("%c", c ^ XorList[i / 8]);
}
return 0;
}

SEKAI{https://www.youtube.com/watch?v=J---aiyznGQ}

Sekai-Craft

考点:MC计分板命令,魔改Tea

核心代码就在Sekai-Craft\mvm\datapacks\mvm\data\mvm\function\mvm.mcfunction中。

可以先用代码提取出里面的Delta、Key[4]、Cipher三个数据。

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
#include <iostream>
#include <windows.h>
#include <fstream>
#include <string>
#include <algorithm>

int main()
{
std::ifstream file("C:\\Users\\Liv\\Desktop\\mvm.mcfunction.txt", std::ios::in);

std::string Buffer;

int Count = 0;

std::string Dest = "scoreboard players set delta_";
// scoreboard players set k0
// scoreboard players set k1
// scoreboard players set k2
// scoreboard players set k3
// scoreboard players set cipher0
// scoreboard players set cipher1
// scoreboard players set cipher2
// scoreboard players set cipher3

std::string Bits;

while (std::getline(file, Buffer))
{
if (!Buffer.empty())
{
auto index = Buffer.find(Dest);
if (index != std::string::npos)
{
index = Buffer.find("bit");
if (index != std::string::npos)
{
Bits += Buffer.substr(index + 4, 1);
}
}
}
}

std::reverse(Bits.begin(), Bits.end());

printf("%s\n", Bits.c_str());

printf("0x%X\n", std::stoul(Bits, nullptr, 2));

return 0;
}

通过观察加密过程,他是把正常4字节运算展开到位单位计算,所以显得复杂,把代码按一块一块分析比较方便。

最后分析结果如下,魔改点就一处,直接异或上key[sum & 3]和key[(sum >> 11) & 3],而标准的是有加上sum值的。

1
2
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (key[sum & 3]);
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (key[(sum >> 11) & 3]);

通过搜索部分代码可知,一共执行了64轮计算,由于有4个4字节密文也就是两组加密,所以Tea加密是32轮计算。

alt text

解密代码:

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
#include <iostream>
#include <windows.h>

void decipher(uint32_t v[2], const uint32_t key[4])
{
unsigned int i;
uint32_t v0 = v[0], v1 = v[1], delta = 0xAEF98DA, sum = delta * 32;
for (i = 0; i < 32; i++)
{
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (key[(sum >> 11) & 3]);
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (key[sum & 3]);
sum -= delta;
}
v[0] = v0;
v[1] = v1;
}

int main()
{
uint32_t Key[]{0x5F7438DA, 0xF1FA60FB, 0x289C2239, 0x88042CB9};

uint32_t Cipher[]{0x1021D4FF, 0xA32B2EAD, 0x4C38D5E, 0x15A65D4B};

decipher(Cipher, Key);

decipher((uint32_t *)((uint8_t *)Cipher + 8), Key);

printf("SEKAI{%.16s}\n", (PUCHAR)Cipher);

return 0;
}

SEKAI{s3k41cr4tg00d:^)}

Alchemy Master

质量最高的题。

考点:服务端逆向,DLL远程线程注入,函数Hook,代码编译,CL编译中元数据计算

server.py

要求输入CPP的代码,然后写到solution.cpp,传入solution.cpp目录参数执行launch.exe。

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
from pathlib import Path
from tempfile import TemporaryDirectory
import subprocess
import sys


launch_exe = Path(__file__).parent / 'launch.exe'


def main() -> None:
if not launch_exe.exists():
print('something is wrong! contact admins')
return

print('hi! please enter your cpp code line by line, then end it with a __END__ line')

lines = []
while True:
l = input()
if l == '__END__':
break
lines.append(l)

code = '\n'.join(lines)

print('gotcha, lets compile this!')
with TemporaryDirectory() as tmpdir:
cwd = str(Path(tmpdir).absolute())
file_path = Path(tmpdir) / 'solution.cpp'
print(file_path)
file_path.write_text(code)

completed = subprocess.run(
[str(launch_exe.resolve()), str(file_path.resolve())],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
cwd=cwd,
)

sys.stdout.write('=== cl.exe ===\n')
sys.stdout.write(completed.stdout.replace(cwd + '\\', ''))
sys.stdout.flush()


if __name__ == '__main__':
main()

launch.exe

main函数,创建运行cl.exe,传入编译参数以及server.py传来的源码路径。

alt text

加载本地plugin.dll,在cl.exe远程申请内存空间,然后将plugin.dll内存数据写到cl.exe进程中,再使用CreateRemoteThread远程调用cl.exe中的LoadLibraryA函数,加载运行刚刚写入的plugin.dll,这个操作也就是所谓的远程注入DLL。

alt text

plugin.dll

查看字符串,可以找到调用该字符串的函数。

alt text

往函数上层走,可以看到这边使用了Hook,方框处是被Hook函数地址,箭头处是Hook调用函数。

alt text

在下面箭头的函数中可以看到另外两个Hook,针对clxx.dllc2.dllDLL的Hook,Hook调用函数都是同一个,也就是第一张输出Flag的地方。

alt text

接下来就是对cl.exe的动调,因为该plugin.dll是注入到该进程的。

动调方法:传参源码目录运行launch.exe,然后断点在注入前的代码,使用plugin.dll的IDA项目附加到当前运行中的cl.exe,然后再运行launch.exe的断点,plugin.dll那边的IDA就会断下。

可以看到Hook了c2.dll中的某个函数,开头jmp到自己的函数中。

alt text

发现Hook函数这边进来,也就输出Flag的函数中,将被Hook函数执行得到的结果传入。

通过输入不同的solution.cpp代码来调试,发现数据结构如下(下文会提到如何得到),第一个int是不同代码对应的编码,第二个int16是用来计算的操作数,最后的int16是未知数据,没用到这边不管。

alt text

此处v3是输入进来的一个CodeType的链式数组结构,对应的是solution.cpp编译过程中不同类型代码对应的数据,v5是全局CodeType数组,v5与v3的代码编码进行循环判断当前v3代码是哪个类型,最后取出对应代码编码的操作码。

alt text

使用上面全局取出的对应代码编码的操作码,对另一个全局数组进行–或者++操作。

下面+1LL那行代码实际就是通过操作计算得到对应全局数组的指针,然后++。

alt text

alt text

最后全局数组++–计算完的结果要与另一个目标数组数据完全相等,然后输出Flag。

alt text

从全局的编码数据来看,一共有9种用到的不同的代码编码,且操作数不一定相等,可以通过输入各种solution.cpp代码,来动调计算,看看不同的代码编码会对qword_7FFE52FA9AB0的全局数据哪几个下标数据进行–以及++。

alt text

被计算的全局数组数据以及最终目标数据

1
2
3
Original = [0x734, 0x0, 0x0, 0x0, 0xBBC, 0x0, 0xB63]

Dest = [0x14D, 0x2D7, 0x161, 0x2EA, 0x1B1, 0x2FD, 0x169]

代码编码分析

以下是不同solution.cpp代码在编译中Hook函数取到的对应CodeType数据,也就是上面的v3指针中的数据。

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
int a()
{
reinterpret_cast<void (*)()>(nullptr)();
return 0;
}
/*
933 -> function def
939

899 -> call()

86c -> return 0;
89b

93a
931
934
*/
int a()
{
int b = 0;
return 0;
}

/*
933
939

852 -> int b;

86c -> return 0;
89b

93a
931
934
*/

int a()
{
return 0;
return 0;
}
/*
933
939

86c -> return 0;
89b

86c -> return 0;
89b

93a
931
934
*/

int a()
{
{
}
{
}
}
/*
933
939

939
93a
89b

93a -> end of {}
931
934
*/

int a()
{
throw;
}
/*
933
939

84f
84f

899 -> call

8a2 -> throw
89b

93a
931
931
934
*/

通过上面几个不同代码的交叉分析,可以得到一些代码编码对应的实际代码类型,共有9种代码编码,这边只识别出其中6种。

1
2
3
4
5
6
7
8
9
10
0x91e -> unk
0x8a3 -> unk
0x917 -> unk

0x852 -> int b;
0x8a2 + 0x899 -> throw;
0x899 -> reinterpret_cast<void(*)()>(nullptr)();
0x86c -> return 0;
0x933 -> int a(){
0x93a -> {}

然后就是动调不同代码编码对应的计算操作。

一共动调到7种编码,另外两个没有识别的并没有动调到过,第一个数组是对全局数组进行++的下标,第二个数组是对全局数组进行–的下标。

发现下面已经识别出的6种代码编码对应的操作中,已经包含0-6所有下标的计算,所以理论上可以仅通过这六个代码编码使用Z3求出各个代码所需要的数量,让所有代码编码进行计算后,全局数组数据会符合目标数组数据。

1
2
3
4
5
6
7
8
9
10
//{code, add_index, dec_index}

{0x91e, {6}, {0,4}},

{0x852, {2}, {4,6}},
{0x8a2, {1}, {2,4,6}},
{0x899, {3}, {0,6}},
{0x86c, {5}, {0,4}},
{0x933, {5}, {0,6}},
{0x93a, {5}, {0,4}},

Z3求解目标代码

使用Z3求解各个不同代码编码所需要的代码数量。

注:上文分析中throw命令会占用0x8a2和0x899两个代码编码,而0x899又是Call的编码,所以最后所需Call的代码数量要减去throw的代码数量,因为throw已经帮Call占用了一部分计算

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
from z3 import *

# 数值代码对应的字符串,以及加一的下标和减一的下标
# code (string, add_index, dec_index)
code_map = {
0x852: ("int b", {2}, {4, 6}),
0x8a2: ("throw;", {1}, {2, 4, 6}),
0x899: ("reinterpret_cast<void(*)()>(nullptr)();", {3}, {0, 6}),
0x86c: ("return 0;", {5}, {0, 4}),
0x933: ("int a(){", {5}, {0, 6}),
0x93a: ("{}", {5}, {0, 4}),
}

# 初始值
Original = [0x734, 0x0, 0x0, 0x0, 0xBBC, 0x0, 0xB63]
# 目标值
Dest = [0x14D, 0x2D7, 0x161, 0x2EA, 0x1B1, 0x2FD, 0x169]

counts = {code: Int(f"c_{code:x}") for code in code_map.keys()}

solver = Solver()

for code_variable in counts.values():
solver.add(code_variable >= 0)

# 给对应下标的全局数组数据进行++或--操作
for i in range(len(Original)):
equation = Original[i]

for code, (_, inc_indices, dec_indices) in code_map.items():
if i in inc_indices:
equation += counts[code]
if i in dec_indices:
equation -= counts[code]

solver.add(equation == Dest[i])

# 函数声明开头数量只能1
solver.add(counts[0x933] == 1)

if solver.check() == sat:
model = solver.model()

results = {}

for code, var in counts.items():
results[code] = model.evaluate(var).as_long()

with open(r"solution.cpp","w") as file:

# int a(){
file.write(code_map[0x933][0]+'\n')

# int b = 0;
int_count = results[0x852]
for i in range(int_count):
file.write(code_map[0x852][0]+str(i)+' = 0;\n')

# throw;
throw_count = results[0x8a2]
file.write((code_map[0x8a2][0]+'\n') * (throw_count))

# return 0;
ret_count = results[0x86c]
file.write((code_map[0x86c][0]+'\n') * (ret_count))

# reinterpret_cast<void(*)()>(nullptr)();
# throw命令包含一次Call命令,所以减去throw的数量
call_count = results[0x899]
file.write((code_map[0x899][0]+'\n') * (call_count - throw_count))

# {}
# 数量-1,因为整个函数的结尾}也占一个0x93a
trunk_count = results[0x93a]
file.write((code_map[0x93a][0]+'\n') * (trunk_count-1))

# last }
file.write("}\n")
else:
print("No solution found.")

最后得到的目标代码文件目录传到launch.exe即可输出Flag。

alt text

代码发送获得Flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from pathlib import Path

with open(r"solution.cpp","rb") as file:
code = file.read()

io = remote('alchemy-master.chals.sekai.team', 1337, ssl=True)

io.send(code)

io.sendline(b'__END__')

io.interactive()

io.close()

alt text