ångstromCTF 2021 - Crypto

ångstromCTF is a CTF event organized by Montgomery Blair High School. This CTF had 5 challenge categories, including cryptography. I managed to solve 11 out of 12 challenges, but I’ll only write up 2 of them here:

  • Substitution (130 points, 140 solves)
  • Oracle of Blair (160 points, 139 solves)

Substitution

nc crypto.2021.chall.actf.co 21601

Author: EvilMuffinHa

Challenge

We’re given a source code and access to a service at crypto.2021.chall.actf.co 21601.

#!/usr/bin/python
from functools import reduce

with open("flag", "r") as f:
    key = [ord(x) for x in f.read().strip()]

def substitute(value):
    return (reduce(lambda x, y: x*value+y, key))%691

print("Enter a number and it will be returned with our super secret synthetic substitution technique")
while True:
    try:
        value = input("> ")
        if value == 'quit':
            quit()
        value = int(value)
        enc = substitute(value)
        print(">> ", end="")
        print(enc)
    except ValueError:
        print("Invalid input. ")

Solution

Our integer input gets used as an argument in the substitute function. The substitute function processes the value using key, where the key values are taken from the flag characters. If we break it down, the substitute function has this equation:

key[0]*x^(n) + key[1]*x^(n-1) + key[2]*x^(n-2) + ... + key[n] mod 691

where $ x $ is the value parameter. To recover all coefficients, we can use Lagrange Polynomial. To use Lagrange, we need to get at least $ n $ pairs of $ x $ and $ y $ (in this case, enc) first, where $ n $ is the flag length. Since we don’t know the flag length, we’ll assume it’s less than 256 characters, so we only need to get 256 pairs of $ x $ and $ y $.

Implementation

from pwn import *

r = remote('crypto.2021.chall.actf.co', 21601)
cts = []

for i in range(256):
    r.sendlineafter('> ', str(i))
    cts.append(int(r.recvline(0).split(' ')[1]))
    print cts
[125, 492, 670, 39, 244, 257, 104, 615, 129, 520, 428, 599, 404, 468, 465, 523, 345, 44, 425, 515, 116, 120, 515, 283, 651, 199, 69, 388, 319, 410, 133, 267, 215, 352, 521, 270, 629, 564, 662, 640, 352, 351, 481, 103, 161, 106, 306, 360, 587, 318, 450, 314, 164, 185, 519, 85, 472, 343, 41, 652, 320, 581, 400, 259, 119, 525, 374, 434, 162, 661, 145, 360, 209, 302, 426, 285, 358, 610, 572, 366, 434, 627, 206, 427, 166, 527, 590, 189, 462, 148, 428, 140, 306, 163, 265, 249, 522, 66, 136, 332, 327, 51, 337, 173, 100, 23, 445, 523, 252, 655, 105, 391, 322, 127, 196, 476, 116, 58, 404, 218, 492, 60, 194, 479, 175, 390, 12, 66, 270, 227, 41, 189, 428, 3, 68, 356, 228, 101, 285, 93, 620, 94, 490, 411, 422, 161, 152, 258, 26, 588, 406, 382, 32, 140, 484, 114, 180, 483, 38, 397, 155, 206, 141, 599, 584, 589, 460, 68, 520, 617, 247, 243, 331, 339, 239, 323, 533, 159, 28, 491, 663, 115, 441, 451, 617, 267, 188, 222, 472, 483, 500, 576, 117, 517, 228, 545, 329, 14, 18, 411, 478, 247, 349, 322, 298, 287, 601, 520, 59, 177, 98, 150, 286, 587, 402, 494, 318, 269, 189, 527, 207, 154, 291, 538, 192, 161, 317, 485, 466, 119, 117, 123, 20, 120, 276, 24, 435, 672, 573, 676, 58, 596, 648, 126, 428, 183, 524, 133, 232, 281, 190, 169, 655, 314, 29, 378]
PRIME = 691
F = FiniteField(PRIME)
P = F['x']

