|
| 1 | +# ICSS (Misc/Crypto, 471p, 18 solved) |
| 2 | + |
| 3 | +A blackbox crypto challenge. |
| 4 | +We get a base64 encoded ciphertext `ypovStywDFkNEotWNc3AxtlL2IwWKuJA1qawdvYynITDDIpknntQR1gB+Nzl` and access to a service which can encrypt for us up to 6 characters of input. |
| 5 | + |
| 6 | +First we need to understand how this encryption works, and for this we played a bit with it, sending specially crafted payloads. |
| 7 | +We can notice some things: |
| 8 | + |
| 9 | +1. Encryption goes character by character, there are no blocks. It's easy to see when we add or remove characters. |
| 10 | + |
| 11 | +2. Ciphertext for a character depends on the character itself and on the position where it is in the input. The same character on a different position encrypts differently, but the same character at the same position, even for different plaintexts gives the same encrypted value. |
| 12 | +For example: |
| 13 | +aaa -> a060a2 |
| 14 | +aba -> a063a2 |
| 15 | + |
| 16 | +So the encryption for the last `a` stayed the same. |
| 17 | + |
| 18 | +3. An exception to above rule is the first character. It affects how rest of the string is encrypted. |
| 19 | + |
| 20 | +Another set of tests we did gave some interesting results. |
| 21 | +By sending `\x00\x00\x00\x00\x00\x00` bytes we got `010102030508` which looks like a Fibonacci sequence! |
| 22 | +It got even better when we sent `\x01\x00\x00\x00\x00\x00` because we got `00020305080d` which is the same sequence just shifter 1 position further (disregarding the first byte). |
| 23 | +If we now send `\x01\x05\x00\x00\x00\x00` we will get `00070305080d` so the sequence is the same but second byte is bigger by 5, which is the value we tried to encrypt. |
| 24 | +First byte encryption is unknown, but it depends only on this character, so we don't really care, it can be brute-forced. |
| 25 | + |
| 26 | +We did a bit more checking and it was quite clear that the encryption does something like: |
| 27 | +1. Encrypt first byte in some special way |
| 28 | +2. Every other byte in position `k` is encrypted as `Fibonacci(first_byte + k) + kth_byte_value` |
| 29 | + |
| 30 | +However, there is some weird stuff happening when overflow is reached, and it seemed some other special cases are present as well for even/odd numbers. |
| 31 | + |
| 32 | +Instead of trying to figure out how to handle those issues, we decided to go the "easy" way instead. |
| 33 | +We know that by changing the first byte we can "shift" the Fibonacci sequence for the rest of the encryption, but it means that we basically shift the positions! |
| 34 | +By sending `Xa` we can get encrypted byte `a` at positon `1` with starting byte `X`, but if we send `(X+1)a` we will shift the sequence and the result will be the same as encrypted `a` at position `2` with starting byte `X`. |
| 35 | + |
| 36 | +This means that we can pretty much get any encrypted byte at any position we want by encrypting only 2 bytes at a time! |
| 37 | +We use this approach with the server as "oracle" serving us the encrypted bytes and we brute-force the flag. |
| 38 | + |
| 39 | +What we want to do: |
| 40 | +1. Take a single encrypted character from the encrypted flag we have at k-th position. |
| 41 | +2. Encrypt via server every possible character at k-th position and compare it with the one we have. Once they match we know what was the plaintext character. |
| 42 | +3. Repeat until we get whole flag. |
| 43 | + |
| 44 | +So we run: |
| 45 | + |
| 46 | +```python |
| 47 | +import base64 |
| 48 | +import string |
| 49 | + |
| 50 | +from crypto_commons.netcat.netcat_commons import nc, send |
| 51 | + |
| 52 | + |
| 53 | +def brute_character_at_position(position, expected): |
| 54 | + for c in string.letters + "{_" + string.digits + string.punctuation: |
| 55 | + if int(get_encrypted_char_at_position(c, position), 16) == ord(expected): |
| 56 | + return c |
| 57 | + return "?" |
| 58 | + |
| 59 | + |
| 60 | +def get_encrypted_char_at_position(character, position): |
| 61 | + return get_ciphertext(chr(ord('E') + position) + character)[2:4] |
| 62 | + |
| 63 | + |
| 64 | +def get_ciphertext(c): |
| 65 | + url = 'icss.ctf.site' |
| 66 | + port = 40112 |
| 67 | + s = nc(url, port) |
| 68 | + s.recv(9999) |
| 69 | + s.recv(9999) |
| 70 | + send(s, c) |
| 71 | + s.recv(9999) |
| 72 | + result = s.recv(9999) |
| 73 | + return base64.b64decode(result).encode("hex") |
| 74 | + |
| 75 | + |
| 76 | +def main(): |
| 77 | + flag_ciphertext = base64.b64decode("ypovStywDFkNEotWNc3AxtlL2IwWKuJA1qawdvYynITDDIpknntQR1gB+Nzl") |
| 78 | + flag_plaintext = "E" |
| 79 | + for i in range(len(flag_ciphertext) - 1): |
| 80 | + expected_encrypted_byte = flag_ciphertext[i + 1] |
| 81 | + flag_plaintext += brute_character_at_position(i, expected_encrypted_byte) |
| 82 | + print(flag_plaintext) |
| 83 | + print(flag_plaintext) |
| 84 | + |
| 85 | + |
| 86 | +main() |
| 87 | +``` |
| 88 | + |
| 89 | +After a while we finally get: `EKO{Mr_Leon4rd0_PisAno_Big0770_AKA_Fib@nacc!}` |
0 commit comments