We will finally cover the non-crypto challenges that I wrote for HKCERT CTF 2023. This includes one misc (Hackforces), two pwn (ISA Jump Scare & ISA Jogger) and two reverse (The Flag Game & Loot and Scoot) challenges.

Hackforces

Challenge Summary

Do you know Codeforces? Well, what you need is to find a corner case to make the given submission not work.

Attachment: hackforces.zip

We are given an introductory competitive programming problem (equivalent to Unique Paths II in LeetCode) with its solution implemented in C.

#include <stdio.h>
#include <string.h>
#define N 102
#define MOD 1000000007

int main () {
    int m, n;
    char a[N][N];
    int dp[N][N];

    memset(dp, 0, sizeof dp);

    scanf("%d %d\n", &m, &n);
    for (int i = 0; i < m; i++) {
        scanf("%s", a[i]);
    }

    int non_zero_count = 0;

    dp[0][0] = a[0][0] != 'x' ? 1 : 0;
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (a[i][j] == 'x') continue;
            if (i > 0) dp[i][j] = (dp[i][j] + dp[i-1][j]) % MOD;
            if (j > 0) dp[i][j] = (dp[i][j] + dp[i][j-1]) % MOD;
            if (dp[i][j]) non_zero_count += 1;
        }
    }

    printf("%d\n", non_zero_count);
    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            if (dp[i][j] > 0)
                printf("%d %d %d\n", i, j, dp[i][j]);
}

There is a platform for us to upload test cases, and the goal is to find an valid test case to the given program that generates a different verdict than “ACCEPTED”.

Solution

Where is the bug?

Note that we will be required to print all the number of paths to each reachable chamber. In the program, it is checked at line 33 and it is assumed that the chamber at $(i, j)$ is reachable if dp[i][j] > 0.

Notably, dp[i][j] is the number of possible paths reaching $(i, j)$ modulo $10^9 + 7$. If the number of paths reaching $(i, j)$ is a multiple of $10^9 + 7$, dp[i][j] would be zero and it will be considered unreachable by the program.

Now we want to find a configuration such that there are $(10^9+7)k$ possible paths to a chamber. For simplicity, we will find a configuration such that there are $10^9 + 7$ paths to the bottom-right chamber.

Constructing numbers

If there are $n+1$ rows and $m+1$ columns that fill with all dots, then there are $\text{C}^{n+m}_{m}$ paths from the top left to the bottom right. However, it is impossible to find small $n$ and $m$ such that $\text{C}^{n+m}_m$ is a multiple of $10^9 + 7$. Therefore we must find ways to “combine” the numbers.

Suppose that we have two grids that there are $a$ and $b$ paths from the top left to the bottom right. There will be $a + b$ and $a \cdot b$ ways to traverse from the top left to the bottom right, respectively.

In our case, we will be using only the additive property. We will use the greedy algorithm to find a sum of $\text{C}^n_k$s such that they sum to $10^9 + 7$. One of the config I found is $\text{C}^{97}_6 + \text{C}^{69}_5 + \text{C}^{39}_5 + \text{C}^{44}_3 + \text{C}^{16}_2 + \text{C}^5_1$. Thus the sizes of the rectangle grids are $92 \times 7$, $65 \times 6$, $35 \times 6$, $42 \times 4$, $15 \times 3$ and $5 \times 2$.

We can build a test case from the above information and submit to the platform, and retrieve the flag: hkcert23{h4v1n9_k_m0d_p_3qu4ls_zer0_d0e5n7_m34n_k_i5_23r0}.

34 94
..............................................................................................
.x............................................................................................
.x............................................................................................
.x............................................................................................
.x............................................................................................
.x............................................................................................
.x............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
..............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxx.................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
..............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
..............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..........................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..........................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx..........................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
..............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...............
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx...............
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.
..............................................................................................
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.....
.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.

ISA Jump Scare

Challenge Summary

Can you jump flag.txt, jump the content and jump them to me?

Web: http://bauhinia-isa-hwuj66.hkcert23.pwnable.hk:28900/?id=5

What is ISA: https://hackmd.io/@blackb6a/bauhinia-isa

Note: There is a guide for this challenge here.

Note: You can find the flag at the file flag.txt

Attachment: isa-jump-scare.zip

In this challenge, we are asked to write a commentless program to read flag.txt. However, there is a catch: There is a checker that validates whether each line of the input program starts with a JMP.

Solution

Let’s first have a look at the below ISA code:

0x400000: JMP 0x40000d; JMP 0x400000
          ⬆️

0x40001b: NOP

The program control (PC) would be 0x400000 (beginning of the code) when we run the program. In this case, the instruction to be executed would be JMP 0x40000d.

Although 0x40000d is not a address of the beginning of an instruction, similar to a large number of interpreters available in real life, the interpreter simply parses the instruction starting from PC. In this case, 0x40000d points at the beginning of the JMP 0x400000 (which is intended to be a comment). The interpreter thinks that this would be the next instruction and decided to run JMP 0x400000 afterwards.

0x400000: JMP 0x40000d; JMP 0x400000
                        ⬆️

0x40001b: NOP

In this case, we can actually “write” two instructions in one line, and the comment is could be executed during runtime.

We will want to execute some other commands than JMP in the challenge. How do we achieve that? We can leverage the JMP instruction and jump to the address you want, even in the middle of an instruction.

Alas, the checker does not allow us to write comments. Fortunately the interpreter does not check whether the instructions are valid at the beginning. It would be fine as long as we are not running invalid instructions. This would be handy in our case.

To execute an arbitrary instruction, say [INSTRUCTION], we can break down into two separate instructions which both of them start with JMP:

JMP +4
JMP [INSTRUCTION]

When the program control points to the beginning of the first instruction, it will jump four bytes ahead and point to [INSTRUCTION]. With this idea, we can write a program to read the flag: hkcert23{jump_1n70_m1dd13_0f_1n57ruc710n_1s_r34l1y_fun}.

JMP +4
JMP PUSH 0x7478742e
JMP +4
JMP PUSH 0x67616c66
JMP +4
JMP MOV R8, 3
JMP +4
JMP MOV R1, SP
JMP +4
JMP MOV R2, 0x410000
JMP +4
JMP MOV R3, 100
JMP +4
JMP SYSCALL
JMP +4
JMP MOV R8, 1
JMP +4
JMP MOV R1, 0x410000
JMP +4
JMP MOV R2, 100
JMP +4
JMP SYSCALL
JMP +4
JMP MOV R8, 2
JMP +4
JMP MOV R1, 0
JMP +4
JMP SYSCALL

ISA Jogger

Challenge Summary

I heard that MOV is Turing complete. Convince me by reading flag.txt!

Web: http://bauhinia-isa-hwuj66.hkcert23.pwnable.hk:28900/?id=6

What is ISA: https://hackmd.io/@blackb6a/bauhinia-isa

Note: You can find the flag at the file flag.txt

Attachment: isa-jogger.zip

This challenge is similar to ISA Jump Scare. Except that we are allowed to use only the MOV instruction instead of the JMP instruction.

Solution

This time we are able to use only the MOV instruction. We are able to write to arbitrary memory segments, including the code segment. We can write a self-modifying code to evade the checker at the beginning. If there is a PUSH instruction after the check, it is still fine.

The instruction MOV [R8+0x400123], 0x54534554 sets TEST to the address 0x400123 if R8 is zero. With the idea, we can modify the end of the program to read the flag: hkcert23{m0v_1s_7ur1n9_c0mp1373_4nd_y0u_ju5t_v3r1fi3d_th4t_f0r_m3}.

MOV [R8+0x4004e3], 0x48535550
MOV [R8+0x4004e7], 0x37783020
MOV [R8+0x4004eb], 0x37383734
MOV [R8+0x4004ef], 0xa653234
MOV [R8+0x4004f3], 0x48535550
MOV [R8+0x4004f7], 0x36783020
MOV [R8+0x4004fb], 0x36313637
MOV [R8+0x4004ff], 0xa363663
MOV [R8+0x400503], 0x20564f4d
MOV [R8+0x400507], 0x202c3852
MOV [R8+0x40050b], 0x4f4d0a33
MOV [R8+0x40050f], 0x31522056
MOV [R8+0x400513], 0x5053202c
MOV [R8+0x400517], 0x564f4d0a
MOV [R8+0x40051b], 0x2c325220
MOV [R8+0x40051f], 0x34783020
MOV [R8+0x400523], 0x30303031
MOV [R8+0x400527], 0x4f4d0a30
MOV [R8+0x40052b], 0x33522056
MOV [R8+0x40052f], 0x3031202c
MOV [R8+0x400533], 0x59530a30
MOV [R8+0x400537], 0x4c414353
MOV [R8+0x40053b], 0x4d0a0a4c
MOV [R8+0x40053f], 0x5220564f
MOV [R8+0x400543], 0x31202c38
MOV [R8+0x400547], 0x564f4d0a
MOV [R8+0x40054b], 0x2c315220
MOV [R8+0x40054f], 0x34783020
MOV [R8+0x400553], 0x30303031
MOV [R8+0x400557], 0x4f4d0a30
MOV [R8+0x40055b], 0x32522056
MOV [R8+0x40055f], 0x3939202c
MOV [R8+0x400563], 0x5359530a
MOV [R8+0x400567], 0x4c4c4143
MOV [R8+0x40056b], 0x4f4d0a0a
MOV [R8+0x40056f], 0x38522056
MOV [R8+0x400573], 0xa32202c
MOV [R8+0x400577], 0x20564f4d
MOV [R8+0x40057b], 0x202c3152
MOV [R8+0x40057f], 0x59530a30
MOV [R8+0x400583], 0x4c414353
MOV [R8+0x400587], 0x4c

