$ cat ~ / posts /reverse /ReverseCTF 1.8k Words ~ 10 Mins
cover.png
Reverse题记

#Reverse题记

exdoubled Lv5

有段时间没有写 CTF Reverse 题目了,在 AI 盛行的时代,手搓题目只能说是自己的兴趣了,在此记录一下以后手搓的 Reverse 题目,作为纪念吧

Attachments_505

找到 main 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
scanf("%s", Str);
if ( strlen(Str) != 49 )
{
puts_0("It's not enough.");
system_0("pause");
exit(0);
}
v10 = 0;
for ( i = 0; i <= 8; ++i )
{
for ( j = 0; j <= 8; ++j )
{
if ( !box[9 * i + j] )
{
v3 = v10++;
box[9 * i + j] = Str[v3] - 48;
}
}
}
check1();
check2();
check3();

判断输入字符串是否为 49

此处 v3v10 其实是一个变量,将对应的 Str[i] 减去 48 储存到 box 中,0 ASCII 码值为 48,实则为字符转数字,49长度的字符串转为每9个一行

check1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for ( i = 0; i <= 8; ++i )
{
for ( j = 1; j <= 9; ++j )
{
for ( k = 0; ; ++k )
{
result = (unsigned int)(char)box[9 * i + k];
if ( j == (_DWORD)result )
break;
if ( k == 8 )
{
printf_0("Wrong!!!Try again!!!");
system_0("pause");
exit(0);
}
}
}
}

每一行,需要满足 1-9 的数字都出现一次

check2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for ( i = 0; i <= 8; ++i )
{
for ( j = 1; j <= 9; ++j )
{
for ( k = 0; ; ++k )
{
result = (unsigned int)(char)box[9 * k + i];
if ( j == (_DWORD)result )
break;
if ( k == 8 )
{
printf_0("Wrong!!!Try again!!!");
system_0("pause");
exit(0);
}
}
}
}

同样的,每一列,需要满足 1-9 的数字都出现一次

check3

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
for ( i = 0; i <= 8; i += 3 )
{
for ( j = 0; j <= 8; j += 3 )
{
for ( k = 1; k <= 9; ++k )
{
v5 = 0;
v4 = 0;
while ( 1 )
{
result = (unsigned int)(char)box[9 * i + 9 * v5 + j + v4];
if ( k == (_DWORD)result )
break;
if ( v5 == 2 && v4 == 2 )
{
printf_0("Wrong!!!Try again!!!");
system_0("pause");
exit(0);
}
if ( ++v4 == 3 )
{
++v5;
v4 = 0;
}
}
}
}
}

为了检查每个 3x3 的小方格是否包含 1-9 的数字

那么 box 实际为一个满足数独要求的 9x9 的二维数组,输入的字符串长度为 49

实际上可以从 puts_0("Enjoy the beauty of reverse and sudoku!"); 看出这里是数独

最后输出:

1
2
3
for ( k = 0; k < strlen(Str); ++k )
putchar_0(Str[k] ^ magic[k]);
putchar_0(125)

magic 数组:

1
2
3
4
5
6
 .data:0000000000404080 magic           db 6Bh, 2, 66h, 70h, 44h, 69h, 7Eh, 6Eh, 43h, 4Ah, 78h
.data:0000000000404080 ; DATA XREF: main+196↑o
.data:000000000040408B db 4Ah, 6Dh, 60h, 56h, 0, 51h, 59h, 50h, 43h, 50h, 51h
.data:0000000000404096 db 6Dh, 74h, 2, 55h, 50h, 52h, 6Eh, 6Fh, 79h, 40h, 5Dh
.data:00000000004040A1 db 4Bh, 1Eh, 19h, 1Ch, 74h, 3, 54h, 7, 4Ch, 52h, 6Ah, 60h
.data:00000000004040AD db 50h, 58h, 40h, 58h, 0Fh dup(0)

64 位,即

1
6B 02 66 70 44 69 7E 6E 43 4A 78 4A 6D 60 56 00 51 59 50 43 50 51 6D 74 02 55 50 52 6E 6F 79 40 5D 4B 1E 19 1C 74 03 54 07 4C 52 6A 60 50 58 40 58 0F 00

