[📓] Writeup - HSCTF 2023 Crypto

Terbit pada tanggal 11 Juni 2023

Ditulis oleh: AnYujin
HSCTF 2023
HSCTF 2023

HSCTF 2023 Writeup - Crypto


during my spare time on 3th until 8th of june, i worked on a few problems on a running CTF competition named HSCTF 10, i managed to solve a few challenge and in this post i want to show you my solution and thought process while solving the 5 cryptography problem that i managed to solve .

double-trouble [586 solves / 137 points]

in this challenge, we're given a pdf file containing this text :

___________ Salad: a green salad of romaine lettuce and croutons dressed with lemon juice, olive oil,
egg, Worcestershire sauce, anchovies, garlic, Dijon mustard, Parmesan cheese, and black pepper. In its
original form, this salad was prepared and served tableside.<br><br>
Hvwg gvcizr bch ps hcc vofr.
Wb toqh, W kwzz uwjs wh hc mci fwuvh bck!
Hvs tzou wg hvs tczzckwbu:
OmqemdOubtqdeMdqOaax
Vcksjsf, wh wg sbqcrsr gc mci vojs hc rsqcrs wh twfgh!
Pkovovovovo
Fsasapsf, hvs tzou tcfaoh wg tzou{}

i assumed that this is a caesar cipher chall, mainly because the "_____ Salad" part so i oppened up the caesar cipher tool in the dcode web to do the decryption and i got the following text

double trouble 1
but as we can see there's still the encrypted sentence "AycqypAgnfcpqYpcAmmj", i just assumed that this is another caesar cipher and try to decrypt it again using the same tool and i got the following flag.
double trouble 2

really-small-algorithm [563 solves / 144 points]

in this chall, we're provided with 3 number as follow:

n = 4155782502547623093831518113976094054382827573251453061239
e = 65537
c = 2669292279100633236493181205299328973407167118230741040683

because this chall was named "Really-Small-Algorithm", i assummed that this is a RSA challenge, because of the small n or the modulo of the rsa, i go to factordb to factorize the n value, after that i got 2 prime number which strengthen my assumption that this is a RSA problem, these are the 2 value which i named p and q

p = 63208845854086540220230493287
q = 65746849928900354177936765297

after that i try to find the private key usually notated in RSA as d, which fulfill the formula d=inverse(e,phi) here is my final script to get the flag

Solver Script
n = 4155782502547623093831518113976094054382827573251453061239
e = 65537
c = 2669292279100633236493181205299328973407167118230741040683
p=63208845854086540220230493287 
q=65746849928900354177936765297
phi=(p-1)*(q-1)
from Crypto.Util.number import *
d=inverse(e,phi)
m=pow(c,d,n)
print(long_to_bytes(m).decode())

cupcakes [490 solves / 173 points]

this one is a really guessy challenge, we're given the description as follow:

You have to make 100 cupcakes for your upcoming end of year party and you only have 3 hours to do so after oversleeping the night before.
However you are in luck because ShopRite just released their Magic Flour that makes any sort of batter immediately.
In order to get the Magic Flour you have to solve a puzzle where you decode the message "avxmsvyusbyxj" using the key word SIFT. Time is ticking!
Note: wrap the result you get in the flag format in all lowercase, eg. flag{example}

i tried to find clue of the encryption method used in the description of the challenge but came out with nothing, then i try to put the provided message and key in viginere cipher as we have a key value here in the dcode website and i got the flag :D

cupcakes 1

trios [139 solves / 422 points]

in this challenge a encryption source code was provided and also the output of the encryption, here is the source code:

Service Code
import random

chars = [chr(i) for i in range(48, 58)] + [chr(i) for i in range(65, 91)] + [chr(i) for i in range(97, 123)]
alphabet = []

while len(alphabet) < 26:
    run = True
    while run:
        string = ""
        for _ in range(3): string += chars[random.randint(0, len(chars) - 1)]
        if string in alphabet: pass
        else: run = False
    alphabet.append(string)

keyboard_chars = [chr(i) for i in range(97, 123)]

dic = {char: term for char, term in zip(keyboard_chars, alphabet)}

msg = "REDACTED"
encoded = ""

for word in msg:
    for letter in word:
        if letter.lower() in dic.keys():
            encoded += dic[letter.lower()]
        else: encoded += letter

print(encoded)

After reading through the source code i understood that this is actually some kind of substitusion cipher, but rather that doing substitute character by character this one substitutes 1 character with 3 character the same character will be encrypted to the same 3 character. After understanding the encryption scheme, my first approach to map the 3 encrypted character to 1 character, so it will be easier for me to decrypt the message later using substitusion cipher tool online, so i first find the frequencies of every 3 consecutive characters, because in the encryption scheme only letters will be encrypted, i noticed that numbers and punctutation and space character wont be encrypted its easy to differ the punctuation and space character because the encryption will only produce digits and letters but for the digits i think it will be hard, so the frequencies will help me on choosing the correct encrypted text used in the encryption, this is the final script to change the 3 char encryption to 1 char

