Post

Writeups for ImaginaryCTF 2024

Some crypto challenge writeups.

Writeups for ImaginaryCTF 2024

Base64

Description

yet another base64 decoding challenge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Util.number import bytes_to_long

q = 64

flag = open("flag.txt", "rb").read()
flag_int = bytes_to_long(flag)

secret_key = []
while flag_int:
    secret_key.append(flag_int % q)
    flag_int //= q

print(f"{secret_key = }")

Output :

1
2
secret_key = [10, 52, 23, 14, 52, 16, 3, 14, 37, 37, 3, 25, 50, 32, 19, 14, 48, 32, 35, 13, 54, 12, 35, 12, 31, 29, 7, 29, 38, 61, 37, 27, 47, 5, 51, 28, 50, 13, 35, 29, 46, 1, 51, 24, 31, 21, 54, 28, 52, 8, 54, 30, 38, 17, 55, 24, 41, 1]

Solution

This is just a base conversion. Reverse from the last element of the array to retrieve the original number.

1
2
3
4
5
6
7
8
secret_key = [10, 52, 23, 14, 52, 16, 3, 14, 37, 37, 3, 25, 50, 32, 19, 14, 48, 32, 35, 13, 54, 12, 35, 12, 31, 29, 7, 29, 38, 61, 37, 27, 47, 5, 51, 28, 50, 13, 35, 29, 46, 1, 51, 24, 31, 21, 54, 28, 52, 8, 54, 30, 38, 17, 55, 24, 41, 1]

secret_key = secret_key[::-1][1:]
from Crypto.Util.number import long_to_bytes
k = 1
for i in secret_key:
    k = 64*k + i
print(long_to_bytes(k))

FLAG : ictf{b4se_c0nv3rs1on_ftw_236680982d9e8449}

Integrity

Description

I think this is how signing works

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import *
from binascii import crc_hqx

p = getPrime(1024)
q = getPrime(1024)

n = p*q
e = 65537
tot = (p-1)*(q-1)
d = pow(e, -1, tot)

flag = bytes_to_long(open("flag.txt", "rb").read())
ct = pow(flag, e, n)

#signature = pow(flag, d, n) # no, im not gonna do that
signature = pow(flag, crc_hqx(long_to_bytes(d), 42), n)

print(f"{n = }")
print(f"{ct = }")
print(f"{signature = }")

Output:

1
2
3
n =         10564138776494961592014999649037456550575382342808603854749436027195501416732462075688995673939606183123561300630136824493064895936898026009104455605012656112227514866064565891419378050994219942479391748895230609700734689313646635542548646360048189895973084184133523557171393285803689091414097848899969143402526024074373298517865298596472709363144493360685098579242747286374667924925824418993057439374115204031395552316508548814416927671149296240291698782267318342722947218349127747750102113632548814928601458613079803549610741586798881477552743114563683288557678332273321812700473448697037721641398720563971130513427
ct =        5685838967285159794461558605064371935808577614537313517284872621759307511347345423871842021807700909863051421914284950799996213898176050217224786145143140975344971261417973880450295037249939267766501584938352751867637557804915469126317036843468486184370942095487311164578774645833237405496719950503828620690989386907444502047313980230616203027489995981547158652987398852111476068995568458186611338656551345081778531948372680570310816660042320141526741353831184185543912246698661338162113076490444675190068440073174561918199812094602565237320537343578057719268260605714741395310334777911253328561527664394607785811735
signature = 1275844821761484983821340844185575393419792337993640612766980471786977428905226540853335720384123385452029977656072418163973282187758615881752669563780394774633730989087558776171213164303749873793794423254467399925071664163215290516803252776553092090878851242467651143197066297392861056333834850421091466941338571527809879833005764896187139966615733057849199417410243212949781433565368562991243818187206912462908282367755241374542822443478131348101833178421826523712810049110209083887706516764828471192354631913614281317137232427617291828563280573927573115346417103439835614082100305586578385614623425362545483289428

