## Grey Cat The Flag Qualifiers 2022 Writeups

### Organized by NUS Greyhats from 6 June - 10 June

·  ☕ 40 min read  ·  🌈🕊️ rainbowpigeon

We got 4th overall and also qualified for the finals as the 3rd verified local team! My teammates were absolute beasts even though they were quite busy :) Challenge names with 🍭 are supposed to be “easier” while those with 🩸 come with first blood prizes (I managed to get 2!).

CTFTime.org Event Page https://ctftime.org/event/1643/
Official Grey Cat The Flag Discord https://discord.gg/d9wbXEP2wN
Official Event Landing Page https://ctf.nusgreyhats.org/

## RE

### 🩸 Angry Robot

You have entered a top secret robot production facilities and your clumsy friend tripped the alarm. You and your friends are about to be “decontaminated”.

Luckily, you have unpacked the authentication firmware during previous reconaissance. Can you use them to override the decontamination process?

MD5 (authentication.tar.gz) = 965b5a18735af4bbfa81879e2cedc9cc

• rootkid

nc challs.nusgreyhats.org 10523
Attached: authentication.tar.gz

#### First Look

In the provided archive, there are 100 stripped 64-bit ELF binaries.

 1 2 3 4 5 6 7 8  a@a:/grey/authentication$ls -1 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0 04480e6a6ad345bf50f4f428f802656264d4738a000270d8967734cecd0ba2a9 08d4a97677fd1c8d14b069b9922518e5c7a680b67417990d998c41357d4c7c14 ... ... a@a:/grey/authentication$ ls -1 | wc -l 100 
 1 2  a@a:/grey/authentication$file 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3f33add36e8e79282d0d0ed3aaa826744ddc5a39, for GNU/Linux 3.2.0, stripped  If we open a few of them in IDA Pro, we’ll see that all of these binaries basically do the same thing: fgets is used to read an input string from stdin and then each character is checked against 2 hardcoded strings using some arithmetic operations. What differs across the binaries, though, are the input length, the 2 hardcoded strings, the constant used for the arithmetic operations, and the addresses of the main function and some locations. Below is the pseudocode for the 03e73f2bd4c480b209caa321eb3e2eb974b680e2ec7cbada8cfd457375eaa3c0 binary with variables and function names renamed by me:   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  _BOOL8 __fastcall main(int a1, char **a2, char **a3) { *input = 0LL; fgets(input, 30, stdin); return check_input(input) == 0; } __int64 __fastcall check_input(__int64 input) { qmemcpy(str1, "Li?ittSAXlsKEi9d2GQNP3uCOTHDD", 29); qmemcpy(str2, "Yp9ASTqGSZpsG3IU7jk3TdeFvwpm", 29); is_success = 1; for ( i = 0; i <= 28; ++i ) { if ( check_chr(*(i + input), i) != *(str1 + i) || check_chr(*(i + input), *(str1 + i)) != *(str2 + i) ) is_success = 0; } return is_success; } __int64 __fastcall check_chr(char a1, int a2) { if ( a1 <= '\x1F' || a1 == '\x7F' ) exit(1); return ((a2 * a1 % 1431 + a1 + a2) % 73 + 48); }  If we connect to the server and port provided in the challenge description, we’ll see that we’re supposed to provide the correct input strings (passwords) for specified binaries (challenge codes) within 10 minutes.  1 2 3 4 5  Unauthorized access detected. Decontamination in 10 minutes Do you wish to override? (y/n) y Password required for challenge code: ffbb5c9d6d32299cc53c90d410282b42f7431c60ee40540f19603f0b4c1cfc90  Technically there’s enough time to do this manually since the server turned out to only request input strings for 5 binaries, but I didn’t know this and went ahead to learn and use angr for automated reverse engineering which I suppose is what’s hinted by the challenge name. It went surprisingly well for my first time! #### Using angr We’ll be using angr’s SimulationManager to help us find a program state with the correct input string by means of symbolic execution, so there’s a few things we need to identify in each binary we process: • Addresses we want to find/reach • Addresses we want to avoid • Input length ##### Addresses The is_success = 0; basic block in check_input with address 0x401305 is what we’ll tell angr to avoid. And in main we want check_input(input) == 0; to fail meaning we don’t want the jz after check_input to be taken, so the location we should tell angr to find for is 0x401395. But of course as mentioned earlier, I noticed these address locations and the address of main change across the binaries provided, so I first located fgets using the ELF binary’s Procedure Linkage Table (PLT) and then retrieved the first and only callgraph predecessor of fgets which is main. To get to the basic block in check_input that we want angr to avoid, I retrieved the second last call site in main which is call check_input. I then retrieved the return site of the last function call in check_input which is a call check_chr, and added the offset of 0xF to reach the mov [rbp+is_success], 0 that we want to avoid. To get to the basic block in main that we want angr to find, I sorted the basic blocks in main by address and took the address of the 5th last basic block. ##### Input I retrieved the first basic block in main and searched through the instructions for mov esi, xx where xx is the length argument given to fgets to retrieve user input. xx - 1 will then represent the actual input length because fgets will also take in a newline character. I also constrained the input to only be printable characters because the code in check_chr appears to try to check for that. #### Solution Script   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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103  import angr import claripy import capstone import logging from pwn import * logging.getLogger("claripy.ast.bv").setLevel("ERROR") logging.getLogger("angr.storage").setLevel("ERROR") logging.getLogger("angr.simos").setLevel("ERROR") logging.getLogger("angr.analyses.cfg").setLevel("ERROR") def char(state, c): return state.solver.And(c <= "~", c >= " ") def solve_file(file): print(file) p = angr.Project("authentication/{}".format(file), auto_load_libs=False) fgets_addr = p.loader.main_object.plt["fgets"] cfg = p.analyses.CFGFast() # find the basic block to FIND fgets_callers = cfg.functions.callgraph.predecessors(fgets_addr) # get the first and only calling predecessor which is main main_func = [cfg.functions[caller] for caller in fgets_callers][0] print("main: {}".format(hex(main_func.addr))) main_blocks = sorted(main_func.blocks, key=lambda b: b.addr) # we want to reach the 5th last block where the jz is not taken FIND = main_blocks[-5].addr print("FIND: {}".format(hex(FIND))) # find length of input required via # mov esi, 1Eh first_block = main_blocks[0] for insn in first_block.capstone.insns: if ( len(insn.operands) == 2 and insn.mnemonic == "mov" and insn.operands[0].reg == capstone.x86.X86_REG_ESI and insn.operands[1].type == capstone.x86.X86_OP_IMM ): INPUT_LENGTH = insn.operands[1].imm - 1 print("input length: {}".format(INPUT_LENGTH)) # find the basic block to AVOID functions = cfg.kb.functions[main_func.addr] main_calls = functions.get_call_sites() # second last call in main is check_input check_input_addr = [functions.get_call_target(call) for call in main_calls][-2] functions = cfg.kb.functions[check_input_addr] check_input_calls = functions.get_call_sites() # get the last return site of the function calls in check_input AVOID = max( filter( None, [functions.get_call_return(call) for call in check_input_calls], ) ) OFFSET = 0xF AVOID += OFFSET print("AVOID: {}".format(hex(AVOID))) flag = claripy.BVS("flag", INPUT_LENGTH * 8) state = p.factory.entry_state(stdin=flag) state.options.add(angr.options.LAZY_SOLVES) for c in flag.chop(8): state.solver.add(char(state, c)) sm = p.factory.simulation_manager(state) sm.explore(find=FIND, avoid=AVOID) print("finding...") if sm.found: found = sm.found[0] solution = found.solver.eval(flag, cast_to=bytes) print("found: {}".format(solution)) else: print("not found") solution = None print("-" * 64) return solution if __name__ == "__main__": conn = remote("challs.nusgreyhats.org", 10523) conn.recvuntil(b")") conn.recvline() conn.sendline(b"y") response = b"" while True: response = conn.recvline() if b": " in response: challenge_code = response.split(b": ")[-1].rstrip(b"\n").decode() password = solve_file(challenge_code) conn.sendline(password) else: break print(response)    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  [x] Opening connection to challs.nusgreyhats.org on port 10523 INFO | 2022-06-11 03:22:06,986 | pwnlib.tubes.remote.remote.2891224537696 | Opening connection to challs.nusgreyhats.org on port 10523 [x] Opening connection to challs.nusgreyhats.org on port 10523: Trying 35.247.145.55 INFO | 2022-06-11 03:22:06,992 | pwnlib.tubes.remote.remote.2891224537696 | Opening connection to challs.nusgreyhats.org on port 10523: Trying 35.247.145.55 [+] Opening connection to challs.nusgreyhats.org on port 10523: Done INFO | 2022-06-11 03:22:07,013 | pwnlib.tubes.remote.remote.2891224537696 | Opening connection to challs.nusgreyhats.org on port 10523: Done a9f59335ea82cf55f4f7f7dd5d8eb3f8ded783bd2d902adcc25a4aca75328895 main: 0x40131f FIND: 0x401380 input length: 25 AVOID: 0x4012f7 finding... found: b'08xsiic4i626rqn2urg5yhx0x' ---------------------------------------------------------------- 65636c7430d22186ac375d173de25ea6305d5aa7b0feb4526e1ec1dd2bcc16cd main: 0x40132b FIND: 0x40138d input length: 27 AVOID: 0x401303 finding... found: b'vccbxfmn6cr1mvfe2f902sgzdpf' ---------------------------------------------------------------- 70b4bed62eb3e1c2cbbd9e393154a7b5a403e973400c662df032d4f59fe8a333 main: 0x40132b FIND: 0x40138d input length: 27 AVOID: 0x401303 finding... found: b'brrb92dt6qeolkxdalihzid6kte' ---------------------------------------------------------------- 5e346c4ce1773d5e4a5bb498dd3c9d5124d03ecfc494e15ee5ac6b643be7ee9d main: 0x40132f FIND: 0x401397 input length: 29 AVOID: 0x401307 finding... found: b'zxokdvjcwbdo0bq55xpoq7qv23df5' ---------------------------------------------------------------- d98f704409467ff7d85c439371b0f1acf1e5850b6e030effe4c9d2a9af83c825 main: 0x401331 FIND: 0x40139d input length: 30 AVOID: 0x401309 finding... found: b'wtg60dw2ew3tgn112c0nybfbr29o9|' ---------------------------------------------------------------- b'grey{A11_H4il_SkyN3t}\n' [*] Closed connection to challs.nusgreyhats.org port 10523 INFO | 2022-06-11 03:23:39,041 | pwnlib.tubes.remote.remote.2891224537696 | Closed connection to challs.nusgreyhats.org port 10523  Flag: grey{A11_H4il_SkyN3t} ### 🩸 Runtime Environment 1 GO and try to solve this basic challenge. FAQ: If you found the input leading to the challenge.txt you are on the right track MD5 (gogogo.tar.gz) = 5515f1c3eee00e4042bf7aba84bbec5c • rootkid Attached: gogogo.tar.gz  1 2 3 4  a@a:/grey/gogogo$ file binary binary: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=OHBJFJh5S4MEkda8Q683/cMydJq6y9QbVjBCjK1KP/8R1f9ddSl9EfpM8KP2Dy/3G9-Ju3BW7WUsgoGNyvl, not stripped a@a:/grey/gogogo$cat challenge.txt GvVf+fHWz1tlOkHXUk3kz3bqh4UcFFwgDJmUDWxdDTTGzklgIJ+fXfHUh739+BUEbrmMzGoQOyDIFIz4GvTw+j--  In the given archive there’s a 64-bit ELF Go binary and a challenge.txt containing encoded text that looks like some variant of Base64 encoding. From blackbox analysis, the encoding scheme also seems similar to Base64 in that 3-byte input blocks (aaa) are independently encoded to 4-byte output blocks (CGTx), and padding (-) is used to keep the output length a multiple of 4 when the input length is not a multiple of 3:   1 2 3 4 5 6 7 8 9 10 11 12  a@a:/grey/gogogo$ echo a | ./binary CV-- a@a:/grey/gogogo$echo aa | ./binary CGJ- a@a:/grey/gogogo$ echo aaa | ./binary CGTx a@a:/grey/gogogo$echo aaaa | ./binary CGTxCV-- a@a:/grey/gogogo$ echo aaaaa | ./binary CGTxCGJ- a@a:/grey/gogogo$echo aaaaaa | ./binary CGTxCGTx  Let’s check out the pseudocode of the binary in IDA Pro for further analysis.   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  // main.main void __cdecl main_main() { v10 = runtime_newobject(&RTYPE_string); v12[0] = &RTYPE__ptr_string; v12[1] = v10; fmt_Fscanln(&go_itab__ptr_os_File_comma_io_Reader, os_Stdin, v12, 1LL, 1LL); v7 = 4 * ((((((v10[1] + 2) * 0xAAAAAAAAAAAAAAABLL) >> 64) + v10[1] + 2) >> 1) - ((v10[1] + 2) >> 63)); v9 = runtime_makeslice(&RTYPE_uint8, v7, v7); v1 = runtime_stringtoslicebyte(v8, *v10, v10[1]); main_Encode(v9, v7, v7, v1, v2); v3 = runtime_slicebytetostring(0LL, v4, v5, v6); v0 = runtime_convTstring(v3, *(&v3 + 1)); v11[0] = &RTYPE_string; v11[1] = v0; fmt_Fprintln(&go_itab__ptr_os_File_comma_io_Writer, os_Stdout, v11, 1LL, 1LL); }  A string is read from stdin, converted into a Go slice of bytes, and then passed into main_Encode. The encoded slice of bytes is then converted back into a string and printed. What does main_Encode do? At the top, we see a string b64str containing 64 unique characters. This is used to form the majority of output in the first while loop where input_ptr is iterated over in chunks of 3 while output is iterated over in chunks of 4.   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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86  // main.Encode __int64 __usercall main_Encode@( __int64 output, unsigned __int64 output_length, __int64 a3, __int64 input_ptr, unsigned __int64 input_length) { qmemcpy(b64str, "NaRvJT1B/m6AOXL9VDFIbUGkC+sSnzh5jxQ273d4lHPg0wcEpYqruWyfZoM8itKe", sizeof(b64str)); i_by_3 = 0LL; j_enc_by_4 = 0LL; while ( i_by_3 < 3 * (input_length / 3) ) { if ( i_by_3 >= input_length ) runtime_panicIndex(); if ( i_by_3 + 1 >= input_length ) runtime_panicIndex(); if ( i_by_3 + 2 >= input_length ) runtime_panicIndex(); v7 = (*(input_ptr + i_by_3) << 16) | (*(input_ptr + i_by_3 + 1) << 8) | *(i_by_3 + input_ptr + 2); if ( j_enc_by_4 >= output_length ) runtime_panicIndex(); *(output + j_enc_by_4) = *(b64str + ((v7 >> 18) & 0x3F)); v8 = v7; v9 = *(b64str + ((v7 >> 12) & 0x3F)); if ( j_enc_by_4 + 1 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 1) = v9; v10 = v8; v11 = *(b64str + ((v8 >> 6) & 0x3F)); if ( j_enc_by_4 + 2 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 2) = v11; v12 = *(b64str + (v10 & 0x3F)); if ( j_enc_by_4 + 3 >= output_length ) runtime_panicIndex(); *(output + j_enc_by_4 + 3) = v12; i_by_3 += 3LL; j_enc_by_4 += 4LL; } remainder = input_length - i_by_3; if ( input_length == i_by_3 ) return a3; if ( i_by_3 >= input_length ) runtime_panicIndex(); if ( remainder == 2 ) { if ( i_by_3 + 1 >= input_length ) runtime_panicIndex(); v14 = (*(input_ptr + i_by_3) << 16) | (*(input_ptr + i_by_3 + 1) << 8); } else { v14 = *(input_ptr + i_by_3) << 16; } v15 = v14; v16 = *(b64str + ((v14 >> 18) & 0x3F)); if ( j_enc_by_4 >= output_length ) runtime_panicIndex(); *(output + j_enc_by_4) = v16; v17 = v15; v18 = *(b64str + ((v15 >> 12) & 0x3F)); if ( j_enc_by_4 + 1 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 1) = v18; if ( remainder == 1 ) // add padding { if ( j_enc_by_4 + 2 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 2) = '-'; if ( j_enc_by_4 + 3 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 3) = '-'; } else if ( remainder == 2 ) { v20 = *(b64str + ((v17 >> 6) & 0x3F)); if ( j_enc_by_4 + 2 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 2) = v20; if ( j_enc_by_4 + 3 >= output_length ) runtime_panicIndex(); *(j_enc_by_4 + output + 3) = '-'; // add padding } return a3; }  At the bottom, we see that for remaining input bytes not covered by the chunks of 3, the character - is padded to output. All these observations match what we found from our blackbox analysis and it really does seem like Base64 encoding. For additional confirmation, I also Googled the >> 18 & 0x3f seen in the code and the results all show code for Base64 encoding. The difference here is that NaRvJT1B/m6AOXL9VDFIbUGkC+sSnzh5jxQ273d4lHPg0wcEpYqruWyfZoM8itKe is used as a custom character set with - as the padding instead of the standard ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ with = as the padding. At this point I just tried decoding the contents of challenge.txt in CyberChef with the custom Base64 character set and it turns out 4 rounds of decoding are needed. Flag: grey{B4s3d_G0Ph3r_r333333} ### 🩸 Runtime Environment 2 This time it HAS to be harder. MD5 (hasbeen.tar.gz) = 46ff7d24975679901d8f8d769e567b09 • rootkid Attached: hasbeen.tar.gz  1 2 3 4 5  a@a:/grey/hasbeen$ file binary binary: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=edaa0989ef693326c9c1cb23f37ce489d54ce939, not stripped a@a:/grey/hasbeen$xxd challenge.bin 00000000: 7cab e724 16c3 6067 1391 49c0 4e20 c4a2 |..$..g..I.N .. 00000010: 48d0 318c 704a 0ce2 c57c 98af 0398 6577 H.1.pJ...|....ew 

