魚脳の池

CTF:Little Twoos

angstromctf 2020 writeup

angstromctf 2020 writeup

チーム Little Twoosとして参戦して、最終的に66位でした
開催時間長いため、普段やらないreversingとmiscなどにもてだして、なんとかメンバーと協力して何問を解くことができた

misc

ws1 (misc 30pts)

recording.pcapng recordingというpcapファイルが渡された。ちょっとスクロールしたら、HTTPのパケットが発見し、クリックしたらflagはそこにあった。flag=xxx

ws2 (misc 80pts)

ws2-1 recording.pcapngの中にスクロールしながら探してみたら、flag.jpgらしき文字列が発見、follow->TCP streamで確認する。 ws2-2 すると一個下にJFIFというJPEGと関わる文字列があり、後にマジックナンバーも一致した。HTTPパケットのフィールドを詳しく見てみると、MIMEの下にJPEG File Interchange Formatというのがありexportしたらflag.jpgがあった ws2-3

ws3 (misc 180pts)

相変わらずpcapが渡されて、見てみたら、どうやらgitのやり取りでした、恐らくgit pushとかのコマンドによる通信だと思う(間違えたらごめん)。そこで、PACKという文字列が目に入り、gitと合わせて検索したらgitのパッケージ的なことが判明、それをexportして、git (repository)の復元作業を行った。git packについて詳しい説明は1こちらの解説を参考にした。

git unpack-objects < xxx.pack #git objectを復元、最終的に
git cat-file -p {hash}     #復元

ws3-1

clam clam clam (misc 70pts)

問題サーバーに接続してみたら、ばーとclamclamみたいな文字列が出てきて、しかもflag形式で出現したから、恐らく大量のclamの中に混じっているじゃないかと推測してとにかく大量に受信してctrl-fで探したら、ヒットした

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}
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}
# coding: utf-8
from pwn import *
io = remote('misc.2020.chall.actf.co',20204)
io.sendline('clamclam')
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)
print io.recv(1000000)

Shifter (misc 160pts)

問題サーバーにつないでみたらどうやらフィボナッチ数列の1-50番目の値分シーザー暗号に打ち込めばOKでしたので、先にフィボナッチの50番目に計算しといて、シーザー暗号で復号化した。

Solve 50 of these epic problems in a row to prove you are a master crypto man like Aplet123!
You'll be given a number n and also a plaintext p.
Caesar shift `p` with the nth Fibonacci number.
n < 50, p is completely uppercase and alphabetic, len(p) < 50
You have 60 seconds!
--------------------
Shift IVSKGTCJETZVRWFRHMNSTPQYHMBXZXHKHI by n=16
:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sympy as sym #sympyのインポート
from pwn import *
from m1z0r3 import *

def Fib(n):
    x = sym.symbols('x',nonnegative=True,integer=True) #変数xの宣言(非負の整数)
    Fib=1/sym.sqrt(5)*(((1+sym.sqrt(5))/2)**(n-1)-((1-sym.sqrt(5))/2)**(n-1)) #一般項の式
    result=Fib.subs(x,n) #xにnを代入
    result=result.evalf() #式を浮動小数点数として評価
    return int(result)

Fiblist=[]
for n in range(1,51):
    Fiblist+=[Fib(n)]

def rotx(x,s):
    r = ''
    for i in s:
        if ord('z') >= ord(i) and ord(i) >= ord('a'):
            r += chr((ord(i)-ord('a')+x)%26+ord('a'))
        elif ord('Z') >= ord(i) and ord(i) >= ord('A'):
            r += chr((ord(i)-ord('A')+x)%26+ord('A'))
        else:
            r += i
    return r

io = remote('misc.2020.chall.actf.co',20300)
print io.recvuntil('--------------------')
for i in range(50):
    io.recvuntil('Shift')
    task = io.recvuntil('\n')
    print task
    n = int(task.split('=')[-1])
    ct = task.split(' ')[1]
    pt = rotx(Fiblist[n],ct)
    print pt
    io.recvuntil(':')
    io.sendline(pt)
io.interactive()

msd (misc 140pts)

LSBのステガノグラフィー方法のMSD(digit)バージョン。 まず問題スクリプトを見てみると、単純に各RGB値の最初の桁にflagのASCII配列をすり替えただけに見えるが、実際1->0の桁落ちや百の位で1/2->3/4/5...に発生するオーバーも考慮しなきゃいけない、だから元の画像が配布されたと思う。解決策としては、エンコードされた画像と元の画像を比較しながら、桁の違いによってそれぞれ対処する感じ。 具体的に: 1. 1,2桁のときかつ桁数一致->最大の位を抽出 2. 3桁かつ元の画像RGBの値は255じゃない->最大の位を抽出、255->とりあえずxで埋める 3. ほかのときは桁落ちなので0で埋める