Solver Script
data="8wnAPR2svyjeRbcAPRRbczwtwDE2svphjIqruZbphjRbc4mL2sv8rvIqruZbBRzuZbAPRzr1Rbc2svphjRbcphj
ZYQHJ8rvzwtIqrRbcHu0InbRbcBRzBRz2svyjeRbctKO8wn4mLRbcJEzphjuZb4mLtKOIqrBRzAPR2svphjyje4zD2svyj
eRbctKOBRzIqruZb8wntKOphjHu02svHu0tKO8wn8wnRbcQHJRbcphjIqrzwtAPR2svtKOphjIqrRbcGawIqruZb8wnIqr
wDERbcBRz2svInbRbcAPR2svphjyje4zD2svyjeRbcAPRuZbphjyjeRbcphjuZb4zDyjewDEIqruZb8wntKOAPRAPRuZbp
hjRbcBRzwDERbcRbcIqruZbQHJBRzuZb2svphjHu0IqrwDERbcphj4mLRbcoZYuZb4zDphjIqrIqrwDERbcuZboZYoZY4z
DQHJQHJRbcphjoZYRbcBRzuZb8wnRbc2svoZYwDEAPRRbcIqrIqrRbcQHJIIqrBRztKOAPRAPRRbcyje2svAPRIqruZbuZ
b4mLphjk7j4zDBRzIqruZbphjRbcyje4zDtKOphjRbc2svzwttKOyjetKOphjS4mLtKOIqr2stRbcQHJAPR2svphjHu0"

asli="8wnAPR2svyje{RbcAPRRbczwtwDE2svphjIqr}. uZbphjRbc 4mL2sv8rv IqruZb BRzuZbAPRzr1Rbc 
2svphj RbcphjoZYQHJ8rvzwtIqrRbcHu0 InbRbcBRzBRz2svyjeRbc, tKO8wn 4mLRbc JEzphjuZb4mL 
tKOIqrBRz APR2svphjyje4zD2svyjeRbc, tKOBRz IqruZb 8wntKOphjHu0 2sv 
Hu0tKO8wn8wnRbcQHJRbcphjIqr zwtAPR2svtKOphjIqrRbcGawIqr uZb8wn IqrwDERbc BRz2svInbRbc 
APR2svphjyje4zD2svyjeRbc APRuZbphjyje RbcphjuZb4zDyjewDE IqruZb 8wntKOAPRAPR uZbphjRbc 
BRzwDERbcRbcIqr uZbQHJ BRzuZb, 2svphjHu0 IqrwDERbcphj 4mLRbc oZYuZb4zDphjIqr IqrwDERbc 
uZboZYoZY4zDQHJQHJRbcphjoZYRbcBRz uZb8wn Rbc2svoZYwDE APRRbcIqrIqrRbcQHJ. IIqrBRz 
tKOAPRAPRRbcyje2svAPR IqruZb uZb4mLphj k7j4zDBRzIqr uZbphjRbc yje4zDtKOphjRbc2sv zwttKOyje 
tKOphj S4mLtKOIqr2stRbcQHJAPR2svphjHu0."
hasil=dict()
chars = [chr(i) for i in range(48, 58)] + [chr(i) for i in range(65, 91)] + [chr(i) for i in range(97, 123)]
for i in range(0,len(data),3):
	temp=data[i:i+3]
	if temp in hasil:
		hasil[temp]+=1
	else:
		hasil[temp]=1
fin=[]
for i in hasil:
	if hasil[i]>1 or i=='zr1' or i=='JEz' or i=='Gaw' or i=='IIq':
		fin.append(i)
keyboard_chars = [chr(i) for i in range(97, 123)]
hasil_akhir=""
i=0

while i < len(asli):
	temp=asli[i:i+3]
	if temp in fin:
		hasil_akhir+=keyboard_chars[fin.index(temp)]
		i+=3
	else:
		hasil_akhir+=asli[i]
		i+=1
print(hasil_akhir)

noticed how the values in "data" and "asli" are different, in "data" i remove the punctuations and spaces to make the process easier and also notice while choosing which character to be put in the "fin" array i only choose the one with frequencies greater than 1 and i manually picked the 3 character with frequency of 1. The script above will produce a new encrypted message which is simpler than the one provided, we now can provide the encrypted text to the online tools to get the decrypted text containing the flag
trios 1
even thought its not perfect we can still read a few sentece clearly and also get the flag.

casino [106 solves / 444 points]

in this challenge we're provided with the code running in the service :

Source code
#!/usr/bin/env python3
import readline
import os
import random
import json
from Crypto.Cipher import AES

FLAG = os.getenv("FLAG")
assert FLAG is not None
key_hex = os.getenv("KEY")
assert key_hex is not None
KEY = bytes.fromhex(key_hex)

money = 0.0
r = random.SystemRandom()

def flag():
	if money < 1000000000:
		print("Not enough money\n")
	else:
		print(FLAG + "\n")

