WDCTF2017

难得的线下赛还是解题的形式,在各方面的优势下成功的被队友带飞,最后做出了一个re,也算是有点成绩把。。


WDCTF2017

problem

此题下载下来会发现一个叫做Vige.exe的文件并且还有要给readme.txt,里面内容为:

1
2
3
4
5
6
7
8
find the flag when

the false key is:
QISVTH1UQT00

the text is:
HEFDSPADVDAGHRHFSSTHLEAFFEWOXGWAFDXNDFUWAERFUBVECADAFHJDSSDAWWFVAXACRZTADAWQTIZADAWBZQQYSBTAPXRAQWDQRAYAIQDWQFFSBTQSFWQRWYNVPADWWDQAWYNXOXVXMUOXCADWQQVXGTNWWQDQVAAQLTNWUWTSMVQDQFVNUUYTVKSJKOLUSUJRLUSFESWSSUJRLYRTIQKTRTIQGJYTYTUSRT

感觉似乎是一个改编的题目啥的?
查看程序,发现有点奇怪:

居然有两次输入的过程,我们用IDA大致看一下逻辑先;

跟踪到输入处理函数中,可以看到函数首先会把我们输入的字符进行&0x80000001,然后将当前字符串对应的字符取出来,并且从002B6438的位置开始,检测当前输入字符串的第i位是否为1:

在这个逻辑里面,我们可以确认几个事情:
当前的函数中存在一个全局数组,存放了当前处理的字符中第i位是否为0的函数。
可以看出来,此时函数将从计算得到的字符串从下列字符串表格中取出对应下标的字符
ZYXWVUTSRQPONMLKJIHGFEDCBA123456
其中计算的算法如下:

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
global_index = index % 5;
if ( index >= index % 5 + 5 ) // 检查当前字符串是否>10
{
v12 = index - 5;
do
{
v13 = 0;
for ( i = index; i > v12; v13 = v11 + 2 * v13 )
v11 = nums[i--];
v15 = &alphabet1;
if ( (unsigned int)dword_2B67B4 >= 0x10 )
v15 = (int *)alphabet1;
strpcy((int)&dword_2B6770, v11, *((_BYTE *)v15 + v13));
v11 = global_index;
index -= 5;
v12 -= 5;
}
while ( index >= global_index + 5 );
v9 = &alphabet1;
}
for ( j = 0; v11 > 0; j = v17 + 2 * j )
v17 = nums[v11--];
if ( (unsigned int)dword_2B67B4 >= 0x10 )
v9 = (int *)alphabet1;
strpcy((int)&dword_2B6770, v11, *((_BYTE *)v9 + j));
for ( k = 1; k <= 5 - global_index; ++k )
strpcy((int)&dword_2B6770, v18, 48);

逻辑为:把我们 输入的字符串转化为bit流,然后将输入的字符串本身按照5bit分组,每组在指定的alphaabet里面取出指定下标的字符串。

首先我们确认num是从打最后往最前放的,但是它处理的时候是从后开始处理

然后我们可以知道,如果要生成指定的false key,我们此时的key长度应该只有6个字符。

然后我们使用一个简单的程序来跑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ans = []
for each in false_key:
for i in range(len(alphabet)):
if alphabet[i] == each:
ans.append(i)

final = []
for each in ans:
t = bin(each)[2:]
final.append(t.zfill(5))

ans_bit2 =''.join(final) + '0'*6
for i in range(0, len(ans_bit2),8):
print(chr(int(ans_bit2[i:i+8],2)),end ='')

但是由于最后一位的处理方法不太一样,是直接选取的字符串剩下三bit,我们知道选取的是"T",也就是6,此时我们得到的对应字符串为"N",因此答案调整为:
LNCKEN
这里记录重要地址:
±-------±-----------------+
| 2B6770 | 处理后的key |
±-------±-----------------+
| 2B6758 | 处理前的key |
±-------±-----------------+

