Category: Crypto Points: 10,10,30,40 Solves: 436,381,224,63 Description:
Decrypt 4 flags.
In this challenge we're given 4 encrypted flags, and ask to decrypt them. They get successively more difficult, with the last one only having 63 successful solves. For each encrypted blob we were given a corresponding URL containing the encryption engine that would allow us to input anything and get back the corresponding output.
From a cryptography perspective, this means that for each of the flags we have at least two types of attacks. We have a chosen plain text attack in which we can decide what to encrypt and look at the results. We also have a known plain text since each flag is of the form "MMA{<values>}". Using these two types of attacks we can solve each of the challenges.
Flag 1
This encrypted flag is 36 36 2a 64 4b 4b 4a 21 1e 4b 1f 20 1f 21 4d 4b 1b 1d 19 4f 21 4c 1d 4a 4e 1c 4c 1b 22 4f 22 22 1b 21 4c 20 1d 4f 1f 4c 4a 19 22 1a 66. I should note here that each of the flags is hex encoded and each byte is separated by a space. This is just part of the mechanics of how it's displayed to the user.
http://bow.chal.mmactf.link/~scs/crypt2.cgi
Here's some example input and output:
Input | Output |
a | 4a |
b | 4b |
c | 4c |
abc | 4a 4b 4c |
We can quickly see that the input is just incrementing. Further, we notice that adding new characters doesn't propagate any changes. Therefore, this would appear to be a shift cipher. Find the length like this:
>>> ord("a")-0x4a 23
Ok. Shift of 23. Now we just unshift to find the flag:
>>> ''.join([chr(int(x,16)+23) for x in "36 36 2a 64 4b 4b 4a 21 1e 4b 1f 20 1f 21 4d 4b 1b 1d 19 4f 21 4c 1d 4a 4e 1c 4c 1b 22 4f 22 22 1b 21 4c 20 1d 4f 1f 4c 4a 19 22 1a 66".split(" ")]) 'MMA{bba85b6768db240f8c4ae3c29f9928c74f6ca091}'
Flag: MMA{bba85b6768db240f8c4ae3c29f9928c74f6ca091}
Flag 2
e3 e3 83 21 33 96 23 43 ef 9a 9a 05 18 c7 23 07 07 07 c7 9a 04 33 23 07 23 ef 12 c7 04 96 43 23 23 18 04 04 05 c7 fb 18 96 43 ef 43 ff
http://bow.chal.mmactf.link/~scs/crypt4.cgi
As usual, let's start by playing with input:
Input | Output |
a | ef |
b | aa |
c | fb |
abc | ef aa fb |
The direct outputs of any given character don't appear to just be a shift. However, putting the characters together ends up just putting together there output. Just as before, there's no propagation of changes. This means we're dealing with a look-up table. The nice thing is, it's looking it up by character. There's only going to be so many characters (26+26+10=52 without special characters). Let's just enumerate it.
>>> import string >>> print(string.printable) i = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"
I've removed the last bit of characters from the printable because we likely wouldn't use them. I apparently forgot to jot down what python script I used to enumerate, but really you can do it any way. Just give those input values to the website and record the output. We get the following output as our lookup table from values i.
>>> r = "04 c7 23 c3 18 96 05 9a 07 12 ef aa fb 43 4d 33 85 45 f9 02 7f 50 3c 9f a8 51 a3 40 8f 92 9d 38 f5 bc b6 da 83 2c 1a 1b 6e 5a a0 52 3b d6 b3 29 e3 2f 84 53 d1 00 ed 20 fc b1 5b 6a cb be fd 93 26 36 3f f7 cc 34 a5 e5 f1 71 d8 31 15 80 e2 eb 27 b2 75 09 39 4a 4c 58 cf d0 21 10 ff f3".split(" ")
Now we create a lookup dictionary in python and reverse the flag:
>>> lookup = {} >>> for x in range(len(i)): >>> lookup[r[x]] = i[x] >>> >>> ''.join([lookup[x] for x in "e3 e3 83 21 33 96 23 43 ef 9a 9a 05 18 c7 23 07 07 07 c7 9a 04 33 23 07 23 ef 12 c7 04 96 43 23 23 18 04 04 05 c7 fb 18 96 43 ef 43 ff".split(" ")]) 'MMA{f52da776412888170f282a9105d2240061c45dad}'
Flag: MMA{f52da776412888170f282a9105d2240061c45dad}
Flag 3
60 00 0c 3a 1e 52 02 53 02 51 0c 5d 56 51 5a 5f 5f 5a 51 00 05 53 56 0a 5e 00 52 05 03 51 50 55 03 04 52 04 0f 0f 54 52 57 03 52 04 4e
http://bow.chal.mmactf.link/~scs/crypt5.cgi
This one is getting tougher. The encryption now does some form of chaining. Here are some examples:
Input | Output |
a | 60 |
aa | 63 00 |
aaa | 62 00 00 |
b | 63 |
bb | 60 00 |
bbb | 61 00 00 |
ab | 63 03 |
abbb | 65 03 00 00 |
So I played around with many different inputs, those are just an example. Right off the bat we see they're not so random. When we repeat characters the first letter changes value and then the rest are filled with 00 values to indicate a repeat. Here's where our known plain text attack helps. Let's look at what we know to be our output:
Input | Output |
M | 4c |
MM | 4f 00 |
MMA | 4e 00 0c |
MMA{ | 49 00 0c 3a |
Now we start to see a pattern. The first output character changes, but the rest do not. It feels like a check-sum of some kind. Because we're allowed to ask the engine to encrypt anything, we can step through the flag one character at a time, looking for a win. I wrote the following script to do just that.
#!/usr/bin/python3 import urllib.parse import urllib.request url = 'http://bow.chal.mmactf.link/~scs/crypt5.cgi' alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~" flag_enc = "60 00 0c 3a 1e 52 02 53 02 51 0c 5d 56 51 5a 5f 5f 5a 51 00 05 53 56 0a 5e 00 52 05 03 51 50 55 03 04 52 04 0f 0f 54 52 57 03 52 04 4e".split(" ") flag_dec = ["M"] def encrypt(value): data = urllib.parse.urlencode({'s':value}) data = data.encode('utf-8') # data should be bytes req = urllib.request.Request(url, data) with urllib.request.urlopen(req) as response: the_page = response.read().decode('utf-8') return the_page.split("</h1>")[1].split(" <")[0].split(" ") while len(flag_dec) != len(flag_enc): for c in alphabet: enc = encrypt(''.join(flag_dec + [c])) if enc[-1] == flag_enc[len(enc)-1]: flag_dec.append(c) print("{0}".format(''.join(flag_dec))) break
I went ahead and started with only the known value of "M" just to test that it was working properly. You can certainly start anywhere in the flag if you happen to know it (such as starting with MMA{). All it's doing is trying each possible character, asking the website to encrypt it, then checking what the output encrypted to in that position to verify if it chose correctly. This means it's a linear search and definitely doable. Here's what the output looks like:
MM MMA MMA{ MMA{e MMA{e7 MMA{e75 MMA{e75f MMA{e75fd MMA{e75fd5 MMA{e75fd59 MMA{e75fd59d MMA{e75fd59d2 MMA{e75fd59d2c MMA{e75fd59d2c9 MMA{e75fd59d2c9f MMA{e75fd59d2c9f9 MMA{e75fd59d2c9f9c MMA{e75fd59d2c9f9c2 MMA{e75fd59d2c9f9c22 MMA{e75fd59d2c9f9c227 MMA{e75fd59d2c9f9c227d MMA{e75fd59d2c9f9c227d2 MMA{e75fd59d2c9f9c227d28 MMA{e75fd59d2c9f9c227d28f MMA{e75fd59d2c9f9c227d28ff MMA{e75fd59d2c9f9c227d28ff4 MMA{e75fd59d2c9f9c227d28ff41 MMA{e75fd59d2c9f9c227d28ff412 MMA{e75fd59d2c9f9c227d28ff412c MMA{e75fd59d2c9f9c227d28ff412c3 MMA{e75fd59d2c9f9c227d28ff412c3f MMA{e75fd59d2c9f9c227d28ff412c3fe MMA{e75fd59d2c9f9c227d28ff412c3fea MMA{e75fd59d2c9f9c227d28ff412c3fea3 MMA{e75fd59d2c9f9c227d28ff412c3fea37 MMA{e75fd59d2c9f9c227d28ff412c3fea378 MMA{e75fd59d2c9f9c227d28ff412c3fea3787 MMA{e75fd59d2c9f9c227d28ff412c3fea3787c MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1 MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1f MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe7 MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe73 MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe73}
Flag: MMA{e75fd59d2c9f9c227d28ff412c3fea3787c1fe73}
Flag 4
62 a9 6c 28 0e 33 31 c6 68 cd 66 66 59 46 cc 53 0c 98 31 65 c6 35 c9 a9 60 4e 37 b0 33 46 0d 60 46 26 66 33 cc e6 a9 f6 6c 07 2b 23 af
http://bow.chal.mmactf.link/~scs/crypt6.cgi
Again, we start off trying values. Here are some examples:
Input | Output |
a | 2c |
b | 4c |
c | 6c |
f | cc |
g | ec |
i | 2d |
aa | 2c c2 |
aaaa | c2 c2 2c c2 |
bbbb | 89 89 4c 89 |
abab | c4 85 2c c4 |
Clearly a pattern. At this point, I was initially considering reversing what the crypto algorithm was as it seems doable. However, I later decided to be lazy and utilize the fact that we can ask the engine to encrypt anything. Basically, if you play around with it long enough you'll notice that there isn't real propagation of changes. Some changes affect 2 spots in the output, and some only affect 1. It's that characteristic that I took advantage of.
We also notice that the input and output character length is the same. So we know that our output will have a length of 45 due to how the flag is formatted (MMA{<40 chars>}). My initial question was how much overlap do those changes have. If there are minimal locations where the output value is dependent on two or more input values, then it becomes either a satisfiability problem or a brute force problem. To answer this question, I created a script:
#!/usr/bin/python3 import urllib.parse import urllib.request from copy import copy url = 'http://bow.chal.mmactf.link/~scs/crypt6.cgi' value = {'s' : 'MMA{'} alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~" flag_enc = "62 a9 6c 28 0e 33 31 c6 68 cd 66 66 59 46 cc 53 0c 98 31 65 c6 35 c9 a9 60 4e 37 b0 33 46 0d 60 46 26 66 33 cc e6 a9 f6 6c 07 2b 23 af".split(" ") flag_dec = ['M', 'M', 'A', '{', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', '}'] def encrypt(value): data = urllib.parse.urlencode({'s':value}) data = data.encode('utf-8') # data should be bytes req = urllib.request.Request(url, data) with urllib.request.urlopen(req) as response: the_page = response.read().decode('utf-8') return the_page.split("</h1>")[1].split(" <")[0].split(" ") # Baseline what it looks like base = encrypt("".join(flag_dec)) diff = {} # Try changing one thing at a time and see what happens for i in range(len(flag_dec)): # Change one thing and look for differences tmp = copy(flag_dec) tmp[i] = "1" o = encrypt("".join(tmp)) for x in range(len(o)): if o[x] != base[x]: #print("Found difference at index {}".format(x)) try: diff[x].append(i) except: diff[x] = [i] print("Here's the diff") print(diff)
The encrypt function, like in the other examples, is just responsible for getting the output of asking the web page to encrypt something. The main logic is in the for loop where I make a change in one spot of the flag, and check the output to see what the corresponding areas of change are. The output, pretty printed for readability, is:
{0: [1, 23], 1: [3, 23], 2: [3], 3: [5, 39], 4: [5], 5: [25], 6: [7], 7: [35], 8: [9], 9: [27], 10: [11], 11: [43], 12: [13], 13: [29], 14: [15], 15: [37], 16: [17], 17: [31], 18: [19], 19: [41], 20: [21], 21: [33], 22: [0], 23: [2], 24: [4], 25: [6], 26: [8], 27: [10], 28: [12], 29: [14], 30: [16], 31: [18], 32: [20], 33: [22], 34: [24], 35: [26], 36: [28], 37: [30], 38: [32], 39: [34], 40: [36], 41: [38], 42: [40], 43: [42], 44: [44]}
What you should notice here is that the majority of the input characters only affect one output. The overlaps where two or more inputs affect the same output are:
3: [1, 2] 5: [3, 4] 23: [0, 1]
Note that we happen to know input index 0,1,2 and 3 (corresponding to M,M,A,{). That means that we actually have no areas in this encryption where we need to solve for two things at the same time. It actually would still likely be possible to solve if there were more overlap, but this makes it easier. Here's the script I wrote to crack it:
#!/usr/bin/python3 import urllib.parse import urllib.request from copy import copy url = 'http://bow.chal.mmactf.link/~scs/crypt6.cgi' alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~" flag_enc = "62 a9 6c 28 0e 33 31 c6 68 cd 66 66 59 46 cc 53 0c 98 31 65 c6 35 c9 a9 60 4e 37 b0 33 46 0d 60 46 26 66 33 cc e6 a9 f6 6c 07 2b 23 af".split(" ") flag_dec = ['M', 'M', 'A', '{', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', 'A', '}'] # bdiff describes what affects the input should have on the output. # For instance, bdiff[1] = [3,23] means that the index 1 of input affects index 3 and 23 of the output bdiff = {0: [1, 23], 1: [3, 23], 2: [3], 3: [5, 39], 4: [5], 5: [25], 6: [7], 7: [35], 8: [9], 9: [27], 10: [11], 11: [43], 12: [13], 13: [29], 14: [15], 15: [37], 16: [17], 17: [31], 18: [19], 19: [41], 20: [21], 21: [33], 22: [0], 23: [2], 24: [4], 25: [6], 26: [8], 27: [10], 28: [12], 29: [14], 30: [16], 31: [18], 32: [20], 33: [22], 34: [24], 35: [26], 36: [28], 37: [30], 38: [32], 39: [34], 40: [36], 41: [38], 42: [40], 43: [42], 44: [44]} def encrypt(value): data = urllib.parse.urlencode({'s':value}) data = data.encode('utf-8') # data should be bytes req = urllib.request.Request(url, data) with urllib.request.urlopen(req) as response: the_page = response.read().decode('utf-8') return the_page.split("")[1].split(" <")[0].split(" ") # Given flag_dec (guess), and an index # Check if the flag_dec guess is correct for the given index def checkOutput(flag_dec,index): # Encrypt it enc = encrypt("".join(flag_dec)) # Check the corresponding indexes for i in bdiff[index]: # If we're wrong if enc[i] != flag_enc[i]: return False return True # Loop through all values assuming it starts with "MMA{" for x in range(4,len(flag_dec)): # Make a working copy tmp = copy(flag_dec) # Create variable to ensure we're finding results found = False # Try everything for c in alphabet: # Modify it tmp[x] = c # Try the answer if checkOutput(tmp,x): # Update our answer with this new knowledge flag_dec[x] = c found = True print("".join(flag_dec)) break # Ensure we didn't just mess up if not found: print("Error: Unable to find answer for index {0}\nExiting".format(x)) exit(1)
As per before, the encrypt function just handles asking the webpage to encrypt for us. The checkOutput function handles calling the encryption function to get the new output, then verifying that the given index of the input maps correctly to the output. Remember, while the input index isn't mapping to the same spot (i.e.: input index 5 doesn't map to encrypted index 5), it is still static (i.e.: input index 5 maps to encrypted index 25). That's why this looks a little stranger than normal brute forcing, but it's really the same idea.
Here's what it looks like running:
MMA{fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7aAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3dddAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5AAAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd57AAAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd571AAAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710AAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710eAAAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e8AAAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85AAAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e851AAAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e8511AAAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116AAAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e851168AAAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e8511681AAAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814AAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fAAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814feAAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fefAAAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef0AAAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01AAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01cAAAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c9AAAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c90AAAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907AAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907fAAAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907f4AAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907f4dAAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907f4dfAAAA} MMA{f9cf7a3ddd5710e85116814fef01c907f4df3AAA} MMA{f9cf7a3ddd5710e85116814fef01c907f4df35AA} MMA{f9cf7a3ddd5710e85116814fef01c907f4df35cA} MMA{f9cf7a3ddd5710e85116814fef01c907f4df35ce}
Flag: MMA{f9cf7a3ddd5710e85116814fef01c907f4df35ce}