Solution

In this challenge, we have to find the plaintext since it is encrypted with a public key. Instead of using the private key to decrypt it, we use the CRC checksum. First, brute force the CRC checksum, as the challenge code uses CRC-16-CCITT, making the checksum brute-forceable by adding the exponent e to the signature modulo equation:

1
2
3
4
5
for i in range(40000):
    if pow(signature,e,n) == pow(ct,i,n):
        print(i)
        break
# output = 30359

After retrive the CRC checksum we can calculate the plain text by solving following system of modulo equations:

[ m^{65537} \equiv ct \ (\text{mod} \ n) ] [ m^{30359} \equiv \text{signature} \ (\text{mod} \ n) ]

FLAG: ictf{oops_i_leaked_some_info}

Tango

Description

Let’s dance!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from Crypto.Cipher import Salsa20
from Crypto.Util.number import bytes_to_long, long_to_bytes
import json
from secrets import token_bytes, token_hex
from zlib import crc32

from secret import FLAG

KEY = token_bytes(32)


def encrypt_command(command):
    if len(command) != 3:
        print('Nuh uh.')
        return
    cipher = Salsa20.new(key=KEY)
    nonce = cipher.nonce
    data = json.dumps({'user': 'user', 'command': command, 'nonce': token_hex(8)}).encode('ascii')
    checksum = long_to_bytes(crc32(data))
    ciphertext = cipher.encrypt(data)
    print('Your encrypted packet is:', (nonce + checksum + ciphertext).hex())


def run_command(packet):
    packet = bytes.fromhex(packet)
    nonce = packet[:8]
    checksum = bytes_to_long(packet[8:12])
    ciphertext = packet[12:]

    try:
        cipher = Salsa20.new(key=KEY, nonce=nonce)
        plaintext = cipher.decrypt(ciphertext)

        if crc32(plaintext) != checksum:
            print('Invalid checksum. Aborting!')
            return

        data = json.loads(plaintext.decode('ascii'))
        user = data.get('user', 'anon')
        command = data.get('command', 'nop')

        if command == 'nop':
            print('...')
        elif command == 'sts':
            if user not in ['user', 'root']:
                print('o_O')
                return
            print('The server is up and running.')
        elif command == 'flag':
            if user != 'root':
                print('You wish :p')
            else:
                print(FLAG)
        else:
            print('Unknown command.')
    except (json.JSONDecodeError, UnicodeDecodeError):
        print('Invalid data. Aborting!')


def menu():
    print('[E]ncrypt a command')
    print('[R]un a command')
    print('[Q]uit')


def main():
    print('Welcome to the Tango server! What would you like to do?')
    while True:
        menu()
        option = input('> ').upper()
        if option == 'E':
            command = input('Your command: ')
            encrypt_command(command)
        elif option == 'R':
            packet = input('Your encrypted packet (hex): ')
            run_command(packet)
        elif option == 'Q':
            exit(0)
        else:
            print('Unknown option:', option)


if __name__ == '__main__':
    main()

Solution

“Salsa20 is a stream cipher that generates a keystream and then XORs it with the ciphertext to generate the corresponding plaintext. In this challenge, we need to bit-flip the ciphertext in order to change the plaintext so it can satisfy the condition to obtain the flag and calculate the checksum of the command we send to the server.

First, encrypt one sample command and retrieve the keystream, then generate the corresponding ciphertext to bit-flip the plaintext.

Then, we need to calculate the checksum of the plaintext. A little trick here is that the server accepts the packet with no nonce key. So, just send {‘user’: ‘user’, ‘command’: command} so we can calculate the CRC32 checksum ourselves:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import json
from Crypto.Util.number import *
from binascii import crc32
k = '8da910e05a5098aaf1b8acae23771f29d7e5f46743288d2d3ad38c7f76b26e6f3ac419777c8ca5d81b5555bd90f7584a8a39b0c3c478a8ad6dcec8946d49b51876edf5cb01f8cb1cdd15de'
k = bytes.fromhex(k)
 
