アセンブリと戯れた記録

## 前書き

某日, （アセンブリ書きてえ！）と思ったので書いてました. その振り返りと記録です.

## 環境

```console:zsh
> uname -snrmo
Linux archlinux 6.0.5.14.realtime1-2-rt x86_64 GNU/Linux
```

たまたま linux-rt を使ってますが, お気になさらず…

```console:zsh
> nasm -v
NASM version 2.15.05 compiled on Sep 24 2020
```

```console:zsh
> gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/lto-wrapper
Target: x86_64-pc-linux-gnu
Configured with: /build/gcc/src/gcc/configure --enable-languages=c,c++,ada,fortran,go,lto,objc,obj-c++,d --enable-bootstrap --prefix=/usr --libdir=/usr/lib --libexecdir=/usr/lib --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.archlinux.org/ --with-build-config=bootstrap-lto --with-linker-hash-style=gnu --with-system-zlib --enable-__cxa_atexit --enable-cet=auto --enable-checking=release --enable-clocale=gnu --enable-default-pie --enable-default-ssp --enable-gnu-indirect-function --enable-gnu-unique-object --enable-libstdcxx-backtrace --enable-link-serialization=1 --enable-linker-build-id --enable-lto --enable-multilib --enable-plugin --enable-shared --enable-threads=posix --disable-libssp --disable-libstdcxx-pch --disable-werror
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 12.2.0 (GCC)
```

```console:zsh
> lldb -v
lldb version 14.0.6
```

## アセンブリで stdin を stdout へ書き出す

何かのツールを模してやろうとかいうわけじゃなかったので, 特に意味は無いです. 強いて言うなら Hello, World! は飽きたので…というくらい.

### 前提: syscall について