The Flag Game

Challenge Summary

Did you beat The Password Game? Let’s beat The Flag Game this time!

We are shown a game similar to the password game, and no source code is given and we need to find a flag that satisfies all the rules.

Solution

Maybe we can do that manually?

There are the first 25 rules:

  1. Your flag must be at least 6 characters.
  2. Your flag must start with hkcert23.
  3. Your flag must include exactly one { and one }.
  4. The length of your flag must be a prime.
  5. The digits in your flag must add up to 26.
  6. The length of your flag must not be greater than the answer to the ultimate question of life, the universe, and everything.
  7. Your flag must contain at least three underscores.
  8. Your flag must not contain the substring flag.
  9. Your flag must fulfil the flag format.
  10. The number of digits in your flag must be a prime.
  11. The number of 1’s in your flag must be thrice the number of 6’s in your flag.
  12. At most 30% of your flag are digits.
  13. Your flag must not contain any characters from fail.
  14. Your flag must include at most two distinct vowels.
  15. Your flag must include five characters, not necessarily distinct, from mystiz.
  16. Your flag must contain exactly 20 distinct characters.
  17. Your flag must not contain upper-case characters.
  18. Your flag must contain the substring y0u.
  19. Your flag must not contain consecutive double letters.
  20. At least 40% of your flag are hexadecimal digits.
  21. Your flag must match the REGEX /[A-Za-z0-9]{9}/.
  22. The ASCII values of the characters in your flag must sum up to a multiple of 65.
  23. Every occurrance of n in your flag must immediately followed by a digit.
  24. No characters in your flag can occur more than 6 times.
  25. Rule 22 should still be satisfied if each of the 3s in your flag have been replaced by a @.

Apparently, there is hope finding the flag. Maybe we can try harder…

What even is that rule?

Doing it in a reverse engineering way

✋🏾 Don’t attempt manually. It might be a good idea doing it manually as to explore the challenge. However, it might be a rabbit hole because the text description might not reflect the actual rule correctly. On the other hand, the number of rules is unknown to us. Maybe there are 1000 or 10000?

It is worth noting that the flag checking procedure is done solely on the client side. Thus we can read everything we need from the source code. Additionally, when we read /js/app.38bf3c86.js, we can see that there is a trailing line:

//# sourceMappingURL=app.38bf3c86.js.map

This is a good sign, and we can see that there is a /js/app.38bf3c86.js.map. This is heavily used during development, because we are able to look up the line of code that causes unexpected errors, even if the code is minified. We are also to recover the source code from the source map, and the browsers are doing that automatically. For Chrome, we can go to “Sources” → “Page” and read the source code.

We can retrieve the full list of rules from the recovered rules.js:

export const rules = [{
  // first 25 rules skipped
}, {
  description: 'Your flag must reach a strength 4 in <a href="https://dropbox.tech/security/zxcvbn-realistic-password-strength-estimation" target="_blank" ref="nofollow">zxcvbn password test</a>.',
  checker: (u) => zxcvbn(u).score === 4
}, {
  description: 'Rule 20 should no longer be satisfied if all of the <code>d</code>s in your flag are removed.',
  checker: (u) => u.replace(/d/g, '').split('').filter(c => '0123456789abcdefABCDEF'.includes(c)).length / u.replace(/d/g, '').length < 0.4
}, {
  description: 'Y̸͇̥͙̙̼̰̬͔̜̠̺̮̍̅̊͆͆̂͆̐͊̏͜͝͠ö̵͓̩̲̱͎̠͓̈́͂͂̑̆̌̈́̈́̀̔́̃͛̕u̷̜̜͎̼͙̝͋͆̿̄̕r̵̛͈͉͕̾͂̏̿͌̉ ̸̛̛̛̝̳̩̙͒̔͋ͅf̴͎̲͈̤̠̗͙̠̮͂̀̇̈́̊̄̏͊͘̕͘͘l̷̡͔̤̣̮̓̐̃̆̀̇̂̓͐͝ą̵̛̹͓͈̟͖̒̂̔̑̀̓̓̈̌̓͌͌̀̕ͅǧ̸̨̉̈́̑͋̀͘ ̸̨̺̬̾̓̃̌̔m̸͕̗̏̏̏̉̽͘͘u̵̡̧̢̺̮̘̪̖͖̗̝̲̥͌͂s̶̛̲̥̦̰̊̃́́̉̎̈̂͗͛́̌͘͝t̵̤̳̩͓̦̹͚̫̹̣̪̠͛̃̏́̓̽̃̈̉̐̿̄͋̑͜ ̷̛̭͙͔͉̪͍͖̪̥͙͓̱͍̉̓̈́́̾̎͋͊̈́̈̈́b̸̡̛̗͙̤̠͖̳̄̃̽͂͆͌͠͝e̵̢̛̬̼͖͕̰͍̳̠̟̱͍̞̼͍̍͌̀́́̐̅̽ ̸̛̞̙̓̓͊͛̉̓̀̀̍͋͗̕͘͝▒̴̡̱͋͒̋̐̈́█̴̲̈̀͊͐̀̐̓̆͆͒̊̈̑̐̕▒̷̢̮̗̤̙͖̤̎̿͐▓̸̮̃▓̷͚̩͚͕̯́̉̓̂͝ͅ█̸͚͌͗̑̂̋͛̉͛͋̋͘̚͜▒̴̖͒͝͝▓̸̣̦̊͗̌́̈̓͝█̵̡̡̼̹͖̼̜̦̟̙͖͓͆͊▓̷̨̡̩̼̗̐̋̏̄̒̔͌͂̆͜͝▒̷̛̥̒͂͋͘͠▓̴̢̧͓̠̲̘̖͓̜̱̦̭̭͓͆̈́̀̋̑̔̔͊͗͜͠▒̸͚̬̺̙̪̗̟͋̆̍̎̃̍͑̌̑̿̔̓̈͘͘͜█̶̧̛̞̯̻̥͍̑̽̉̂͂͆͋͌̍̓̋̉͝͝▒̷̫̞̄͛̐́̃̚̕▓̴̨̡̲̬̯̝͉̥̅̇͐͠.̸̨̨̛̠̲̹̮̙̤̬̞̼̖̿͐̒̉̓̎̔̾́͆͝',
  checker: (u) => sha224(u.substr(0, 8)).startsWith('b08c89')
}, {
  description: 'Y̴̢͚͉̞̙͕̫͚̝̜͕̩̍̈́̈́̽̃ǫ̴̧̢̳̫̤̰̝͖͖̜̼͚̼̓̇̀́̇̾͑͒̑̈̀̂̓u̷̯̫͎͙̤̰̩̜͇͐̋̌̄́̇̽̈̒̒̈̕͜͜͝r̴̡̧̗̩̼͕͓̗̬͈̤̘̣̟͚̊͒̍̈́͊̚͝ ̷̹̹̺̜̗̮͕̩̗̪̩̗̓̃̋̌͂̐̆͌͘̕̚͝f̶̘̥̘͚̣̝͖̫̬̬̘̝͉̈́̈́̿̈́̑͆̽̾͘͝l̴̢͈̱̜̤̞̬̜̬̟̯̻͒̈́͒̽̓̇͒̊͆͘͠͠ą̸̢̛̞̥̤̞̭̬̻̥̱͆̇͛̒̐̀̅̍̌̉̈́͝͝g̵̡̜͈͎͙̺̘̱̩͈͓̭̈́͊̃̍̃̊̈́͆̓̄͜͝͝ ̵̧̞̜̝̰̻̯̟͖̭̫̒͋͒́̄̓̓͘̕͜͝ͅͅm̸̡̨̧̧͍̻͈̼̜͕͖͚̯̪̅̄̋̉̊̈͐̈́͗̈́̚ǘ̵̮̱̩̋́̈́̀̌̓͒̄̐͌̚͝s̷̨̘͎͓̜͈̙̮̙͍͖͊t̴̙̲͗̽̿͛̊̽̔͋̒̐̎ ̷̝̱͓̞͉̫̏̐́́̔̆̓̀̌͘͝͠b̴̝̗͙͇̘͖̬̲̘̗͍͂̈́̔̾͗̓̽̔̓̕̕͘͝ͅę̴͍̘̲͕́͂͛̅́̓̾̽̚͘̚͜͝͝ ̵̨̛͎͈̙̬̎͐̑̀͗͆͘ͅ▒̸̛͍͓̈́̈́́̂͂̐́̕͠▓̷͚̥̳̼̔͗̒͑̀̃ͅ█̸̥̟̬̝͚̖͔̹̋̃̂͌͑͗̚̕█̸̨̖͓͚͙͙̙̝͔̻̯̲̮͚͐͆͑͗̈̄̎̀̂͒̈͘͜͝█̶̱̳͚̜̘͖̱̼̊̈́͆̓͋͑̄̀̉̐̚▓̴̧̢̡̱̺͚̠̖͋́̑̃̓́̊̔̈́̊͘̚̚▒̵̨̡̢̻̗͎͕͖̱̹̜̝̝̲̉̅͊̀▒̶̡͙̪͕̜͔̬̜͍͗▓̸̥̣̣͗̍͐͠͠█̶̯̯̤̊͋̋̎͊͆͋̾̂̌̅̈́̄̑▓̸͇͓̝̈̒̈́͂█̵̡͎̥̦̙̑̉͒̽͐̂̊̌̔̄̌̽̕͝▒̶̡̨̙͖̬̗͓̞͚̆̚▓̸̧͔͙͓̹͈̫͕̗̅͗̀͑̂͗̂̄̓̀̐̚▒̶͈̭̄͜█̶̨̢̧̧̥͖͕̯͕̗͙͐͊̃̀̎̍̉̍͐̐̃̉̿͜.̴̧̧̡͎̳̻̯̞͖͔̦͔͆͒̅̇̋̍͋̑̿͜',
  checker: (u) => sha256(u.substr(0, 14)).startsWith('d0687a')
}, {
  description: 'Ỷ̶̭͙̈́̋͗͒̔̂̋͗̋͝͝ơ̶̡̨͉̤̜̫͍̩̂̿͊̒̄͛͜ų̸̻̙̭̱̆͑̃̋̚͝ͅȑ̴̢̝͈̠͙͍͈̭͙̪̳͉͇̂ ̸̲͇͐̃̋͛͂̓̓̀̾̚͠f̸̨͚̗̺̹̀̈́͆̈̈́̊̀͝l̵̥̝̯̥̗̗̝̙̜̦̝̩̺̊̍̇̑̂̈͑̍̄̇̑͐͘͝͝a̶̡̮͕̩̰͍̩̩͐̽̄̒̇͑̈́̉̍̍͛͊͊͘ģ̸̨̡͇̩̼̪̫̘̬̭̦͓̈́̀̂͝ ̴̛̱̙̜͇͚͖͒͗̑̅́͂̅̊̊̕̚̚͜͝m̷͍̫̖̰͓̀͛̂̿̿͠͝u̷̖͎̰̙̰̘̬͕̒̐͛́s̴̱͍̯͎̘̬̻͎̟̄̋͛͝ṭ̷̨̙̪̗̥͙̝̦̲́̍̃̋̉̑̒͛͘͝ ̸̨̢̡̛͉̹̝̟̜̩͕̼̆̇̄̊̾̓͌̓̃̊́ͅb̴̡̨̡̛͔̥̱̟̱̫̠͚̖̺̀̃̐̇̍̽̿̀̇̏͒͂̚͝ẹ̴͚̟̟͚͖̺̱͕͙̬͎̯̫̝̉ ̴̧̢̘͖͎̤̩̙̳̺̖̅͆́̀̈́̍̉̓͐͛̀̏͂͘͠ͅ█̸̨̤̫̗̬̮̞̤̟́̾̀͘█̷̛͙̰̤̹̼̥̏̐̅̑̊͆́̎͛͋͝͝▓̵̡͈̗͇͔̟̖̮͚̳̙̝͓͗̍͊́̆͊̋͐͊̈́̔̚͠█̷̠̤̽͂̀̎̀̈́̕̚█̷̨̧̦̰̘̱͚̰̣̏͌̇͝▒̸̛̪͕̐͒̑́̀̊͑̃̇̂̈̚▒̶̦̣̙̮̦͓̖̹͚͇̯̋̐͛̔͛͛̊̈́̂̇̌͗͘̕▓̷̬̂̈̈͑̇͛̚▓̸͔̞̬̩͉͍̈́͋͐̑͂͗̀̌̄͒̒̕͠█̴̝͕͙̩̦̺̰̖͍̩̙͖̻̬̖̽̃́͌̈́̌̒̊͠▓̵̢̛̞̳̻͓̣̻̙̖̘̲̐̔́́͗̈́̇̅̒̇͝▓̸͇͔̩̜̖̗̼̔̋̎̽̍̔͂̔̓̒̑͂̔▒̵̡͖̼̺̰͔̜̩͔̺̫̯͉̖̝͗͂̑̎̊͐̉̀̈́̄̔͑͗́█̷̢̛̳̤̣͔͔̯̂͋͑̓̏̔̈́͝ͅ█̵̧̰̓̾̈́͊̒̀͛́̐̐▒̷̨̨̢̧̜͕̲̗̒̾͛͋̎͠͠.̷̧̦͓͇̫̮͚͌͛͊̐̑̉̍̊̈́̚̕ͅ',
  checker: (u) => sha256(u.substr(0, 12)).startsWith('87b3c7')
}, {
  description: 'Ŷ̴̝ǒ̶̬̼͝ȕ̵͉̺̊r̵͈̎́ ̷̘̆͌f̶̟̎l̸̲͐ạ̶̜͐̈́g̷̨̖͝ ̷̛̰̽m̶͉͙̽ū̶͖̥̀ś̸̢͔͝ẗ̴͕̮̌ ̸̪̾́b̵̜̋e̵̟̟͌̓ ̵̨̍̚▓̸̬̕▒̷̻̙̉▒̴̳̘͂█̴̜̆͛▓̸̽ͅ▓̷̬̅͂▒̴̬̗̏▒̴̳͚́̇█̷̞͛▓̶̱͕̐▒̷̳̒▒̵̲̿▓̵̝̯͘▓̶̠̈̈́▓̵̜̘͐▒̷̼̙̓̏.̸̧̹̎',
  checker: (u) => sha224(u.substr(0, 22)).startsWith('8b035b')
}, {
  description: 'Ỷ̴̮͍͒ǫ̴̈́ŭ̸̧͚͒r̴̳̓ ̵̘͇̅͆f̷̮̑ḻ̴̡̂ă̵͇͋͜g̵͇͆ ̸̝̯͐̇m̵͚̿u̵͙̜͆̽s̷̢͠t̴̩̳̔̕ ̴͈̤̒̒b̵͓̄è̷̞̉ ̴̘̓̍█̷̧̖̈́▒̸̈́̕ͅ▒̵̟͛█̵̟͑̀▓̴͍͊▓̴͖͚̓̎█̶͙̝̍█̶̠̐̕▒̴͍̐̊█̴̮̑̾ͅ█̵̿ͅ▓̸͓̂█̴̧̯̊̒█̶̲̞̍▒̴̱̉͠▒̸̪̑.̷̻̅',
  checker: (u) => sha224(u.substr(0, 40)).startsWith('8a9de8')
}, {
  description: 'Y̶̼̥̔̚ȏ̴̟̦u̶̗͆r̴̨̛ ̸̤͖̉f̶̪̣͒l̷͎̾a̵͖̜̒́ḡ̵̣͖̇ ̸̺͕͌͗m̸̞̫͊̉u̶̫͎̓s̸̜͑̊t̵͈͕̚ ̶̹̋̽b̶̯̑̈ẻ̴̜̈́ ̴̟́̽▓̶̛̹͂▓̴̡̈́█̴͚̻̃̉█̸̦͇̓▓̴͓̻͊̅▓̴̗̹͂▓̸̯͈̈́̉█̸̹̠̏▓̷̐̾͜█̸͔̃▒̶̺͐͜▓̶͚̯͗͛▓̴̹̖̈́͑█̴̙̬̄▓̶̖͔̔̔█̶͔͖͊͛.̷͓̎̾',
  checker: (u) => sha256(u.substr(0, 26)).startsWith('7be965')
}, {
  description: 'Y̴̬͊͘͝ö̵̺́ȗ̴͈̌͐r̸͇̀͐͗ ̷̗̬̭͒͛͐f̴̗͓̂ḻ̴͎̝̄a̵̡̚g̷̻̹̯̒̾ ̷̜̎͘m̶̪̎͌ṷ̴̿ṡ̶̺̇̕t̷͖́̃ ̷̺͛͝b̸͈͑̋́e̷̢͇͒ ̷̭̪̈́█̶̲̔▒̵̧̺͂͂͜▓̴̦̰̉̄▒̸̗̓̔▓̴͉̻̇̑͘͜▓̶̛͎̀▒̶̧̆█̴̢̪̻̐▓̸̥̗̞̓̒͊▒̷̹̋̂̀▓̶̧̠͖̈̌▒̷̯͆█̶̙͈̟̓͛▓̶̰̉▒̷̡̟̾̕▒̴̟̑̎̽ͅ.̸̯͈̾͌̔ͅ',
  checker: (u) => sha224(u.substr(0, 30)).startsWith('bf7eeb')
}, {
  description: 'Y̴̨̠̓̕͘ő̶̥̖̈́u̸̪̎͆̊r̵͕̀ ̶̧̞͔̄̈́̕f̴͙͌l̸̲̩̫̀ḁ̸͚̎̅͑g̵͉̲̬̀͊̔ ̵̤͎͒̅̾m̵̼̮͋̑̌u̷͉̤͔̔̏̃s̵̬̋̔t̷̥͚̘̃̑ ̷̞͆̏b̴̩͙̓̕ê̸̖ ̷̨͚̒́▓̷͉̉▓̷̙̯̹̆͐█̸̡̨̮͗̍▓̷͋͜▒̷͍̩͌̈́̏▒̶̢̽͂͒▒̵̙̹̯̑̈́̈́▒̷̥̮̣̋͂̾▓̵̢̈̅̏▒̴̰͚̈́̎́▒̵͇͎͛̀█̷̲̦̈̓̔█̷̡̇̈́█̸̜̬͆̈́̀▒̷̡̦̆̓▓̵͎̱̱͂̍.̶̮̿̓̕',
  checker: (u) => sha256(u.substr(0, 36)).startsWith('7ef31a')
}, {
  description: 'Y̷̡͍̠͆̈́o̵̰̹̫̍͑ǘ̴̳͍̑̂r̵͍̀͆̃ͅ ̶̲̿̋̽f̴̤̊l̶̢̐͒͜a̷̧̘̥͋́̋g̵̳̅ ̵̼̣̱̀͂m̴̰͍̮̂u̸̜̞̲͐̽̀s̴̪̄̑ṯ̷̬̋ ̸̟̪̓͘͝b̴͍̄ë̸͖͖̙͘̕ ̴̛̟̃̍ͅ▒̵̖̊͊͜█̵̢͎͒█̵͕̯̈́█̸̞̊̋̌▓̷̘́̐▓̵͙͐▒̶̤̏͊͝▓̶̜͒▓̷̤̗͙̊̚▒̶̟̙͕͑̀̔▓̴̢̦͗̀͝▒̶̙͈̈▓̴͖̌█̶̖͚̥͝▓̵̡̮̂▓̴̥͉̄.̴̛͔̙̓̀',
  checker: (u) => sha256(u.substr(0, 24)).startsWith('40f34c')
}, {
  description: 'Y̴̨̤̒͜ŏ̸̝͇͜u̷̡̹̇r̷̺͎̘͝ ̷͕͚̋f̵͕͖̗̈̋l̵͉̣̕ậ̵̛̂ǵ̸͓ ̴̨̣̪̄͐͐m̸̻̀̋̑ṵ̵̡̖̌ś̶̩̗͌͒t̵͓̙̀̽̒ ̴̰͓̱̓͗b̴̢̈́͛͜ȇ̴̹ ̵̰̐█̸̱̰̭̂█̶̘̈̉█̸̖̟͇͆█̵̧̪̒̿█̴̜͂͗▓̴̥͚͓̀█̶̡̛̼̥̅͘▒̴̻͓͂▒̷̳̿▓̵̰̹̬̍▓̶̜̗̉̾▓̶̺̆̊̏ͅ█̶̳̬̆▓̸̨̬̤̂͑̏▓̶̼̠̹̈́█̵̹̞͗̀̚.̶̘̍̏̇',
  checker: (u) => sha256(u.substr(0, 20)).startsWith('b72709')
}, {
  description: 'Y̷̳̮̓͑́o̵̰̩̾̓̎ű̷͙͇r̸̰̫̰̃̇̑ ̵̨̙̟͂̔̄f̴͓̝͑l̵̝̺̥̑̈́a̷͖͓̔͜g̵̝͇̅́͂ ̵͉̺̊̓̀ṁ̵̭̙̆u̵̲͂̉s̵̩̅͑ͅt̵̙̳̒̚ͅ ̴̢̲̩͒̊b̷̳̤̈è̷̡̈ ̸̞͙͛̋̒█̴̨̺̿̃ͅ▒̷̹͑͜█̸̯̎͒̌█̸͍̘͐̈́▒̷̩̲͈͆▓̵̥̖̈́▒̴̺̂̽█̴̨̪̣̚▒̵̲̮̊▓̷̳͐▒̷̼̹͈͋͝█̵̟̳̻̂͘▒̶̧̪̇▒̸̦͉̭̈█̸̃͛ͅ█̸̡̇.̶͙̻͒̄̋',
  checker: (u) => sha256(u.substr(0, 38)).startsWith('e3c817')
}, {
  description: 'Y̶̮͙͆̓̄o̵͈͎̅̉̔u̴̲̻͋̑r̷̰̰͐̚ ̷̦̫̪̎͠f̶̼̀l̷̹̇̆͝a̴̛͓̰̺g̵̯͒ ̴̛̯͉͛͠m̷̹͙̓̉̏ụ̷̊s̵̨̠̕t̸͓̫̞̆ ̷͍͝b̷̲́̉͠ė̶̡̺̱ ̴͖̀█̸̝͗̂▒̷̠́́͝▓̸̙̝͈̎́▓̴͓͑▒̸͎̈́̿̕▓̶̪̀▓̶̧͕̇ͅ█̴̜̰͛̎▓̴̡̯̰̇͒▒̵͖̌▓̶͓̦̈̀▒̵͎͎͚̔█̵͔͉͆̔█̶̩̦̞̃█̷̨̣͈́█̵̱̣̩̊̃.̵̛̗͒',
  checker: (u) => sha224(u.substr(0, 28)).startsWith('0b67cb')
}, {
  description: 'Ȳ̷̢͙̒͒o̵̡̩̳̗͑̄ũ̴͔̱̉̒̃r̶͎̯̱͐́ ̷̠̦̀̑̚ͅf̵̠̤̭́͠l̴͕̲̿͝͠a̸̘͕̭̹͌g̸̬̹̏ ̵̮͐̑m̶̮̫̟͋̔ụ̸͇̅s̴͎͇̆̔͆̚ţ̶̙̮̼̐̌ ̴͙͒̓͋̇b̷̢̒͝e̶̙͈͖̊̽͜͠ ̸̩̝̕͝▓̵̼͈́͜▒̷̙͈̓͛̕͜█̸͓̘̤͂͑͋͌▒̶̙̱̗͇̑͆͠͝█̸̦͆̓█̵͔͓́▓̵̡̩́▒̵̛̦̫̬͔▓̶̦̹̯̒ͅ█̵͔͖̊̿̇▒̸͍̜͂̚▒̴̲̋͐▒̴͎̻͝▓̸̫̾͊█̷̡͍͓̼̄̀̿̒▓̴̄̃̄͜.̷̢̟͌̔̎̈',
  checker: (u) => sha256(u.substr(0, 32)).startsWith('f9f48b')
}, {
  description: 'Y̵͖͇͐̌̇ö̶͖͆u̴̪̍ŗ̵̛̼̗̦̒ ̷̠̣̠͆̋͜͝f̸͖͛l̸̡̢̛̠͒a̷̡̩̗͗g̶̖͍̅́̋͛ ̸̨̮̺̯͝m̵̩͍͛͜͝͠u̶͎̩͑̊̈́̀s̵͕̀͛ṫ̴̩̣̰̺ ̶͍̦̃͂b̷̲̣͝e̶͇̮̊̈́ ̴̢͓͊͝▓̶̣̳̠̻͗͑▓̵̌̀̈́́ͅ▓̶̘͎̅͋̇͊█̷̣̰̥͎̽█̵̢̜̩͎̆̅̀̓▓̴̘̦̦͉͂̉͒̚█̴̟̭͎̱̈́▓̴̭͔̗͐̏̇͗͜▓̷̻̪̝͆͝█̶͉̬̠̥̉̑̔█̵͚̠͈̦̈́́͊▒̸͓͎͈́́▒̷̱̲̖̾̽▓̷̱̺̋̈́͋̽▒̸̨͗̾͐▒̶̛͈͓̺̯̃́.̴̱̺͎̽̈́̿',
  checker: (u) => sha224(u.substr(0, 34)).startsWith('69260f')
}, {
  description: 'Ỵ̶͕̹͆́o̵̪͙̖͛̈́́͝ȕ̸͓̭̈́͠ṟ̵̌͗̎ ̸͚̘͐́f̶̡̛̫̻̤̔l̵͈͂̉͝ͅa̸͈͠͝g̷̝͂̒͐̒ ̵̢̦̈́̊̀͊m̸̗̅̌̋͜͝u̴̢̩̇̎͘͠s̸͇̟̻͐͘t̸̩̮̯͗̀͠ ̸̖̄͜͠b̵̮̤́̓͘ē̵̙̙͋̒ ̵̨̠̈́̾█̴̞̌͠▓̵̧͉̲̼͑█̸̗̫̽▒̷̃͒̃͜█̷̻̼̯̽̈́̔▓̶͓̩̪̌█̴̡̙͍̻̈͊█̸̨̬̱̎͑̎█̵̦͈̈́̎̔ͅ▒̸̝͍̼̥͂͂͊͘▒̴̛̯̥̯̣̇̕▓̵̬̮̟͉̋▒̷̤̪͓̼͒▒̴̧̛͈͖͊̌͒͜▓̵̡̂͆▓̷̭̙̫̿.̸̳̚',
  checker: (u) => sha256(u.substr(0, 18)).startsWith('c25dd2')
}, {
  description: 'Ỳ̷̼͝o̴͈̊u̴̞̱̹̒̑̕ṙ̴̼̳̱͖̚͜ ̴̠̦̃͋̕͝f̷̨̲̣́̈́l̷͚͊̉̎ã̶̧̲̱̩g̴̠͓̊̑̾ ̶̺͇̗̂̂͑ḿ̴̳̎̋̈́̕u̸̯̭̳͂͌̊s̸͕͖̬͓͋̉̃̏̑ţ̷̯̟̃̉̏̄ ̶̗̞̣̦̤̄͗͌͑̀b̷̨̭̗̈́̓̐e̸̹̰̱̫͛̑̑̚ ̸̨̬̱̲̮͑̓̓▒̷̪̲͆̇▒̷̠͌̈̓͂͝▓̴͎̖̃́͗̇█̴̖̲̀▓̶̨̭͈̗̃͜▒̸̢̭̑̊̆█̷̭͍͈̍̑͘͝█̵̠̼̺̠̭̒̑̃́█̵̣̈͘█̷͚̫̮̔̆́̕▓̶͖̭͙̼͗̐█̸̱͔̇͊̐͗̓͜█̷̡̤̯͖̉͗͊̉̉█̴̡͛̔͠█̷̜͛͑̃̂█̴̢̦̥̯̽.̸̳͔͓͙̈̈̏̔ͅ',
  checker: (u) => sha256(u.substr(0, 16)).startsWith('cbe2c9')
}, {
  description: 'Ÿ̶̻́̍͒͂̒ǫ̵̔̍͝û̶̯̯͙̌̾͛̂ŕ̸̜̥̪̓̂̈́ ̵̝͇͑̉̓̽͠ͅf̷̧̖͖̌ḻ̵̨̅̒a̸̳͍͔̭͗̽͋̽g̴̼̉̄͑̔̕ ̷̪̹̃̇̆̾̇ḿ̷͔͖̤͘ų̴̫̓̈́̍s̷̢̹̎t̴̞̬͋̑̒͜ ̷̡̭͙̠̃͑̈́̅͒b̷̻̟̜͇̏̾̏͌̕ȇ̶̢͕̜͖̜́̄̈ ̷̝͕̦̦̾̒▒̵̧͖̤̑͛͐̎̈́▓̵̨̝͗̔̈́͘█̵̰̻̐̿͌͂͌▓̸͔̙͎̥̀̌́̓▓̸̛̯̤͈̦̓͛▓̴̧̢͇͔̈́̃͆͘̕█̵̯̤̲̦̗̈█̷̢̣̳̙̰̈́̚█̸̙̬̩̗̂̃̓̍͐█̷̪̗͇͙͒̔͜▓̷͈̱̻̬͇̑█̵̱̫̯̪̙́̋͐█̴͙͕̟̱̞͋█̸͓̏͋̀▒̵̥͕͆͊̾͘͝▒̸̺̗͎̫̏.̵̺͝',
  checker: (u) => sha224(u.substr(0, 10)).startsWith('ce45fd')
}, {
  description: 'Y̶̤̺̝͋͊̑͠͝o̸͎͈̩͌͂̅̒ṷ̸̪̈́̽r̴̙̯̦̗̈̄̿̀͗ ̸̜̿̌f̵̧̼͎͍͗̓ͅļ̶̛̼͇̅a̴̖̦̯͆͆͝͝g̴̘̅̿̓͛ ̸̰͓̩͙̐͘͝m̵̫̳̙͌̀̎ụ̶̉̃s̶͋ͅt̶̥͖̜̟̒̽͂͝ ̶̲̜̘͑̐͋̈́b̷̞̐̈̔̋̽͜e̷̡̻̰̅̍͗̓̒ ̵͚̠̠͍̒̑▒̸̫̜̉̉́͠▓̴̢͚́̑̿̈́█̶̬̖̞̓͂̀͒▒̴̩͙͜͝▒̵̗̦̕͠█̷̯̫̝͒̽̾̋▓̷̭̺̻͕̄̄̉̏̇▒̷̧̎▒̶̣̫̱̐̒▒̶̫͚̙̈́▒̴̺̬̼͈̏̎̐̕█̶͓̩͗͂̐̐▒̸̱̗̹̬͒̍▓̸̠̦̋̿̊̈▒̵̝̑̐̊̓̈█̶̜̔̊ͅ.̶̧̛̣͊͐̂',
  checker: (u) => sha256(u.substr(0, 42)).startsWith('e3c817')
}]