そしてその出力からactf{のASCII配列9799116102123を検索すればflagがでるはず

from PIL import Image

im = Image.open('output.png')
im2 = Image.open('breathe.jpg')

width, height = im.size



def encode(i, d):
    i = list(str(i))
    i[0] = d

    return int(''.join(i))
    
msb = []

for j in range(height):
    for i in range(width):
        for idx,a in enumerate(im.getpixel((i,j))):
            _n = len(str(a))
            b = im2.getpixel((i,j))[idx]
            _n2 = len(str(b))

            if _n == 2 and _n == _n2:
                msb.append(str(a)[0])
            elif _n == 3 and a == 255:
                if im2.getpixel((i,j))[idx] == 255:
                    msb.append(str(a)[0])
                else:
                    msb.append('x')
            elif _n == 1 and _n == _n2:
                msb.append(str(a)[0])
            else:
                msb.append('0')

print(''.join(msb))        

crypto

Keysar(crypto 40pts)

どうやらkey ANGSTROMCTFを使ってflagを暗号化した->agqr{yue_stdcgciup_padas} key付きのシーザー暗号自分聞いたことないので、key caesarで検索したらそれのonline toolが出てきて打ち込んだら一発に出た

Reasonably Strong Algorithm (crypto 70pts)

n = 126390312099294739294606157407778835887
e = 65537
c = 13612260682947644362892911986815626931

問題分見たらnが小さくて、素因数分解できると思ってfactordb使ってp,qを吐かせて、そのまま復号化のスクリプトを書いた。

from Crypto.Util.number import *

p = 9336949138571181619
q = 13536574980062068373
phi = (p-1) * (q-1)
d = inverse(65537,phi)
ct = 13612260682947644362892911986815626931
n = 126390312099294739294606157407778835887

print long_to_bytes(pow(ct,d,n))

Wacko Images (crypto 90pts)

問題スクリプトとその暗号化された画像が配布された。 見てみると 画像[index](RGB値) * key[index] % 251のような簡易な暗号化でしたので、それの逆算スクリプトを書いて実行したらflagの画像が出た

key = [41, 37, 23]
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
from Crypto.Util.number import *
from PIL import Image
from numpy import *

ct = Image.open('enc.png')

img = array(ct)

key = [41,37,23]
n = 251
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] * inverse(key[i],n) % n
        img[x][y] = pixel

pt = Image.fromarray(img)
pt.save('flag.png')

wacko

Confused Streaming (crypto 100pts)

