#ångstromCTF writeup
ångstromCTF was held from March 13 to March 18,2020. We(team:KUDoS) are 107th out of 1782 teams getting 2505 points. I solved mainly crypto and got 470 points out of it.
(note: This is my first English write up, so I might make a mistake. please understand it and correct it if you can)
##Keysar (40pt)
Hey! My friend sent me a message... He said encrypted it with the key ANGSTROMCTF.
He mumbled what cipher he used, but I think I have a clue.
Gotta go though, I have history homework!!
agqr{yue_stdcgciup_padas}
Firstly, I tried Vigenere cipher, but it didn't work. After that, when i looked to the title, it rang a bell. it's keyed caesar!(shown as hint later). I googled it and got a flag
actf{yum_delicious_salad}
##Reasonably Strong Algorithm (70pt)
n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931
n is small enough to factorize here and found n = 9336949138571181619 * 13536574980062068373
that's p and q respectively and culculated totient (p-1 * q-1). Using this, got a decrypt key d and decoded the cipher as following
import gmpy2
n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931
p = 9336949138571181619
q = 13536574980062068373
totient = (p-1)*(q-1)
d = gmpy2.invert(e,totient)
m = pow(c,d,n)
s = bin(m)[2:].zfill(len(bin(m)[2:]) + (8 - len(bin(m)[2:])%8))
ans = ""
for i in range(0,len(s),8):
ans += chr(int(s[i:i+8],2))
print(ans)
actf{10minutes}
##Wacko Images (90pt)
How to make hiding stuff a e s t h e t i c? And can you make it normal again? enc.png image-encryption.py
The flag is actf{x#xx#xx_xx#xxx} where x represents any lowercase letter and # represents any one digit number
I got a encrypted image file and code below.
from numpy import *
from PIL import Image
flag = Image.open(r"flag.png")
img = array(flag)
key = [41, 37, 23]
a, b, c = img.shape
for x in range (0, a):
for y in range (0, b):
pixel = img[x, y]
for i in range(0,3):
pixel[i] = pixel[i] * key[i] % 251
img[x][y] = pixel
enc = Image.fromarray(img)
enc.save('enc.png')
In summary, All the brightness value of red, green, blue in the original picture are multiplied by [41,37, 23] and result in the remainder divided by 251
Firstly, i researched the remainder of dividing from 0 to 251 by 251 each color. As I expected, each remainder is independent! so I made a list each color. Based on the encrypted image, let the brightness value of each color to be the index of the remainder corresponded to the brightness value. (hard to explain... ;( )
I implemented this with the code below.
s_41 = [0, 41, 82, 123, 164, 205, 246, 36, 77, 118, 159, 200, 241, 31, 72, 113, 154, 195, 236, 26, 67, 108, 149, 190, 231, 21, 62, 103, 144, 185, 226, 16, 57, 98, 139, 180, 221, 11, 52, 93, 134, 175, 216, 6, 47, 88, 129, 170, 211, 1, 42, 83, 124, 165, 206, 247, 37, 78, 119, 160, 201, 242, 32, 73, 114, 155, 196, 237, 27, 68, 109, 150, 191, 232, 22, 63, 104, 145, 186, 227, 17, 58, 99, 140, 181, 222, 12, 53, 94, 135, 176, 217, 7, 48, 89, 130, 171, 212, 2, 43, 84, 125, 166, 207, 248, 38, 79, 120, 161, 202, 243, 33, 74, 115, 156, 197, 238, 28, 69, 110, 151, 192, 233, 23, 64, 105, 146, 187, 228, 18, 59, 100, 141, 182, 223, 13, 54, 95, 136, 177, 218, 8, 49, 90, 131, 172, 213, 3, 44, 85, 126, 167, 208, 249, 39, 80, 121, 162, 203, 244, 34, 75, 116, 157, 198, 239, 29, 70, 111, 152, 193, 234, 24, 65, 106, 147, 188, 229, 19, 60, 101, 142, 183, 224, 14, 55, 96, 137, 178, 219, 9, 50, 91, 132, 173, 214, 4, 45, 86, 127, 168, 209, 250, 40, 81, 122, 163, 204, 245, 35, 76, 117, 158, 199, 240, 30, 71, 112, 153, 194, 235, 25, 66, 107, 148, 189, 230, 20, 61, 102, 143, 184, 225, 15, 56, 97, 138, 179, 220, 10, 51, 92, 133, 174, 215, 5, 46, 87, 128, 169, 210]
s_37 = [0, 37, 74, 111, 148, 185, 222, 8, 45, 82, 119, 156, 193, 230, 16, 53, 90, 127, 164, 201, 238, 24, 61, 98, 135, 172, 209, 246, 32, 69, 106, 143, 180, 217, 3, 40, 77, 114, 151, 188, 225, 11, 48, 85, 122, 159, 196, 233, 19, 56, 93, 130, 167, 204, 241, 27, 64, 101, 138, 175, 212, 249, 35, 72, 109, 146, 183, 220, 6, 43, 80, 117, 154, 191, 228, 14, 51, 88, 125, 162, 199, 236, 22, 59, 96, 133, 170, 207, 244, 30, 67, 104, 141, 178, 215, 1, 38, 75, 112, 149, 186, 223, 9, 46, 83, 120, 157, 194, 231, 17, 54, 91, 128, 165, 202, 239, 25, 62, 99, 136, 173, 210, 247, 33, 70, 107, 144, 181, 218, 4, 41, 78, 115, 152, 189, 226, 12, 49, 86, 123, 160, 197, 234, 20, 57, 94, 131, 168, 205, 242, 28, 65, 102, 139, 176, 213, 250, 36, 73, 110, 147, 184, 221, 7, 44, 81, 118, 155, 192, 229, 15, 52, 89, 126, 163, 200, 237, 23, 60, 97, 134, 171, 208, 245, 31, 68, 105, 142, 179, 216, 2, 39, 76, 113, 150, 187, 224, 10, 47, 84, 121, 158, 195, 232, 18, 55, 92, 129, 166, 203, 240, 26, 63, 100, 137, 174, 211, 248, 34, 71, 108, 145, 182, 219, 5, 42, 79, 116, 153, 190, 227, 13, 50, 87, 124, 161, 198, 235, 21, 58, 95, 132, 169, 206, 243, 29, 66, 103, 140, 177, 214]
s_23 = [0, 23, 46, 69, 92, 115, 138, 161, 184, 207, 230, 2, 25, 48, 71, 94, 117, 140, 163, 186, 209, 232, 4, 27, 50, 73, 96, 119, 142, 165, 188, 211, 234, 6, 29, 52, 75, 98, 121, 144, 167, 190, 213, 236, 8, 31, 54, 77, 100, 123, 146, 169, 192, 215, 238, 10, 33, 56, 79, 102, 125, 148, 171, 194, 217, 240, 12, 35, 58, 81, 104, 127, 150, 173, 196, 219, 242, 14, 37, 60, 83, 106, 129, 152, 175, 198, 221, 244, 16, 39, 62, 85, 108, 131, 154, 177, 200, 223, 246, 18, 41, 64, 87, 110, 133, 156, 179, 202, 225, 248, 20, 43, 66, 89, 112, 135, 158, 181, 204, 227, 250, 22, 45, 68, 91, 114, 137, 160, 183, 206, 229, 1, 24, 47, 70, 93, 116, 139, 162, 185, 208, 231, 3, 26, 49, 72, 95, 118, 141, 164, 187, 210, 233, 5, 28, 51, 74, 97, 120, 143, 166, 189, 212, 235, 7, 30, 53, 76, 99, 122, 145, 168, 191, 214, 237, 9, 32, 55, 78, 101, 124, 147, 170, 193, 216, 239, 11, 34, 57, 80, 103, 126, 149, 172, 195, 218, 241, 13, 36, 59, 82, 105, 128, 151, 174, 197, 220, 243, 15, 38, 61, 84, 107, 130, 153, 176, 199, 222, 245, 17, 40, 63, 86, 109, 132, 155, 178, 201, 224, 247, 19, 42, 65, 88, 111, 134, 157, 180, 203, 226, 249, 21, 44, 67, 90, 113, 136, 159, 182, 205, 228]
from numpy import *
from PIL import Image
enc = Image.open(r"enc.png")
img = array(enc)
a, b, c = img.shape
for x in range (0, a):
for y in range (0, b):
pixel = img[x, y]
for i in range(0,3):
if i ==0:
pixel[i] = s_41.index(pixel[i])
if i ==1:
pixel[i] = s_37.index(pixel[i])
if i ==2:
pixel[i] = s_23.index(pixel[i])
img[x][y] = pixel
flag = Image.fromarray(img)
flag.save('flag.png')
##Confused Streaming (100pt)
I made a stream cipher!
nc crypto.2020.chall.actf.co 20601
I was given a code as below
from __future__ import print_function
import random,os,sys,binascii
from decimal import *
try:
input = raw_input
except:
pass
getcontext().prec = 1000
def keystream(key):
random.seed(int(os.environ["seed"]))
e = random.randint(100,1000)
while 1:
d = random.randint(1,100)
ret = Decimal('0.'+str(key ** e).split('.')[-1])
for i in range(d):
ret*=2
yield int((ret//1)%2)
e+=1
try:
a = int(input("a: "))
b = int(input("b: "))
c = int(input("c: "))
# remove those pesky imaginary numbers, rationals, zeroes, integers, big numbers, etc
if b*b < 4*a*c or a==0 or b==0 or c==0 or Decimal(b*b-4*a*c).sqrt().to_integral_value()**2==b*b-4*a*c or abs(a)>1000 or abs(b)>1000 or abs(c)>1000:
raise Exception()
key = (Decimal(b*b-4*a*c).sqrt() - Decimal(b))/Decimal(a*2)
except:
print("bad key")
else:
flag = binascii.hexlify(os.environ["flag"].encode())
flag = bin(int(flag,16))[2:].zfill(len(flag)*4)
ret = ""
k = keystream(key)
for i in flag:
ret += str(next(k)^int(i))
print(ret
'key' equals to a solution that satisfies the equation ax^2 + bx + c = 0 (x is not imaginary number and is not integer , a,b,c are not 0 and within -1000 to 1000)
After culculated key**e (e:100~1000) , retrieved the digits after decimal point (that's ret), then ret turn to be ret*(2**d) (d:1~100)
the flag strings are encrypted to int((ret//1) %2)^(bit of flag)
number d is too small compared to number e ,so if key is under 0.5, regardless of number e,d, ret is always under 1.0 which means int((ret//1)%2) equals to 0. I let a,b,c to be 1,10,2 (key is about 0.2) and got flag's binary. It was decoded as below.
s = "01100001011000110111010001100110011110110110010001101111011101110110111001011111011101000110111101011111011101000110100001100101010111110110010001100101011000110110100101101101011000010110110001111101"
ans = ""
for i in range(0,len(s),8):
ans += chr(int(s[i:i+8],2))
print(ans)
actf{down_to_the_decimal}
##one time bad (100pt)
My super secure service is available now!
Heck, even with the source, I bet you won't figure it out.
nc misc.2020.chall.actf.co 20301
Cautions! This is dirty solution!
The problem code is below
import random, time
import string
import base64
import os
def otp(a, b):
r = ""
for i, j in zip(a, b):
r += chr(ord(i) ^ ord(j))
return r
def genSample():
p = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(random.randint(1, 30))])
k = ''.join([string.ascii_letters[random.randint(0, len(string.ascii_letters)-1)] for _ in range(len(p))])
x = otp(p, k)
return x, p, k
random.seed(int(time.time()))
print("Welcome to my one time pad service!\nIt's so unbreakable that *if* you do manage to decrypt my text, I'll give you a flag!")
print("You will be given the ciphertext and key for samples, and the ciphertext for when you try to decrypt. All will be given in base 64, but when you enter your answer, give it in ASCII.")
print("Enter:")
print("\t1) Request sample")
print("\t2) Try your luck at decrypting something!")
while True:
choice = int(input("> "))
if choice == 1:
x, p, k = genSample()
print(base64.b64encode(x.encode()).decode(), "with key", base64.b64encode(k.encode()).decode())
elif choice == 2:
x, p, k = genSample()
print(base64.b64encode(x.encode()).decode())
a = input("Your answer: ").strip()
if a == p:
print(os.environ.get("FLAG"))
break
else:
print("Wrong! The correct answer was", p, "with key", k)
In short, we have to send a byte p that is culculated by x^k. x is given, so if i found random k, got a flag.
the k's length is from 1 to 30 and the number of character is approx 50 which means the k would be the same letter with a probability of 1/1500
I just solved by brute-force
from pwn import *
random_binary = ""
p = remote("misc.2020.chall.actf.co", 20301)
mes = p.recv(1024)
for i in range(2000):
sleep(0.5)
print("now at",i)
p.sendline("2")
mes = p.recvuntil(b"Your answer: ")
p.sendline("a")
mes = p.recv(1024)
if b"Wrong" in mes:
continue
else:
print(mes)
break
actf{one_time_pad_more_like_i_dont_like_crypto-1982309}
##clam clam clam (70pt)
clam clam clam clam clam clam clam clam clam nc misc.2020.chall.actf.co 20204 clam clam clam clam clam clam
When i connected to the server, i received a message in a row.
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
clam{clam_clam_clam_clam_clam}
malc{malc_malc_malc_malc_malc}
.
.
.
.
the hint said "U+000D", so i googled it, finding that it means 'CARRIAGE RETURN'. I thought CARRIAGE RETURN was mixed with \n (break line)
I wrote a code below
from pwn import *
p = remote("misc.2020.chall.actf.co",20204)
mes = p.recvuntil(b"\x0d")
print(mes)
I got a message 'type "clamclam" for salvation', so I modified the code
from pwn import *
p = remote("misc.2020.chall.actf.co",20204)
mes = p.recvuntil(b"\x0d")
p.sendline(b"clamclam")
p.interactive()
actf{cl4m_is_my_f4v0rite_ctfer_in_th3_w0rld}