We can see that the “obfuscated” rules are checking the SHA256 and SHA224 digests of the prefixes. We can rewrite the above rules as a checker function:

function isFlag(flag) {
  if (!sha224(flag.substr(0, 8)).startsWith('b08c89')) return false
  if (!sha224(flag.substr(0, 10)).startsWith('ce45fd')) return false
  if (!sha256(flag.substr(0, 12)).startsWith('87b3c7')) return false
  if (!sha256(flag.substr(0, 14)).startsWith('d0687a')) return false
  if (!sha256(flag.substr(0, 16)).startsWith('cbe2c9')) return false
  if (!sha256(flag.substr(0, 18)).startsWith('c25dd2')) return false
  if (!sha256(flag.substr(0, 20)).startsWith('b72709')) return false
  if (!sha224(flag.substr(0, 22)).startsWith('8b035b')) return false
  if (!sha256(flag.substr(0, 24)).startsWith('40f34c')) return false
  if (!sha256(flag.substr(0, 26)).startsWith('7be965')) return false
  if (!sha224(flag.substr(0, 28)).startsWith('0b67cb')) return false
  if (!sha224(flag.substr(0, 30)).startsWith('bf7eeb')) return false
  if (!sha256(flag.substr(0, 32)).startsWith('f9f48b')) return false
  if (!sha224(flag.substr(0, 34)).startsWith('69260f')) return false
  if (!sha256(flag.substr(0, 36)).startsWith('7ef31a')) return false
  if (!sha256(flag.substr(0, 38)).startsWith('e3c817')) return false
  if (!sha224(flag.substr(0, 40)).startsWith('8a9de8')) return false
  if (!sha256(flag.substr(0, 42)).startsWith('e3c817')) return false
  return true
}