急に思い出せなくなったのでちゃんと合ってるのか自信がないです...まず問題文を見てみる。注目するのはkeystreamという関数、keyは他の処理で二次元関数の解になる。最後のyield int((ret//1)%2)によって0か1が出力する感じでした。もしここでretは小数だったら全部0になるはず。そこでkeyをできるだけ小さくしてe,dをどう変換してもretを小数になれるkeyを探してサーバーに送ればflag ^ ¥x00*len(flag)のような2進数が降って来ると思う。
ちなみに、自分はa=1 b=4 a=-2にしたはず...

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

one time bad (crypto 100pts)

問題文を見てみると、randomによってp,s,xが生成され、xだけが見せられるような挙動でした。ゴールとしては正しいpを送ることですが、sがわからないとうまくできない。
そこでrandom.seed(int(t))というrandomの初期設定が接続たびに行われて、tもその時のunixtimeであることも判明。そこで接続する時のtimeを記録すれば、randomをサーバーと同じ初期設定ができることによって同じp,s,xが手元に生成することが可能になった。そのpを送ればflagがでると思う、もしできなかったら、unixtimeが多少早かったかもしれないので、微調整すればOKのはず

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
# 省略
t = time.time()
random.seed(int(t))
import random, time
import string
import base64
import os
from pwn import *

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



#base_time = int(time.time())-1000000
#base_time = 1584147600
now_time = int(time.time())-10
io = remote('misc.2020.chall.actf.co',20301)
#io = remote('localhost',10001)
print(io.recvuntil('> '))
io.sendline('2')
recv_x = io.recvline().strip()
print(recv_x)
io.recvuntil('answer: ')

# for t in range(base_time,now_time):

while True:
    random.seed(now_time)
    x,p,k = genSample()
    x_pre = base64.b64encode(x.encode())
    print(x_pre)
    
    if x_pre == recv_x:
        print('bingo')
        print(p)
        io.sendline(p)
        io.interactive()
        break
    else:
        pass
        #print 'wrong'
        # io.sendline(str(10))
        # ans = io.recvline()
        #print base_time
    now_time += 1


RSA-OTP (crypto 210pts)

他のメンバーのアイディアで協力して解けた問題、ここでは自分の理解のもとに書きます、あってなかったらごめんなさい。 まず問題をみてみると、以下の内容がわかる
1. enc(flag)が降ってくる
2. 暗号化関数は普通のRSA
3. decryptがやってくれる
4. ↑の結果はgetrandbits(1)に邪魔される

そこでRSAのLSBが容易に思いつくが、getrandbits(1)によって使えないですが、decrypt(input)のbit数は把握できる。そこで以下の試みをしました(と思う): 1. flagを左シフトできるようにpow(2,e,n)をどんどんかけていく
python for i in range(464): inp = pow(2,x*e,n)*enc(flag) io.sendline(inp) 当たり前のようにxの増加につれてエられた2進数のbit数は増えます

  1. 仮にx1=460,x2=461にして、その中間値をかけて送ったらどうなるか
    python 1020 1019 1020 ... 1020と1019の間に徘徊することがわかりました、ですので
    (flag * a % n).bit_length() => [1019,1020]
    上の式のa部分は二分探索によってある程度絞られる  flagは近似的にlong_to_bytes(pow(2,1019,n)/a)に等しいといえる よってaを二分探索するようなスクリプトを書いてみた
from pwn import *
from Crypto.Util.number import *
from fractions import Fraction

n = 136018504103450744973226909842302068548152091075992057924542109508619184755376768234431340139221594830546350990111376831021784447802637892581966979028826938086172778174904402131356050027973054268478615792292786398076726225353285978936466029682788745325588134172850614459269636474769858467022326624710771957129
e = 0x10001

# Encrypted flag:
ct = 17482644844951175640843255713372869422739097498066773957636359990466096121278949693816080016671592558403643716793132479255285512907247513385850323834210899918531077167485767118313722022095603863840851451191536627814100144146010392752308431038754246815068245448456643024387011488032896209253644172833489422733
io = remote('crypto.2020.chall.actf.co',20600)


# left = pow(2,460,n)
# right = pow(2,461,n)
# twomod = 1

bounds = [Fraction(pow(2,463,n)),Fraction(pow(2,464,n))]
while True:
    # middle = (left + right)//2
    middle = sum(bounds)/2
    middle = middle.numerator / middle.denominator
    # print long_to_bytes(pow(2, 1019, n) / (middle))
    print long_to_bytes(pow(2, 1022, n) / (middle))
    print middle
    if middle == 0:
        print middle 
        break
    io.recvuntil('Enter message to sign: ')
    io.sendline(str(ct * pow(middle,e,n) % n ))
    io.recvline()
    revflag = io.recvline().strip()
    
    print len(revflag)
    
    # if len(revflag) == 1020:
    if len(revflag) == 1023:
        bounds[1] = sum(bounds)/2
        #right = middle
    else:
        bounds[0] = sum(bounds)/2
        #left = middle

これによってflagの58文字分はわかるようになった actf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_w 残りの12文字はメンバーのプロパワーによってguessingできた 最後flagはactf{this_is_not_what_i_meant_when_i_told_you_to_use_rsa_with_padding} もっと良い方法があればぜひ教えてくれたら幸いです。

reversing

普段あんまりreversingやらないので手探り状態で簡単な何問しか解けなかった

Revving up (rev 50pts)

とりあえず実行して、支持に従ってgive flagを入力したら、「Now run the program with a command line argument of "banana" and you'll be done!」ということで、./revving up bananaで実行し、give flag入力したらflagが出た。

root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Now run the program with a command line argument of "banana" and you'll be done!
root@ae548dfbc2cd:/CTF/2020/actf/rev# ./revving_up banana
Congratulations on running the binary!
Now there are a few more things to tend to.
Please type "give flag" (without the quotes).
give flag
Good job!
Well I think it's about time you got the flag!

Windos Opportunity (rev 50pts)

IDAとかで開いたらflagはそこに書いてあった...

Taking Off (rev 70pts)

ghidraデコンパイルしたら、mainは↓のようになった

undefined8 main(int iParm1,long lParm2)

{
  int iVar1;
  undefined8 uVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  uint local_b4;
  uint local_b0;
  uint local_ac;
  int local_a8;
  int local_a4;
  char *local_a0;
  byte local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  puts("So you figured out how to provide input and command line arguments.");
  puts("But can you figure out what input to provide?");
  if (iParm1 == 5) {
    string_to_int(*(undefined8 *)(lParm2 + 8),&local_b4,&local_b4);
    string_to_int(*(undefined8 *)(lParm2 + 0x10),&local_b0,&local_b0);
    string_to_int(*(undefined8 *)(lParm2 + 0x18),&local_ac,&local_ac);
    iVar1 = is_invalid((ulong)local_b4);
    if (iVar1 == 0) {
      iVar1 = is_invalid((ulong)local_b0);
      if (iVar1 == 0) {
        iVar1 = is_invalid((ulong)local_ac);
        if ((iVar1 == 0) && (local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4)) {
          iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken");
          if (iVar1 == 0) {
            puts("Well, you found the arguments, but what\'s the password?");
            fgets((char *)local_98,0x80,stdin);
            local_a0 = strchr((char *)local_98,10);
            if (local_a0 != (char *)0x0) {
              *local_a0 = 0;
            }
            sVar3 = strlen((char *)local_98);
            local_a4 = (int)sVar3;
            local_a8 = 0;
            while (local_a8 <= local_a4) {
              if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8]) {
                puts("I\'m sure it\'s just a typo. Try again.");
                uVar2 = 1;
                goto LAB_00400bc7;
              }
              local_a8 = local_a8 + 1;
            }
            puts("Good job! You\'re ready to move on to bigger and badder rev!");
            print_flag();
            uVar2 = 0;
            goto LAB_00400bc7;
          }
        }
      }
    }
    puts("Don\'t try to guess the arguments, it won\'t work.");
    uVar2 = 1;
  }
  else {
    puts("Make sure you have the correct amount of command line arguments!");
    uVar2 = 1;
  }
LAB_00400bc7:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar2;
}


