LoginSignup
2
0

More than 3 years have passed since last update.

ångstromCTF writeup

Posted at

å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')

and got a flag !
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}

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0