nonce = k[:8]
checksum = k[8:12]
ciphertext = k[12:]


meme = b'{"user": "user", "command": "mem", "nonce": "kkk"}'
lolo = meme[:35]
keystream = bytes(a^b for a,b in zip(lolo,ciphertext[:35]))
check = b'{"user": "root", "command": "flag"}'
print(len(check))
new_ciphertext = bytes(a^b for a,b in zip(keystream,check))
print(bytes(a^b for a,b in zip(new_ciphertext,keystream)))
 
checksum = long_to_bytes(crc32(check))
nonce = nonce
print((nonce + checksum + new_ciphertext).hex())

FLAG = ictf{F0xtr0t_L1m4_4lph4_G0lf}

Solitute

Description

The best thinking has been done in solitude. The worst has been done in turmoil.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import random

def xor(a: bytes, b: bytes):
  out = []
  for m,n in zip(a,b):
    out.append(m^n)
  return bytes(out)

class RNG():
  def __init__(self, size, state=None):
    self.size = size
    self.state = list(range(self.size+2))
    random.shuffle(self.state)
  def next(self):
    idx = self.state.index(self.size)
    self.state.pop(idx)
    self.state.insert((idx+1) % (len(self.state)+1), self.size)
    if self.state[0] == self.size:
      self.state.pop(0)
      self.state.insert(1, self.size)
    idx = self.state.index(self.size+1)
    self.state.pop(idx)
    self.state.insert((idx+1) % (len(self.state)+1), self.size+1)
    if self.state[0] == self.size+1:
      self.state.pop(0)
      self.state.insert(1, self.size+1)
    if self.state[1] == self.size+1:
      self.state.pop(1)
      self.state.insert(2, self.size+1)
    c1 = self.state.index(self.size)
    c2 = self.state.index(self.size+1)
    self.state = self.state[max(c1,c2)+1:] + [self.size if c1<c2 else self.size+1] + self.state[min(c1,c2)+1:max(c1,c2)] + [self.size if c1>c2 else self.size+1] + self.state[:min(c1,c2)]
    count = self.state[-1]
    if count in [self.size,self.size+1]:
      count = self.size
    self.state = self.state[count:-1] + self.state[:count] + self.state[-1:]
    idx = self.state[0]
    if idx in [self.size,self.size+1]:
      idx = self.size
    out = self.state[idx]
    if out in [self.size,self.size+1]:
      out = self.next()
    return out

if __name__ == "__main__":
  flag = open("flag.txt", "rb").read()
  while True:
    i = int(input("got flag? "))
    for _ in range(i):
      rng = RNG(128)
      stream = bytes([rng.next() for _ in range(len(flag))])
      print(xor(flag, stream).hex())

Solution

In this challenge, we have a custom RNG that generates a random keystream and then XORs it with the flag. After analyzing the sample keystreams, I realized that in about 20,000 keystreams, the number that appears most at every index of the keystream is 0. So, just obtain 20,000 ciphertexts and then find the ASCII character that appears the most at each index so we can recover the flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from collections import Counter
from Crypto.Util.number import long_to_bytes

with open("flag1.txt", "r") as f:
    lines = f.readlines()

a1 = [a.strip() for a in lines]
b = [bytes.fromhex(a) for a in a1]
charset = b'QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890!@#$%^&*(_){}'
for idx in range(33):
    posible = []
    count = []




    for i in b:
        byte_value = long_to_bytes(i[idx])
        if byte_value in charset and byte_value not in posible:
            posible.append(byte_value)
            count.append(0)
        if byte_value in posible:
            count[posible.index(byte_value)] += 1

    
    if count:
        max_count_index = count.index(max(count))
        print(posible[max_count_index].decode(),end ="")
    else:
        print("No valid entries found.")

FLAG : ictf{biased_rng_so_sad_6b065f93}

This post is licensed under CC BY 4.0 by the author.

Trending Tags