第一部分完成了,然后看第二部分:
第二部分有点长,但是大致也可以分析一下;
首先的逻辑为:

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
do
{
v6 = &our_input;
if ( (unsigned int)dword_2B67CC >= 16 )
v6 = (int *)our_input;
if ( *((_BYTE *)v6 + i) == '{' )
{
strpcy((int)&dst_string, dword_2B67CC, 'J');
}
else
{
v7 = &our_input;
if ( (unsigned int)dword_2B67CC >= 0x10 )
v7 = (int *)our_input;
if ( *((_BYTE *)v7 + i) == '_' )
{
strpcy((int)&dst_string, dword_2B67CC, 'K');
}
else
{
v8 = &our_input;
if ( (unsigned int)dword_2B67CC >= 0x10 )
v8 = (int *)our_input;
if ( *((_BYTE *)v8 + i) == '}' )
{
strpcy((int)&dst_string, dword_2B67CC, 'L');
}
else
{
v9 = &our_input;
if ( (unsigned int)dword_2B67CC >= 0x10 )
v9 = (int *)our_input;
strpcy((int)&dst_string, dword_2B67CC, *((_BYTE *)v9 + i));
}
}
}

大致看下来,就是说将【字符串中的{}和_进行替换,换成JKL,否则的话我们呢对字符串函数进行赋值】
然后就有一个骚操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  len = i + 1;
if ( i + 1 >= 1 )
{
v12 = i + 1;
do // 长度为1就复制一个,为2就复制2个
{
v13 = &alphabet1;
if ( (unsigned int)dword_2B67B4 >= 0x10 )
v13 = (int *)alphabet1;
strpcy((int)&dst_string, v10, *((_BYTE *)v13 + i));
--v12;
}
while ( v12 );
}
++i;
}
while ( len < 20 );

这一段将会计算当前的下标,然后计算当前字符串所属于的位置,然后【在当前第一个字符串之后复制1个Z,第二个字符串之后复制两个Y这样】,然后我们看到明文为230个字符这么长,那么我们大概可以猜到,我们输入的字符串的长度也要为这么长,通过计算可以得知此事的flag长度为20(在程序逻辑中也有体现)

HEFDSPADVDAGHRHFSSTHLEAFFEWOXGWAFDXNDFUWAERFUBVECADAFHJDSSDAWWFVAXACRZTADAWQTIZADAWBZQQYSBTAPXRAQWDQRAYAIQDWQFFSBTQSFWQRWYNVPADWWDQAWYNXOXVXMUOXCADWQQVXGTNWWQDQVAAQLTNWUWTSMVQDQFVNUUYTVKSJKOLUSUJRLUSFESWSSUJRLYRTIQKTRTIQGJYTYTUSRT

±---------±--------------+
| 002B67B8 | our_input |
±---------±--------------+
| 002B67D0 | dst_input |
±---------±--------------+
| 0146a2b8 | new_dst_input |
±---------±--------------+

然后我们可以得知当前最关键的替换内容为:

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
while ( len < 20 );
index_ = 0;
v15 = 0;
if ( dst_string_len )
{
v16 = dst_string_final;
v17 = dst_string;
do
{
mod = v15 % -6;
v19 = &input_string;
if ( (unsigned int)input_string_is_long >= 0x10 )
v19 = (int *)input_string;
chr = *((_BYTE *)v19 + mod);
dst_str = &dst_string;
num = chr - 'A';
if ( v16 >= 0x10 )
dst_str = (int *)v17;
dst_str_ = &dst_string;
if ( v16 >= 0x10 )
dst_str_ = (int *)dst_string;
*((_BYTE *)dst_str_ + index_) = num + *((_BYTE *)dst_str + index_);
v24 = &dst_string;
v16 = dst_string_final;
v17 = dst_string;
if ( (unsigned int)dst_string_final >= 0x10 )
v24 = (int *)dst_string;
if ( *((_BYTE *)v24 + index_) > 'Z' )
{
v25 = &dst_string;
if ( (unsigned int)dst_string_final >= 0x10 )
v25 = (int *)dst_string;
*((_BYTE *)v25 + index_) -= 26; // 小写转换成大写
v16 = dst_string_final;
v17 = dst_string;
}
++index_;
v15 = mod + 1;
}
while ( index_ < dst_string_len );
}

