bctf23_electronical.md (8533B)
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 ```