そこからわかった事としては
1. iParm1 == 5 => 引数は4 ./taking_off a1 a2 a3 a4 2. local_ac + local_b0 * 100 + local_b4 * 10 == 0x3a4a1*10+a2*100+a3 == 932./taking_off 3 9 2 a4 3. iVar1 = strcmp(*(char **)(lParm2 + 0x20),"chicken") => a4 = "chicken" 4. if ((local_98[(long)local_a8] ^ 0x2a) != desired[(long)local_a8])さらなるpasswordが求められ、passwordの文字のそれぞれ0x20とxorとった結果をdesiredと比較してるみたい 最後はdesiredに保存される5a464f4b594f0a4d435c4f0a4c464b4d2aを↑の方法でxorを取るとpasswordはgive me flag\x00と判明最後向こうのサーバーで実行したらflagを取れた

# coding: utf-8
from pwn import *
from Crypto.Util.number import *
des = 0x5a464f4b594f0a4d435c4f0a4c464b4d2a
des = long_to_bytes(des)
print xor(des,chr(0x2a)*len(des))

Autorev,Asseble! (rev 125pts)

ghidraデコンパイルしたらすごい分岐の多いスクリプトが出てきた、自力で読める自信がなく、一か八かで先日インストールしたangrでやってみた。
いくつのチュートリアルを参考して、とりあえず基本のスクリプトを書いた。そこでfindは正解した時表示する関数のアドレスで、avoidは間違えた時表示する関数のアドレスになる

import angr

project = angr.Project('./autorev_assemble',auto_load_libs=False)

state = project.factory.entry_state()
simgr = project.factory.simulation_manager(state)
simgr.explore(find=0x0408953,avoid=0x0408961)
state = simgr.found[0]

print(state.posix.dumps(0))

↑のスクリプトを回して、flagそのまま出た

Patcherman (rev 100pts)

if ((_DAT_00201054 != 0xffffffff) && (_DAT_00201050 == 0x1337beef))この分岐判断の結果によってプログラムがフリーズするかどうかがわかる。
後に_DAT_00201054ptrace(0,1337,4919,1337)の返り値だと判明し成功したら0エラーしたら-1を返す
_DAT_00201050は既存のやつで0x1337beefと違う値が入ってる
そこで↑の条件全部を仮に成立させて、次のスクリプトをそのままコピペして実行したらflagが出た
(solverは汚くてごめん...)

#include <stdio.h>

int dword_601050 = 322420463;
int dword_6010AC = 322420463;
int dword_601054 = 0;
int dword_601060 [] = {0x5cd883ea,0xef6680c0,0xafcc7086,0x7e3ab87f,0x7dc5773a,0x38208035,0x0456c3f5};

