Translation
This post is also available in
Simplified Chinese.
Check-in
Press F12 in Chrome to open Developer Tools. The input field has the following attribute:
1
| <p>Key: <input type="text" name="key" maxlength="13"/></p>
|
Remove the maxlength attribute, then enter hackergame2018.
flag: flag{Hackergame2018_Have_Fun!}
Kitty Quiz
These are straightforward questions whose answers can be found with Google:
1
| 1958 9211B026 9 TP311.1/94 3A202
|
Submit them to obtain the flag. (Believe it or not, I solved this one on my phone and even watched the video all the way through.)
flag: flag{G00G1E-is-always-YOUR-FRIEND}
Fairground Stamp Card
Just assemble the pieces in Photoshop. (I was too lazy to fit the final piece.)

flag: flag{H4PPY_1M4GE_PR0CE551NG}
Kitty and the Keyboard
Apparently, once one line is reconstructed, its ordering can be used to rebuild the other lines. I did not spot that trick and reconstructed them one by one. Worse, the definitions of the three macros ABC, BAC, and CAB also have to be rearranged, which held me up for quite a while. The final section is shown below. Once everything is assembled, compile and run it with g++.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #define ABC "FfQ47if9Zxw9jXE68VtGA"
#define BAC "JDk6Y6Xc88UrUtpK3iF8p"
#define CAB "7BMs4y2gzdG8Ao2gv6aiJ"
int main()
{
def_typed_printf(f_l_x_g_1, "%s%s%s%s");
f_l_x_g_1("fl")("a")("g")("{");
def_typed_printf(a_a_a_a_a_a_a_a_a, "%s%s%s%s%s%s%d");
a_a_a_a_a_a_a_a_a(ABC)("")(BAC)("")(CAB)("")('}');
def_typed_printf(def_typed_printf_, "%s%d%s");
def_typed_printf_("typed_printf")('_')("}");
return 0;
}
|
flag: flag{FfQ47if9Zxw9jXE68VtGAJDk6Y6Xc88UrUtpK3iF8p7BMs4y2gzdG8Ao2gv6aiJ125typed_printf95}
Word Document
As everyone knows, a Word document is a ZIP archive. Rename and extract it, and you will find flag.txt inside:
1
| $ cat flag.txt | tr -d '\n'
|
flag: flag{xlsx,pptx,docx_are_just_zip_files}
Obsidian Browser
The page requires the Obsidian browser. HEICORE hints at a notoriously expensive Chrome skin. Using HEICORE directly as the User-Agent did not work, but an online search turned up its official site. Registration there also demanded the Obsidian browser. The response time suggested that the User-Agent check happened locally, so the JavaScript had to contain HEICORE’s actual UA. Opening Developer Tools triggered anti-debugging, but the previously loaded index.html was still available in the Network panel:

With the UA in hand, curl does the rest:
1
| $ curl -H 'User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36' http://202.38.95.46:12001/
|
flag: flag{H3ic0re_49.1.2623.213_sai_kou}
Back to the Past
After a few guesses, I found the right sequence. The initial q ed exits and then starts ed again. Once inside ed, replay the recorded keystrokes, inserting w flag before the final q to save the result, then inspect the flag. One recorded Esc+C clears the screen; it can simply be ignored.
flag: flag{t4a2b8c44039f93345a3d9b2}
Who Am I? :: Philosophical Reflections
Developer Tools shows the response status 418 I'M A TEAPOT. This status was defined by a classic April Fools’ RFC about controlling coffee and tea pots over HTTP. The answer is therefore teapot, which yields the flag.
flag: flag{i_canN0t_BReW_c0ffEE!}
Who Am I? :: Can I Help Me?
RFC 7168 defines two methods for requests to a teapot: POST and BREW. POST would be too ordinary, so send a BREW request:
1
2
3
4
5
6
7
8
9
10
| from pwn import *
def main():
msg='BREW /the_super_great_hidden_url_for_brewing_tea/ HTTP/1.1\r\nContent-type: message/teapot\r\nHost: 202.38.95.46:12005\r\n\r\n'
r=remote('202.38.95.46',12005)
r.send(msg)
print r.recvall()
if __name__=='__main__':
main()
|
The response says this teapot can brew only black tea. Add black_tea to the request path and send it again to obtain the flag.
flag: flag{delivering_tea_to_DaLa0}
Kitty Remote Control
Other people used canvas to draw the image; I simply assembled character art with Python, opened it in VS Code, zoomed out with Ctrl+- and inspected it by eye:

flag: flag{MeowMeow}
Her Poem
This one was particularly devious. I spent a long time searching the decoded poem for steganography. The original poem.txt uses uuencode, where the first character of each line represents its decoded length. Here that character was deliberately set to a value slightly shorter than the original line. Python therefore truncated the end of every decoded line, including the flag. Another uuencode implementation reveals the discarded bytes:

flag: flag{STegAn0grAPhy_w1tH_uUeNc0DE_I5_50_fun}
Kitty Nemesis
Evaluate each expression sent by the server, after replacing several troublesome constructs. (Fortunately it never sent anything like __import__('os').system('rm -rf ~').) My pwntools installation used Python 2, but the script works under Python 3 after updating print. The final error message contains the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| from pwn import *
import re
def main():
r = remote('202.38.95.46', 12009)
r.recvuntil('nds\n')
while True:
c = r.recvuntil('\n')
print c
c = re.subn(r'exit\(\)', '0', c)[0]
c = re.subn(r'__import__\(\'time\'\).sleep\(\d*\)', '0', c)[0]
c = re.subn(r'print\(\'[a-z0-9\\x]*\'\)', '0', c)[0]
c = re.subn(r'__import__\(\'os\'\).system\(\'find ~\'\)', '0', c)[0]
print c
print eval(c)
r.sendline(str(eval(c)))
if __name__ == '__main__':
main()
|
flag: flag{'Life_1s_sh0rt_use_PYTH0N'*1000}
Kitty Circuit
This is a redstone circuit. Work backward from the beacon. After solving it, I got distracted and kept playing for more than an hour.
flag: flag{0110101000111100101111111111111111111010}
FLXG’s Secret :: A Message in a Bottle from the Future
The story is wonderfully imaginative. There are several competing orders for the hexagrams; after trying a few, I finally found the correct one:
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
| import base64
txt = open('flxg.txt', encoding='utf-8').read()
gua64 = ['坤', '剥', '比', '观', '豫', '晋', '萃', '否', '谦', '艮', '蹇', '渐', '小过', '旅', '咸', '遁',
'师', '蒙', '坎', '涣', '解', '未济', '困', '讼', '升', '蛊', '井', '巽', '恒', '鼎', '大过', '姤',
'复', '颐', '屯', '益', '震', '噬嗑', '随', '无妄', '明夷', '贲', '既济', '家人', '丰', '离', '革', '同人',
'临', '损', '节', '中孚', '归妹', '睽', '兑', '履', '泰', '大畜', '需', '小畜', '大壮', '大有', '夬', '乾']
b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
out = ''
assert(len(b64) == 64)
i = 0
l = len(txt)
while i < l:
if txt[i] in gua64:
out += b64[gua64.index(txt[i])]
i += 1
elif txt[i:i+2] in gua64:
out += b64[gua64.index(txt[i:i+2])]
i += 2
else:
print(txt[i:i+2])
raise ValueError
dec_out = base64.b64decode(out.encode('ascii'))
print(dec_out)
open('flxg_dec', 'wb').write(dec_out)
|
The flag is at the end of the file. Extract the resulting file with tar. Unfortunately, I did not reverse the series of locks that followed.
flag: flxg{Power_of_the_Hexagram}
FLXG’s Secret :: An Incomprehensible Secret
I did not solve this one. Nothing to see here.
C Programming Homework
IDA shows that __err can execute a command that does not contain sh, and that it is registered as the handler for several signals. We therefore need to trigger one of them. Entering -2147483648/-1 (-0x80000000/-1) produces 2147483648 (0x80000000), which cannot be represented by an int, triggering SIGFPE. Inside __err, launch Vim. Running :! cat /flag reveals that the flag is stored in -; then :! cat /- displays it:

flag: flag{816484e67b21efd5de8f1661d180a007}
Encryption and Decryption Algorithms
A simple Brainfuck parser (src contains the Brainfuck program):
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
| a = open('src').read()
ptr = 0
addsum = 0
indent = 0
decsum = 0
for ch in a:
if ch == '+':
addsum += 1
continue
if ch == '-':
decsum += 1
continue
if addsum != 0:
print(' '*indent+'*p+=%d;' % addsum)
addsum = 0
if decsum != 0:
print(' '*indent+'*p-=%d;' % decsum)
decsum = 0
if ch == ',':
print(' '*indent+'*p=getchar();')
if ch == '.':
print(' '*indent+'print(*p);')
if ch == '>':
print(' '*indent+'p++;')
ptr += 1
if ch == '<':
print(' '*indent+'p--;')
ptr -= 1
if ch == '[':
print(' '*indent+'while(*p){')
indent += 1
if ch == ']':
print(' '*(indent-1)+'}')
indent -= 1
|
The program consists roughly of input processing followed by output. Split the input-processing section at its ten calls to getchar(). For each input character it runs two while loops; the first pair looks like this:
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
| *p=getchar();
while(*p){
*p-=1;
p++; p++; *p+=7;
p++; p++; *p+=5;
p++; p++; *p+=3;
p++; p++; *p+=2;
p++; p++; *p+=4;
p++; *p+=6;
p--; p--; *p+=3;
p--; p--; *p+=9;
p--; p--; *p+=8;
p--; p--; *p+=6;
p--; p--; *p+=8;
p--;
}
p++;
while(*p){
*p-=1;
p++; p++; *p+=5;
p++; p++; *p+=9;
p++; p++; *p+=9;
p++; p++; *p+=2;
p++; p++; *p+=4;
p--; *p+=9;
p--; p--; *p+=4;
p--; p--; *p+=4;
p--; p--; *p+=2;
p--; p--; *p+=2;
p--;
}
p--;
|
The input character is stored in a[0]. In the first loop, the pointer returns to a[0] after every iteration, so the loop can be simplified to:
1
2
3
4
5
6
7
| a[0]=getchar();
while(a[0]){
a[0]-=1; a[2]+=7; a[4]+=5;
a[6]+=3; a[8]+=2; a[10]+=4;
a[11]+=6; a[9]+=3; a[7]+=9;
a[5]+=8; a[3]+=6; a[1]+=8;
}
|
The pointer likewise returns to its starting position after each iteration of the second loop, so it can also be simplified. Note that between the two loops the pointer moves from a[0] to a[1]:
1
2
3
4
5
6
| while(a[1]){
a[1]-=1; a[3]+=5; a[5]+=9;
a[7]+=9; a[9]+=2; a[11]+=4;
a[10]+=9; a[8]+=4; a[6]+=4;
a[4]+=2; a[2]+=2;
}
|
The pointer then returns to a[0], reads the next character, and runs another pair of loops until all ten characters have been consumed. The output section prints a[2] through a[11]. Each is a linear combination of the ten inputs—in other words, this is a Hill cipher. A script can extract the coefficient matrix; the task then becomes solving the linear congruence system Ax = b (mod 64). Since 256 is a multiple of 64, reducing directly modulo 64 works here.
I did not know how to model this in Z3, so I computed the inverse matrix with Gaussian elimination. Place coefficient matrix A beside identity matrix E; row-reduce A to the identity, and the adjacent E becomes A’s inverse: [A|E] -> … -> [E|A^-1]. To invert a matrix modulo 64, replace division during Gaussian elimination with multiplication by the divisor’s modular inverse modulo 64. Multiplying the ciphertext vector by the resulting inverse yields the plaintext vector. Remember to account for the constants added by the output stage:
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
| EP = [[23, 69, 40, 61, 47, 21, 62, 73, 18, 81],
[46, 67, 40, 54, 31, 23, 54, 75, 64, 69],
[21, 80, 63, 33, 60, 26, 39, 32, 48, 39],
[80, 27, 69, 53, 37, 81, 24, 61, 23, 50],
[35, 22, 66, 43, 68, 36, 67, 22, 58, 37],
[81, 64, 51, 46, 37, 44, 75, 77, 71, 18],
[34, 79, 74, 52, 27, 19, 38, 79, 30, 68],
[19, 38, 52, 72, 49, 71, 36, 40, 60, 45],
[76, 55, 41, 68, 39, 62, 48, 65, 21, 66],
[38, 78, 43, 59, 55, 74, 50, 18, 36, 77]]
INV = [0 for i in range(64)]
for i in range(64):
for j in range(64):
if (i*j) % 64 == 1:
INV[i] = j
break
EXT = [EP[i]+[1 if i == j else 0 for j in range(10)]for i in range(10)]
def show(martix):
for i in range(10): print(martix[i])
def add(martix, k, line1, line2):
for i in range(20): martix[line2][i] = (k*martix[line1][i]+martix[line2][i]) % 64
def mul(martix, x, line):
for i in range(20): martix[line][i] = (martix[line][i]*x) % 64
def swap(martix, line1, line2):
for i in range(20): martix[line1][i], martix[line2][i] = martix[line2][i], martix[line1][i]
def mulline(martix, x):
y = [0]*10
for i in range(10):
for j in range(10): y[i] += martix[i][j]*x[j]
y[i] = (y[i]) % 64
return y
for i in range(10):
if INV[EXT[i][i]] == 0:
for k in range(i+1, 10):
if INV[EXT[k][i]] != 0:
swap(EXT, i, k)
break
else: raise ValueError
mul(EXT, INV[EXT[i][i]], i)
for j in range(10):
if j == i: continue
add(EXT, (-EXT[j][i]) % 64, i, j)
DP = [EXT[i][10:] for i in range(10)]
BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
ADD = [2, 6, 8, 8, 3, 5, 5, 7, 4, 9]
def enc(s):
sin = [BASE64.find(s[i]) for i in range(10)]
sout = mulline(EP, sin)
return ''.join([BASE64[sout[i]+ADD[i]] for i in range(10)])
def dec(s):
sin = [(BASE64.find(s[i])-ADD[i]) % 64 for i in range(10)]
sout = mulline(DP, sin)
return ''.join([BASE64[sout[i]] for i in range(10)])
def main():
cipher = ['JzRVPiVpqo', '4iDM8celyu', 'eIs4ff4DKe', 'G3EMKihzuH']
print('flag{%s}' % (''.join(map(dec, cipher))))
if __name__ == '__main__':
main()
|
(As an aside, one ZJU School Bus challenge used exactly the same solution.)
flag: flag{h1ll-c1ph3r-w1th-10x10-r3v3rs1bl3-matr1x}
Her Gift
The challenge implies that the flag can be computed after entering the tenth line. IDA shows that sleep(2u) and system("echo -en '\a' > /dev/tty5") in main do not affect the calculation. Patch each call _system and call _sleep into five NOPs. The modified program stops again after ten seconds, this time because of alarm(0xAu) in sub_401260. Patch call _alarm in the same way and run it again to obtain the flag. (I did not bother removing the lyric output.)
1
| $ ./gift_patch2 "However, someday, someone will find it." | tail
|
flag: flag{HowEVER,_Somedaj,_sOMe0NE_wILl_FiND_it.}
The Puzzling flxg Program
Windows reversing can be confusing. IDA reveals that the apparent main function is a decoy. Beneath several output strings in .rdata, at .rdata:00000001400054D8, lies data that clearly resembles an encoded flag. Cross-references lead to sub_140004980. Reversing it shows that the function Base64-encodes the input, reverses it with strrev, XORs successive bytes with 0, 1, 2, and so on, then compares the result with that data. Reverse those operations to recover the flag (which looks almost fake):
1
2
3
4
5
6
7
8
| import base64
a = '\x39\x65\x45\x54\x77\x5F\x34\x5F\x64\x5F\x66\x68\x3C\x34\x58\x55\x7F\x43\x21\x4B\x7F\x20\x43\x76\x5F\x20\x4C\x4D\x7A\x53\x70\x7D\x56\x4D\x65\x47\x4C\x5D\x71\x43\x18\x6F\x47\x48\x42\x18\x1C\x4D\x74\x45\x01\x69\x00\x4D\x5B\x6D'
b = ''
for i in range(len(a)):
b += chr(ord(a[i]) ^ i)
c = b[::-1]
print(base64.b64decode(c.encode('ascii')))
|
flag: flxg{Congratulations_U_FiNd_the_trUe_flXg}
Some Universal Truths
The task requires verifying 80 proofs. In the forked source repository, the example program verifies a proof by calling verify_proof(keypair.vk, *proof, h2_bv, h1_bv, x_bv). Following that example, I wrote a verifier of my own (rough, but sufficient):
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
| #include <stdlib.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <boost/optional/optional_io.hpp>
#include "snark.hpp"
#include "test.h"
using namespace libsnark;
using namespace std;
int main(int argc, char const *argv[])
{
int ress[80];
default_r1cs_ppzksnark_pp::init_public_params();
r1cs_ppzksnark_verification_key<default_r1cs_ppzksnark_pp> vk_in;
ifstream vk_stream("toolkit/vk");
stringstream vkf;
if (vk_stream) { vkf << vk_stream.rdbuf(); vk_stream.close(); }
vkf >> vk_in;
std::vector<bool> h1_bv(256), h2_bv(256), x_bv(256), r1_bv(256), r2_bv(256);
{
h1_bv = int_list_to_bits({169, 231, 96, 189, 221, 234, 240, 85, 213, 187, 236, 114, 100, 185, 130, 86, 231, 29, 123, 196, 57, 225, 159, 216, 34, 190, 123, 97, 14, 57, 180, 120}, 8);
h2_bv = int_list_to_bits({253, 199, 66, 55, 24, 155, 80, 121, 138, 60, 36, 201, 186, 221, 164, 65, 194, 53, 192, 159, 252, 7, 194, 24, 200, 217, 57, 55, 45, 204, 71, 9}, 8);
x_bv = int_list_to_bits({122, 98, 227, 172, 61, 124, 6, 226, 115, 70, 192, 164, 29, 38, 29, 199, 205, 180, 109, 59, 126, 216, 144, 115, 183, 112, 152, 41, 35, 218, 1, 76}, 8);
r1_bv = int_list_to_bits({180, 34, 250, 166, 200, 177, 240, 137, 204, 219, 178, 17, 34, 14, 66, 65, 203, 6, 191, 16, 141, 210, 73, 136, 65, 136, 152, 60, 117, 24, 101, 18}, 8);
r2_bv = int_list_to_bits({206, 64, 25, 10, 245, 205, 246, 107, 191, 157, 114, 181, 63, 40, 95, 134, 6, 178, 210, 43, 243, 10, 217, 251, 246, 248, 0, 21, 86, 194, 100, 94}, 8);
}
for (int i = 1; i < 80; i++) {
r1cs_ppzksnark_proof<default_r1cs_ppzksnark_pp> proof;
stringstream pfns;
pfns << "toolkit/proof_" << i;
std::string pfn = pfns.str();
ifstream pfis(pfn);
pfis >> proof;
bool res = verify_proof(vk_in, proof, h1_bv, h2_bv, x_bv);
ress[i] = res;
}
cout << "flag{";
for (int i = 1; i < 80; i++) cout << ress[i];
cout << "}" << endl;
return 0;
}
|
The program has a remarkable number of dependencies. git clone repeatedly failed, forcing me to download them one at a time—perhaps why so few people solved this challenge. Add two lines to the original Makefile so that make builds the verifier automatically.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| OPTFLAGS = -march=native -mtune=native -O2
CXXFLAGS += -g -Wall -Wextra -Wno-unused-parameter -std=c++11 -fPIC -Wno-unused-variable
CXXFLAGS += -I $(DEPSRC)/libsnark -I $(DEPSRC)/libsnark/depends/libfqfft -I $(DEPSRC)/libsnark/depends/libff -DUSE_ASM -DCURVE_ALT_BN128
LDFLAGS += -flto
DEPSRC=depsrc
DEPINST=depinst
LDLIBS += -L . -lsnark -lgmpxx -lgmp -lff -lprocps
LDLIBS += -lboost_system
all:
$(CXX) -o test.o src/test.cpp -c $(CXXFLAGS)
$(CXX) -o test test.o $(CXXFLAGS) $(LDFLAGS) $(LDLIBS)
$(CXX) -o my.o src/my.cpp -c $(CXXFLAGS)
$(CXX) -o src/my my.o $(CXXFLAGS) $(LDFLAGS) $(LDLIBS)
clean:
$(RM) test.o test
|
1
2
3
4
| $ g++ -o my.o src/my.cpp -c -g -Wall -Wextra -Wno-unused-parameter -std=c++11 -fPIC -Wno-unused-variable -I depsrc/libsnark -I depsrc/libsnark/depends/libfqfft -I depsrc/libsnark/depends/libff -DUSE_ASM -DCURVE_ALT_BN128
$ g++ -o src/my my.o -g -Wall -Wextra -Wno-unused-parameter -std=c++11 -fPIC -Wno-unused-variable -I depsrc/libsnark -I depsrc/libsnark/depends/libfqfft -I depsrc/libsnark/depends/libff -DUSE_ASM -DCURVE_ALT_BN128 -flto -L . -lsnark -lgmpxx -lgmp -lff -lprocps -lboost_system
$ cd src
$ ./my
|
The challenge provides only 79 proof files. After running the verifier, the flag contains only 39 zeroes, so the 80th proof must be 0; append it to the flag.
flag: flag{10100100000101100110000001111011110101100101000011111111101000100100110100101110}
Conclusion
I still have a great deal to learn.