参考: [yamnikov-oleg/calling_conventions.md](https://gist.github.com/yamnikov-oleg/454f48c3c45b735631f2)

| arch/ABI | arg1 | arg2 | arg3 | arg4 | arg5 | arg6 | arg7 | Notes |
| -------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ----- |
| x86_64   | rdi  | rsi  | rdx  | r10  | r8   | r9   | -    |       |

つまりこう:

```c
_return = syscall(rdi)(rdi, rsi, rdx, r10, r8, r9);
```

| number | abi    | name  | entry point |
| ------ | ------ | ----- | ----------- |
| 0      | common | read  | sys_read    |
| 1      | common | write | sys_write   |
| 60     | common | exit  | sys_exit    |

引数などの詳細まで書いてなかったので man を見る. あと clangd が吐く定義を見てる.

#### read

参考: [read(2) — Arch manual pages](https://man.archlinux.org/man/read.2)

```c
ssize_t read(int fd, void *buf, size_t count);
```

#### write

参考: [write(2) — Arch manual pages](https://man.archlinux.org/man/write.2)

```c
ssize_t write(int fd, const void *buf, size_t count);
```

#### exit

参考: [exit(2) — Arch manual pages](https://man.archlinux.org/man/exit.2)

```c
void exit(int status);
```

### 前提: レジスタについて

参考: [CPU Registers x86-64 - OSDev Wiki #General Purpose Registers](https://wiki.osdev.org/CPU_Registers_x86-64#General_Purpose_Registers)

| Monikers (64-bit) | Description                             |
| ----------------- | --------------------------------------- |
| RAX               | Accumulator                             |
| RBX               | Base                                    |
| RCX               | Counter                                 |
| RDX               | Data (commonly extends the A register)  |
| RSI               | Source index for string operations      |
| RDI               | Destination index for string operations |
| RSP               | Stack Pointer                           |
| RBP               | Base Pointer (meant for stack frames)   |
| R8                | General purpose                         |
| R9                | General purpose                         |
| R10               | General purpose                         |
| R11               | General purpose                         |
| R12               | General purpose                         |
| R13               | General purpose                         |
| R14               | General purpose                         |
| R15               | General purpose                         |

## 実装

あとは気合で書く. **書いた**. コメントの関数シグネチャ風記法は Rust っぽく書いた.

```asm
        global  _start
        section .text

; _start()
_start:
        mov  rbp, rsp
        sub  rsp, 32

        lea  rdi, [rbp-32]
        call init

        lea  rdi, [rbp-32]
        call read

        lea  rdi, [rbp-32]
        call write

        call exit

; init(*mut char)
init:
        mov  QWORD [rdi+24], 0
        mov  QWORD [rdi+16], 0
        mov  QWORD [rdi+ 8], 0
        mov  QWORD [rdi+ 0], 0
      ; rbp[0..32] fills 0;

        ret

; read(*const char)
read:
        mov  r15, rdi

        mov     rax, 0
        mov     rdi, 0
        mov     rsi, r15
        mov     rdx, 32
        syscall
      ; read(stdin, r15, 32);

        ret

; write(*mut char)
write:
        mov  r15, rdi

        mov     rax, 1
        mov     rdi, 1
        mov     rsi, r15
        mov     rdx, 32
        syscall
      ; write(stdout, rbp[-32], 32);

        ret

; exit() -> !
exit:
        mov     rax, 60
        mov     rdi, 0
        syscall
      ; exit(0);
```

詳しい命令諸々は [Compiler Explorer](https://godbolt.org/z/6Pqbn1jTn) (※permalink) でも見れば分かる (分からない).

`read`, `write`, `exit` はまんま syscall のラッパをしている. 別に引数を全部とって `unistd.h`
みたいに…という訳ではなく, 単に操作を wrap してるだけ.

そしてこんな感じで実行. mold は [rui314/mold](https://github.com/rui314/mold) です.

```console:zsh
> nasm -f elf64 main.asm -o main.o
> mold main.o -o main
> ./main
```

実行すると改行で flash されるまでの最大 32 バイトを読み込んで吐き出してくれます.

```console:zsh
> ./main
Hi, echo!
Hi, echo!
```

改行も読み込むのでちょっとびっくりした (改行コード忘れの `%` が見当たらなかったから違和感). 32 バイトを超えると単に読み込まなくなる. (多分, 少なくとも吐き出しはしない).

```console:zsh
> ./main
01234567890123456789012345678901234567890
01234567890123456789012345678901%
```

ちゃんと 32 バイト目までしか吐き出されてないですね, `%` は改行されずに終了した痕跡です (改行はコード側ではしていないので).

#### 詰まった所

- 取り敢えず C みたいに return しとけば終わるかな〜とか思ったら終わらなかった (SEGV)
- ret の挙動を分かってなかった (C の吐くアセンブリ見て適当にレジスタ弄ってたら変になっちゃった)
- `mov QWORD PTR [rbp-32], 0` って書き方がまんまだと動かなくて狼狽えた (`QWORD [...]` でした, 参考は忘れた)

## C で入力から任意関数を呼び出させる

要約: 脆弱性を生み出して, 攻撃する (**したかった**).

### 初期実装

まず書いたのがこれ:

```c:attack.c
#include <stdio.h>

int main(void) {
    char s[8] = {};

    scanf("%s", s);

    return 0;
}
```

で, ここから stdin で境界外アクセスを誘発させ, 何かのアドレスを書き換えてやろう！と意気込んだのですが… **あれ, そもそも何を書き換えれば良いか知らないや**.

### 調査

取り敢えず境界外アクセスさせよう.

```console:zsh
> gcc attack.c -o attack
```

```console:zsh
> ./attack
12345678
```

これは大丈夫.

```console:zsh
> ./attack
1234567890123456
*** stack smashing detected ***: terminated
zsh: IOT instruction (core dumped)  ./attack
```

うん, なんか取り敢えずめっちゃ書き込めば怒られる. …でもただ SEGV するだけだな…
じゃあ次に, 取り敢えずバイナリを覗いてみよう.

```console:zsh
> objdump -dM intel attack
attack:     file format elf64-x86-64

# --- snip ---

0000000000001149 <main>:
    1149:   55                      push   rbp
    114a:   48 89 e5                mov    rbp,rsp
    114d:   48 83 ec 10             sub    rsp,0x10
    1151:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
    1158:   00 00
    115a:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
    115e:   31 c0                   xor    eax,eax
    1160:   48 c7 45 f0 00 00 00    mov    QWORD PTR [rbp-0x10],0x0
    1167:   00
    1168:   48 8d 45 f0             lea    rax,[rbp-0x10]
    116c:   48 89 c6                mov    rsi,rax
    116f:   48 8d 05 8e 0e 00 00    lea    rax,[rip+0xe8e]        # 2004 <_IO_stdin_used+0x4>
    1176:   48 89 c7                mov    rdi,rax
    1179:   b8 00 00 00 00          mov    eax,0x0
    117e:   e8 bd fe ff ff          call   1040 <__isoc99_scanf@plt>
    1183:   b8 00 00 00 00          mov    eax,0x0
    1188:   48 8b 55 f8             mov    rdx,QWORD PTR [rbp-0x8]
    118c:   64 48 2b 14 25 28 00    sub    rdx,QWORD PTR fs:0x28
    1193:   00 00
    1195:   74 05                   je     119c <main+0x53>
    1197:   e8 94 fe ff ff          call   1030 <__stack_chk_fail@plt>
    119c:   c9                      leave
    119d:   c3                      ret

# --- snip ---
```

なるほどわからん.

参考:

https://qiita.com/tobira-code/items/75d3034aed8bb9828981

```asm
push   rbp
mov    rbp,rsp
sub    rsp,0x10
mov    rax,QWORD PTR fs:0x28

mov    QWORD PTR [rbp-0x8],rax
xor    eax,eax
mov    QWORD PTR [rbp-0x10],0x0
```

これがまず関数の冒頭の処理. スタックの様子はこんな感じ:

```
data:   [retadr][ %rbp ][canary][<zero>]
       ^ . . . . . . . ^ . . . . . . . ^
rbp: +0x10           -0x00           -0x10
```

上の行が格納されてるデータの模式図, 下の行が `%rbp` (base pointer) からの相対位置を示しています.

`retadr` は何かと言うと, `main` を `call` した際に格納された戻りアドレスのことです. **攻撃にうってつけの場所**.
`%rbp` は `push rbp` で格納された `call` 元の base pointer のこと.
`canary` (呼び名はよく分かってない) は …後述.
`<zero>` は `mov QWORD PTR [rbp-0x10],0x0` によってゼロ埋めされています. 後に解りますがこれは `char s[8]` の領域のことです.

では順を追ってスタックの様子を考えてみましょう.

```
data:  [retadr]
rbp:
rsp:          ^
```

`^` が `%rbp`, `%rsp` の指している番地を表しています.
今, `main` を `call` した直後です. `%rbp` の番地は不明です (多分図よりもっと左側を指しています).

```
data:  [retadr][ %rbp ]
rbp:
rsp:                  ^
```

`%rbp` をスタックに載せます.

```
data:  [retadr][ %rbp ]
rbp:                  ^
rsp:                  ^
```

`%rbp` を `%rsp` で書き換えます.

```
data:  [retadr][ %rbp ][??????????????]
rbp:                  ^
rsp:                                  ^
```

`-0x10` だけ `%rsp` の指す先が移動します.

```
data:  [retadr][ %rbp ][canary][??????]
rbp:                  ^
rsp:                                  ^
```

`fs:0x28` から `canary` を読み出して, スタックの末尾に書き込みます.

```
data:  [retadr][ %rbp ][canary][<zero>]
rbp:                  ^
rsp:                                  ^
```

最後に `char s[8]` をゼロで埋めて, 冒頭の処理は完了です.

```asm
lea    rax,[rbp-0x10]
mov    rsi,rax
lea    rax,[rip+0xe8e]        # 2004 <_IO_stdin_used+0x4>
mov    rdi,rax
mov    eax,0x0
call   1040 <__isoc99_scanf@plt>
mov    eax,0x0
```

次に, `scanf("%s", s)` の処理. ざっくり言うと:

- `scanf(%rdi, %rsi)` と呼び出している
  - `%rdi` は別の場所に格納されている `"%s"` の番地
  - `%rsi` は先にゼロ埋めされた領域, つまり `char s[8]` の先頭番地
  - `call 1040` で `scanf` を呼び出す処理へ繋がる
- `%eax` は帰り値
  - 何も処理してないので全部ゼロ埋めされちゃってる

```
mov    rdx,QWORD PTR [rbp-0x8]
sub    rdx,QWORD PTR fs:0x28

je     119c <main+0x53>
call   1030 <__stack_chk_fail@plt>
leave
ret
```

最後に関数の末尾の処理. スタックの図を再掲.

```
data:   [retadr][ %rbp ][canary][<zero>]
       ^ . . . . . . . ^ . . . . . . . ^
rbp: +0x10           -0x00           -0x10
```

`mov rdx,QWORD PTR [rbp-0x8]` はまず `canary` を `%rdx` に読み出す. そして `sub rdx,QWORD PTR fs:0x28` で先に読み出したはずの `canary` 同士の差を取っている. **…って, え？**

参考:

https://stackoverflow.com/questions/10325713

https://stackoverflow.com/questions/14267081

まず `fs:0x28` について.
`%fs` は前述した General Purpose Registers とは違う, Segment Registers と呼ばれるレジスタです. `:0x28` というのはオフセットを指しているそう.
`%fs`, というか Segment Registers は環境に依って使われ方が違うそうですが, Linux では (恐らく glibc により) `fs:0x28` からバッファオーバーランの保護機構として用いられる `canary` という値が読み出せます.

ではどう使われているかと言うと, 先に示した図の通りローカル変数の領域は `-0x08 .. -0x10` ですが, 例えば `scanf("%s", s)` で 8 文字以上の入力を受け取ったとします (つまりヌル文字を考慮すると 9 文字以上に相等).
するとこの `char s[8]` を超えた書き込みは `canary` の領域に行われます. しかしこれでは, 関数の冒頭で `fs:0x28` から読み出した値と, 関数の末尾で確認する `canary` の値は異なってしまいます.
これは `sub rdx,QWORD PTR fs:0x28` によって `%rdx` に差が取られるので, `0` でないなら値が異なっていることとなります. 注目すべきはこの次にある `je 119c` であり, `je` は `rdx` の値が false に評価できる時にジャンプを行います.
つまりこの場合, `scanf("%s", s)` がバッファオーバーランを起こした場合にのみ `call 1030` に到達することとなります (これは debugger でも確認できます). つまり, `*** stack smashing detected ***` というメッセージはまさにこの呼び出しに到達したことを意味しているのです (多分).

話が逸れましたが, 最後に `leave` と `ret` について. `leave` は関数冒頭で行った `push rbp`, `mov rbp,rsp` とは逆の処理である `mov rsp,rbp`, `pop rbp` を行います.
具体的には, base pointer を呼び出し元のものに戻すということです (ざっくり). 次に `ret` は, `pop rip` を行うので, つまり `retadr` を読み出して, 次に実行する命令の番地を指定していることになります.

図を書き換えながら様子を考えてみると:

```
data:  [retadr][ %rbp ][canary][<zero>]
rbp:                  ^
rsp:                                  ^
```

`^` が `%rbp`, `%rsp` の指している番地を表しています.
今, `leave` の直前の状態です.

```
data:  [retadr][ %rbp ][canary][<zero>]
rbp:                  ^
rsp:                  ^
```

`leave` により, まず `%rsp` が `%rbp` の値で書き換えられます.

```
data:  [retadr][ %rbp ][canary][<zero>]
rbp:
rsp:          ^
```

次に `%rbp` がスタック上の値で書き換えられます (図上では書いていませんが, 多分図の左側のどこかを指しているでしょう).

```
data:  [retadr][ %rbp ][canary][<zero>]
rbp:
rsp:  ^
```

最後に `ret` で `retadr` が `%rip` を上書きします. これにより呼び出し元へ戻ることとなります.

```
data:  [[   before   ]][retadr][ %rbp ][canary][<zero>]
rbp:  ^
rsp:                  ^
```

(上の図は概略的です, `before` が呼び出し元のローカル変数の領域を表しています)
一見良くわからない領域を指しているように感じられるかもしれませんが, つまるところ呼び出し元の関数について, `main` 冒頭の状態と同じようにしていることが分かります. こう処理を追っていくと "スタックに積まれる" っていうのがどういうことかよく分かるなあなんて (分からない？そう…).

### 境界外アクセスの容認

先に述べましたが, `canary` を用いたバッファオーバーラン機構により `ret` に辿り着く前にプロセスが殺されてしまいます. ので, 参考に書いてあるとおり `-fno-stack-protector` を付けて `gcc` でコンパイルをかけます.

```console:zsh
> gcc -fno-stack-protector attack.c -o attack
```

では, バイナリを覗いてみましょう.

```console:zsh
> objdump -dM intel attack
attack:     file format elf64-x86-64

# --- snip ---

0000000000001139 <main>:
    1139:   55                      push   rbp
    113a:   48 89 e5                mov    rbp,rsp
    113d:   48 83 ec 10             sub    rsp,0x10
    1141:   48 c7 45 f8 00 00 00    mov    QWORD PTR [rbp-0x8],0x0
    1148:   00
    1149:   48 8d 45 f8             lea    rax,[rbp-0x8]
    114d:   48 89 c6                mov    rsi,rax
    1150:   48 8d 05 ad 0e 00 00    lea    rax,[rip+0xead]        # 2004 <_IO_stdin_used+0x4>
    1157:   48 89 c7                mov    rdi,rax
    115a:   b8 00 00 00 00          mov    eax,0x0
    115f:   e8 cc fe ff ff          call   1030 <__isoc99_scanf@plt>
    1164:   b8 00 00 00 00          mov    eax,0x0
    1169:   c9                      leave
    116a:   c3                      ret

# --- snip ---
```

なんか処理が小さくなったような気がしますが, まんまさっきの保護機構が消えているだけです. ついでにちょっとスタックの様子も変わっています.

```
data:   [retadr][ %rbp ][<zero>][??????]
       ^ . . . . . . . ^ . . . . . . . ^
rbp: +0x10           -0x00           -0x10
```

なんかしらんけど 0x10 バイトはローカル変数の領域を確保するっぽい. なんで？ (別に良いけど…)
では, 実行してみましょう:

```console:zsh
> ./attack
1234567890123456
zsh: segmentation fault (core dumped)  ./attack
```

無事保護機構を貫通することが出来ました.

※逆に言うと, 保護機構をつけておけば**その関数内のローカル変数で起きたバッファオーバーラン**はすぐに検知することが出来ます. これは `main` に限らずついてくるので, 保護機構は外さず付けておきましょう.

### 簡易実装

stdin から番地を上書きするのは *なんかうまく行かなかった* ので, **コード内で直接書き換えます** (え？).
取り敢えず `main` を呼び出させます.

```c:attack.c
#include <stdio.h>

int main(void) {
    char s[8] = {};

    scanf("%s", s);

    ((void **)s)[2] = main;

    return 0;
}
```

先のスタックの図を見ましょう.

```
data:   [retadr][ %rbp ][<zero>][??????]
       ^ . . . . . . . ^ . . . . . . . ^
rbp: +0x10           -0x00           -0x10
               ^       ^       ^
s:            [2]     [1]     [0]
```

`char *` を `void **` にキャストしているので, ポインタ演算は `sizeof(void *) = 8` バイト単位で行われます.
そして `[2]` を指定しているので, ちょうど `retadr` の位置に `main` の先頭番地を書き込んでいます. つまり:

```
data:   [ main ][ %rbp ][<zero>][??????]
```

こうなりますね. ではコンパイルしたものを実行してみましょう.

```console:zsh
> ./attack
12345678
zsh: segmentation fault (core dumped)  ./attack
```

…

### 妥協実装

まず, 何が起こっているのかを説明しましょう. 普段は起き得ないことですが, まあ変なことをしているのでこういうこともあるのです.
では, 関数一回分の処理でスタックがどのように変移したかを再掲しましょう:

```
data:  [[   before   ]][retadr][ %rbp ][canary][<zero>]
rbp:  ^
rsp:                  ^
```

正常に `main` が終了すれば, 状態は呼び出し元の関数に準じたものに戻るはずです. しかし, 今は `retadr` を書き換えています.

```
data:  [[   before   ]][ main ][ %rbp ][canary][<zero>]
rbp:  ^
rsp:                  ^
```

では, **ここから `main` を始めてみましょう**. まず冒頭の処理をしてみます.

```
data:  [[   before   ]][ %rbp ][<zero>][??????]
rbp:                  ^
rsp:                                  ^
```
さっきより -0x08 分ずれていますね. 比較してみましょう.

```
prev data: [[   before   ]][ main ][ %rbp ][<zero>][??????]
                          ^ . . . . . . . ^ . . . . . . . ^
prev rbp:               +0x10           -0x00           -0x10

new data:  [[   before   ]][ %rbp ][<zero>][??????]
                  ^ . . . . . . . ^ . . . . . . . ^
new rbp:        +0x10           -0x00           -0x10
```

このずれの正体は `call` によって `push` された `retadr` が `pop` されたまま `main` の先頭に**戻って**いることにあります. 本来であれば関数の先頭に来るということは `call` された後のはずですが, 今は `ret` の戻り先で指定されているので, その分 `%rsp` がずれているのです.

<!--
そして debugger で追うと分かりますが, SEGV しているのは `scanf` もとい `__isoc99_scanf` の内部実装である `__vfscanf_internal` の中です. ちなみにここで見れます:

https://github.com/bminor/glibc/blob/7e31d166510ac4adbf53d5e8144c709a37dd8c7a/stdio-common/vfscanf-internal.c#L275

(なんか適当にミラー拾ってきただけです, 悪しからず)
-->

そして, これをなんとなくの考えと気合で解決してみました (冷静に考えたらちょっとおかしいのだけど).

参考:

https://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s5

```c:attack.c
#include <stdio.h>

size_t offset = 0;
int main(void) {
    char s[8] = {};

    asm("sub %0, %%rsp" ::"r"(offset));

    scanf("%s", s);

    ((void **)s)[2] = main;

    offset += 8;
    return 0;
}
```

要は, 呼出回数に応じて `%rsp` をずらしています. これによって `%rsp` の位置は何度 `ret` を繰り返しても同様に維持することが出来ました. **…で？** `%rbp` は変わらず -0x08 分ずれています. これで動くようになるとでも？
コンパイルしたものを実行してみましょう.

```console:zsh
> ./attack
a
b
c
d
e
f
g
h
i
# --- snip ---
```

**なんか動く**. SEGV しない. なんで？
ハックを消して, SEGV している部分を観察してみる.

```
# --- snip ---
    movq   %rdx, -0x6b8(%rbp)
    movl   %ecx, -0x658(%rbp)
    movq   %fs:0x28, %rax
    movq   %rax, -0x38(%rbp)
    xorl   %eax, %eax
    movq   0x17be74(%rip), %rax
    movq   %rsi, -0x5f0(%rbp)
    movq   %fs:(%rax), %rax
    movq   %rax, -0x628(%rbp)
    movq   (%rax), %rax
    movq   %r13, -0x460(%rbp)
    movdqu (%rdx), %xmm1
    movq   %rax, -0x678(%rbp)
    movups %xmm1, -0x5a8(%rbp)
    movq   0x10(%rdx), %rax
    movq   $0x400, -0x458(%rbp)      ; imm = 0x400
    movq   %rax, -0x598(%rbp)
    movl   0xc0(%rdi), %eax
--> movaps %xmm1, -0x600(%rbp)
# --- snip ---
```

この `movaps` が処理されると SEGV しますが, しかしそもそも正常に動作していればこの処理 (関数) は実行されないのです **…？？？？？** じゃあ, これは何で呼び出されてるんだよ？
SEGV, SEGV と言っていますが正確には, debugger 使用時に `signal SIGSEGV: invalid address (fault address: 0x0)` というメッセージが出ているのでこう呼んでいます (余談).
`%rbp` も正常だし, 何なんでしょう？

**まあ, なんか満足したしええか！**

## 他参考

https://www.felixcloutier.com/x86/index.html

## おわりに

アセンブリ楽しい.

皆様も良きアセンブリライフを.