## Challenge 1: frog Reading the source code, it is easy to see that when the character jumps to the cell with coordinates (10, 10), the code to generate the flag is called: ```python= def GenerateFlagText(x, y): key = x + y*20 encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3 \x92\xb4\xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf" return ''.join([chr(ord(c) ^ key) for c in encoded]) ``` Solve script: ```python= key = 210 encoded = "\xa5\xb7\xbe\xb1\xbd\xbf\xb7\x8d\xa6\xbd\x8d\xe3\xe3\x92\xb4 \xbe\xb3\xa0\xb7\xff\xbd\xbc\xfc\xb1\xbd\xbf" flag = ''.join([chr(ord(c) ^ key) for c in encoded]) print(flag) ``` Flag: `welcome_to_11@flare-on.com` ## Challenge 2: checksum Running the exe file, we see that the challenge requires solving a series of calculations. If the answer is wrong, it will exit; if correct, it will ask for an input, which is the key of the challenge. Using IDA, observing the `main_main` function, we see the code to generate the flag: ![image](https://hackmd.io/_uploads/HyHZMS1GJe.png) Analyzing the program flow, we see that this code is called based on the condition of the `main_a` function, which takes a parameter as the string we need to input. Thus, `main_a` is the "check key" function we are looking for: ![Screenshot 2024-11-11 153712](https://hackmd.io/_uploads/HJHWXrkMyx.png) Checking the `main_a` function, we see that the input is XORed with the string `FlareOn2024`, then encoded with base64: ![image](https://hackmd.io/_uploads/r1ciSBkGkx.png) Solve script: ```python= import base64 import binascii key = b"FlareOn2024" encode_str = b'cQoFRQErX1YAVw1zVQdFUSxfAQNRBXUNAxBSe15QCVRVJ1pQEwd /WFBUAlElCFBFUnlaB1ULByRdBEFdfVtWVA==' flag = b"" decode_str = base64.b64decode(encode_str) for i in range(len(decode_str)): c = key[i % 11] ^ decode_str[i] flag += bytes([c]) print(flag) # 7fd7dd1d0e959f74c133c13abb740b9faa61ab06bd0ecd177645e93b1e3825dd ``` Result: ![image](https://hackmd.io/_uploads/BJV3IHJz1g.png) But where is the flag?? Going back to the code in the `main_main` function, we see that right above the code to generate the flag is the `os_UserCacheDir()` function, which gets the destination path to save the flag => C:\Users\user_name\AppData\Local. Flag: ![image](https://hackmd.io/_uploads/ByPfqrkM1x.png) ## Challenge 3: aray The challenge provides a yara rule file, in which the size of the file to be found is only 85 bytes, so it can be predicted that the file to be found is a text containing the flag. Noticing the `==` conditions, we can accurately find the character at that offset, so we will write a script to find these characters first (then we can brute force because the size has been reduced and we have the file's hash): ```python= import string import hashlib import zlib from Cryptodome.Util.number import * with open("aray.yara","r") as f: data = f.read().split("and")[2:] hash = [] xor = [] sub = [] add = [] mod = [] flag = ["_"]*85 check = [] def brute_md5(s): for i in range(256): for j in range(256): byte = bytes([i,j]) if md5_hash(byte) == s: return byte def brute_sha256(s): for i in range(256): for j in range(256): byte = bytes([i,j]) if sha256_hash(byte) == s: return byte def brute_crc32(s): for i in range(256): for j in range(256): byte = bytes([i,j]) if crc32_hash(byte) == s: return byte def md5_hash(data): return hashlib.md5(data).hexdigest() def sha256_hash(data): return hashlib.sha256(data).hexdigest() def crc32_hash(data): return zlib.crc32(data) & 0xffffffff def solve_hash(s): global flag index = s[s.find("(")+ 1:s.find(")")].split(",")[0] h = s[6:s.find("(")] s = s.split() if h == "md5": d = s[-1][1:-1] f = brute_md5(d) elif h == "sha256": d = s[-1][1:-1] f = brute_sha256(d) elif h == "crc32": d = s[-1][2:] f = brute_crc32(int(d,16)) for i in range(int(index),int(index)+len(f)): flag[i] = long_to_bytes(f[i-int(index)]).decode() def solve_xor(s): global flag index = s[s.find("(")+ 1:s.find(")")] s = s.split() s = long_to_bytes(int(s[2])^int(s[4])) if len(s.decode()) == 4: s = s[::-1] for i in range(int(index),int(index)+len(s)): flag[i] = long_to_bytes(s[i-int(index)]).decode() def solve_sub(s): global flag index = s[s.find("(")+ 1:s.find(")")] s = s.split() s = long_to_bytes(int(s[2])+int(s[4])) if len(s.decode()) == 4: s = s[::-1] for i in range(int(index),int(index)+len(s)): flag[i] = long_to_bytes(s[i-int(index)]).decode() def solve_add(s): global flag index = s[s.find("(")+ 1:s.find(")")] s = s.split() s = long_to_bytes(int(s[4])-int(s[2])) if len(s.decode()) == 4: s = s[::-1] for i in range(int(index),int(index)+len(s)): flag[i] = long_to_bytes(s[i-int(index)]).decode() for i in data: if i.find("^") != -1 and i.find("==")!= -1: solve_xor(i) if i.find("hash")!= -1 and i.find("==")!= -1: #print(i) solve_hash(i) if i.find("-") != -1 and i.find("==")!= -1: solve_sub(i) if i.find("+") != -1 and i.find("==")!= -1: solve_add(i) flag = "".join(flag) print(flag) ``` A lucky thing in this challenge is that we can find the flag immediately: `rule flareon { strings: $f = "1RuleADayK33p$Malw4r3Aw4y@flare-on.com" condition: $f }` ## Challenge 4: FLARE Meme Maker 3000 Export the javascript code in the HTML file of the challenge, then use [this tool](https://deobfuscate.relative.im/) to deobfuscate: ```javascript= function a0f() { document.getElementById('caption1').hidden = true document.getElementById('caption2').hidden = true document.getElementById('caption3').hidden = true const a = document.getElementById('meme-template') var b = a.value.split('.')[0] a0d[b].forEach(function (c, d) { var e = document.getElementById('caption' + (d + 1)) e.hidden = false e.style.top = a0d[b][d][0] e.style.left = a0d[b][d][1] e.textContent = a0c[Math.floor(Math.random() * (a0c.length - 1))] }) } a0f() const a0g = document.getElementById('meme-image'), a0h = document.getElementById('meme-container'), a0i = document.getElementById('remake'), a0j = document.getElementById('meme-template') a0g.src = a0e[a0j.value] a0j.addEventListener('change', () => { a0g.src = a0e[a0j.value] a0g.alt = a0j.value a0f() }) a0i.addEventListener('click', () => { a0f() }) function a0k() { const a = a0g.alt.split('/').pop() if (a !== Object.keys(a0e)[5]) { return } const b = a0l.textContent, c = a0m.textContent, d = a0n.textContent if ( a0c.indexOf(b) == 14 && a0c.indexOf(c) == a0c.length - 1 && a0c.indexOf(d) == 22 ) { var e = new Date().getTime() while (new Date().getTime() < e + 3000) {} // a = boy_friend0.jpg // b = FLARE On // c = Security Expert // d = Malware var f = d[3] + // w 'h' + a[10] + // 0 b[2] + // A a[3] + // _ c[5] + // i c[c.length - 1] + // t '5' + a[3] + // _ '4' + a[3] + // _ c[2] + // c c[4] + // r c[3] + // u '3' + d[2] + // l a[3] + // _ 'j4' + a0c[1][2] + // v d[4] + // a '5' + c[2] + // c d[5] + // r '1' + c[11] + // p '7' + a0c[21][1] + // @ b.replace(' ', '-') + // flare-on a[11] + // . a0c[4].substring(12, 15) // com f = f.toLowerCase() alert(atob('Q29uZ3JhdHVsYXRpb25zISBIZXJlIHlvdSBnbzog') + f) } } const a0l = document.getElementById('caption1'), a0m = document.getElementById('caption2'), a0n = document.getElementById('caption3') a0l.addEventListener('keyup', () => { a0k() }) a0m.addEventListener('keyup', () => { a0k() }) a0n.addEventListener('keyup', () => { a0k() }) ``` Flag: `wh0a_it5_4_cru3l_j4va5cr1p7@flare-on.com` ## Challenge 5: sshd First, I build an docker image from tar file provided by challenge. Follow the decription, the server has crashed so I look for a sshd core dump file. Its name is `sshd.core.93794.0.0.11.1725917676` and located in `/var/lib/systemd/coredump`. Using gdb to debug sshd with this core dump file: ![image](https://hackmd.io/_uploads/B1gkxN7z1l.png) This process crashed since calling to a null pointer. Saved rip is `0x7f4a18c8f88f`, this indicates that before this address is a `call` instruction. To examine where this address belongs to, I use `info sharedlibrary`: ![Screenshot 2024-11-14 151242](https://hackmd.io/_uploads/B1M4Z4Xf1g.png) Here it is, `/lib/x86_64-linux-gnu/liblzma.so.5`. Now I calculate offset from above address and got the result `988f`, so I find this function in IDA: ```clike= __int64 __fastcall sub_9820(unsigned int a1, _DWORD *a2, __int64 a3, __int64 a4, unsigned int a5) { const char *v9; // rsi void *v10; // rax void *v12; // rax void (*v13)(void); // [rsp+8h] [rbp-120h] char v14[200]; // [rsp+20h] [rbp-108h] BYREF unsigned __int64 v15; // [rsp+E8h] [rbp-40h] v15 = __readfsqword(0x28u); v9 = "RSA_public_decrypt"; if ( !getuid() ) { if ( *a2 == 0xC5407A48 ) { sub_93F0(v14, a2 + 1, a2 + 9, 0LL); v12 = mmap(0LL, dword_32360, 7, 0x22, 0xFFFFFFFF, 0LL); v13 = memcpy(v12, &unk_23960, dword_32360); sub_9520(v14, v13, dword_32360); v13(); sub_93F0(v14, a2 + 1, a2 + 9, 0LL); sub_9520(v14, v13, dword_32360); } v9 = "RSA_public_decrypt "; } v10 = dlsym(0LL, v9); return (v10)(a1, a2, a3, a4, a5); } ``` This function is trying hook `RSA_public_decrypt` by another patch (with space at the end of the name). It examine uid and 4 bytes header pointed by `a2` before run the suspicios code. I look in `sub_93F0` and `sub_9520` but their logic is so confusing, so I try to search and use ChatGPT to analyze them. Finally, two function are parts of ChaCha20 stream cipher, so what to do now is extract this cipher's key and nonce. As you can see, the key and nonce are pointed by `a2`, which argument used for `call` instruction that caused this crash. Compare psudecode with assembly, I realize that register `rsi` save `a2`, so I can print it by command in gdb: ![image](https://hackmd.io/_uploads/HJNNVU7z1g.png) 4 bytes header is `0xC5407A48`, 32 bytes follow is key, and 12 bytes end is nonce (48=4+32+12). With cipher text at address 0x23960 and its size is 0xF96, I use CyberChef to decrypt it and replace these encrypted bytes in `liblzma.so.5.4.1` by output: ```clike= __int64 __fastcall sub_24722(__int64 a1, __int64 a2, __int64 a3) { unsigned int v3; // ebx signed __int64 v4; // rax signed __int64 v5; // rax signed __int64 v6; // rax signed __int64 v7; // rax signed __int64 v8; // rax signed __int64 v9; // rax unsigned __int64 v10; // kr08_8 signed __int64 v11; // rax signed __int64 v12; // rax char ubuf[32]; // [rsp+410h] [rbp-1278h] BYREF char v15[16]; // [rsp+430h] [rbp-1258h] BYREF char filename[256]; // [rsp+440h] [rbp-1248h] BYREF char buf[4224]; // [rsp+540h] [rbp-1148h] BYREF unsigned int size; // [rsp+15C0h] [rbp-C8h] BYREF unsigned int size_4; // [rsp+15C4h] [rbp-C4h] BYREF LOWORD(a3) = 1337; v3 = (sub_2397A)(a1, a2, a3); v4 = sys_recvfrom(v3, ubuf, 32uLL, 0, 0LL, 0LL); v5 = sys_recvfrom(v3, v15, 12uLL, 0, 0LL, 0LL); v6 = sys_recvfrom(v3, &size, 4uLL, 0, 0LL, 0LL); v7 = sys_recvfrom(v3, filename, size, 0, 0LL, 0LL); filename[v7] = 0; v8 = sys_open(filename, 0, 0); v9 = sys_read(v8, buf, 128uLL); v10 = strlen(buf) + 1; size_4 = v10 - 1; sub_24632(&buf[v10], buf, ubuf, v15, 0LL); sub_246A9(&buf[v10], buf, buf, size_4); v11 = sys_sendto(v3, &size_4, 4uLL, 0, 0LL, 0); v12 = sys_sendto(v3, buf, size_4, 0, 0LL, 0); sub_2396B(); sub_239EF(v3, buf, 0LL); return 0LL; } ``` Realize that `sub_2397A` is function create socket, with the appearances of `recv_from`, `open`, `read`, `send_to` so we can guest this shellcode is responsible for receive data from remote, do something with this and send output to remote. Analyze `sub_24632` and `sub_246A0` I realize that these instructions are similar to ChaCha20 stream cipher mentioned above, but these function use 128-bit const `expand 32-byte K` instead of `expand 32-byte k`. So I think I must rewrite script to decrypt this custom ChaCha20, but first I must find key and nonce. Let's look at `sub_24722`, we can see that: - Shellcode store 32 bytes in `ubuf` (`rbp - 0x1278`) => this is key - Shellcode store 12 bytes in `v15` (`rbp - 0x1258`) => this is nonce - Shellcode store 4 bytes in `&size` (`rbp - 0xC8`) => this is size of data - Shellcode store `size` bytes in `filename` (`rbp - 0x1248`) => this is file's name - Shellcode read data from `filename` and store in `buf` (`rbp - 0x1148`)=> this is encrypted data And now, we know that we need to calculate `rbp` when `sub_24722` called to get key, nonce and encrypted data. But how? Go back a bit, I know when program crashed the return address (saved rip) is saved at `0x7ffcc6601e98`, so before the `call` instruction execute `rsp = 0x7ffcc6601e98 + 8 = 0x7ffcc6601ea0`. This value of `rsp` is also the value of `rsp` right before the shellcode is called. After disassembly, I noticed that 2 `call` instructions and 6 `push` instruction are executed before `mov rbp, rsp` in `sub_24722` => so now the value of `rsp` is `rsp = 0x7ffcc6601ea0 -(6 + 2) * 8 = 0x7ffcc6601e60`. On the other hand, `rbp = rsp` so `rbp = 0x7ffcc6601e60`. ![image](https://hackmd.io/_uploads/By0tRRNGke.png) ![image](https://hackmd.io/_uploads/S1VH1yHM1e.png) ![image](https://hackmd.io/_uploads/rJ3tfJHMyl.png) I know some way to get flag, but because of limited level I choose the easiest way - reimplement ChaCha20 with custom constant. You will get source code [here](https://github.com/pts/chacha20/blob/master/chacha20_python3.py). ```python= import struct import binascii def yield_chacha20_xor_stream(key, iv, position=0): """Generate the xor stream with the ChaCha20 cipher.""" if not isinstance(position, int): raise TypeError if position & ~0xffffffff: raise ValueError('Position is not uint32.') if not isinstance(key, bytes): raise TypeError if not isinstance(iv, bytes): raise TypeError if len(key) != 32: raise ValueError if len(iv) != 12: raise ValueError def rotate(v, c): return ((v << c) & 0xffffffff) | v >> (32 - c) def quarter_round(x, a, b, c, d): x[a] = (x[a] + x[b]) & 0xffffffff x[d] = rotate(x[d] ^ x[a], 16) x[c] = (x[c] + x[d]) & 0xffffffff x[b] = rotate(x[b] ^ x[c], 12) x[a] = (x[a] + x[b]) & 0xffffffff x[d] = rotate(x[d] ^ x[a], 8) x[c] = (x[c] + x[d]) & 0xffffffff x[b] = rotate(x[b] ^ x[c], 7) ctx = [0] * 16 ctx[:4] = (1634760805, 857760878, 2036477234, 1260414324) ctx[4 : 12] = struct.unpack('<8L', key) ctx[12] = position ctx[13 : 16] = struct.unpack('<3L', iv) while 1: x = list(ctx) for i in range(10): quarter_round(x, 0, 4, 8, 12) quarter_round(x, 1, 5, 9, 13) quarter_round(x, 2, 6, 10, 14) quarter_round(x, 3, 7, 11, 15) quarter_round(x, 0, 5, 10, 15) quarter_round(x, 1, 6, 11, 12) quarter_round(x, 2, 7, 8, 13) quarter_round(x, 3, 4, 9, 14) for c in struct.pack('<16L', *( (x[i] + ctx[i]) & 0xffffffff for i in range(16))): yield c ctx[12] = (ctx[12] + 1) & 0xffffffff if ctx[12] == 0: ctx[13] = (ctx[13] + 1) & 0xffffffff def chacha20_encrypt(data, key, iv, position=0): """Encrypt (or decrypt) with the ChaCha20 cipher.""" if not isinstance(data, bytes): raise TypeError if iv is None: iv = b'\0' * 12 if isinstance(key, bytes): if not key: raise ValueError('Key is empty.') if len(key) < 32: # TODO(pts): Do key derivation with PBKDF2 or something similar. key = (key * (32 // len(key) + 1))[:32] if len(key) > 32: raise ValueError('Key too long.') return bytes(a ^ b for a, b in zip(data, yield_chacha20_xor_stream(key, iv, position))) uh = lambda x: binascii.unhexlify(bytes(x, 'ascii')) print(chacha20_encrypt(uh('a9f63408422a9e1c0c03a8089470bb8daadc6d7b24ff7f247cda839e92f7071d0263902ec1580000d0b4586db455000020ea78194a7f0000d0b4586db4550000'), uh('8dec9112eb760eda7c7d87a443271c35d9e0cb878993b4d904aef934fa2166d7'), uh('111111111111111111111111'))) #b'supp1y_cha1n_sund4y@flare-on.com\n\x86Xm\xb4U5G\xa5\xfc\xfb\xdfz\xb8z\xaa\xa0\xc1\r\xa9\xf0\x01)\xb3\x94:*s\x03\x97\x83V' ``` Flag: `supp1y_cha1n_sund4y@flare-on.com`