Since we know the flag starts with hkcert23, we know 8 bytes of the flag. To recover the full flag, we can repeatedly exhaust two more bytes to check if the next rule is valid.

Eventually we will have the flag: hkcert23{h0p3_y0u_u53d_th3_s0urc3m4p}.

Loot and Scoot

Challenge Summary

Collect the keys and unlock the crypt to the flag.

Attachment: loot-and-scoot.zip

In the challenge, we are given a binary (written in C) which is the server, as well as a web client for interaction.

The goal is to unlock the chambers and interact with the flag within 100 seconds, as we have 100 HP and we lose 1 HP every second.

Solution

We are given the binary for the backend written in C and a HTTP client. The backend uses protocol buffers and websocket to allow interactions with the web.

As the challenge author, I will just skip the reverse engineering part and share the source code directly.


#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <ws.h>
#include "maze_runner.pb-c.h"

#define GRID_COLS 55
#define GRID_ROWS 55
#define COORD(x, y) (GRID_COLS * y + x)
#define DIST2(x, y) ((x) * (x) + (y) * (y))
#define TILESET_SIZE 12
#define ITEM_COUNT 4
#define MAX_CLIENTS 1024

typedef struct GameTile GameTile;
typedef struct Player Player;
typedef struct Monster Monster;

typedef struct Context
{
    GameTile **tileset;
    GameTile **grid;
    Player *player;
    Monster *monster;
    int *inventory;
    char *message;
    ws_cli_conn_t *client;
} Context;

typedef struct Player
{
    int x;
    int y;
    int facing;
    int health;
} Player;