同时可查询到 box 数组默认值

1
2
3
4
5
6
7
8
.data:0000000000404020 box             db 2 dup(0), 5, 2 dup(0), 4, 3, 6, 5 dup(0), 5, 2 dup(0)
.data:0000000000404020 ; DATA XREF: check1+69↑o
.data:0000000000404020 ; check2+69↑o ...
.data:0000000000404030 db 2, 4, 0, 4, 9, 6, 7, 4 dup(0), 1, 0, 6, 0, 2, 2 dup(0)
.data:0000000000404042 db 3, 0, 9, 2 dup(0), 7, 2 dup(0), 1, 0, 8, 0, 3, 3 dup(0)
.data:0000000000404052 db 5, 0, 9, 0, 2, 2 dup(0), 5, 0, 7, 2 dup(0), 9, 7, 0
.data:0000000000404061 db 4, 3 dup(0), 8, 3 dup(0), 9, 2 dup(0), 4, 3 dup(0)
.data:0000000000404070 db 6, 0Fh dup(0)

1
2
3
4
5
6
7
8
9
10
11
0 0 5 | 0 0 4 | 3 6 0
0 0 0 | 0 5 0 | 0 2 4
0 4 9 | 6 7 0 | 0 0 0
------+-------+------
1 0 6 | 0 2 0 | 0 3 0
9 0 0 | 7 0 0 | 1 0 8
0 3 0 | 0 0 5 | 0 9 0
------+-------+------
2 0 0 | 5 0 7 | 0 0 9
7 0 4 | 0 0 0 | 8 0 0
0 9 0 | 0 4 0 | 0 0 6

解数独

1
2
3
4
5
6
7
8
9
10
11
8 2 5 | 9 1 4 | 3 6 7
6 7 1 | 3 5 8 | 9 2 4
3 4 9 | 6 7 2 | 5 8 1
------+-------+------
1 8 6 | 4 2 9 | 7 3 5
9 5 2 | 7 6 3 | 1 4 8
4 3 7 | 1 8 5 | 6 9 2
------+-------+------
2 6 8 | 5 3 7 | 4 1 9
7 1 4 | 2 9 6 | 8 5 3
5 9 3 | 8 4 1 | 2 7 6

对应应该输入的字符串为:

1
8291767138932581849755263447186268341129653538127
1
2
3
4
5
6
7
8
9
# ./chall.exe
<--- moectf2021 --->
[A_game] Welcome to moectf2021.
Let's play a game!
Now input your answer, and if you are right, I will give you flag
input : 8291767138932581849755263447186268341129653538127
Congratulations!!!!
Enjoy the beauty of reverse and sudoku!
And here is your flag : moectf{S0_As_I_prAy_Un1imited_B1ade_WOrks---E1m1ya_Shiro}
1
moectf{S0_As_I_prAy_Un1imited_B1ade_WOrks---E1m1ya_Shiro}

Attachments_493

静态链接,C++ 代码编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::operator<<<std::char_traits<char>>(&_TMC_END__, "Welcome to MoeCTF 2020!\nPlease input your flag here >>", envp);
__isoc99_scanf("%22s", flag);
sub_0();
for ( i = 0; i <= 21; ++i )
{
v3 = (unsigned __int8)flag[i];
if ( (_BYTE)v3 != key[i] )
{
v4 = std::operator<<<std::char_traits<char>>(&_TMC_END__, "Ruaaaaa~Wrong!", v3);
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
return 0;
}
}
v6 = std::operator<<<std::char_traits<char>>(
&_TMC_END__,
"Congratulations!!!\nWhat you input here is the true flag!!",
v3);
std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);

输入了一个长度为 22 的字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall sub_0()
{
x = (byte_741B1 | y) ^ x & byte_741B1 ^ (2 * x);
y = (byte_741B1 | x) ^ y & byte_741B1 ^ (2 * y);
byte_741A9 = ~(6 - byte_741A9);
sub_1();
sub_1998();
return sub_1999();
}

