😭

Alpaca CTF SECCON CTF 13 決勝観戦CTF WriteUp

2025/03/02に公開

こんにちは、TRSUTDOCKのよもぎたです。

CTFはWriteUpを書くまでがCTFです。
というわけで、SECCON13電脳会議で行われたAlpaca CTFさんのSECCON CTF 13 決勝観戦CTFに参加してきました。解けた問題のWriteUpです。

Welcome!

FLAG形式の確認ですね。

Long Flag

出力からフラグを復元してください🐍

import os
from Crypto.Util.number import bytes_to_long

print(bytes_to_long(os.getenv("FLAG").encode()))

出力:

35...

bytes_to_long() メソッドは、Python3.2以上では int.from_bytes(b'P', 'big')置き換えできるそうです

というわけで試してみます。

$ python3
Python 3.12.3 (main, Feb  4 2025, 14:48:35) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> print(int.from_bytes(b'Alpaca{example}','big'))
339698699736349728488459535622366589

訳が分からないのでもっと簡単にしてみます。

>>> print(int.from_bytes(b'A','big'))
65
>>> print(int.from_bytes(b'Al','big'))
16748

10進数の16748は16進数で0x416Cなので、なんとなくわからなくはありません。
試してみると、

>>> hex(int.from_bytes(b'Alpaca{Example}','big'))
'0x416c706163617b4578616d706c657d'

あー。はい。というわけで、

>>> hex(35...)
'0x416c706163617b...7d'

で、FLAGのASCIIコードが分かったので、文字列にして提出して一つ目ゲット!

Cookie

ある条件を満たすとフラグが得られるようです

import Fastify from "fastify";
import fastifyCookie from "@fastify/cookie";

const fastify = Fastify();
fastify.register(fastifyCookie);

fastify.get("/", async (req, reply) => {
  reply.setCookie('admin', 'false', { path: '/', httpOnly: true });
  if (req.cookies.admin === "true")
    reply.header("X-Flag", process.env.FLAG);
  return "can you get the flag?";
});

fastify.listen({ port: process.env.PORT, host: "0.0.0.0" });

curlコマンドは-bオプションでCookieを送信できます

FLAGはヘッダで送信されてくるので、-Iオプションで表示します。

というわけで、

$ curl -I -b 'admin=true' http://34.170.146.252:6407/
HTTP/1.1 200 OK
x-flag: Alpaca{...}
content-type: text/plain; charset=utf-8
set-cookie: admin=false; Path=/; HttpOnly; SameSite=Lax
content-length: 21
Date: Sat, 01 Mar 2025 10:39:24 GMT
Connection: keep-alive
Keep-Alive: timeout=72

Beginner's Flag Printer

フラグを出力するアセンブリです🤖

.LC0:
        .string "Alpaca{%x}\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 539232261
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        leave
        ret

アセンブリわからん、まじなんもわからん。

というわけで、Copilotさんに聞いてみます。

次のアセンブリのLinuxでの実行結果を教えてください

Copilotさんのこたえ

おおお、きたーーー。と思って送信してみると、Wrongでした。
でも、なんとなく何をしているかはつかめました。

539232261を16進数に変換すると 0x20240805なので、Copilotさんの答えを修正してFALGゲットです。

アセンブリ分かるのに10進数と16進数の変換ミスるCopilotさんウケるw

AIさんには敬意を払いましょう。お茶目ですね、位にとどめておきます。

parseInt

a < b && parseInt(a) > parseInt(b) となるような a, b を見つけてください🐟

const rl = require("node:readline").createInterface({
    input: process.stdin,
    output: process.stdout,
});

rl.question("Input a,b: ", input => {
    const [a, b] = input.toString().trim().split(",").map(Number);
    if (a < b && parseInt(a) > parseInt(b))
        console.log(process.env.FLAG);
    else
        console.log(":(");
    rl.close();
});

えーわからん。というわけで、問題プログラムを改造してテストプログラムを書いてみます。

const rl = require("node:readline").createInterface({
    input: process.stdin,
    output: process.stdout,
});

rl.question("Input a,b: ", input => {
    const [a, b] = input.toString().trim().split(",").map(Number);
    console.log(a);
    console.log(b);
    console.log(parseInt(a));
    console.log(parseInt(b));
    if (a < b && parseInt(a) > parseInt(b))
        console.log(process.env.FLAG);
    else
        console.log(":(");
    rl.close();
});

いろいろ試行錯誤して、bにIntで表現できないような数字を渡せばよいことが分かってきました。というわけで、

$ node test.js
Input a,b: 10,999999999999999999999999
10
1e+24
10
1
undefined

です。

1e+24を1にしちゃうのって、よくある仕様なのかな?

play with memory

次のCプログラムが与えられました。

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

void print_flag() {
  char flag[256];
  int fd = open("./flag.txt", O_RDONLY);
  if (fd < 0) { puts("./flag.txt not found"); return; }
  write(1, flag, read(fd, flag, sizeof(flag)));
}

int main() {
  setbuf(stdout, NULL);

  int number = 0;
  printf("input your number!: ");
  scanf("%4s", &number);

  if (number == 12345) {
    print_flag();
  } else {
    printf("number: %d (0x%x)", number, number);
  }
  return 0;
}

number12345になるような文字列を渡せばよいようです。桁数から考えて16進数にして渡せばよさそうです。

>>> hex(12345)
'0x3039'

コンパイルして実行してみます。

$ ./play-weth-memory
input your number!: 3039
number: 959655987 (0x39333033)

16進数をASCIIコードととらえて文字にして渡してやればよさそうです。

$ ./play-weth-memory
input your number!: 09
number: 14640 (0x3930)

あー。まぁここまでくれば、何を渡せばよいかわかります。というわけで、問題サーバに送信してFALGゲットです。

Flag Printer

フラグを出力するアセンブリです🤖

f(char*):
        push    rbp
        mov     rbp, rsp
...中略...

.LC0:
        .string "Alpaca{%s}\n"
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-7], 1197424961
        mov     DWORD PTR [rbp-4], 4672071
        lea     rax, [rbp-7]
        mov     rdi, rax
        call    f(char*)
        lea     rax, [rbp-7]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        mov     eax, 0
        leave
        ret

アセンブリです、というわけでまたCopilotさんに聞いてみました。
Copilotさんによると、f(char*)はROT13変換しているそうです。あとは、変換元の文字列が分かればFALGゲットできそうですが、Copilotさんは聞くたびに違う文字列を言ってきます。あてになりません。

というわけで、自分でプログラムを書いてみます。紙に図も書きます。Grokさんにも聞いてみました。Grokさんもf(char*)はROT13だと言ってきます。でも、どーしても、

        mov     DWORD PTR [rbp-7], 1197424961
        mov     DWORD PTR [rbp-4], 4672071

の結果、メモリにどのような値がどのように格納されるのか、わかりませんでした。

降参です!!(涙)

まぁf(char*)がROT13じゃない可能性もありますが…

これからWriteUp書かれる方のを参考に、勉強したいと思います!

最後までお読みいただきありがとうございました。

TRUSTDOCK テックブログ

Discussion