typedef struct Monster
{
    int x;
    int y;
} Monster;

typedef struct GameTile
{
    int type;
    void (*on_enter)(Context *ctx, int x, int y);
    void (*on_interact)(Context *ctx, int x, int y);
    int is_walkable;
    int is_interactable;
} GameTile;

// ---

unsigned long long int GRID[] = {
    0, 0, 0, 2459530691549855744, 144680483117212162, 2450523355428823554, 2306443351725048354, 137975832616, 9007199793717280, 9042384163708960, 2450558539798685984, 153722729841951266, 2450523492867768866, 2345624797730, 2305843009215791104, 35321811050496, 536879136, 2450523492865671168, 153687544932999714, 2459565738516677154, 2305878202748379682, 2314885393377402912, 2314885530281582624, 2305843147189518368, 2450558676702994944, 2450523492867768834, 153687682908815906, 2305878193621565986, 35321813147648, 9042384163708928, 9007336695791648, 153687544930902562, 144680483115115010, 153687682906718722, 2305878194122793474, 2314885392842629152, 2305843009750573056, 2449958335265374208, 144680483115115010, 2459565876492509730, 144680346215129602, 9007337230565922, 538968064, 9007337230565376, 153685474722979872, 2459565875957735938, 2459530691583549986, 153722729841959426, 2314850345907396610, 9042383628935168, 2305878194122653728, 153722721216233504, 2450523492330897954, 2459565738516677154, 144715668026163714, 2314885530279477280, 2314885393379491872, 0, 2459530554110902272, 144715530048242178, 2459565875955638818, 9607679205057058, 2305878331563704352, 2314885392840532000, 0, 144715530585120768, 2450523354891944450, 2459565875955630626, 9044591812682274, 9007199254749216, 2314885530816356352, 2305843009213693952, 2459565738518774304, 153687544932991490, 2459565875955630626, 2305878339648365090, 2305878331026841600, 2314885530279485472, 2314885392840523776, 153687545467765250, 153687682906726946, 2459565875955630594, 2314885530850034178, 2305878331024736288, 2314885530816356384, 153157709678510080, 2450558539261936130, 153687682369856002, 144715667487195650, 35322350141954, 8192, 2314885530816356352, 2459563668847731234, 2459565876494606882, 5927337589569888802, 144680487954305634, 137438962178, 35184908967936, 2459000718894309408, 2459530683498897442, 144680346213032482, 144680345678250530, 144680346215121410, 2305843146652647458, 9007199254749216, 137977929760, 2459565739021967392, 144715667489292802, 144680346215121442, 2450523355428823586, 9007336693702656, 9007337232662528, 2105376, 144680345678127136, 153722867280912898, 2459530554144588290, 2315415366607708706, 2305878331563712544, 2314885530279485472, 32, 153722729839861792, 144680345678250498, 2459565875955638818, 9044591812682274, 2305843147191615520, 9007199793717248, 0, 153687682371944992, 2450523354889855522, 2459565876494598690, 2305878340187333154, 2305843146652655616, 2105376, 0, 153687682371953152, 2459565875957735970, 153722866744042018, 137474744866, 137977921536, 2305843009750573088, 35184911056928, 153687682906726946, 153687544930902562, 2459565738516677122, 2314850346444390914, 2314885530279477248, 2314885530816348192, 144150509886898208, 2450558539798807074, 2450523492330897922, 153722729302983170, 2305843146654745122, 9042383628935200, 2305878331563712512, 153685474725068832, 2459565739053548066, 144715668024066562, 144715530050339330, 9042384163700738, 9007336695791648, 2305878194122661888, 2459565730432032800, 2450523354889855490, 2450558677237760546, 153722729302991362, 9007337230565408, 2314885530281574432, 9007199791620128, 153722729271402528, 153722729302991362, 144715530048242210, 565157602402850, 2314885393377402880, 2105344, 35184911065088, 2450523424146063392, 144680345678250498, 144680483117203970, 3035428494472512002, 2314850346444267552, 2314850346446364672, 2314885393379500064, 2459530691585638443, 2459530692120420898, 2459565875957735970, 9160491554, 0, 0, 0, 0};

void on_default_enter(Context *ctx, int x, int y) {}

void on_default_interact(Context *ctx, int x, int y) {}