A 64-bit ELF binary is given as well as a challenge.bin containing some encoded/encrypted data.

In IDA Pro we see that the ELF’s main references functions hs_main and ZCMain_main_closure which is a sign that this is a GHC-compiled Haskell binary.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24  int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { HIDWORD(v5) = HIDWORD(defaultRtsConfig); LODWORD(v5) = 3; hs_main( argc, (_DWORD)argv, (unsigned int)&ZCMain_main_closure, (_DWORD)argv, v3, v4, v5, 1LL, 0LL, 1LL, 0LL, (__int64)&FileEventLogWriter, (__int64)FlagDefaultsHook, (__int64)OnExitHook, (__int64)StackOverflowHook, (__int64)OutOfHeapHook, (__int64)MallocFailHook, 0LL); } 

The decompiler available at https://github.com/gereeter/hsdecomp produces incomplete output with errors so I looked for forks and found this one https://github.com/Timeroot/hsdecomp/ to give better output:

  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  Main_main_closure = >>= $fMonadIO (fmap$fFunctorIO unpack getLine) (\loc_4232104_arg_0 -> >>= $fMonadIO getCurrentTime (\loc_4231968_arg_0 -> >>=$fMonadIO ($(\loc_4231776_arg_0 loc_4231776_arg_1 loc_4231776_arg_2 loc_4231776_arg_3 loc_4231776_arg_4 -> return$fMonadIO) ($(map (\loc_4231432_arg_0 loc_4231432_arg_1 loc_4231432_arg_2 loc_4231432_arg_3 loc_4231432_arg_4 -> . (\loc_4231360_arg_0 loc_4231360_arg_1 loc_4231360_arg_2 loc_4231360_arg_3 loc_4231360_arg_4 -> fromIntegral$fIntegralInt32 $fNumInteger) (\loc_4231256_arg_0 -> .&.$fBitsInt32 loc_4231256_arg_0 (\loc_4231128_arg_0 loc_4231128_arg_1 loc_4231128_arg_2 loc_4231128_arg_3 loc_4231128_arg_4 -> fromInteger $fNumInt32 (IS 255))))) ($ (\loc_4230968_arg_0 -> loc_4229392 $fIntegralInt loc_4230968_arg_0) (. (case$fRealFracFixed $fHasResolutionTYPEE12 of loc_4228312_case_tag_DEFAULT_arg_0@_DEFAULT -> floor$fIntegralInt ) (. nominalDiffTimeToSeconds utcTimeToPOSIXSeconds) loc_4231968_arg_0 ) ) ) ) (\loc_4230768_arg_0 -> >>= $fMonadIO ($ (\loc_4230568_arg_0 loc_4230568_arg_1 loc_4230568_arg_2 loc_4230568_arg_3 loc_4230568_arg_4 -> return $fMonadIO) (zipWith (\loc_4230360_arg_0 loc_4230360_arg_1 loc_4230360_arg_2 loc_4230360_arg_3 loc_4230360_arg_4 -> xor$fBitsInt32) (map (\loc_4230184_arg_0 loc_4230184_arg_1 loc_4230184_arg_2 loc_4230184_arg_3 loc_4230184_arg_4 -> fromIntegral $fIntegralWord8$fNumInt32) loc_4232104_arg_0) (map (fromIntegral $fIntegralInteger$fNumInt32) loc_4230768_arg_0))) (\loc_4229920_arg_0 -> $putStr ($ pack (map (\loc_4229592_arg_0 loc_4229592_arg_1 loc_4229592_arg_2 loc_4229592_arg_3 loc_4229592_arg_4 -> . chr (fromIntegral $fIntegralInt32$fNumInt)) loc_4229920_arg_0)))) ) ) loc_4229392 = \loc_4229392_arg_0 loc_4229392_arg_1 -> : loc_4229128 (loc_4229392 $fIntegralInt32 loc_4229128) loc_4229128 = xor$fBitsInt32 loc_4228928 (shiftL $fBitsInt32 loc_4228928 (I# 5)) loc_4228928 = xor$fBitsInt32 loc_4228728 (shiftR $fBitsInt32 loc_4228728 (I# 17)) loc_4228728 = xor$fBitsInt32 loc_4228568 (shiftL $fBitsInt32 loc_4228568 (I# 13)) loc_4228568 = fromIntegral loc_4229392_arg_0$fNumInt32 loc_4229392_arg_1 

The drawback though is that 5 unused arguments are added to some of the anonymous functions thus cluttering up the output. In the cleaned-up output with renamed variables below, I’ve just renamed these arguments to {5argsthing} so that it’s less of an eyesore.

  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 72 73 74 75 76 77  Main_main_closure = >>= $fMonadIO (fmap$fFunctorIO unpack getLine) (\input -> >>= $fMonadIO getCurrentTime (\currentTimeUTC -> >>=$fMonadIO ($(\{5argsthing} -> return$fMonadIO) (${- for each of the random numbers in the generate list, mask it with & 0xff -} (map ( \{5argsthing} -> . (\{5argsthing} -> fromIntegral$fIntegralInt32 $fNumInteger) ( \loc_4231256_arg_0 -> .&.$fBitsInt32 loc_4231256_arg_0 ( \{5argsthing} -> fromInteger $fNumInt32 (IS 255) ) ) ) ) {- generate list of random numbers based on current floored time in seconds -} ($ (\flooredtime_4230968 -> rng_4229392 $fIntegralInt flooredtime_4230968) {- floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds -} (. (case$fRealFracFixed $fHasResolutionTYPEE12 of loc_4228312_case_tag_DEFAULT_arg_0@_DEFAULT -> floor$fIntegralInt ) (. nominalDiffTimeToSeconds utcTimeToPOSIXSeconds)currentTimeUTC ) ) ) ) ( \randnumbers_4230768 -> >>= $fMonadIO ($ (\{5argsthing} -> return $fMonadIO) ( zipWith (\{5argsthing} -> xor$fBitsInt32) (map (\{5argsthing} -> fromIntegral $fIntegralWord8$fNumInt32) input) (map (fromIntegral $fIntegralInteger$fNumInt32) randnumbers_4230768) ) ) ( \output_4229920 -> $putStr ($ pack ( map ( \{5argsthing} -> . chr (fromIntegral $fIntegralInt32$fNumInt) ) output_4229920 ) ) ) ) ) ) {- i think this recursively creates a list of random numbers and returns it -} rng_4229392 = \IntegralIntarg timearg -> : a4_4229128 (rng_4229392 $fIntegralInt32 a4_4229128) a4_4229128 = xor$fBitsInt32 a3_4228928 (shiftL $fBitsInt32 a3_4228928 (I# 5)) a3_4228928 = xor$fBitsInt32 a2_4228728 (shiftR $fBitsInt32 a2_4228728 (I# 17)) a2_4228728 = xor$fBitsInt32 a1_4228568 (shiftL $fBitsInt32 a1_4228568 (I# 13)) a1_4228568 = fromIntegral IntegralIntarg$fNumInt32 timearg {- xorshift32?: a1 = from timearg a2 = a1 ^ (a1 << 13) a3 = a2 ^ (a2 >> 17) a4 = a3 ^ (a3 << 5) return a4? -} 

Now to be frank I can’t explain in-depth how the Haskell code flow exactly works, but this is my general interpretation:

1. Input string is taken from stdin
2. Current UTC time is retrieved, converted to POSIX time, and floored to seconds
3. POSIX time in seconds is used to seed a Xorshift32 generator function
4. Each byte in the input string is XOR’d with a 0xFF-masked integer generated from the Xorshift32 function
5. Final output is converted to characters, packed together and printed

The possible UTC time used to produce the contents in challenge.bin could range from the time binary was made to the time challenge.bin was made. I replicated everything in Python and bruteforced for the time used although it turns out to be just the exact last modified time of challenge.bin.

  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  import binascii # # alternative from https://stackoverflow.com/questions/1375897/how-to-get-the-signed-integer-value-of-a-long-in-python # def toSigned32(n): # n = n & 0xffffffff # return (n ^ 0x80000000) - 0x80000000 def toSigned(n, bits): n = n & (2**bits - 1) return n | (-(n & (1 << (bits - 1)))) if __name__ == "__main__": challenge_bin = binascii.unhexlify( "7cabe72416c36067139149c04e20c4a248d0318c704a0ce2c57c98af03986577" ) max_time = 1654353866 # last modified time of challenge.bin: Saturday, June 4, 2022 10:44:26 PM GMT+08:00 min_time = 1654352191 # last modified time of binary: Saturday, June 4, 2022 10:16:31 PM GMT+08:00 for time in range(max_time, min_time, -1): # reset seed state seed = time def xorshift32(): global seed seed ^= toSigned(seed << 13, 32) seed ^= toSigned(seed >> 17, 32) seed ^= toSigned(seed << 5, 32) return seed output = "" for c in challenge_bin: o = c ^ (xorshift32() & 0xFF) output += chr(o) # print(time, output) if output[:5] == "grey{": print("found: {} {}".format(time, output)) 
 1 2  C:\grey\hasbeen>py re2.py found: 1654353866 grey{Funct1on41_P4rad1s3_iZ_Fun} 

Flag: grey{Funct1on41_P4rad1s3_iZ_Fun}

### 🍭 flappy-js

Are you a pro gamer? Can you get a score of 31337 in this game?

(flag format is greyctf{…} because I made a mistake :p)

• daniellimws

hxxp://challs.nusgreyhats.org:10529/

Hint: Use your browser’s dev tools.

The website lets us play a game called “Clumsy Bird” which is just a Flappy Bird clone. In the HTML we can see two scripts being loaded.

 1 2 3 4 5 6 7 8   

melonJS is just a library so I searched for “flag” in clumsy-min.js which is helpfully pretty-printed by the Firefox Developer Tools.

  1 2 3 4 5 6 7 8 9 10 11 12 13  draw: function (a) { game.data.start && me.state.isCurrent(me.state.PLAY) && ( this.stepsFont.draw(a, game.data.steps, me.game.viewport.width / 2, 10), game.data.steps >= 31337 && ( 0 == this.flag.length && (this.flag = genFlag()), this.flagFont = new me.Font('roboto', 35, '#000', 'center'), this.flagFont.draw(a, 'greyctf{' + this.flag + '}', me.game.viewport.width / 2, 110) ) ) } 

From the Javascript snippet above where “flag” is found, it seems that we just need to call genFlag() so that’s what I did right in my Firefox Browser Console and out came the flag “5uch_4_pr0_g4m3r”.

Flag: greyctf{5uch_4_pr0_g4m3r}

### flappy-o

I know you cheated in flappy-js. This time the game is written in C, I don’t think you can cheat so easily. Or can you?

Show me your skills by getting a score of at least 64.

MD5 (flappybird) = f1f36482358dc35992f076e6ea483df8

• daniellimws

Hint: Consider patching

Attached: flappybird

This one is solved together with 🩸 flappy-o2 below.

Flag: grey{y0u_4r3_pr0_4t_7h1s_g4m3_b6d8745a1cc8d51effb86690bf4b27c9}

### 🩸 flappy-o2

This challenge uses the same binary as flappy-o.

Every 10000 points you reach, you unlock a character of the bonus flag. This is the real test of your skills.

MD5 (flappybird) = f1f36482358dc35992f076e6ea483df8

• daniellimws

Attached: flappybird

 1  flappybird: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7b93a7bc7eee1ab668de0cad3ed2fb3927d0ec45, for GNU/Linux 3.2.0, with debug_info, not stripped 

This time the Flappy Bird game runs in an unstripped 64-bit ELF and you’re hinted to patch it, but I found it easier to just lift out the flag decoding functions and run them separately.

In main, gameLoop() is called which calls updateAndDrawFlag().

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  void __cdecl updateAndDrawFlag() { mvprintw(3, col / 2 - 20, "%s", flag1); mvprintw(4, col / 2 - 20, "%s", flag2); actualScore = score / 8; if ( score / 8 != prevScore ) { if ( actualScore - prevScore != 1 ) reportCheater(); if ( actualScore <= 0x3F ) flag1[actualScore - 1] = genFlag1(actualScore - 1); lfsr2(1); if ( !(actualScore % 10000) && (actualScore / 10000) <= 25 ) *&flag2[4 * (actualScore / 10000) - 4] = genFlag2(actualScore - 10000); prevScore = actualScore; } } 

The arrays flag1 and flag2 are populated by genFlag1() and genFlag2() respectively based on your score and actualScore. Linear-feedback shift register (LFSR) functions lsfr1() and lsfr2() are used to achieve this along with hardcoded arrays key1 and key2 as well as a hardcoded lsfr2_seed.

Once we know these key components of the flag generation, we can copy the relevant pseudocode out, compile it and run it to print both flags. We just have to increment score in a loop to simulate how it is incremented by checkAndHandleCollision() in gameLoop(). More specifically, it should be incremented until 10000 * 8 * 25 because actualScore = score / 8; and the condition (actualScore / 10000) <= 25 is used to generate the 25 DWORDs of the 2nd flag.

  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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113  #include unsigned char key1[] = { 0xAA, 0x7A, 0xE1, 0xBB, 0x9A, 0xE7, 0xFF, 0x7C, 0x35, 0x01, 0x06, 0x09, 0xC2, 0x50, 0x62, 0x38, 0xDB, 0x76, 0xD5, 0xE1, 0x68, 0xA9, 0xBF, 0xB4, 0x52, 0x8F, 0xC0, 0x17, 0x0E, 0x2F, 0xDA, 0xEA, 0x8A, 0xCF, 0xA2, 0x90, 0xE7, 0x08, 0xEB, 0x0E, 0x3B, 0x14, 0x72, 0xBE, 0x9A, 0xDE, 0xD5, 0x51, 0x97, 0x2C, 0xBC, 0xF3, 0x35, 0xB6, 0x21, 0x29, 0x7D, 0xA8, 0xD7, 0x2B, 0xED, 0xFE, 0xF0, 0x00 }; unsigned int key2[25] = { 2091533035u, 3444833704u, 4148299095u, 1382543044u, 2034135277u, 1758572224u, 953820139u, 1864200113u, 961059707u, 4109923606u, 3982998552u, 2329111456u, 2938792015u, 2156409201u, 2261679400u, 1133293590u, 1468307596u, 1270163339u, 3218919622u, 1843033993u, 2968558243u, 1433696446u, 3279073000u, 1084392560u, 2824157242u }; unsigned int lfsr1(int n) { int i; // [rsp+8h] [rbp-Ch] int seed; // [rsp+Ch] [rbp-8h] int lsb; // [rsp+10h] [rbp-4h] seed = 43981; for (i = 0; i < n; ++i) { lsb = seed & 1; seed >>= 1; if (lsb) seed ^= 0x82EEu; } return seed; } int lfsr2_seed = 439041101; unsigned int lfsr2(int n) { int i; // [rsp+Ch] [rbp-8h] int lsb; // [rsp+10h] [rbp-4h] for (i = 0; i < n; ++i) { lsb = lfsr2_seed & 1; lfsr2_seed >>= 1; if (lsb) lfsr2_seed ^= 0x80000DD7; } return lfsr2_seed; } char genFlag1(int n) { unsigned int v1; // bl v1 = key1[n]; return v1 ^ lfsr1(n); } int genFlag2(int i) { unsigned v1; // ax v1 = lfsr1(i); return lfsr2(v1) ^ key2[i / 10000]; } int main() { int actualscore; int prevscore; int dword; char flag1[68]; int flag2[100]; for (int score = 0; score <= 10000 * 8 * 25; score++) { actualscore = score / 8; if (actualscore != prevscore) { lfsr2(1); if (!(actualscore % 10000) && (unsigned int)(actualscore / 10000) <= 25) { dword = genFlag2(actualscore - 10000); int fourth = (dword >> 24) & 0xff; int third = (dword >> 16) & 0xff; int second = (dword >> 8) & 0xff; int first = (dword)&0xff; flag2[4 * (actualscore / 10000) - 1] = fourth; flag2[4 * (actualscore / 10000) - 2] = third; flag2[4 * (actualscore / 10000) - 3] = second; flag2[4 * (actualscore / 10000) - 4] = first; } if (actualscore <= 63) { flag1[actualscore - 1] = genFlag1(actualscore - 1); printf("%c", flag1[actualscore - 1]); } prevscore = actualscore; } } printf("\n"); for (int i = 0; i < 100; i++) { printf("%c", flag2[i]); } printf("\n"); return 0; } 

Output:

 1 2  grey{y0u_4r3_pr0_4t_7h1s_g4m3_b6d8745a1cc8d51effb86690bf4b27c9} grey{y0u_4r3_v3ry_g00d_4t_7h1s_g4m3_c4n_y0u_t34ch_m3_h0w_t0_b3_g00d_ef4bd282d7a2ab1ebdcc3616dbe7afb} 

Flag: grey{y0u_4r3_v3ry_g00d_4t_7h1s_g4m3_c4n_y0u_t34ch_m3_h0w_t0_b3_g00d_ef4bd282d7a2ab1ebdcc3616dbe7afb}

### 🍭 Memory Game (Part 1)

Here’s a fun game to destress.

Do you know where the image assets are stored? I’ve made a nice drawing for you.

MD5 (memory-game.apk) = 1c491c078269748d1abf65dff6449149

• daniellimws

Attached: memory-game.apk

We’re supposed to look for some image asset in the APK, so I used apktool to decode the APK with apktool d memory-game.apk and searched with ripgrep in the res/drawable* folders because that’s where image resources are supposed to be stored according to the Android Developer Documentation.
Turns out that there’s a flag.png in res/drawable-hdpi/ with the flag text in the image.

 1 2  a@a:/apktool/memory-game/res$rg --files res/drawable* | rg flag res/drawable-hdpi/flag.png  Flag: grey{th1s_1s_dr4w4bl3_bu7_e4s13r_t0_7yp3} ### 🩸 Memory Game (Part 2) Can you finish MASTER difficulty in 20 seconds? If you can, the flag will be given to you through logcat with the tag FLAG. MD5 (memory-game.apk) = 1c491c078269748d1abf65dff6449149 • daniellimws Hint: Frida is nice. Attached: memory-game.apk I opened the APK in JADX and did a text search for “flag” and found results in com.snatik.matches.engine.Engine.onEvent(FlipCardEvent). This should be what we’re looking for as the code Log.i("FLAG", ...) matches the challenge’s description of the flag being printed “through logcat with the tag FLAG”. Now that we know which part of the code to look at, I switched to Bytecode Viewer to decompile the APK because I prefer the built-in Procyon Decompiler’s output. Below is the relevant segment of the code in com.snatik.matches.engine.Engine.onEvent(FlipCardEvent) where the flag is logged if MASTER difficulty was chosen and the game was finished within the 20 seconds time limit:   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  if (this.mPlayingGame.boardConfiguration.difficulty == 6 && passedSeconds < time) { final Key key = null; try { instance = (FlipCardEvent)SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); try { final Cipher instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding"); } catch (final NoSuchPaddingException ex) {} catch (final NoSuchAlgorithmException ex) {} } catch (final NoSuchPaddingException ex) {} catch (final NoSuchAlgorithmException ex2) {} instance = null; final NoSuchPaddingException ex; ex.printStackTrace(); final Cipher instance2 = null; Rnd.reSeed(); final byte[] array = new byte[16]; for (int i = 0; i < 16; ++i) { array[i] = (byte)Rnd.get(256); } final PBEKeySpec pbeKeySpec = new PBEKeySpec("1.01.001007".toCharArray(), array, 65536, 256); Key key2; try { key2 = new SecretKeySpec(((SecretKeyFactory)instance).generateSecret(pbeKeySpec).getEncoded(), "AES"); } catch (final InvalidKeySpecException ex3) { ex3.printStackTrace(); key2 = key; } final byte[] array2 = new byte[16]; for (int j = 0; j < 16; ++j) { array2[j] = (byte)Rnd.get(256); } final IvParameterSpec ivParameterSpec = new IvParameterSpec(array2); try { instance2.init(2, key2, ivParameterSpec); } catch (final InvalidKeyException ex4) { ex4.printStackTrace(); } catch (final InvalidAlgorithmParameterException ex5) { ex5.printStackTrace(); } try { Log.i("FLAG", new String(instance2.doFinal(Base64.decode("diDrBf4+uZMtDV+0k/3BCGM4xyTpEyGEuUFYegIaSjQyQcgfIfZRbvGQ9hHMqnuflNCKv4HW/NXq93j4QqLc/Q==", 0)), StandardCharsets.UTF_8)); } catch (final IllegalBlockSizeException ex6) { ex6.printStackTrace(); } catch (final BadPaddingException ex7) { ex7.printStackTrace(); } }  I was too lazy to set up emulators and Android Debug Bridge (adb) so I decided to just copy and port everything above over to Java including com.snatik.matches.rng.MTRandom.class and com.snatik.matches.rng.Rnd.class in order to print out the flag.   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 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319  import javax.crypto.spec.*; import java.security.spec.*; import javax.crypto.*; import java.security.*; import java.util.*; import java.nio.charset.*; import java.io.*; class Main { public static void main(String args[]) { SecretKeyFactory instance = null; Cipher instance2 = null; try { instance = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); instance2 = Cipher.getInstance("AES/CBC/PKCS5Padding"); } catch (final NoSuchAlgorithmException ex9) {} catch (final NoSuchPaddingException ex10) {} Rnd.reSeed(); final byte[] array = new byte[16]; for (int i = 0; i < 16; ++i) { array[i] = (byte)Rnd.get(256); } final PBEKeySpec pbeKeySpec = new PBEKeySpec("1.01.001007".toCharArray(), array, 65536, 256); Key key2 = null; try { key2 = new SecretKeySpec(((SecretKeyFactory)instance).generateSecret(pbeKeySpec).getEncoded(), "AES"); } catch (final InvalidKeySpecException ex3) {} final byte[] array2 = new byte[16]; for (int j = 0; j < 16; ++j) { array2[j] = (byte)Rnd.get(256); } final IvParameterSpec ivParameterSpec = new IvParameterSpec(array2); try { instance2.init(2, key2, ivParameterSpec); } catch (final InvalidKeyException ex4) {} catch (final InvalidAlgorithmParameterException ex5) {} try { System.out.println(new String(instance2.doFinal(Base64.getDecoder().decode(new String("diDrBf4+uZMtDV+0k/3BCGM4xyTpEyGEuUFYegIaSjQyQcgfIfZRbvGQ9hHMqnuflNCKv4HW/NXq93j4QqLc/Q==").getBytes("UTF-8"))), StandardCharsets.UTF_8)); } catch (final IllegalBlockSizeException ex6) {} catch (final BadPaddingException ex7) {} catch (final UnsupportedEncodingException ex8) {} } } class Rnd { private static MTRandom rnd; static { Rnd.rnd = new MTRandom((long)(-1966625086)); } public Rnd() { super(); } public static float get() { return Rnd.rnd.nextFloat(); } public static int get(final int n) { final double nextDouble = Rnd.rnd.nextDouble(); final double n2 = n; Double.isNaN(n2); return (int)Math.floor(nextDouble * n2); } public static int get(final int n, final int n2) { final double nextDouble = Rnd.rnd.nextDouble(); final double n3 = n2 - n + 1; Double.isNaN(n3); return n + (int)Math.floor(nextDouble * n3); } public static boolean nextBoolean() { return Rnd.rnd.nextBoolean(); } public static double nextDouble() { return Rnd.rnd.nextDouble(); } public static double nextGaussian() { return Rnd.rnd.nextGaussian(); } public static int nextInt() { return Rnd.rnd.nextInt(); } public static int nextInt(final int n) { final double nextDouble = Rnd.rnd.nextDouble(); final double n2 = n; Double.isNaN(n2); return (int)Math.floor(nextDouble * n2); } public static void reSeed() { Rnd.rnd = new MTRandom((long)(-1966625086)); } } class MTRandom extends Random { private static final long DEFAULT_SEED = 5489L; private static final int LOWER_MASK = Integer.MAX_VALUE; private static final int M = 397; private static final int[] MAGIC; private static final int MAGIC_FACTOR1 = 1812433253; private static final int MAGIC_FACTOR2 = 1664525; private static final int MAGIC_FACTOR3 = 1566083941; private static final int MAGIC_MASK1 = -1658038656; private static final int MAGIC_MASK2 = -272236544; private static final int MAGIC_SEED = 19650218; private static final int N = 624; private static final int UPPER_MASK = Integer.MIN_VALUE; private static final long serialVersionUID = -515082678588212038L; private transient boolean compat; private transient int[] ibuf; private transient int[] mt; private transient int mti; static { MAGIC = new int[] { 0, -1727483681 }; } public MTRandom() { super(); this.compat = false; } public MTRandom(final long n) { super(n); this.compat = false; } public MTRandom(final boolean compat) { super(0L); this.compat = compat; long currentTimeMillis; if (compat) { currentTimeMillis = 5489L; } else { currentTimeMillis = System.currentTimeMillis(); } this.setSeed(currentTimeMillis); } public MTRandom(final byte[] seed) { super(0L); this.compat = false; this.setSeed(seed); } public MTRandom(final int[] seed) { super(0L); this.compat = false; this.setSeed(seed); } public static int[] pack(final byte[] array) { final int length = array.length; final int n = array.length + 3 >>> 2; final int[] array2 = new int[n]; int n2; for (int i = 0; i < n; i = n2) { n2 = i + 1; int n3; if ((n3 = n2 << 2) > length) { n3 = length; } int n4; int n5; for (n4 = n3 - 1, n5 = (array[n4] & 0xFF); (n4 & 0x3) != 0x0; --n4, n5 = (n5 << 8 | (array[n4] & 0xFF))) {} array2[i] = n5; } return array2; } private void setSeed(int mti) { if (this.mt == null) { this.mt = new int[624]; } this.mt[0] = mti; mti = 1; while (true) { this.mti = mti; mti = this.mti; if (mti >= 624) { break; } final int[] mt = this.mt; mt[mti] = (mt[mti - 1] ^ mt[mti - 1] >>> 30) * 1812433253 + mti; ++mti; } } @Override protected final int next(final int n) { try { if (this.mti >= 624) { int n2 = 0; int i; while (true) { i = n2; if (n2 >= 227) { break; } final int[] mt = this.mt; final int n3 = mt[n2]; final int n4 = n2 + 1; final int n5 = (Integer.MAX_VALUE & mt[n4]) | (Integer.MIN_VALUE & n3); mt[n2] = (MTRandom.MAGIC[n5 & 0x1] ^ (mt[n2 + 397] ^ n5 >>> 1)); n2 = n4; } while (i < 623) { final int[] mt2 = this.mt; final int n6 = mt2[i]; final int n7 = i + 1; final int n8 = (n6 & Integer.MIN_VALUE) | (mt2[n7] & Integer.MAX_VALUE); mt2[i] = (MTRandom.MAGIC[n8 & 0x1] ^ (mt2[i - 227] ^ n8 >>> 1)); i = n7; } final int[] mt3 = this.mt; final int n9 = (Integer.MAX_VALUE & mt3[0]) | (Integer.MIN_VALUE & mt3[623]); mt3[623] = (MTRandom.MAGIC[n9 & 0x1] ^ (mt3[396] ^ n9 >>> 1)); this.mti = 0; } final int n10 = this.mt[this.mti++]; final int n11 = n10 ^ n10 >>> 11; final int n12 = n11 ^ (n11 << 7 & 0x9D2C5680); final int n13 = n12 ^ (n12 << 15 & 0xEFC60000); return (n13 ^ n13 >>> 18) >>> 32 - n; } finally {} } @Override public final void setSeed(final long n) { synchronized (this) { if (this.compat) { this.setSeed((int)n); } else { if (this.ibuf == null) { this.ibuf = new int[2]; } final int[] ibuf = this.ibuf; ibuf[0] = (int)n; ibuf[1] = (int)(n >>> 32); this.setSeed(ibuf); } } } public final void setSeed(final byte[] array) { this.setSeed(pack(array)); } public final void setSeed(int[] mt) { try { final int length = mt.length; if (length != 0) { int n; if (624 > length) { n = 624; } else { n = length; } this.setSeed(19650218); int n2 = 1; int n3 = 0; int n7; for (int i = n; i > 0; --i, n3 = n7) { final int[] mt2 = this.mt; final int n4 = mt2[n2]; final int n5 = n2 - 1; mt2[n2] = (n4 ^ (mt2[n5] >>> 30 ^ mt2[n5]) * 1664525) + mt[n3] + n3; final int n6 = n2 + 1; ++n3; if ((n2 = n6) >= 624) { mt2[0] = mt2[623]; n2 = 1; } if ((n7 = n3) >= length) { n7 = 0; } } for (int j = 623; j > 0; --j) { mt = this.mt; final int n8 = mt[n2]; final int n9 = n2 - 1; mt[n2] = (n8 ^ (mt[n9] >>> 30 ^ mt[n9]) * 1566083941) - n2; if (++n2 >= 624) { mt[0] = mt[623]; n2 = 1; } } this.mt[0] = Integer.MIN_VALUE; return; } throw new IllegalArgumentException("Seed buffer may not be empty"); } finally {} } }  You can run it here in OnlineGDB and it’ll give you the flag as the output. Flag: grey{hum4n_m3m0ry_i5_4lw4y5_b3tt3r_th4n_r4nd0m_4cc3ss_m3m0ry} ### 🍭 Parcel The binary is packed. Try to decompile it and you will find unreadable code. Find out how it is packed and unpack it to recover the original binary. MD5 (parcel) = 903adf25f54687ef3d875b51442e20df • daniellimws Hint: Run strings on the binary. There are some interesting strings in there. Attached: parcel  1  parcel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header  If we run strings on the provided ELF binary as hinted, we’ll see the strings “UPX!”.   1 2 3 4 5 6 7 8 9 10 11  a@a:/grey$ strings parcel | tail /@1dC ?88l XX$/ ?2 7dCI 6{$O .? ; /QqQ UPX! UPX! 

This means that the ELF is likely packed with UPX. If vanilla UPX is used, such binaries can simply be unpacked with the official upx binary itself by adding the -d flag:

  1 2 3 4 5 6 7 8 9 10  C:\grey>upx -d parcel -o unpackedparcel Ultimate Packer for eXecutables Copyright (C) 1996 - 2020 UPX 3.96w Markus Oberhumer, Laszlo Molnar & John Reiser Jan 23rd 2020 File size Ratio Format Name -------------------- ------ ----------- ----------- 428392 <- 105224 24.56% linux/amd64 unpackedparcel Unpacked 1 file. 
 1 2  a@a:/grey$file unpackedparcel unpackedparcel: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f616d88f62c910d6dddddd732eb503f9c61fa88a, for GNU/Linux 3.2.0, not stripped  Ok now where’s the flag? If we run parcel or unpackedparcel, it’ll ask us for the address of certain functions like h12. Now that we have the unpacked unstripped binary unpackedparcel, we can get those addresses easily using nm -t d unpackedparcel | grep <function_name>.   1 2 3 4 5 6 7 8 9 10 11  a@a:/grey$ nm -t d unpackedparcel | grep h12 0000000004207125 T h12 a@a:/grey$nm -t d unpackedparcel | grep t80 0000000004221073 T t80 a@a:/grey$ nm -t d unpackedparcel | grep g20 0000000004206113 T g20 a@a:/grey$./parcel Tell me the address of the function h12 (in decimal): 4207125 Tell me the address of the function t80 (in decimal): 4221073 Tell me the address of the function g20 (in decimal): 4206113 Congrats! grey{d1d_y0u_us3_nm_0r_objdump_0r_gdb_0r_ghidra_0r_rizin_0r_ida_0r_binja?}  Flag: grey{d1d_y0u_us3_nm_0r_objdump_0r_gdb_0r_ghidra_0r_rizin_0r_ida_0r_binja?} ## Misc ### Ghost I tried to send you a bunch of messages :) MD5 (ghost) = 3f61d4a3d19c3f3ac1f8223f7c7672af • Tensor Attached: ghost   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22  a@a:/grey$ cat ghost I got message for you: NTQgNjggNjkgNzMgMjAgNjkgNzMgMjAgNmUgNmYgNzQgMjAgNjkgNzQgMjAgNmQgNjEgNmUgMmMgMjAgNzQgNzIgNzkgMjAgNjggNjEgNzIgNjQgNjUgNzIgMjE= ++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>++++++++++++++.>++++.+.++++++++++.<<++.>>----------.++++++++++.<<.>>----------.+++++++++++.<---------------------.<.>>------.-------------.+++++++. pi pi pi pi pi pi pi pi pi pi pika pipi pi pipi pi pi pi pipi pi pi pi pi pi pi pi pipi pi pi pi pi pi pi pi pi pi pi pichu pichu pichu pichu ka chu pipi pipi pipi pipi pi pi pi pi pi pi pi pi pikachu pi pi pi pikachu ka ka ka pikachu pichu pichu pi pi pikachu pipi pipi pi pi pikachu pi pikachu KRUGS4ZANFZSA3TPOQQHI2DFEBTGYYLHEBWWC3RAIQ5A==== synt{abgGungFvzcyr:C} Me: Message received. 

The text file given contains a few different kinds of encoded messages, but the huge chunk of empty spaces/lines in the middle are the most suspicious. Let’s take a closer look at the hex values used.

  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  a@a:/grey$xxd -s 0x270 ghost 00000270: 7069 2070 696b 6163 6875 2070 6920 7069 pi pikachu pi pi 00000280: 6b61 6368 750d 0a20 0909 2020 0909 0920 kachu.. .. ... 00000290: 0909 0920 2009 2020 0909 2020 0920 0920 ... . .. . . 000002a0: 0909 0909 2020 0920 0909 0909 2009 0920 .... . .... .. 000002b0: 0909 2020 0909 0920 0909 2009 2020 2020 .. ... .. . 000002c0: 2009 0920 2020 2020 0909 0920 2009 0920 .. ... .. 000002d0: 2009 0920 0909 0920 0920 0909 0909 0920 .. ... . ..... 000002e0: 0909 2020 2009 2020 0909 0909 2020 0920 .. . .... . 000002f0: 0909 0920 0920 2020 2009 0920 2009 0920 ... . .. .. 00000300: 2009 2020 0920 2020 0920 0909 0909 0920 . . . ..... 00000310: 0909 2009 0909 2020 2009 0920 2020 2020 .. ... .. 00000320: 0909 0920 0920 2020 0920 0909 0909 0920 ... . . ..... 00000330: 2009 0920 2020 0920 0909 2009 0909 2020 .. . .. ... 00000340: 0909 0920 0909 2020 0909 2009 2020 0920 ... .. .. . . 00000350: 0909 0920 2009 0920 0920 2009 2020 0920 ... .. . . . 00000360: 0909 2020 2009 2020 0909 2009 0920 2020 .. . .. .. 00000370: 2009 0920 2009 0920 0909 0909 0920 090d .. .. ..... .. 00000380: 0a4b 5255 4753 345a 414e 465a 5341 3354 .KRUGS4ZANFZSA3T 00000390: 504f 5151 4849 3244 4645 4254 4759 594c POQQHI2DFEBTGYYL 000003a0: 4845 4257 5743 3352 4149 5135 413d 3d3d HEBWWC3RAIQ5A=== 000003b0: 3d0d 0a0d 0a73 796e 747b 6162 6747 756e =....synt{abgGun 000003c0: 6746 767a 6379 723a 437d 0d0a 0d0a 4d65 gFvzcyr:C}....Me 000003d0: 3a20 4d65 7373 6167 6520 7265 6365 6976 : Message receiv 000003e0: 6564 2e0d 0a ed...  The empty space chunk consists of only 2 values — 0x20s and 0x09s, so my first thought was that it represented either binary encoding or morse code. Brief experimentation in CyberChef will reveal that it is indeed binary encoding where the 0x20s represent 0s and the 0x09s represent 1s. Here is the decoding done in CyberChef. Flag: grey{gh0s7_byt3$_n0t_1nvisIbl3}

### Calculator

Calculator is a nice invention

• mechfrog88

US Instance: nc 35.184.68.167 15521

EU Instance: nc 34.77.124.65 15521
nc challs.nusgreyhats.org 15521

  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  a@a:~$nc challs.nusgreyhats.org 15521 __________ .'----------. | .--------. | | |########| | __________ | |########| | /__________\ .--------| --------' |------| --=-- |-------------. | ----,-.-----' |o ====== | | | ______|_|_______ |__________| | | / %%%%%%%%%%%% \ | | / %%%%%%%%%%%%%% \ | | ^^^^^^^^^^^^^^^^^^^^ | +-----------------------------------------------------+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ How fast are you on calculating basic math expression? Answer 100 math questions in 30 seconds to the flag Operations include: 1. add x y -> returns x + y 2. mul x y -> returns x * y 3. sub x y -> returns x - y 4. neg x -> returns -x 5. inc x -> returns x + 1 Example1: mul add 1 2 sub 5 1 Ans1: (1 + 2) * (5 - 1) = 12 Example2: add mul sub 3 2 inc 5 3 Ans2: (3 - 2) * (5 + 1) + 3 = 9 Send START when you are ready!  Prefix notation is used to evaluate these math expressions, so I just took a Python prefix calculator script from https://stackoverflow.com/a/5307565 and edited the tokens to match those required in this challenge:   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  from collections import deque from pwn import * def parse(tokens): token=tokens.popleft() if token==b'add': return parse(tokens)+parse(tokens) elif token==b'sub': return parse(tokens)-parse(tokens) elif token==b'mul': return parse(tokens)*parse(tokens) elif token==b'neg': return parse(tokens)*(-1) elif token==b"inc": return parse(tokens)+1 else: # must be just a number return int(token) if __name__=='__main__': conn = remote('challs.nusgreyhats.org', 15521) conn.recvuntil(b'!') conn.recvline() conn.sendline(b'START') for i in range(100): problem = conn.recvline() problem = problem.rstrip(b'\n') answer = (parse(deque(problem.split()))) conn.sendline(bytes(str(answer), 'ascii')) conn.recvline() result = conn.recvline() print(result) flag = conn.recvline() print(flag)  Script output:  1 2 3 4 5  a@a:/grey$ python3 prefix_calculator.py [+] Opening connection to challs.nusgreyhats.org on port 15521: Done b'You done it in 21.74351406097412 seconds!\n' b'grey{prefix_operation_is_easy_to_evaluate_right_W2MQshAYVpGVJPcw}\n' [*] Closed connection to challs.nusgreyhats.org port 15521 

Flag: grey{prefix_operation_is_easy_to_evaluate_right_W2MQshAYVpGVJPcw}

### 🍭 Firmware

Router firmware is a nice target to start your bug hunting journey. But you have to first understand how the firmware is loaded.

There is always a file in the firmware image that tells the router what services to start. Find this file.

• daniellimws

Attached: firmware.img.gz

I ran binwalk -e on firmware.img to extract the Squashfs filesystem from the image so that we can find the file we are told to look for.

  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17  a@a:/grey$binwalk -e firmware.img DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 1105744 0x10DF50 uImage header, header size: 64 bytes, header CRC: 0x39150100, created: 1978-08-22 13:45:13, image size: 204509413 bytes, Data Address: 0x33FF2FE1, Entry Point: 0x2099E5, data CRC: 0x52E3, OS: 4.4BSD, image name: "" 1164816 0x11C610 CRC32 polynomial table, little endian 1187240 0x121DA8 device tree image (dtb) 1189888 0x122800 uImage header, header size: 64 bytes, header CRC: 0x2E68CDF5, created: 2021-10-24 09:01:35, image size: 596535 bytes, Data Address: 0x4000000, Entry Point: 0x4000000, data CRC: 0x3982880E, OS: Firmware, CPU: ARM, image type: Firmware Image, compression type: none, image name: "U-Boot 2019.07 for zynq board" 1191944 0x123008 CRC32 polynomial table, little endian 1524744 0x174408 device tree image (dtb) 1549184 0x17A380 SHA256 hash constants, little endian 1772908 0x1B0D6C device tree image (dtb) 1789952 0x1B5000 device tree image (dtb) 1790180 0x1B50E4 gzip compressed data, maximum compression, from Unix, last modified: 1970-01-01 00:00:00 (null date) 5248724 0x5016D4 MySQL MISAM index file Version 10 6232576 0x5F1A00 device tree image (dtb) 18874368 0x1200000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 2285698 bytes, 840 inodes, blocksize: 262144 bytes, created: 2021-10-24 09:01:35  After that, I just searched for the flag fragment grey in the extracted filesystem using ripgrep and the flag was found in /etc/inittab.  1 2 3  a@a:/grey/_firmware.img.extracted/squashfs-root$ rg grey etc/inittab 5:# you found me! grey{inittab_1s_4n_1mp0rt4nt_pl4c3_t0_l00k_4t_wh3n_r3v3rs1ng_f1rmw4r3} 

Flag: grey{inittab_1s_4n_1mp0rt4nt_pl4c3_t0_l00k_4t_wh3n_r3v3rs1ng_f1rmw4r3}

HTTP is not secure. Inspect this packet dump and you will know why.

Find the packet related to an image upload and extract the image. Then, find the name of the creator of this image.

MD5 (dump.pcap) = 2565cf775ac71c217102ace91cc922ec

• daniellimws

Attached: dump.pcap

Open the given PCAP in Wireshark and use File -> Export Objects -> HTTP to view HTTP objects transmitted.

There’s a ctf.png in the multipart/form-data HTTP object with the flag string in a PNG tEXt chunk labelled “Author”.

Flag: grey{wireshark_exiftool_are_good}

## Web

### 🍭 Too Fast

Something went by too quickly!

• Dragonym

hxxp://challs.nusgreyhats.org:14004/

I analyzed the given website but nothing stood out in the HTML source or the HTTP request/response headers, so I ran a “Discover content” scan with Burp Suite and it quickly found /admin.php with the flag in the response body.

Request:

 1 2 3 4 5 6 7 8  GET /admin.php HTTP/1.1 Host: challs.nusgreyhats.org:14004 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en-US;q=0.9,en;q=0.8 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.67 Safari/537.36 Connection: close Cache-Control: max-age=0 

Response:

 1 2 3 4 5 6 7 8 9  HTTP/1.1 302 Found Host: challs.nusgreyhats.org:14004 Date: Fri, 10 Jun 2022 13:27:20 GMT Connection: close X-Powered-By: PHP/7.4.29 Location: ./index.php Content-type: text/html; charset=UTF-8

grey{why_15_17_571LL_ruNn1n_4Pn39Mq3CQ7VyGrP}



Flag: grey{why_15_17_571LL_ruNn1n_4Pn39Mq3CQ7VyGrP}