diff options
Diffstat (limited to 'posts/bctf23_electronical.md')
| -rw-r--r-- | posts/bctf23_electronical.md | 301 |
1 files changed, 301 insertions, 0 deletions
diff --git a/posts/bctf23_electronical.md b/posts/bctf23_electronical.md new file mode 100644 index 0000000..3f82ecb --- /dev/null +++ b/posts/bctf23_electronical.md | |||
| @@ -0,0 +1,301 @@ | |||
| 1 | title: BCTF23 crypto/Electronical (medium) Writeup | ||
| 2 | date: 2023-10-26 12:00 | ||
| 3 | --- | ||
| 4 | |||
| 5 | > I do all my ciphering electronically. https://electronical.chall.pwnoh.io/ | ||
| 6 | |||
| 7 | When going to the linked site, you get told to encrypt any message or view the | ||
| 8 | site's source code. After submitting a message to encrypt, it returns some hex | ||
| 9 | string. | ||
| 10 | |||
| 11 | The source is: | ||
| 12 | ```python | ||
| 13 | from Crypto.Cipher import AES | ||
| 14 | from flask import Flask, request, abort, send_file | ||
| 15 | import math | ||
| 16 | import os | ||
| 17 | |||
| 18 | app = Flask(__name__) | ||
| 19 | |||
| 20 | key = os.urandom(32) | ||
| 21 | flag = os.environ.get('FLAG', 'bctf{fake_flag_fake_flag_fake_flag_fake_flag}') | ||
| 22 | |||
| 23 | cipher = AES.new(key, AES.MODE_ECB) | ||
| 24 | |||
| 25 | def encrypt(message: str) -> bytes: | ||
| 26 | length = math.ceil(len(message) / 16) * 16 | ||
| 27 | padded = message.encode().ljust(length, b'\0') | ||
| 28 | return cipher.encrypt(padded) | ||
| 29 | |||
| 30 | def decrypt(msg: str) -> bytes: | ||
| 31 | return cipher.decrypt(msg) | ||
| 32 | |||
| 33 | @app.get('/encrypt') | ||
| 34 | def handle_encrypt(): | ||
| 35 | param = request.args.get('message') | ||
| 36 | |||
| 37 | if not param: | ||
| 38 | return abort(400, "Bad") | ||
| 39 | if not isinstance(param, str): | ||
| 40 | return abort(400, "Bad") | ||
| 41 | |||
| 42 | print(encrypt(param + flag)) | ||
| 43 | |||
| 44 | return encrypt(param + flag).hex() | ||
| 45 | |||
| 46 | @app.get('/source') | ||
| 47 | def handle_source(): | ||
| 48 | return send_file(__file__, "text/plain") | ||
| 49 | |||
| 50 | @app.get('/') | ||
| 51 | def handle_home(): | ||
| 52 | return """ | ||
| 53 | <style> | ||
| 54 | form { | ||
| 55 | display: flex; | ||
| 56 | flex-direction: column; | ||
| 57 | max-width: 20em; | ||
| 58 | gap: .5em; | ||
| 59 | } | ||
| 60 | |||
| 61 | input { | ||
| 62 | padding: .4em; | ||
| 63 | } | ||
| 64 | </style> | ||
| 65 | <form action="/encrypt"> | ||
| 66 | <h2><i>ELECTRONICAL</i></h2> | ||
| 67 | <label for="message">Message to encrypt:</label> | ||
| 68 | <input id="message" name="message"></label> | ||
| 69 | <input type="submit" value="Submit"> | ||
| 70 | <a href="/source">Source code</a> | ||
| 71 | </form> | ||
| 72 | """ | ||
| 73 | |||
| 74 | if __name__ == "__main__": | ||
| 75 | app.run() | ||
| 76 | ``` | ||
| 77 | It seems that the flag is appended to the user's message and then encrypted with | ||
| 78 | AES-ECB. The total message is also padded to be a multiple of 16 bytes. | ||
| 79 | |||
| 80 | According to Wikipedia, ECB (electronic codebook) works by dividing a message | ||
| 81 | into blocks of a certain size (like 16 bytes). The problem however is that ECB | ||
| 82 | doesn't attempt to make any encrypted block unique like by adding a salt or | ||
| 83 | nonce, so any blocks of data that are identical would also be identical when | ||
| 84 | encrypted. Wikipedia also has an interesting example of encrypting an image of | ||
| 85 | Tux and a mountain (on French Wikipedia) with AES. | ||
| 86 | |||
| 87 |  | ||
| 88 | |||
| 89 |  | ||
| 90 | |||
| 91 | Through some more searching online, it seems a way to exploit this is with | ||
| 92 | something called a Chosen Plaintext Attack. Since the message before the flag is | ||
| 93 | controlled by us the user (attacker?) and the flag is appended to the end, the | ||
| 94 | provided message can be made in a way that only one byte of the flag needs to be | ||
| 95 | bruteforced at a time. | ||
| 96 | |||
| 97 | Let's say that this is our message: `thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}` | ||
| 98 | |||
| 99 | This string is 45 characters, so the server would pad this with 3 \0 characters | ||
| 100 | to make it evenly divisible by 16 characters. | ||
| 101 | |||
| 102 | ``` | ||
| 103 | b'thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}\x00\x00\x00' | ||
| 104 | ``` | ||
| 105 | |||
| 106 | We know what "thischallengesucks" is, but FLAG and anything else after is | ||
| 107 | appended by the server and is what we're trying to find. | ||
| 108 | |||
| 109 | "thischallengesucks" is 18 characters, but if we send a 15 character string, | ||
| 110 | then the first block to be encrypted would be "thischallengesu?", where ? is the | ||
| 111 | mystery character. | ||
| 112 | |||
| 113 | For readability purposes, I'm going to use repeated "0" characters needed | ||
| 114 | instead of "thischallengesucks". | ||
| 115 | |||
| 116 | When passing "000000000000000?" to the server, a certain hex string would be | ||
| 117 | returned (newlines every 32 characters not included in original): | ||
| 118 | |||
| 119 | ``` | ||
| 120 | b57189530dacbb9c5707c1cb0b044a34 | ||
| 121 | 5377049685bb9553a73e4408565505dd | ||
| 122 | 0c614f69c4749b10f8cbc9c735fd7314 | ||
| 123 | 5a9ae527825603a8eb0dba6a0347a4e5 | ||
| 124 | ``` | ||
| 125 | Replacing the ? with any other character would result in only the first row being | ||
| 126 | changed, like with "000000000000000A": | ||
| 127 | |||
| 128 | ``` | ||
| 129 | b2457a857e82a1d5ad919a4bdaf9133a | ||
| 130 | 7835c84bc75d836fad8ca5fbcec086ff | ||
| 131 | 937cf83a682fa26162a65f2295b2b119 | ||
| 132 | 6b398dd6f75e212b1633c5189bdb5689 | ||
| 133 | ``` | ||
| 134 | Since the other three blocks remained the same, the last character in the | ||
| 135 | message being sent simply needs to bruteforced with every printable character | ||
| 136 | until it results in the same block from ?. In this case, that character would be | ||
| 137 | F: | ||
| 138 | |||
| 139 | ``` | ||
| 140 | b57189530dacbb9c5707c1cb0b044a34 | ||
| 141 | 5377049685bb9553a73e4408565505dd | ||
| 142 | 0c614f69c4749b10f8cbc9c735fd7314 | ||
| 143 | 5a9ae527825603a8eb0dba6a0347a4e5 | ||
| 144 | ``` | ||
| 145 | ``` | ||
| 146 | (0000000000000, 14 characters long) | ||
| 147 | 5ec61f1209adfeff202edbba28339f83 | ||
| 148 | 4f7cc7a4c0c553380874383e93408678 | ||
| 149 | ca91d95b091956edb162da583b51051b | ||
| 150 | 33b91ab14b8807348fc98bf223b4b3a5 | ||
| 151 | |||
| 152 | (0000000000000FL, 16 characters long) | ||
| 153 | 5ec61f1209adfeff202edbba28339f83 | ||
| 154 | 4f7cc7a4c0c553380874383e93408678 | ||
| 155 | ca91d95b091956edb162da583b51051b | ||
| 156 | 33b91ab14b8807348fc98bf223b4b3a5 | ||
| 157 | ``` | ||
| 158 | Then the 0 left pad would be decreased by one character and the process repeats | ||
| 159 | until the whole block is done. However, a flag usually won't be just 16 | ||
| 160 | characters long. I had some difficulty trying to bruteforce the 17th character | ||
| 161 | and above because I was prepending and appending the zeros within a single block | ||
| 162 | (between 0 and 15 padded 0s), but that was among a few other issues I had that | ||
| 163 | were the result of the message I was sending being in the format of "pad + | ||
| 164 | known_flag + brute_single_char + pad" where this only worked for the first | ||
| 165 | block. This did not work later because those messages would have the pad bytes | ||
| 166 | in the middle of the message, which did not go well. | ||
| 167 | |||
| 168 | In the end, I realized that I can check the target block hexstring by sending | ||
| 169 | only the padded 0 bytes (or anything else of that length) without other | ||
| 170 | characters and then append my known bytes of the flag and a single other character | ||
| 171 | to fill that block to brute force that last character. | ||
| 172 | |||
| 173 | A visual representation is this: | ||
| 174 | |||
| 175 | ``` | ||
| 176 | Block size: 8 characters | ||
| 177 | |||
| 178 | 7 pad, 0 known | ||
| 179 | XXXXXXX? | ||
| 180 | XXXXXXXF | ||
| 181 | |||
| 182 | 6 pad, 1 known | ||
| 183 | XXXXXX?? | ||
| 184 | XXXXXXFL | ||
| 185 | |||
| 186 | 5 pad, 2 known | ||
| 187 | XXXXX??? | ||
| 188 | XXXXXFLA | ||
| 189 | |||
| 190 | ... | ||
| 191 | |||
| 192 | 0 pad, 7 known | ||
| 193 | FLAG{5o? | ||
| 194 | |||
| 195 | 8 known | ||
| 196 | FLAG{5om | ||
| 197 | ``` | ||
| 198 | This only decrypts the first block, so how I decrypted each additional block was | ||
| 199 | by prepending another block of pad characters (blocksize - 1) and repeating the | ||
| 200 | process. | ||
| 201 | |||
| 202 | ``` | ||
| 203 | 7 pad, 7 known | ||
| 204 | XXXXXXXFLAG{5om? | ||
| 205 | XXXXXXXFLAG{5om3 | ||
| 206 | |||
| 207 | 6 pad, 8 known | ||
| 208 | XXXXXXFLAG{5om3? | ||
| 209 | XXXXXXFLAG{5om3_ | ||
| 210 | |||
| 211 | ... | ||
| 212 | |||
| 213 | 5 pad, 26 known | ||
| 214 | XXXXXFLAG{5om3_!mp0r74nt_$3cr37? | ||
| 215 | XXXXXFLAG{5om3_!mp0r74nt_$3cr37} | ||
| 216 | |||
| 217 | ... | ||
| 218 | 0 pad, 31 known | ||
| 219 | FLAG{5om3_!mp0r74nt_$3cr37}\0\0? | ||
| 220 | FLAG{5om3_!mp0r74nt_$3cr37}\0\0\0 | ||
| 221 | ``` | ||
| 222 | After some automating help with python, I was able to finally get the flag. | ||
| 223 | |||
| 224 | ``` | ||
| 225 | Flag: bctf{1_c4n7_b3l13v3_u_f0und_my_c0d3b00k} | ||
| 226 | ``` | ||
| 227 | |||
| 228 | My python file to solve this was: | ||
| 229 | ```python | ||
| 230 | from requests import get | ||
| 231 | from requests.utils import quote | ||
| 232 | |||
| 233 | # list of characters that will be bruteforced, these are the printable chars | ||
| 234 | chars = [chr(i) for i in range(ord(' '), ord('~') + 1)] | ||
| 235 | # nul character is also checked because that's the pad character | ||
| 236 | chars += '\0' | ||
| 237 | |||
| 238 | def encrypt(msg): | ||
| 239 | #url = "https://electronical.chall.pwnoh.io/encrypt?message=" | ||
| 240 | url = "http://localhost:5000/encrypt?message=" | ||
| 241 | return get(url + quote(msg)).content; | ||
| 242 | |||
| 243 | def calc_padding_for_known(): | ||
| 244 | # divided by 2 because each hex byte is 2 characters long | ||
| 245 | cur = len(encrypt("0")) // 2 | ||
| 246 | |||
| 247 | # 16 chosen because that's the padding chosen in app.py on the server | ||
| 248 | for i in range(2, 16): | ||
| 249 | tmp = len(encrypt("0" * i)) // 2 | ||
| 250 | if tmp > cur: | ||
| 251 | return tmp, tmp - cur, i - 1 | ||
| 252 | # shouldn't come here | ||
| 253 | return 0,0,0 | ||
| 254 | |||
| 255 | totalblocks, bs, pad = calc_padding_for_known() | ||
| 256 | |||
| 257 | print(f"Block size of padding: {bs}, {pad}") | ||
| 258 | |||
| 259 | # first block are known to not be the flag (is all 0 being encrypted) | ||
| 260 | # second block is what's being bruteforced | ||
| 261 | # third block's last character is unknown and being compared with second block | ||
| 262 | #msg = "0" * (bs + bs - 1) + "a" * 1 + "0" * (bs - 1) | ||
| 263 | #cur = encrypt(msg) | ||
| 264 | #print(cur) | ||
| 265 | flag = "" | ||
| 266 | curflag = "" | ||
| 267 | |||
| 268 | tbs = bs * 2 | ||
| 269 | |||
| 270 | for j in range(totalblocks // bs): | ||
| 271 | for i in range(1, bs + 1): | ||
| 272 | known = "0" * (bs * (1 + j) - len(flag) - 1) | ||
| 273 | msg = known | ||
| 274 | target = encrypt(msg).decode("utf-8") | ||
| 275 | |||
| 276 | print(f"\nNew target message: {msg}") | ||
| 277 | print("New target message return:") | ||
| 278 | print('\n'.join([target[A:A + tbs] for A in range(0, len(target), tbs)])) | ||
| 279 | |||
| 280 | target = target[tbs * (0 + j):tbs * (1 + j)] | ||
| 281 | print(f"New target block: {target}") | ||
| 282 | |||
| 283 | for c in chars: | ||
| 284 | msg = known + flag + c | ||
| 285 | print(f"Current character: {c}") | ||
| 286 | print(f"Current message: {msg}") | ||
| 287 | print(f"Target block: {target}") | ||
| 288 | cur = encrypt(msg).decode("utf-8") | ||
| 289 | print('\n'.join([cur[A:A + tbs] for A in range(0, len(cur), tbs)])) | ||
| 290 | |||
| 291 | print(f"Current block: {cur[:tbs]}") | ||
| 292 | |||
| 293 | if (cur[tbs * (0 + j):tbs * (1 + j)] == target): | ||
| 294 | flag += c | ||
| 295 | print(flag) | ||
| 296 | break | ||
| 297 | |||
| 298 | print("\n") | ||
| 299 | curflag += flag | ||
| 300 | print(flag) | ||
| 301 | ``` | ||
