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.)

Assembled stamp-card fragments

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:

The User-Agent shown on the HEICORE page

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:

Character art for Kitty Remote Control

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:

uuencode decoding result

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:

Vim in the C Programming Homework challenge

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.