summaryrefslogtreecommitdiff
path: root/posts/bctf23_electronical.md
diff options
context:
space:
mode:
Diffstat (limited to 'posts/bctf23_electronical.md')
-rw-r--r--posts/bctf23_electronical.md301
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 @@
1title: BCTF23 crypto/Electronical (medium) Writeup
2date: 2023-10-26 12:00
3---
4
5> I do all my ciphering electronically. https://electronical.chall.pwnoh.io/
6
7When going to the linked site, you get told to encrypt any message or view the
8site's source code. After submitting a message to encrypt, it returns some hex
9string.
10
11The source is:
12```python
13from Crypto.Cipher import AES
14from flask import Flask, request, abort, send_file
15import math
16import os
17
18app = Flask(__name__)
19
20key = os.urandom(32)
21flag = os.environ.get('FLAG', 'bctf{fake_flag_fake_flag_fake_flag_fake_flag}')
22
23cipher = AES.new(key, AES.MODE_ECB)
24
25def 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
30def decrypt(msg: str) -> bytes:
31 return cipher.decrypt(msg)
32
33@app.get('/encrypt')
34def 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')
47def handle_source():
48 return send_file(__file__, "text/plain")
49
50@app.get('/')
51def 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
74if __name__ == "__main__":
75 app.run()
76```
77It seems that the flag is appended to the user's message and then encrypted with
78AES-ECB. The total message is also padded to be a multiple of 16 bytes.
79
80According to Wikipedia, ECB (electronic codebook) works by dividing a message
81into blocks of a certain size (like 16 bytes). The problem however is that ECB
82doesn't attempt to make any encrypted block unique like by adding a salt or
83nonce, so any blocks of data that are identical would also be identical when
84encrypted. Wikipedia also has an interesting example of encrypting an image of
85Tux 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
91Through some more searching online, it seems a way to exploit this is with
92something called a Chosen Plaintext Attack. Since the message before the flag is
93controlled by us the user (attacker?) and the flag is appended to the end, the
94provided message can be made in a way that only one byte of the flag needs to be
95bruteforced at a time.
96
97Let's say that this is our message: `thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}`
98
99This string is 45 characters, so the server would pad this with 3 \0 characters
100to make it evenly divisible by 16 characters.
101
102```
103b'thischallengesucksFLAG{5om3_!mp0r74nt_$3cr37}\x00\x00\x00'
104```
105
106We know what "thischallengesucks" is, but FLAG and anything else after is
107appended 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,
110then the first block to be encrypted would be "thischallengesu?", where ? is the
111mystery character.
112
113For readability purposes, I'm going to use repeated "0" characters needed
114instead of "thischallengesucks".
115
116When passing "000000000000000?" to the server, a certain hex string would be
117returned (newlines every 32 characters not included in original):
118
119```
120b57189530dacbb9c5707c1cb0b044a34
1215377049685bb9553a73e4408565505dd
1220c614f69c4749b10f8cbc9c735fd7314
1235a9ae527825603a8eb0dba6a0347a4e5
124```
125Replacing the ? with any other character would result in only the first row being
126changed, like with "000000000000000A":
127
128```
129b2457a857e82a1d5ad919a4bdaf9133a
1307835c84bc75d836fad8ca5fbcec086ff
131937cf83a682fa26162a65f2295b2b119
1326b398dd6f75e212b1633c5189bdb5689
133```
134Since the other three blocks remained the same, the last character in the
135message being sent simply needs to bruteforced with every printable character
136until it results in the same block from ?. In this case, that character would be
137F:
138
139```
140b57189530dacbb9c5707c1cb0b044a34
1415377049685bb9553a73e4408565505dd
1420c614f69c4749b10f8cbc9c735fd7314
1435a9ae527825603a8eb0dba6a0347a4e5
144```
145```
146(0000000000000, 14 characters long)
1475ec61f1209adfeff202edbba28339f83
1484f7cc7a4c0c553380874383e93408678
149ca91d95b091956edb162da583b51051b
15033b91ab14b8807348fc98bf223b4b3a5
151
152(0000000000000FL, 16 characters long)
1535ec61f1209adfeff202edbba28339f83
1544f7cc7a4c0c553380874383e93408678
155ca91d95b091956edb162da583b51051b
15633b91ab14b8807348fc98bf223b4b3a5
157```
158Then the 0 left pad would be decreased by one character and the process repeats
159until the whole block is done. However, a flag usually won't be just 16
160characters long. I had some difficulty trying to bruteforce the 17th character
161and 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
163were the result of the message I was sending being in the format of "pad +
164known_flag + brute_single_char + pad" where this only worked for the first
165block. This did not work later because those messages would have the pad bytes
166in the middle of the message, which did not go well.
167
168In the end, I realized that I can check the target block hexstring by sending
169only the padded 0 bytes (or anything else of that length) without other
170characters and then append my known bytes of the flag and a single other character
171to fill that block to brute force that last character.
172
173A visual representation is this:
174
175```
176Block size: 8 characters
177
1787 pad, 0 known
179XXXXXXX?
180XXXXXXXF
181
1826 pad, 1 known
183XXXXXX??
184XXXXXXFL
185
1865 pad, 2 known
187XXXXX???
188XXXXXFLA
189
190...
191
1920 pad, 7 known
193FLAG{5o?
194
1958 known
196FLAG{5om
197```
198This only decrypts the first block, so how I decrypted each additional block was
199by prepending another block of pad characters (blocksize - 1) and repeating the
200process.
201
202```
2037 pad, 7 known
204XXXXXXXFLAG{5om?
205XXXXXXXFLAG{5om3
206
2076 pad, 8 known
208XXXXXXFLAG{5om3?
209XXXXXXFLAG{5om3_
210
211...
212
2135 pad, 26 known
214XXXXXFLAG{5om3_!mp0r74nt_$3cr37?
215XXXXXFLAG{5om3_!mp0r74nt_$3cr37}
216
217...
2180 pad, 31 known
219FLAG{5om3_!mp0r74nt_$3cr37}\0\0?
220FLAG{5om3_!mp0r74nt_$3cr37}\0\0\0
221```
222After some automating help with python, I was able to finally get the flag.
223
224```
225Flag: bctf{1_c4n7_b3l13v3_u_f0und_my_c0d3b00k}
226```
227
228My python file to solve this was:
229```python
230from requests import get
231from requests.utils import quote
232
233# list of characters that will be bruteforced, these are the printable chars
234chars = [chr(i) for i in range(ord(' '), ord('~') + 1)]
235# nul character is also checked because that's the pad character
236chars += '\0'
237
238def 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
243def 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
255totalblocks, bs, pad = calc_padding_for_known()
256
257print(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)
265flag = ""
266curflag = ""
267
268tbs = bs * 2
269
270for 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
300print(flag)
301```