也就是说,最关键的比较逻辑为【将当前的字符串和我们原先得到的字符串中的明文对应字符串进行轮转相加,并且将得到的大写字母换成小写】
最后的比较函数的内容为:

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
signed int sub_2B15A0()
{
int v0; // eax@1
signed int v1; // eax@1
int i; // esi@1
int *dst; // ecx@2
int *text; // edx@2

v0 = input(std::cout, "Please input the text:");
std::basic_ostream<char,std::char_traits<char>>::operator<<(v0, sub_2B2260);
real_input(std::cin, (int)&text_string);
v1 = 0;
i = 0;
while ( 1 )
{
dst = &dst_string;
text = &text_string;
if ( (unsigned int)dst_string_final >= 0x10 )
dst = (int *)dst_string;
if ( (unsigned int)dword_2B67FC >= 0x10 )
text = (int *)text_string;
if ( *((_BYTE *)dst + i) != *((_BYTE *)text + i) )
break;
i += ++v1 + 1;
if ( v1 >= 20 )
return 0;
}
return 1;
}

可以看出,比较的过程中只会比较我们输入的字符串是否为正确的答案。因此我们直接拿到text的明文进行反向解密即可。这里我们给出解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "LNCKEN"
ans = ''
text = 'HEFDSPADVDAGHRHFSSTHLEAFFEWOXGWAFDXNDFUWAERFUBVECADAFHJDSSDAWWFVAXACRZTADAWQTIZADAWBZQQYSBTAPXRAQWDQRAYAIQDWQFFSBTQSFWQRWYNVPADWWDQAWYNXOXVXMUOXCADWQQVXGTNWWQDQVAAQLTNWUWTSMVQDQFVNUUYTVKSJKOLUSUJRLUSFESWSSUJRLYRTIQKTRTIQGJYTYTUSRT'
for i,each in enumerate(text):
temp = ord(each) - (ord(key[i%6]) - ord('A'))
if temp < 65:
temp += 26
ans += chr(temp)

while i < len(ans):
print(ans[i + j], end='')
i += 1
j += i
if i + j < len:
break

re

这个题目其实感觉是能够做出来的。。比赛到了后期没睡午觉实在是困的不行,根本没办法集中集成看程序啊。。就没能做出来。
这一题下了反调试。。。我们先好好理清楚一下思路好了
一开始有一个调用反调试的函数,看了一下对函数本身逻辑没有什么影响,直接patch了。
然后的逻辑里面会有一个检查输入的位置,其中可以知道我们的输入格式为WDFLAG{58个字符}
然后往后看,看到在后面的内容中有一个函数中有关键字maze,猜测是一个根据输入的字符串会走迷宫的一个程序。

顺着逻辑看下去,能够看到当前函数:

从当前逻辑中,我们能够推断出当前maze的大小为21*21,总共大小为441

1
2
3
+-----------+---------------+
| B84018 | 迷宫的起始地址 |
+-----------+---------------+

然后我们找到了迷宫的核心逻辑:

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
row = 1;
for ( clos = ::clos; i < len; ++i )
{
chr = str_temp[i];
if ( (unsigned __int8)(chr - '0') > 9u ) // 如果比0大,就视为是使用字符行走,否则视为使用数字行走
{
if ( (unsigned __int8)(chr - 'a') > 5u ) // 字母行走的话,不能比'g'更大(包括g)
puts("get_next error");
step = chr - 'W';
}
else
{
step = chr - '0';
}
v13 = step & 3; // 我们的低2bit和高2bit表示不一样
v12 = ((unsigned int)step >> 2) & 3;
o = 0;
do
{
v9 = *(&v12 + o);
row += *((_DWORD *)&row_temp + v9);
clo += *((_DWORD *)&clos + v9);
if ( row > 21 || clo > 21 || *(&maze[22 * row] + clo) & 1 )
puts("run maze error");
++o;
}
while ( o < 2 );
str_temp = str;
}
v10 = *(&maze[22 * row] + clo);//记录下此时步行到达的位置