void on_key_1_interact(Context *ctx, int x, int y)
{
    char *message = malloc(20 + 1);
    strcpy(message, "You picked up a key.");
    ctx->message = message;

    ctx->inventory[0] += 1;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_key_2_interact(Context *ctx, int x, int y)
{
    char *message = malloc(20 + 1);
    strcpy(message, "You picked up a key.");
    ctx->message = message;

    ctx->inventory[1] += 1;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_key_3_interact(Context *ctx, int x, int y)
{
    char *message = malloc(20 + 1);
    strcpy(message, "You picked up a key.");
    ctx->message = message;

    ctx->inventory[2] += 1;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_key_4_interact(Context *ctx, int x, int y)
{
    char *message = malloc(20 + 1);
    strcpy(message, "You picked up a key.");
    ctx->message = message;

    ctx->inventory[3] += 1;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_gate_1_interact(Context *ctx, int x, int y)
{
    if (ctx->inventory[0] == 0)
    {
        char *message = malloc(32 + 1);
        strcpy(message, "You don't have the required key!");
        ctx->message = message;
        return;
    }
    else
    {
        char *message = malloc(22 + 1);
        strcpy(message, "You unlocked a gate.");
        ctx->message = message;
    }
    ctx->inventory[0]--;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_gate_2_interact(Context *ctx, int x, int y)
{
    if (ctx->inventory[1] == 0)
    {
        char *message = malloc(32 + 1);
        strcpy(message, "You don't have the required key!");
        ctx->message = message;
        return;
    }
    else
    {
        char *message = malloc(22 + 1);
        strcpy(message, "You unlocked a gate.");
        ctx->message = message;
    }
    ctx->inventory[1]--;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_gate_3_interact(Context *ctx, int x, int y)
{
    if (ctx->inventory[2] == 0)
    {
        char *message = malloc(32 + 1);
        strcpy(message, "You don't have the required key!");
        ctx->message = message;
        return;
    }
    else
    {
        char *message = malloc(22 + 1);
        strcpy(message, "You unlocked a gate.");
        ctx->message = message;
    }
    ctx->inventory[2]--;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_gate_4_interact(Context *ctx, int x, int y)
{
    if (ctx->inventory[3] == 0)
    {
        char *message = malloc(32 + 1);
        strcpy(message, "You don't have the required key!");
        ctx->message = message;
        return;
    }
    else
    {
        char *message = malloc(22 + 1);
        strcpy(message, "You unlocked a gate.");
        ctx->message = message;
    }
    ctx->inventory[3]--;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

void on_flag_interact(Context *ctx, int x, int y)
{
    FILE *fd = fopen("flag.txt", "r");
    char flag[128];
    char *message = malloc(256);

    fscanf(fd, "%s", flag);
    sprintf(message, "Congratulations! You got the flag: %s.\n", flag);
    ctx->message = message;
}

int walk(Context *ctx, int dx, int dy)
{
    int x1 = ctx->player->x, y1 = ctx->player->y;

    int x2 = x1 + dx, y2 = y1 + dy;

    if (!ctx->grid[COORD(x2, y2)]->is_walkable)
        return 1;
    if (ctx->monster->x == x2 && ctx->monster->y == y2)
        return 1;
    ctx->player->x += dx;
    ctx->player->y += dy;
    ctx->grid[COORD(x2, y2)]->on_enter(ctx, x2, y2);
    return 0;
}

int DXS[4] = {+0, +1, +0, -1};
int DYS[4] = {-1, +0, +1, +0};

int FAN_XXS[4] = {0, +1, 0, -1};
int FAN_XYS[4] = {+1, 0, -1, 0};
int FAN_YXS[4] = {-1, 0, +1, 0};
int FAN_YYS[4] = {0, +1, 0, -1};

Context *ctxs[MAX_CLIENTS];

void send_state_via_ws(Context *ctx)
{
    if (ctx == NULL)
        return;
    ws_cli_conn_t *client = ctx->client;

    int x = ctx->player->x;
    int y = ctx->player->y;

    int x2 = ctx->player->x + DXS[ctx->player->facing];
    int y2 = ctx->player->y + DYS[ctx->player->facing];

    Response res = RESPONSE__INIT;

    // grids
    Tile **grids = malloc(25 * sizeof(Tile *));
    int n_grids = 0;

    for (int i = 0; i < 5; i++)
    {
        for (int j = -i; j <= i; j++)
        {
            Tile *tile = malloc(sizeof(Tile));
            tile__init(tile);
            tile->offset_horizontal = i;
            tile->offset_vertical = j;

            int x = ctx->player->x + FAN_XXS[ctx->player->facing] * i + FAN_XYS[ctx->player->facing] * j;
            int y = ctx->player->y + FAN_YXS[ctx->player->facing] * i + FAN_YYS[ctx->player->facing] * j;

            int type;
            if (x < 0 || x >= GRID_COLS)
                type = 0;
            else if (y < 0 || y >= GRID_ROWS)
                type = 0;
            else
                type = ctx->grid[COORD(x, y)]->type;
            tile->type = type;
            grids[n_grids++] = tile;
        }
    }

    res.n_grids = n_grids;
    res.grids = grids;

    // entities
    Entity **entities = malloc(25 * sizeof(Entity *));
    int n_entities = 0;

    for (int i = 0; i < 5; i++)
    {
        for (int j = -i; j <= i; j++)
        {
            Entity *entity = malloc(sizeof(Entity));
            entity__init(entity);
            entity->offset_horizontal = i;
            entity->offset_vertical = j;

            int x = ctx->player->x + FAN_XXS[ctx->player->facing] * i + FAN_XYS[ctx->player->facing] * j;
            int y = ctx->player->y + FAN_YXS[ctx->player->facing] * i + FAN_YYS[ctx->player->facing] * j;

            if (ctx->monster->x == x && ctx->monster->y == y)
                entity->type = 1;

            entities[n_entities++] = entity;
        }
    }

    res.n_entities = n_entities;
    res.entities = entities;

    // inventory
    res.n_item_counts = ITEM_COUNT;
    res.item_counts = ctx->inventory;

    // message
    res.message = ctx->message;

    // possible moves
    int n_possible_moves = 0;
    Move possible_moves[4] = {0};
    possible_moves[n_possible_moves++] = MOVE__LEFT;
    possible_moves[n_possible_moves++] = MOVE__RIGHT;
    if (0 <= x2 && x2 < GRID_COLS &&
        0 <= y2 && y2 < GRID_ROWS &&
        ctx->grid[COORD(x2, y2)]->is_walkable)
        possible_moves[n_possible_moves++] = MOVE__FORWARD;
    if (0 <= x2 && x2 < GRID_COLS &&
        0 <= y2 && y2 < GRID_ROWS &&
        ctx->grid[COORD(x2, y2)]->is_interactable)
        possible_moves[n_possible_moves++] = MOVE__INTERACT;

    res.n_possible_moves = n_possible_moves;
    res.possible_moves = possible_moves;

    res.health = ctx->player->health;

    size_t packed_response_size = response__get_packed_size(&res);
    char *packed_response = malloc(packed_response_size);
    response__pack(&res, packed_response);
    ws_sendframe_bin(client, packed_response, packed_response_size);

    if (ctx->message != NULL)
    {
        free(ctx->message);
        ctx->message = NULL;
    }
    for (int i = 0; i < 25; i++)
        free(grids[i]);
    free(grids);
    free(packed_response);

void onopen(ws_cli_conn_t *client)
{
    char *cli;
    cli = ws_getaddress(client);

    int client_id = ws_get_id(client);

    // ---

    GameTile *wall_tile = malloc(sizeof(GameTile));
    wall_tile->type = 0;
    wall_tile->on_enter = on_default_enter;
    wall_tile->on_interact = on_default_interact;
    wall_tile->is_walkable = 0;
    wall_tile->is_interactable = 0;

    GameTile *fake_wall_tile = malloc(sizeof(GameTile));
    fake_wall_tile->type = 1;
    fake_wall_tile->on_enter = on_default_enter;
    fake_wall_tile->on_interact = on_default_interact;
    fake_wall_tile->is_walkable = 1;
    fake_wall_tile->is_interactable = 0;

    GameTile *walk_tile = malloc(sizeof(GameTile));
    walk_tile->type = 2;
    walk_tile->on_enter = on_default_enter;
    walk_tile->on_interact = on_default_interact;
    walk_tile->is_walkable = 1;
    walk_tile->is_interactable = 0;

    GameTile *flag_tile = malloc(sizeof(GameTile));
    flag_tile->type = 3;
    flag_tile->on_enter = on_default_enter;
    flag_tile->on_interact = on_flag_interact;
    flag_tile->is_walkable = 0;
    flag_tile->is_interactable = 1;

    GameTile *gate_1_tile = malloc(sizeof(GameTile));
    gate_1_tile->type = 4;
    gate_1_tile->on_enter = on_default_enter;
    gate_1_tile->on_interact = on_gate_1_interact;
    gate_1_tile->is_walkable = 0;
    gate_1_tile->is_interactable = 1;

    GameTile *gate_2_tile = malloc(sizeof(GameTile));
    gate_2_tile->type = 5;
    gate_2_tile->on_enter = on_default_enter;
    gate_2_tile->on_interact = on_gate_2_interact;
    gate_2_tile->is_walkable = 0;
    gate_2_tile->is_interactable = 1;

    GameTile *gate_3_tile = malloc(sizeof(GameTile));
    gate_3_tile->type = 6;
    gate_3_tile->on_enter = on_default_enter;
    gate_3_tile->on_interact = on_gate_3_interact;
    gate_3_tile->is_walkable = 0;
    gate_3_tile->is_interactable = 1;

    GameTile *gate_4_tile = malloc(sizeof(GameTile));
    gate_4_tile->type = 7;
    gate_4_tile->on_enter = on_default_enter;
    gate_4_tile->on_interact = on_gate_4_interact;
    gate_4_tile->is_walkable = 0;
    gate_4_tile->is_interactable = 1;

    GameTile *key_1_tile = malloc(sizeof(GameTile));
    key_1_tile->type = 8;
    key_1_tile->on_enter = on_default_enter;
    key_1_tile->on_interact = on_key_1_interact;
    key_1_tile->is_walkable = 0;
    key_1_tile->is_interactable = 1;

    GameTile *key_2_tile = malloc(sizeof(GameTile));
    key_2_tile->type = 9;
    key_2_tile->on_enter = on_default_enter;
    key_2_tile->on_interact = on_key_2_interact;
    key_2_tile->is_walkable = 0;
    key_2_tile->is_interactable = 1;

    GameTile *key_3_tile = malloc(sizeof(GameTile));
    key_3_tile->type = 10;
    key_3_tile->on_enter = on_default_enter;
    key_3_tile->on_interact = on_key_3_interact;
    key_3_tile->is_walkable = 0;
    key_3_tile->is_interactable = 1;

    GameTile *key_4_tile = malloc(sizeof(GameTile));
    key_4_tile->type = 11;
    key_4_tile->on_enter = on_default_enter;
    key_4_tile->on_interact = on_key_4_interact;
    key_4_tile->is_walkable = 0;
    key_4_tile->is_interactable = 1;

    GameTile **tileset = malloc(TILESET_SIZE * sizeof(GameTile *));
    tileset[0] = wall_tile;
    tileset[1] = fake_wall_tile;
    tileset[2] = walk_tile;
    tileset[3] = flag_tile;
    tileset[4] = gate_1_tile;
    tileset[5] = gate_2_tile;
    tileset[6] = gate_3_tile;
    tileset[7] = gate_4_tile;
    tileset[8] = key_1_tile;
    tileset[9] = key_2_tile;
    tileset[10] = key_3_tile;
    tileset[11] = key_4_tile;

    GameTile **grid = malloc(GRID_ROWS * GRID_COLS * sizeof(GameTile *));
    for (int x = 0; x < GRID_COLS; x++)
        for (int y = 0; y < GRID_ROWS; y++)
        {
            unsigned long long int range_grid = GRID[COORD(x, y) >> 4];
            grid[COORD(x, y)] = tileset[(range_grid >> (4 * (COORD(x, y) & 0xf))) & 0xf];
        }

    Player *player = malloc(sizeof(Player));
    player->x = 3;
    player->y = 27;
    player->facing = 1;
    player->health = 100;

    Monster *monster = malloc(sizeof(Monster));
    monster->x = GRID_COLS - 4;
    monster->y = GRID_ROWS - 4;

    int *inventory = malloc(sizeof(int) * ITEM_COUNT);

    memset(inventory, 0, sizeof(int) * ITEM_COUNT);

    Context *ctx = malloc(sizeof(Context));
    ctx->tileset = tileset;
    ctx->grid = grid;
    ctx->player = player;
    ctx->monster = monster;
    ctx->inventory = inventory;
    ctx->client = client;
    ctx->message = NULL;

    send_state_via_ws(ctx);

    ctxs[client_id] = ctx;
}

void onclose(ws_cli_conn_t *client)
{
    char *cli;
    cli = ws_getaddress(client);
    int client_id = ws_get_id(client);

    Context *ctx = ctxs[client_id];

    for (int i = 0; i < TILESET_SIZE; i++)
        free(ctx->tileset[i]);
    free(ctx->tileset);
    free(ctx->grid);
    free(ctx->player);
    free(ctx->monster);
    free(ctx->inventory);
    free(ctx);
    ctxs[client_id] = NULL;
}

void onmessage(ws_cli_conn_t *client,
               const unsigned char *msg, uint64_t size, int type)
{
    char *cli;
    cli = ws_getaddress(client);

    int client_id = ws_get_id(client);
    Context *ctx = ctxs[client_id];

    Request *req = request__unpack(NULL, size, msg);
    if (req == NULL)
        return;

    switch (req->move)
    {
    case MOVE__LEFT:
        ctx->player->facing = (ctx->player->facing - 1) & 3;
        break;
    case MOVE__RIGHT:
        ctx->player->facing = (ctx->player->facing + 1) & 3;
        break;
    case MOVE__FORWARD:
        int dx = DXS[ctx->player->facing];
        int dy = DYS[ctx->player->facing];
        walk(ctx, dx, dy);
        break;
    case MOVE__INTERACT:
        int x = ctx->player->x + DXS[ctx->player->facing];
        int y = ctx->player->y + DYS[ctx->player->facing];
        ctx->grid[COORD(x, y)]->on_interact(ctx, x, y);
        break;
    default:
        return;
    }
    send_state_via_ws(ctx);
}

int sgn(int x)
{
    if (x == 0)
        return 0;
    if (x < 0)
        return -1;
    return 1;
}

// Return 1 if the game is still running, 0 otherwise
int tick(Context *ctx)
{
    int dx = ctx->player->x - ctx->monster->x;
    int dy = ctx->player->y - ctx->monster->y;

    ctx->player->health -= 1;
    if (ctx->player->health == 0)
        return 0;

    if (DIST2(dx, dy) == 1)
    {
        ctx->player->health -= 10;
        if (ctx->player->health <= 0)
        {
            ctx->player->health = 0;
            return 0;
        }
        char *message = malloc(27 + 1);
        strcpy(message, "You were bitten by a snake!");
        ctx->message = message;
    }
    else if (DIST2(dx, dy) <= 10 * 10)
    {
        int facing = 0;
        if (abs(dx) >= abs(dy))
        {
            facing = 2 - sgn(dx);
        }
        else
        {
            facing = 1 + sgn(dy);
        }

        int x = ctx->monster->x + DXS[facing];
        int y = ctx->monster->y + DYS[facing];
        if (ctx->grid[COORD(x, y)]->is_walkable)
        {
            ctx->monster->x = x;
            ctx->monster->y = y;
        }
    }

    return 1;
}

int main()
{
    struct ws_events evs;
    evs.onopen = &onopen;
    evs.onclose = &onclose;
    evs.onmessage = &onmessage;

    ws_socket(&evs, 8080, 1, 1000);

    while (1)
    {
        for (int i = 0; i < MAX_CLIENTS; i++)
        {
            Context *ctx = ctxs[i];
            if (ctx == NULL)
                continue;
            int is_alive = tick(ctx);
            if (!is_alive)
            {
                char *message = malloc(25 + 1);
                strcpy(message, "You ran out of stamina...");
                ctx->message = message;
            }
            send_state_via_ws(ctx);
            if (!is_alive)
            {
                ws_cli_conn_t *client = ctx->client;
                ws_close_client(client);
            }
        }
        sleep(1);
    }

    return 0;
}

When connected to the server, the binary will run the onopen function and define a context specific to the client. A Context struct is created and the struct members are defined and it stores the current game state, which will be constantly updated during the gameplay. If we look at the variable GRID and the tileset, we are able to draw the game map by ourselves:

The player spawns in the middle left at the beginning. There are four keys at the corners and there are five gates yet to be unlocked with these keys. The objective is to interact with the flag in the middle right.

There are some difficulties identified by reading the map:

  1. We might need to penetrate through walls to grab the keys on the left.
  2. We have to dodge the snake to grab the key on the bottom right.
  3. We might need to duplicate the key on the top right.

Penetrating the walls, part one

To start with, we will grab on the bottom left. Observe that there is a different wall tile near the key.

The bottom right corner of the map.
😡 They are visually the same in the game! I intentionally used a different sprite for the different wall tiles, which actually looked the same in the game. However, I expect one could note the differences by extracting the type IDs of the tiles.

The normal wall tiles (type 0) are given the attribute is_walkable = 0, while the special wall tile (type 1) has the attribute is_walkable = 1. Although it looks like a wall, we can actually walk through it.

Dodging the snake

We are going to grab without getting bitten by snakes, which is fatal. We will lost 10 HP every bite. Also, the snake will attempt to move forward to our direction if we are too close to it. Fortunately, the snake AI is pretty dumb and you could easily lure it away. Since we did not rate limit the client’s requests, it is also fine to walk 100 blocks per second. This allow us easily avoiding the venom.

Juking the snake.

Duplicating the key

is easy to grab. There are no snakes, no impenetrable walls, no nothing. We can walk towards the key and grab that manually. Since the corresponding key will be consumed when we open a gate, we need multiple s. That is actually not the case, and we will actually unlock both gates at once.

This is actually a flaw (intended!) in the program. Look at the source code when a gate is interacted:

void on_gate_2_interact(Context *ctx, int x, int y)
{
    if (ctx->inventory[1] == 0)
    {
        char *message = malloc(32 + 1);
        strcpy(message, "You don't have the required key!");
        ctx->message = message;
        return;
    }
    else
    {
        char *message = malloc(22 + 1);
        strcpy(message, "You unlocked a gate.");
        ctx->message = message;
    }
    ctx->inventory[0]--;
    ctx->grid[COORD(x, y)]->type = 2;
    ctx->grid[COORD(x, y)]->is_walkable = 1;
    ctx->grid[COORD(x, y)]->on_interact = on_default_interact;
    ctx->grid[COORD(x, y)]->is_interactable = 0;
}

The grid is copied to client’s context when the connection is made. Also, the line

ctx->grid[COORD(x, y)]->is_walkable = 1;

sets the gate into a walkable grid when we have the key. It is important to note that ctx->grid[COORD(x, y)] is a pointer to the GameTile. Instead of setting the grid on a particular coordinate to a walkable path, it sets the “gate 2” tile into a walkable tile. With that said, every “gate 2” tile will be replaced by a path tile.

Opening two gates with one key.

Penetrating the walls, part two

We will try to get through the inpenetratable wall to get on the top left… Well, you simply can’t. You must get through the first gate without having that key.

The map in this game is implemented as an one-dimensional array and the indexes are calculated by the COORD macro from 2D coordinates. Smell something fishy?

Look at the map again and label the indexes and the coordinates on the tiles:

The middle left and middle right of the map.

However, the state sent to us includes a list of possible moves. If we are at $x = 0$ and we are facing leftwards, $x_2$ below is $x_2 = -1 < 0$. Now we do not have FORWARD in our possible_moves:

void send_state_via_ws(Context *ctx)
{
    // (skipped...)
    int x2 = ctx->player->x + DXS[ctx->player->facing];
    int y2 = ctx->player->y + DYS[ctx->player->facing];
    // (skipped...)
    if (0 <= x2 && x2 < GRID_COLS &&
        0 <= y2 && y2 < GRID_ROWS &&
        ctx->grid[COORD(x2, y2)]->is_walkable)
        possible_moves[n_possible_moves++] = MOVE__FORWARD;
    // (skipped...)
}

Thus we will be unable to move forward as it has been blocked by the web client:

function moveForward() {
  if (!possibleMovesList.includes(proto.Move.FORWARD)) return
  const request = new proto.Request()
  request.setMove(proto.Move.FORWARD)

  ws.send(request.serializeBinary())
}

To deal with that, we can patch the function and throw the guard clause away. Since the server does not check require $x_2 \geq 0$ in the walk function. In this way, we can simply “teleport” from the left to the right.

We moved in a warped space!

Finally, let me share a screenshot of the game Danganronpa v3, where I borrowed the idea in this part.

Taken from the game Danganronpa v3, where this part is inspired from.

Now we can skip the first gate and unlock the remaining gates. What is left is to walk towards the flag tile and interact with it, yielding the flag:

hkcert23{h0w_d1d_y0u_f1nd_s0_m4ny_vu1n3r461117y_47_th3_s4m3_t1m3??}

Postmortem

I am very happy that the challenge actually worked. This is one of the challenges I wrote in late October, which is less than one month before the CTF begins. Good that it is still a decent challenge. Since the main focus is to identify bugs rather than dig deep into reverse engineering, I decided to implement the backend in C. Given that I am such an inexperienced C programmer, I am very grateful that the challenge was not pwnable (at least during the contest period).

Apart from handling the C pointers, I also struggled a lot of visualing stuffs in 3D. Eventually, I decided to build everything from the ground up using CSS. That was not easy for someone like me (who is bad at geometry), and I spent around one day to get this implemented. Without this blogpost from Franklin Ta, I think I can never have the challenge finished. This is one of the screenshots I took while I develop:

Finally, I did not work on the graphics at all. What I did is to glue the royalty-free resources below into a viable user interface. Kudos to the project owners!