def play():
	global money
	cont = True
	while cont:
		print("Enter bet")
		try:
			bet = int(input("> "))
		except ValueError:
			print("Invalid bet")
		else:
			if bet > 5 or bet < 0:
				print("Invalid bet")
			else:
				winnings = round(bet * r.uniform(-2, 2), 2)
				money += winnings
				print(f"You won {winnings:.2f} coins! You now have {money:.2f} coins\n")
				while True:
					print("""Choose an option:
1. Continue
2. Return to menu""")
					try:
						inp = int(input("> "))
					except ValueError:
						print("Invalid choice")
					else:
						if inp == 1:
							break
						elif inp == 2:
							cont = False
							break
						else:
							print("Invalid choice")

def load():
	global money
	print("Enter token:")
	try:
		nonce, ciphertext = map(bytes.fromhex, input("> ").split("."))
	except ValueError:
		print("Invalid token")
	else:
		cipher = AES.new(KEY, AES.MODE_CTR, nonce=nonce)
		plaintext = cipher.decrypt(ciphertext)
		print(plaintext)
		money = json.loads(plaintext)["money"]

def save():
	cipher = AES.new(KEY, AES.MODE_CTR)
	plaintext = json.dumps({"money": money}).encode("utf-8")
	ciphertext = cipher.encrypt(plaintext)
	print(f"Save token: {cipher.nonce.hex()}.{ciphertext.hex()}")

def main():
	try:
		while True:
			print(
				f"""You have {money:.2f} coins.
Choose an option:
1. Get flag
2. Play
3. Enter save token
4. Quit"""
			)
			try:
				inp = int(input("> "))
			except ValueError:
				print("Invalid choice")
			else:
				if inp == 1:
					flag()
				elif inp == 2:
					play()
				elif inp == 3:
					load()
				elif inp == 4:
					save()
					return
				else:
					print("Invalid choice")
	except (EOFError, KeyboardInterrupt):
		pass

if __name__ == "__main__":
	main()

i'm having fun doing this one challenge, so first of all we know that this is a some kind of casino service in which we can win or lose money randomly, the interesting part of this service is the save token feature, to find out why this is interesting let's find out what this feature do, so first of all it will dump our current money to json data format and then it will encrypt the json data using AES encryption with CTR mode after that we will be provided with the hex values of the nonce used and the encrypted text, let us see what does AES encryption in CTR mode do
aes ctr enc as we can see above, it will use the iv/nonce and the key value to be feed into block cipher encryption to produce an output that later will be xor'ed with the plaintext to produce the ciphertext, this mean that we can actually retrieve the output made from the block cipher encryption to then be used to xor our new plaintext to produce our forged ciphertext based on the property of xor which is

when a xor b = c
then a xor c = b or c xor b = a

the thing hear is i read somewhere about the CTR mode of AES that it will not produce output based on our declared block size for the AES, for example in other AES mode if we declare the block size to be 16, it will produce encrypted text with length multiple of 16, but in CTR mode that was not the case, rather it will produce encrypted text with the same length as the plaintext.
So now after knowing that fact, i think about another way of manipulating the plaintext, and i noticed something, we can actually change the period symbol to a value for example 9 and the number will increase by 3 more digits as we change the period into a number digits, and then we can bet it again to produce another period with 2 decimal value behind the period symbol and do the process over and over till we reach the money needed to get the flag, so i made a script which will forge a new ciphertext based on our current money and nonce by changing all number digits including the period symbol into the value "9" then we can load it in the service.

Solver Script
import random
import json
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.number import *
from Crypto.Util.Padding import pad
import binascii
#e2b1e41595d77874.1122f2df8e5906619d84b3b799ce 0.00
#cee4f808c70c2378.770a2e88379125d4635572a1a35eb83dd484ecd45ef38d 999999992.75
nonce="e2b1e41595d77874"
enc="1122f2df8e5906619d84b3b799ce"
duit=0.0
while True:
	f=remote('casino.hsctf.com','1337')
	nonce=int(nonce,16)
	enc=int(enc,16)
	nonce2=nonce+1
	nonce2=long_to_bytes(nonce2)
	nonce=long_to_bytes(nonce)
	
	enc=long_to_bytes(enc)

	plaintext=json.dumps({"money":	duit}).encode('utf-8')
	print(plaintext)
	panjang_duit=len(plaintext)-11
	key=xor(plaintext,enc)
	save=panjang_duit*"9"
	money=int(save)
	new_pt=json.dumps({"money":money}).encode('utf-8')
	print(new_pt)
	new_pt=xor(new_pt,key)
	print(f"{nonce.hex()}.{new_pt.hex()}")
	payload=f"{nonce.hex()}.{new_pt.hex()}".encode()
	f.sendlineafter(b'> ',b'3')

	f.sendlineafter(b'> ',payload)
	f.sendlineafter(b'> ',b'2')
	f.sendlineafter(b'> ',b'4')
	print(f.readline())
	f.sendlineafter(b'> ',b'2')
	f.sendlineafter(b'> ',b'4')
	print(f.readline())
	break
	f.close()

after we have reach the money needed for the flag, we can just ask for the flag in the service.

Made with♥️by CCUG Core Team.