从核心逻辑中可以看出,我们的起始地址为(1,0),然后我们通过输入字符串,根据要求形成特定的step,然后在step内行走,在行走的过程中将会根据我们步行到达的位置,决定此时我们的行走路线。然后我们可以看一下此时的迷宫:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
D6 29 EF 19 58 BF 10 41 73 C4 68 96 3A 06 DE A6 84 AD 91 2D 46 00 
7D B8 D1 B6 11 94 BE 7B 21 79 6A CD A7 56 CF D1 60 74 CF BD B0 00
C5 25 41 FC 5E AA D0 33 75 63 08 DD CA C6 34 D5 34 E0 AE BF 9E 00
EE 27 7D 2C 0F 37 C9 29 18 F6 6B 05 B9 F3 72 2D 63 46 54 40 7C 00
27 7B 17 52 53 E4 87 73 5F A6 07 E0 9E BE 71 84 02 C1 0A 2D 79 00
4D 83 BF 43 2D F5 0B BD A2 BC B0 AA 99 F1 39 27 39 08 82 29 6A 00
1A 54 A8 6E B5 5D F1 D5 4F DD 1F 0E 43 1F 97 36 97 C7 50 A9 F5 00
C4 A0 48 A6 21 85 6A 4B BA 06 F2 A0 49 21 D8 AA 38 77 D7 61 70 00
1F FD 6E 02 1C CA 19 C7 F3 FD 69 4D B1 C8 DA FA 04 42 47 5C 9F 00
4F E6 6B F2 8B 1E 54 32 88 FF AE 18 5B 44 D6 CF B6 27 E6 E4 63 00
43 12 C1 2A C7 F0 43 66 AA 5A 86 B9 11 70 2B AB 2C C4 E1 A6 DD 00
FF 2D 04 66 33 E6 F6 23 99 A6 1F D9 74 2B 52 6E AF 54 FD 94 4A 00
3D 8F D6 43 DA BC 19 9F 9A C1 D9 B1 DF 7A A2 EB DC C8 15 85 3F 00
5F 85 E7 93 1D 33 E5 75 62 29 0D A3 7F E9 2A B2 E0 D9 9B 6D 33 00
0D 8D 72 74 F6 BC A5 FE 06 C9 46 A9 DB 0A C8 C7 07 36 56 40 2B 00
03 24 C8 E2 08 1E FA C1 B0 DF E7 AC AB F4 86 BC C2 EF FE 64 E8 00
8A 47 28 7F CE C8 91 9E EE 3E 7C 9D 2D 9F 08 0B 4E 52 C5 D2 80 00
4B 78 F7 EF DE 51 22 35 B4 F5 39 5A 22 0D 11 50 9E E1 A9 26 2A 00
53 6B D6 F7 DB FC 81 D5 A7 0F 91 42 1F CC F9 8D 32 2A A4 08 9D 00
3D 6F 89 73 EF 24 F6 EA E8 BA 5E 09 7B A1 B0 6D 74 D9 7A C0 00 00
73 93 C5 23 C2 A0 1B 69 5F 83 44 E4 98 B1 BB 9B 53 A1 82 5E F8 00

可以看到,函数在最后有一个记录当前节点的过程,可以看出,此时答案的结果就是一个指定的值。

于是直接上动态调试:

1
2
3
+-----------+---------------+
| 0079FC8C | 输入字符串 |
+-----------+---------------+

然后在
009E9000
处会存放当前的地址
在程序的开始有一个字符串的处理函数:

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
  v8 = dst;
v7 = input;
if ( len & 1 )
{
result = 0;
}
else
{
i = 0;
if ( len >> 1 )
{
while ( 1 )
{
v5 = strchr("BCDFGHJKMPQRTVWXY2346789", *(_BYTE *)(dst + 2 * i));
v6 = strchr("BCDFGHJKMPQRTVWXY2346789", *(_BYTE *)(v8 + 2 * i + 1));
if ( !v5 || !v6 )
break;
dst = v8;
input = v7;
*(_BYTE *)(i++ + v7) = (unsigned int)&Str[-(_BYTE)v6 + 23] | 16
* ((_BYTE)v5
- (unsigned int)"BCDFGHJKMPQRTVWXY2346789");
// int t = v5 - &Str; 也就是当前字符串为开始第几个
// input[i++] = t | len - (23 + v6 - &Str); 如果这样处理的话,似乎无论如何都不能让当前的

if ( i >= len >> 1 )
goto LABEL_7;
}
result = 0;
}
else
{
LABEL_7:
*(_BYTE *)(i + input) = 0;
result = input;
}
}
return result;
}

换句话说,这里的功能是说【单数位置上的字符在符号表中的下标作为高四位,双数位置上的字符 23 - 在符号表中的下标 为低四位组成新的字符】
然后我们跟踪一下看一看我们的字符串将要如何被使用:

1
2
3
4
5
6
7
8
9
10
if ( (unsigned __int8)(chr - '0') > 9u )    // 如果比0大,就视为是使用字符行走,否则视为使用数字行走
{
if ( (unsigned __int8)(chr - 'a') > 5u ) // 字母行走的话,不能比'g'更大(包括g)
puts("get_next error");
step = chr - 'W';
}
else
{
step = chr - '0';
}

