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.
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>#defineN102#defineMOD1000000007intmain(){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”.
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 109+7. If the number of paths reaching (i,j) is a multiple of 109+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 (109+7)k possible paths to a chamber. For simplicity, we will find a configuration such that there are 109+7 paths to the bottom-right chamber.
If there are n+1 rows and m+1 columns that fill with all dots, then there are Cmn+m paths from the top left to the bottom right. However, it is impossible to find small n and m such that Cmn+m is a multiple of 109+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⋅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 Ckns such that they sum to 109+7. One of the config I found is C697+C569+C539+C344+C216+C15. Thus the sizes of the rectangle grids are 92×7, 65×6, 35×6, 42×4, 15×3 and 5×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}.
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.
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.
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}.
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}.
✋🏾 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:
exportconst 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:
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}.
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.
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:
We might need to penetrate through walls to grab the keys on the left.
We have to dodge the snake to grab the key on the bottom right.
We might need to duplicate the key on the top right.
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.
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.
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:
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.
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, x2 below is x2=−1<0. Now we do not have FORWARD in our possible_moves:
To deal with that, we can patch the function and throw the guard clause away. Since the server does not check require x2≥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:
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!