points = [125, 492, 670, 39, 244, 257, 104, 615, 129, 520, 428, 599, 404, 468, 465, 523, 345, 44, 425, 515, 116, 120, 515, 283, 651, 199, 69, 388, 319, 410, 133, 267, 215, 352, 521, 270, 629, 564, 662, 640, 352, 351, 481, 103, 161, 106, 306, 360, 587, 318, 450, 314, 164, 185, 519, 85, 472, 343, 41, 652, 320, 581, 400, 259, 119, 525, 374, 434, 162, 661, 145, 360, 209, 302, 426, 285, 358, 610, 572, 366, 434, 627, 206, 427, 166, 527, 590, 189, 462, 148, 428, 140, 306, 163, 265, 249, 522, 66, 136, 332, 327, 51, 337, 173, 100, 23, 445, 523, 252, 655, 105, 391, 322, 127, 196, 476, 116, 58, 404, 218, 492, 60, 194, 479, 175, 390, 12, 66, 270, 227, 41, 189, 428, 3, 68, 356, 228, 101, 285, 93, 620, 94, 490, 411, 422, 161, 152, 258, 26, 588, 406, 382, 32, 140, 484, 114, 180, 483, 38, 397, 155, 206, 141, 599, 584, 589, 460, 68, 520, 617, 247, 243, 331, 339, 239, 323, 533, 159, 28, 491, 663, 115, 441, 451, 617, 267, 188, 222, 472, 483, 500, 576, 117, 517, 228, 545, 329, 14, 18, 411, 478, 247, 349, 322, 298, 287, 601, 520, 59, 177, 98, 150, 286, 587, 402, 494, 318, 269, 189, 527, 207, 154, 291, 538, 192, 161, 317, 485, 466, 119, 117, 123, 20, 120, 276, 24, 435, 672, 573, 676, 58, 596, 648, 126, 428, 183, 524, 133, 232, 281, 190, 169, 655, 314, 29, 378]
shares = [(F(i), F(c)) for i, c in enumerate(points)]
coefs = P.lagrange_polynomial(shares)
coefs = [chr(x) for x in coefs]

print ''.join(coefs)[::-1]

Flag

actf{polynomials_20a829322766642530cf69}

Oracle of Blair

Not to be confused with the ORACLE of Blair

nc crypto.2021.chall.actf.co 21112

Author: lamchcl

Challenge

We’re given a source code and access to a service at crypto.2021.chall.actf.co 21112.

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os

key = os.urandom(32)
flag = open("flag","rb").read()

while 1:
    try:
        i = bytes.fromhex(input("give input: "))
        if not i:
            break
    except:
        break
    iv = os.urandom(16)
    inp = i.replace(b"{}", flag)
    if len(inp) % 16:
        inp = pad(inp, 16)
    print(
        AES.new(key, AES.MODE_CBC, iv=iv).decrypt(inp).hex()
    )

Solution

The input must be in hex format, and when decoded, it should contain the string {} which the program will replace with the flag. The key and IV are 32 and 16 random bytes respectively. The IV gets regenerated each time the program decrypts an input.

In this case, we can put the {} string anywhere we want, so we can work around the random IV to get specific plaintext blocks.

Source: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)

The idea is to input a payload that has an extra block after the flag block. This way, the plaintext of the extra block will be the result of XORing the intermediate block with the previous block (the flag block). How do we know the intermediate block of the extra block? We can work around it by making the extra block the same as the block before the flag. To make sure the block before the flag isn’t affected by random IV bytes, we need to add a dummy block before it.

But remember, the idea above can only be implemented when we know the flag length. To find the exact flag length, we can try inputting different numbers of characters and observe when the number of plaintext blocks increases.

def panjang_flag():
    for i in range(16):
        pload = '{}' + 'A'*i
        print i, pload, len(kirim(pload))

panjang_flag()
0 {} 32
1 {}A 32
2 {}AA 32
3 {}AAA 32
4 {}AAAA 32
5 {}AAAAA 32
6 {}AAAAAA 32
7 {}AAAAAAA 32
8 {}AAAAAAAA 48
9 {}AAAAAAAAA 48
10 {}AAAAAAAAAA 48
11 {}AAAAAAAAAAA 48
12 {}AAAAAAAAAAAA 48
13 {}AAAAAAAAAAAAA 48
14 {}AAAAAAAAAAAAAA 48
15 {}AAAAAAAAAAAAAAA 48

In this case, the flag length is 32 - 7 = 25 chars, so we need to recover two flag blocks. The method is similar, but for the first flag block, we’ll use the second flag block as the extra block. Here’s the code:

Implementation

from deom import *

r = remote('crypto.2021.chall.actf.co', 21112)

def kirim(pload):
    r.sendlineafter('give input: ', pload.encode('hex'))
    return r.recvline(0).decode('hex')

LEN = 25
print 'LEN', LEN 

# ==================================================

pt = 'A'*32 + '{}' + 'A'*(16 - (LEN % 16)) + 'A'*16 # 5 blocks
ct = kirim(pt)
cts = [ct[i:i+16] for i in range(0, len(ct), 16)]
print len(cts)

# 2nd flag
inter_a = xor(cts[1], 'A'*16)
flag2 = xor(inter_a, cts[4])
print flag2 # ke_ecb_c}AAAAAAA

pt = flag2 + flag2 + '{}' + 'A'*(16 - (LEN % 16)) + flag2 # 5 blocks
ct = kirim(pt)
cts = [ct[i:i+16] for i in range(0, len(ct), 16)]
print len(cts)

# 1st flag
inter_flag2 = xor(cts[1], flag2)
flag1 = xor(inter_flag2, cts[3])
print flag1 # actf{cbc_more_li

Flag

actf{cbc_more_like_ecb_c}