这一段之前提到过,是迷宫的移动过程。不过这里我们要提一下,由于我们知道我们的字符串并不是由一开始确定的,而是要取出字符串的高4bit和低4bit进行或处理,此时得到的值要满足其大小在[‘0’ - ‘h’]之间,不然的话此时的得到的数字将很难达到这个大小。然后我们可以知道,由于这个字符串表的长度为17,也就是说【第一位字符至少也要是从第3个开始才行】,不然的话连0都不会大于。。。

然后目前我们需要知道以下数据:

  • 在运算过程中产生的迷宫
  • 处理后的字符后哪些操作分别表示 ↑ ↓ ← →
  • 运算结束后,所寻找的重点究竟是哪个

首先我们找到加密后的迷宫变成了什么样子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
19 FB CB 7D 75 45 39 B1 27 9B 85 73 19 F9 3D F9 E9 CB 2B C1 9B 00 
40 68 20 1A 3A 10 40 6C B1 72 E8 D2 80 74 B9 46 CA 68 B2 20 23 00
89 81 A3 4C 5F FB 29 2B 1F AE F7 56 EF 8D 1D 13 67 C0 BD 34 3F 00
BB F4 4D C6 D0 10 E5 E4 6E 18 E9 9C 78 28 2B D0 A0 8E 1F C4 24 00
F1 12 73 5E 57 81 9B 33 53 9E 95 A9 5B 47 3F 91 A7 B6 E9 D2 77 00
D7 DA 3A D2 A0 64 94 20 B3 42 2D FC 67 22 67 96 7B F4 A1 FE EB 00
69 19 F9 CE 6B 2D 47 67 B1 22 35 D6 BD 50 15 1A 11 90 2D FF 1B 00
93 B2 FA 8C B2 90 EE 9C 12 F8 5E CE 86 C4 60 F2 69 12 15 A6 2D 00
57 5D C7 A5 57 EE 71 85 23 52 8D AD 45 8F 1D 1B ED B4 9D 4A 55 00
D1 9A F5 38 EE 44 17 26 FC 46 CE 7C 67 E4 71 B6 7C F2 FB EA BD 00
BD 38 A7 FA 93 29 B5 81 E1 67 83 75 31 38 E5 E0 A7 E1 C7 FC 25 00
33 A4 86 94 31 9E 1C B4 62 9A 4A 7A 16 EE 2A CA C6 1E 52 62 95 00
21 B5 7D D6 1B 43 6F 54 7D A5 63 23 51 61 A1 EA 51 FE 11 97 45 00
47 EA F8 54 21 52 E3 66 B5 EE C0 B4 50 E2 FF 3A B5 7A 69 DC 21 00
D1 D7 2F 1E 71 22 E5 3E FB 82 FF EB 79 F8 EF 95 95 1F 1D B0 ED 00
AD 96 FE AE FB 4C 6A C8 42 36 F6 AE 91 6A 92 42 BE D6 5C 8E E9 00
FB 88 F5 4E C1 FE 35 4F CB 4C 25 2B A1 35 8B 94 BF 34 89 3E AD 00
F1 6E 11 D0 44 3E A4 1E 1D 16 72 6E AC AE 4F 4A 3D C4 81 FC F7 00
3D F3 1F 14 73 49 A3 1C 89 CD 3B AA 41 DD 73 D1 57 96 ED 89 27 00
25 B6 EA 3C D7 88 1C FA E9 7A 40 FC 6E BA 3D D2 18 72 6E C0 C7 00
53 55 E5 D5 DB A3 7F E3 53 B7 8F 89 6B 3F 69 69 2D B5 AD 49 7B 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

然后我们确认需要前进:

  • 0 – ↑
  • 1 – →
  • 2 – ↓
  • 3 – ←

最后我们确认一下最后的迷宫,发现在复杂的变化之后会变回原来的样子。此时取出来的值为0x40,也就是说,我们的目的地是0x40^100 = 0x24,这个位置的坐标即为(3, 20)。然后我们发现,我们走迷宫的时候,只能够走【偶数】,那么我们可以走的路线规划一下就是:

大概是60步就能够走过去(不过注意到起点在(1,0))。。此时计算一下01串就能够得到:

1
01010110101010101001011010111110101010101010100101000001010000000001010101010101010000010100000000000000000101101001

这个就是到达目的地的前进路线。

回溯算法,我们得知数字的来历为

"BCDFGHJKMPQRTVWXY2346789"

这个字母表中下标为2n的再字母表中出现第i个字符的下标作为高四位,和2n+1出现在第17-i个的顺序组成的数字作为低四位即为我们目标数字。后来发现有一个陷阱:

1
2
3
4
5
6
7
8
9
10
11
// v6为我们转换过的数字
if ( (unsigned __int8)(v6 - 0x30) > 9u )
{
if ( (unsigned __int8)(v6 - 0x61) > 5u )
puts("get_next error");
v7 = v6 - 0x57; // 这里要减去01010111
}
else
{
v7 = v6 - 0x30; // 这里居然要剪掉b00110000,再允许的范围内的值很少啊
}

这里可以看到,我们输入的字符串处理之后得到的数字范围其实是有限的:
如果要符合v6 - 0x30 > 9的话,我们能够使用的数字只有

1
2
[0x30 - 0x39] // 因为有符号,所以只有这部分不会发生反转
00110000, 00110001,00110010,00110011,00110100,00110101,00110110,00110111,00111000

否则的话,就会进入到下一个判断v6 - 0x61 > 5 ,其中有效范围内的数值为;

1
2
3
4
[0x61 - 0x66]
01100001,01100010,01100011,01100100,01100101,01100110
减去0x57之后
1010,1011,1100,1101,1110,1111

然后发现这个地方会把高位的bit去掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
v13 = v7 & 3;								// 低2bit
v12 = ((unsigned int)v7 >> 2) & 3; // 高2bit
v8 = 0;
do
{
v9 = *(&v12 + v8);
row += *((_DWORD *)&v16 + v9); // 取出第二次算的值,分别作用在row和clo上
clo += *((_DWORD *)&i + v9);
if ( row > 0x15 || clo > 21 || *(&byte_B84018[22 * row] + clo) & 1 )
puts("run maze error");
++v8;
}
while ( v8 < 2 );

换句话说,此时【只有低位的4bit可以参与到走迷宫中来】。比如说,我们的第一部分的内容是【两次向右行走】,那么这里我们可以考虑让答案为0x35,那么此时使用字符F3应该可以打到目的,测试一下发现的确可以,相当于是说利用F来保证高位为0011,然后低位的话就是我们此时需要的内容。
以下为解题脚本:

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
#	-*- coding:utf-8 -*-

right = '01'
left = '11'
up ='00'
down = '10'
step = right*3 + down*6 +right*2 + down*2 + left*2 + 8*down + \
right*2+up*2+right*2+4*up + 8*right +2*up+2*right+8*up+2*right+2*down+right+left+right
print("step is {}".format(len(step)/8))
alphabet = "BCDFGHJKMPQRTVWXY2346789"
nums = [int(step[i:i+8], 2) for i in range(0,len(step), 8)]
ans = []

def checknum(num, step = 0x30):
# first, we now what number now is
tmp = step + num
# then, we check high bit and low bit
bit = bin(tmp)[2:].zfill(8)
high = int(bit[:4], 2)
low = int(bit[4:],2)

# finally, check character we need
return alphabet[high], alphabet[0x17 - low]

for each in nums:
# we shuold ensure that this operation is satisfied 0x30, else we use 0x57
tmp = bin(each)[2:].zfill(8)
# print(tmp)
i = int(tmp[:4], 2)
if i > 9:
padding, char = checknum(i, 0x57)
else:
padding, char = checknum(i, 0x30)

ans.append(padding)
ans.append(char)

i = int(tmp[4:], 2)
if i > 9:
padding, char = checknum(i, 0x57)
else:
padding, char = checknum(i, 0x30)

ans.append(padding)
ans.append(char)

print(len(ans))
# tmp_ans = "F3F2J8J8FWF2J7J3J8J8J8FWF4F8F4F9F8F3F3F3F4F8F4F9F9F9F8F2FW"
# because finally two use to let's it can be 8 times, so we do not need last two
print("flag is WDFLAG{"+''.join(ans)[:-2]+"}")

得到flag为:

WDFLAG{F3F2J8J8FWF2J7J3J8J8J8FWF4F8F4F9F8F3F3F3F4F8F4F9F9F9F8F2FW}