__int64 __fastcall sub_1()
{
x = (byte_741A9 | y) ^ x & byte_741A9 ^ (2 * x);
y = (byte_741A9 | x) ^ y & byte_741A9 ^ (2 * y);
byte_741A9 = ~(-8 - byte_741A9);
sub_2();
if ( 2 * y + ((((_BYTE)x - 1) * (_BYTE)x) & 1) == (((((((_BYTE)x - 1) * (_BYTE)x) & 1) + 2 * y) | 1) == 1) )
exit(0);
sub_1996();
return sub_1997();
}

....

进行了多次加密,顺序为

1
sub_0 -> sub_1 -> sub_2 -> ... ->sub_1996 -> sub_1997 -> sub_1998 -> sub_1999

主函数主要是比较输入的字符串的和 key 字符串的每个字符是否相同,key 字符串为:

1
2
3
.data:0000000000074060 key             db 2, 0Ah, 6, 16h, 13h, 1Ch, 2, 4Dh, 41h, 28h, 3Dh, 56h
.data:0000000000074060 ; DATA XREF: main+5A↑o
.data:000000000007406C db 52h, 39h, 19h, 70h, 51h, 1Bh, 5Ah, 59h, 7Ah, 22h, 2 dup(0)

1
02 0A 06 16 13 1C 02 4D 41 28 3D 56 52 39 19 70 51 1B 5A 59 7A 22

通过动态调试知道第 k 个输入只影响输出 kk-1

我们知道 flag 字符串前面是固定的 meoctf{,所以可以尝试写 gdb_python 脚本爆破

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
import gdb, string

flag_addr = int(gdb.parse_and_eval('(char*)&flag'))
key_addr = int(gdb.parse_and_eval('(char*)&key'))
x_addr = int(gdb.parse_and_eval('(int*)&x'))
y_addr = int(gdb.parse_and_eval('(int*)&y'))
inf = gdb.selected_inferior()
key = bytes(inf.read_memory(key_addr, 22))

charset = string.ascii_letters + string.digits + '_{}-!@#$%^&*()+='
charset += ''.join(chr(i) for i in range(33, 127) if chr(i) not in charset)

def transform(s):
s = s.ljust(22, '\x00')[:22]
inf.write_memory(x_addr, (0xdeadbeef).to_bytes(4, 'little'))
inf.write_memory(y_addr, (0xdeadbeef).to_bytes(4, 'little'))
inf.write_memory(flag_addr, s.encode('latin1'))
gdb.execute('call (void) sub_0()', to_string=True)
return bytes(inf.read_memory(flag_addr, 22))

paths = ['m']
for j in range(21):
new = []
for p in paths:
if j + 1 < len('moectf{'):
candidates = 'moectf{'[j + 1]
elif j + 1 == 21:
candidates = '\x00'
else:
candidates = charset
for c in candidates:
s = p + c
if transform(s)[j] == key[j]:
new.append(s)
paths = new
print(j, paths[:5])

print(paths)

上面这个代码的逻辑就是不断尝试调用 sub0 来爆破 flag,如果下一位算出来的答案是 key[j] 那么就说明这个字符是正确的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pwndbg> source solution.py
0 ['mo']
1 ['moe']
2 ['moec']
3 ['moect']
4 ['moectf']
5 ['moectf{']
6 ['moectf{y']
7 ['moectf{y0']
8 ['moectf{y0u']
9 ['moectf{y0u_']
10 ['moectf{y0u_a']
11 ['moectf{y0u_a2']
12 ['moectf{y0u_a2e']
13 ['moectf{y0u_a2e_']
14 ['moectf{y0u_a2e_G']
15 ['moectf{y0u_a2e_G0']
16 ['moectf{y0u_a2e_G0d']
17 ['moectf{y0u_a2e_G0d~']
18 ['moectf{y0u_a2e_G0d~!']
19 ['moectf{y0u_a2e_G0d~!}']
20 ['moectf{y0u_a2e_G0d~!}\x00']
['moectf{y0u_a2e_G0d~!}\x00']

得到 flag

1
moectf{y0u_a2e_G0d~!}
$ discussion
# Comments
waline
On this page
Reverse题记