site_vineetk

Source for vineetk.net website
Log | Files | Refs | LICENSE

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 ![Tux AES](images/bctf23_electronical-tux_aes.png)
     88 
     89 ![Mountain AES](images/bctf23_electronical-mountain_aes.png)
     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 ```