int sub_4006FF(int a1, int a2)
{
  return a1 + a2;
}

int sub_400672()
{
  int v0; // ebx
  int v1; // ebx
  int v2; // ebx
  int v3; // eax
  v0 = sub_400657(dword_6010AC, 0);
  v1 = (unsigned int)sub_400657(dword_6010AC, 2) ^ v0;
  v2 = (unsigned int)sub_400657(dword_6010AC, 3) ^ v1;
  v3 = sub_400657(dword_6010AC, 5);
  dword_6010AC = (unsigned int)dword_6010AC >> 1;
  dword_6010AC |= (v2 ^ v3) << 31;
  return (unsigned int)dword_6010AC;
}

int sub_400657(int a1, int a2)
{
  return (a1 >> a2) & 1;
}

int main(){
    int dword_601090 [7];
    for ( int i = 0; i <= 6; ++i )
    {
        int v3 = sub_400672();
        
        dword_601090[i] = sub_4006FF(dword_601060[i] ^ (unsigned int)dword_601054, v3);
    }
    printf("Here have a flag:\n%s\n", dword_601090);
}

Califrobnication (rev 120pts)

ディレクトリにあるflag.txtを↓にあるスクリプトを経てエンコードされた文字列が表示される

  memfrob(local_48,__n);
  strfry(local_48);
  printf("Here\'s your encrypted flag: %s\n",local_48);
  • memfrobはただ文字ごと42とxorを取るだけそんな気にしない
  • strfryはshuffle関数で配列を撹乱する たが、他のメンバーによるとstrfryのrandomstateという初期設定は時間とpid依存であることが判明
    そこでホームディレクトリに以下のpythonスクリプトを置き、問題ディレクトリに入り、pythonスクリプトを実行して、暗号文を獲得する同時にtimeとpidを入手する
import  time
import subprocess
from subprocess import Popen
import os
import signal

cmd = "/problems/2020/califrobnication/./califrobnication"
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
print( "process id = %s" % proc.pid )
out = proc.stdout.readline().strip()
with open('/home/team5469/out-hex.txt','w') as f:
    f.write(out.encode('hex'))
print(time.time())

そこから配布されたc言語ファイルを基づいて、strfryを書き換え、pidとtimeをすり替え,flagのところにstring.printable[:48]を変えて実行

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

// typedef struct random_data {
//         int     *fptr;
//         int     *rptr;
//         int     *state;
//         int     rand_type;
//         int     rand_deg;
//         int     rand_sep;
//         int     *end_ptr;
// } RANDOMD;
// time -= 1
char * strfry_mod (char *string) { static int init; static struct random_data rdata; int t = 1584547877; if (!init) { static char state[32]; rdata.state = NULL; initstate_r (t ^ 7269, state, sizeof (state), &rdata); init = 1; } size_t len = strlen (string); if (len > 0)
    for (size_t i = 0; i < len - 1; ++i)
      {
    int32_t j;
    random_r (&rdata, &j);
    j = j % (len - i) + i;

    char c = string[i];
    string[i] = string[j];
    string[j] = c;
      }

  return string;
}


int main() {
    FILE *f;
    char flag[50];
    f = fopen("flag.txt", "r");
    fread(flag, 50, 1, f);
    strtok(flag, "\n");
    //memfrob(&flag, strlen(flag));
    strfry_mod(&flag);
    printf("Here's your encrypted flag: %s\n", &flag);
}

これでprintable[:48]をどのようにshuffleされるのがわかるようになって、このパターンを基づいて組み直したらflagが出た

# coding: utf-8
from string import printable
ct = '48657265277320796f757220656e6372797074656420666c61673a2043474543574b75754f18124e4e4c1e4c4c484b4b5845441b4e465e44494b1f131e1f511b1a494c755e49451c58434b49'.decode('hex')
ct = ct.split(' ')[-1]
t = ''
for i in range(0, len(ct)):
    t += chr(ord(ct[i]) ^ 42) 
flag = [0 for _ in range(48)]
a = printable[:48]
pattern = 'n9bhL0du7IBH5iKw3l8Gjstvyg2mefJDzA4ECocaqFkx6rp1'
for x,y in zip(pattern,t):
    
    idx = a.index(x)
    flag[idx] = y
    
print ''.join(flag) 

感想

5日開催マジで長すぎて、結構疲れた...また色々他ジャンルの問題に挑戦できてすごくいい経験になった

tags: ctf writeup