1 # General retrospective
2 In general, the CTF was very fun. The web challenges were a bit guess heavy but the other challenges were fine. Apart from the 4 challenges in this writeup I also looked at some other challenges, mostly Battle of the Galaxies, Summer Adventure Game and Lost in Maze.
4 **Time spent:** 24 hours (incl. other challenges not part of the writeup)
8 Not too interesting but I learned about AZTEC QR codes (which are apparently often used by railway companies, etc.).
12 The organizers posted this image on twitter with no additional information
13 ![mattermost login required](https://mattermost.w0y.at/api/v4/files/oksmhc5kcfdxmgzu4csnchqowc "challenge image")
16 Clearly, the first instinct is to use QR code scanner. This results in `137:64:137:154:171:146:63:175` which can be interpreted as ASCII in octal notation (the `:` being separators between the letters). This results in the string `_4_lyf3}`. Because of the `}` this part is presumably the end of the flag. However, some other information must be hidden somewhere.
18 I cut out the QR code and used `zxing` and to try out different formats. `zxing --barcode_format=AZTEC qr.png` (Note the Aztec looking letters in the image!) resulted in `414f54577b6234726330643373`. Interpreting this as ASCII in hex notation results in `AOTW{b4rc0d3s`.
20 The flag is then given by `AOTW{b4rc0d3s_4_lyf3}`.
22 # christmaSSE keygen (day 10)
24 The challenge was fun and a learning experience since I never heard of SIMD-instructions or used numpy before. An especially memorable lesson was to never forget to think about whether `\\` or `\` is appropriate... This error cost me like 2 hours. Funnily enough, @georg was working on the challenge in parallel. However, he also had one tiny error somewhere in his decoding (endianess was wrong iirc). Before fixing my error I was able to get the flag by combining the output of his matrix exponentiation and my decyption. Teamwork makes the dreamwork! ^^
26 **Time spent:** 6 hours
29 A program which uses SIMD-instructions is given. It takes an input string, does some calculations, transforms the input with the result of the calculations and then prints the transformed string (i.e. the flag). The catch is that the calculations contains loops, an outer and an inner loop. The outer loop has `1234567890123456789` iterations and the inner loop has `1000` iterations. Clearly, this does not terminate for a long time so it has to be optimized.
32 @HaH translated the assembly instructions to pseudocode. From this it is clear that the SIMD-instructions are used to emulate some sort of operations on matrices and vectors. To be more precise, the instructions can be interpreted as follows:
34 The initial matrix is the identity matrix of size 4, i.e.
45 One iteration $`i`$ of the outer loop takes the matrix $`A_{i-1}`$ and computes the following matrix product:
56 The inner loop implements a modulo operation on $`A_i'`$. Essentially, using some SIMD-instructions the prime modulus $`p`$ is subtracted repeatedly from every value in the matrix $`x`$ long as the $`x - n \geq 0`$. The inner loop runs a thousand times which is enough since the values are only 32 bit integers. Thus, after the inner loop the resulting matrix is given by
61 Therefore, the nested loops are just applying modular exponentiation on a matrix and the result after all outer loop iterations is given by
68 \end{pmatrix} ^ {1234567890123456789} \mod p
71 This can easily be calculated in e.g. Python. The result is then used to transform the input in some manner to yield the flag. However, this can be translated straight from the decompiled assembly into Python code without thinking to much about what it is doing. Below is a script that correctly computes the flag using numpy and a rather fast matrix modular exponentiation algorithm taken from [Wikipedia](https://en.wikipedia.org/wiki/Modular_exponentiation#Generalizations). However, @georg implemented a simpler algorithm which was fast enough as well.
76 m = np.matrix("1 2 3 4; 5 6 7 8; 9 10 11 12; 13 14 15 16")
77 exponent = 1234567890123456789
79 flag_pt1 = [0xfc, 0x14, 0xeb, 0x09, 0xbc, 0xae, 0xe7, 0x47, 0x4f, 0xe3, 0x7c, 0xc1, 0x52, 0xa5, 0x02, 0x8e]
80 flag_pt2 = [0x89, 0x71, 0xc8, 0x8d, 0x96, 0x23, 0x01, 0x6d, 0x71, 0x40, 0x5a, 0xea, 0xfd, 0x46, 0x1d, 0x23]
81 flag = flag_pt1 + flag_pt2
83 # Matrix modular exponentiation algorithm taken from wikipedia
85 return np.mod(np.matmul(a, b), c)
89 return np.matrix("1 0 0 0; 0 1 0 0; 0 0 1 0; 0 0 0 1")
91 return mmulmod(a, mexpm(a, b-1, c), c)
93 return mmulmod(d, d, c)
95 # Matrix exponentiation and turn into list for transformation
96 pos = mexpm(m, exponent, prime).tolist()
98 # Translate the assembly code to python
99 pos_added = [i for s in pos for i in s]
100 key = [i for s in [list(struct.pack("<I", x)) for x in pos_added] for i in s]
103 flag[rax] = key[rax*2] ^ flag[rax]
104 flag[rax+1] = key[rax*2+1] ^ flag[rax+1]
107 print("AOTW{" + "".join([chr(x) for x in flag]) + "}")
110 The resulting flag is `AOTW{M4tr1x_3xp0n3nti4t1on_5728391723}`.
112 # naughty list (day 12)
114 Rather guess-y web challenge and really strange setup overall. Therefore, it wasn't such a nice challenge but rather naughty ;) However, it was a nice team effort solving this annoying challenge.
116 **Time spent:** 6 hours
119 Given is a website with login/register capabilities. Upon registering and logging in one is greeted by some token counter that is set to `0/5000`. Thus, one needs to acquire `5000` tokens to get the flag. It's possible to send other people tokens by inputting some sort of encrypted string. However, only addresses (they change every refresh) for the user `santa` is visible and the own address is not. Furthermore, the page has some weird state management via URL parameters e.g. `http://3.93.128.89:1212/?page=404&from_page=kxcX1b-P5IhR0olPNWc9PbPAUr1tGB01Tig2sP6MkC8`.
123 @superwayne found out that the token transfer has a race condition. Essentially, sending a token transfer simultaneously results in more tokens being sent than are available. However, without a way to send money to a chosen account this race condition is not that useful.
125 ### Getting the address for a user
126 As mentioned above, the page has some weird state behaviour. When one tries to open the page `http://3.93.128.89:1212/?page=wtf` it e.g. redirects to `http://3.93.128.89:1212/?page=404&from_page=kxcX1b-P5IhR0olPNWc9PbPAUr1tGB01Tig2sP6MkC8`. The `from_page` attribute is the previous page, i.e. `wtf`, indeterministically encrypted. If one enters a valid page, e.g. `http://3.93.128.89:1212/?page=contact` and then changes the `from_page` parameter to `page` the `contact` page is opened again. Thus, this constitutes and encryption oracle for arbitrary plaintext.
128 After lots of trial and error a human fuzzer (aka @lavish) encrypted the payload `lavish:ciaociao`. Trying to send tokens to the address resulted in `That user does not exist in our system.` After playing around with it a little bit the workings of the site became obvious. Any payload of the form `<random stuff>:<existing username>` generates an address for the specified username since the code probably splits at `:` and checks the second value.
130 ### Putting it together
131 @lavish created a script to combine the two exploits. This resulted in an account getting `13` tokens from one account (note that 5000 are needed). However, registering new users was rate limited. The obvious idea is then to juggle tokens between two accounts using the race condition. I.e. the account with 13 tokens then uses the race condition to transfer 13 tokens as often as possible to the previous account and so forth. This generates enough tokens extremely quickly and resulted in the flag `AOTW{S4n7A_c4nT_hAv3_3lF-cOnTroL_wi7H0uT_eLf-d1sCipl1N3}`.
133 # snowflake idle (day 17)
135 Another web challenge where the guessing part was the most difficult. To be fair, the guessing part was way more obvious than in the previous challenge but we were too stupid to notice. Otherwise, the challenge was rather straightforward.
137 **Time spent:** 5 hours
140 On navigating to the page one is asked to chose a username. After choosing a name one is presented with a website which essentially is some cookie clicker like thing but instead of cookies one can generate snowflakes. After having collected a certain amount, one can upgrade the collection speed by 1 (similar to cookie clicker games). The amount of snowflakes over time is visualized as a graph and there exists some button to buy the flag after having reached a large amount of snowflakes.
142 The immediately visible endpoints are `/control` for registering a user, `/client` for actions related to the game (e.g. getting snowflakes) and `/history/client` which is used by the history graph and returns the history as a json. The history is composed of every action that was `POST`ed to `/client`.
145 The snowflake gathering cannot be scripted efficiently as it is checked server-side and the amount of snowflakes cannot be underflowed as negative snowflake values are possible. Many people in MM and myself tried to find something for quite a long time until @TwentyOneCool used dirbuster (I believe) to find the endpoint `/history/control` (this was the guessing part which is obvious in retrospect. Similar to `/history/client` this endpoint returns a history of the `/control` endpoint. Therefore, we saw that the endpoint `/control` offered the capabilities to `load` and `save` a client state, i.e. the amount of snowflakes! Furthermore, the client state was displayed for the `save` command. By registering a really long username consisting of `a...a` we noticed that the encrypted pattern repeats. Therefore, the client data was encrypted using some sort of poly-alphabetic shift cipher. Using a script it is easy to extract the key:
150 testblock = "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"
152 testblock2 = "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"
155 return [a ^ b for a, b in zip(b1, b2)]
158 s = list(base64.b64decode(s))
159 key = xor([ord("a")]*60, s[36:96]) # repetition starts at index 36
160 return key[-16:] + key[:-16] # the last 16 bytes are actually the start of the key
163 k = get_key(testblock)
165 print("".join([chr(x) for x in xor(list(base64.b64decode(testblock))[:60], k)]))
166 print("".join([chr(x) for x in xor(list(base64.b64decode(testblock2))[:60], k)]))
168 if __name__ == "__main__":
173 Running the script outputs the following:
175 [249, 30, 132, 234, 14, 215, 113, 76, 15, 57, 182, 100, 166, 36, 156, 73, 117, 150, 141, 60, 249, 30, 132, 234, 14, 215, 113, 76, 15, 57, 182, 100, 166, 36, 156, 73, 117, 150, 141, 60, 249, 30, 132, 234, 14, 215, 113, 76, 15, 57, 182, 100, 166, 36, 156, 73, 117, 150, 141, 60]
176 {"money": 5.0, "speed": 1, "name": "aaaaaaaaaaaaaaaaaaaaaaaa
177 {"money": 1.0, "speed": 2, "name": "aaaaaaaaaaaaaaaaaaaaaaaa
179 The first array is the key in decimal notation, the second is the beginning of the state which the key was derived from and the third line is the beginning of an earlier game state. Clearly, the decryption key stays the same and thus it is very easy to encrypt a custom json object with the required amount of money. Sadly, I can't try it anymore since the service is down but teammates solved the challenge without having the full key (which is a bit more cumbersome).
181 After getting enough money one can buy the flag `AOTW{leaKinG_3ndp0int5}`.