カーネルを読む

  • Linux kernel 4.14.40
    • 744.1MB
  • NetBSD 7.1.2
    • 250MBぐらい

システムコール関係

Linux: arch/x86

  • $ ag syscall してみる – include/asm/sys_ia32.h — * sys_ia32.h - Linux ia32 syscall interfaces – include/asm/syscall.h — syscall_get_return_value —- raxを返すだけ — syscall_set_return_value(struct task_struct task, — syscall_get_arguments(struct task_struct *task, — syscall_set_arguments(struct task_struct *task, struct pt_regs *regs, unsigned int i, unsigned int n, const unsigned long *args) —- iに対応するレジスタ(rdi, rsi, rdx, …)にargsの要素をn個順にset – um/sys_call_table_32.c — * System call table for UML/i386, copied from arch/x86/kernel/syscall_.c – kernel/cpu/common.c — void syscall_init(void) – include/asm/syscalls.h — * syscalls.h - Linux syscall interfaces (arch-specific) – entry/syscalls/syscall_32.tbl — 9:0 i386 restart_syscall sys_restart_syscall
  • vsyscallとvDSO – システムコールをするときにkernel modeとuser modeを切り替える手間 – vsyscall: システムコールに対応する関数をユーザー領域にマップして高速化する — entry/vsyscall/vsyscall_64.c map_vsyscall() で VSYSCALL_PAGE (0xffffffffff600000から) に関数の入った__vsyscall_page(4KB)をマップ — マップできるシステムコールが限られている —- entry/vsyscall/vsyscall/vsyscall_emu_64.S によると gettimeofday, time, getcpu — アドレスが固定で危ないので、エミュレーションが使える —- エミュレーション→VSYSCALL_PAGEをread-onlyにして(map_vsyscall())、実行されたときのpage faultからemulate_vsyscall()を呼び出す(mm/fault.c) —- (vsyscall)_nrはnumberという意味らしい – vDSO: clock_gettime, gettimeofday, getcpu, time システムコールに対応する関数をプロセスの領域に動的に配置して高速化する — vdso_imageというELFを作って配置してるっぽい — アドレスはランダム — 通常のシステムコールをsysenter/syscall (ない場合はint 0x80; 自動的に判断してくれる) で呼び出す __kernel_vsyscall もここにある(glibcが使っている)
  • アプリケーション側からのシステムコール呼び出し – pt_regs構造体 – entry/entry_64.S → entry_ ‘‘SYSCALL’’ 64 — AT&T読みづらい… — PUSH_AND_CLEAR_REGS (macro)でスタックを退避してから call do_syscall_64 — POP_REGSで戻す — SYSRETとIRETを使い分けてる – entry/entry_32.S→entry ‘‘INT80’’ 32 — 第6引数→ebp — call do_int80_syscall_32 – entry/entry_32.S→entry ‘‘SYSENTER’’ _32 — 第6引数→0(%ebp) (ebpはstackを指している) — call do_fast_syscall_32 になるぐらいで、他は同じ – entry/common.c — do_fast_syscall_32() — do_syscall_64() —- enter_from_user_mode(), local_irq_enable() どこ? —- likely/unlikely マクロ → __builtin_expect というGCCの機能で条件分岐を高速化 —- sys_call_tableはsys_call_table_64.c → entry/syscalls/Makefileで生成されるsyscalls_64.hに内容が(__SYSCALL_64マクロの羅列) —- デフォルト値はsys_ni_syscall (ni = not implemented?) —- 呼び出した後 —– local_irq_disable() —– prepare_exit_to_usermode() → user_enter_irqoff() → kernel/context_tracking.c context_tracking_enter(CONTEXT_USER) でuser modeに切り替え – 実際のシステムコールの実装はこんな感じで見つかる {code} sei0o@sei0o-opensuse ~/D/S/linux-4.14.40> rg ‘SYSCALL_DEFINE\d(read’ fs/read_write.c 566:SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count) {/code} – IRQ: Interrupt Request – IRET? context tracking? MSR?

{code} mov edx, len mov ecx, msg mov ebx, 1 mov eax, 4 int 0x80 {/code}

{code} mov rdx, len mov rsi, msg mov rdi, 1 mov rax, 1 syscall {/code}

  • kvm?
  • xen?
  • seccomp?

*** Linux: arch/arm

  • kernel/にいろいろ入ってごっちゃになっている
  • mach-*/ディレクトリがたくさん – mach-sunxi/ は Allwinner という中国のメーカが出している sunxi ブランド – mach-rockchip/ も中国のメーカ MP4/3デコーダに使われたりしているらしい – tools/mach-types
  • plat-*/ ディレクトリもいくつか → platform? – plat-samsung/ は SAMSUNG やろなあ — 関数名にsamsung入ってるのが個人的になんか変わってておもしろい — mach-s3c24xx と mach-s3c64xx と関係があるっぽい — devs-uart.cとか見ると、組み込みを感じる(組み込みわからないけれど —- UART触ってみたいなあ
  • kernel/entry-common.S – ENTRY(vector_swi) でSWI(ソフトウェア割り込み)を処理している — eABI→SWIの引数が0、システムコールの番号はr7 (=scno)に入れる — oABI→ (SWIの引数 & 0xff000000) がシステムコールの番号
  • include/asm/syscall.h – syscall_get_return_value — r0を返すだけ – syscall_set_return_value(struct task_struct *task, – syscall_get_arguments(struct task_struct *task, – syscall_set_arguments(struct task_struct *task, struct pt_regs *regs, unsigned int i, unsigned int n, const unsigned long *args) — (vs x86) switch-caseではなくてmemcpyするアドレスをiの値だけずらしてset/get – #define SYSCALL_MAX_ARGS 7 → システムコールに使えるレジスタは7個 — r0〜r6
  • include/uapi/asm/ptrace.h – pt_regsに入っているレジスタの名前と番号の対応が載っている
  • vDSO – kernel/vdso.c vdso_initも健在
  • eABIはoABIの10倍浮動小数点の処理が速いABI

*** NetBSD: x86

  • Linuxに比べて – ファイルが少ない、もしくは整理されている – アセンブラで記述された部分が少ない – アーキテクチャ間でシステムコール番号が同じ — sys/kern/syscalls.masterに一覧が — Linuxなどのエミュレーション用のsyscalls.masterもある — Linuxではそれぞれのアーキテクチャの本家OS(?)のシステムコール番号に合わせている、これは本家OSのバイナリをそのまま動かしたい意図があるのかも —- だからELFロードするときにOS ABIをチェックしていないのでは?←北海道回で話した – Linuxでのlikely/unlikelyマクロと同様に__predict_true/falseマクロを利用している

  • sys/arch/i386以下 – freebsd, linux, svr4, ibcs2 _syscall.c →エミュレーション – locore.SのIDTVEC(syscall): call *P_MD_SYSCALL(%edx)でシステムコール(p_md.md_syscall)を呼び出している (edx = trapframeへのポインタ) — このIDTVEC(syscall)はi386/machdep.cでextern宣言されていて、あとからsetgate()→(interrupt/trip) gate descriptorを作ってidt128に格納 — IDTはlidt命令によって保存される、arch/i386/i386.cにlidtという同名のルーチンが用意されていて、machdep.cのcpu_init_idt()から呼ばれる — ここにあえてidtをすべて0にした不正なデータを入れるとcpu resetを引き起こせるみたい(machdep.c cpu_reset()) — IDT: Interrupt Descriptor Table

{code}

  • movl L_PROC(%edi),%edx
  • movl %esp,L_MD_REGS(%edi) # save pointer to frame
  • pushl %esp
  • call *P_MD_SYSCALL(%edx) # get pointer to syscall() function {/code}

— AST: Asynchronous System Traps = SWIみたいなもの

  • sys/arch/amd64
  • sys/arch/x86 – さらにx86ディレクトリが下にある – x86/syscall.c — syscall_intern(): p->p_md.md_syscall = syscall; (locore.Sから呼ばれるmd_syscallに定義した関数をセット) — syscall() —- trapframe *frameから読み出している —- ただx86_64での引数の順番がrdi, rsi, rdx, ‘‘r10’’, r8, r9だと書かれている←syscallがrcxを書き換えるからlibc stubというところで退避している —- sy_invoke(callp (=p->p_emul->e_sysent + code), l, args, rval, code)を呼び出して実行。あとでrax=rval0, rdx=rval1となっている。2つの返り値を持つsyscallがあるみたい —- エラーとしてsy_invoke()がERESTART, EJUSTRETURNを返すのでそれに応じた処理をする、それ以外が返ってくれば普通にraxにエラー番号を格納
  • sys/arch/usermode以下 – 仮想化機能らしい — http://www.13thmonkey.org/documentation/NetBSD/EuroBSD2012-NetBSD_usermode-paper.pdf – syscall.c: syscall_args_print() など – copy.c — copyin()の中身はmemcpy, 引数のdestとsrcが入れ替わっていてややこしい — copyout()もある — 引数名がkaddr, uaddr→kernel領域とuser領域のことで、copyinがkernel領域への書き込み・copyoutがuser領域への書き出しっぽい
  • sys/arch/usermode/target以下 – cpu_arm.c, cpu_i386.c – CPUの差異を吸収できるように共通の関数にまとめられている
  • sys/arch/usermode/modules/syscallemu以下 – syscallemu_x86.c — syscallemu_getsce(p)の結果に応じてエミュレーション or 通常のsyscall扱い(md_syscall; md = machine dependent) —- processのspecific dataを扱っているみたいだが、よくわからない kern/kern_proc.c
  • sys/sys以下 – アーキテクチャに依存しない部分 – syscallvar.h — sy_invoke() — →sy_call()→sysent->sy_call(l, uap=引数args, rval=return value) —- syscallのデバッグ用のフックtrace_exit()もここから呼ばれる —- ちなみに実装は分散していて、例えばreadはsys/kern/sys_generic.c, mmapはsys/uvm/uvm_mmap.cにある
  • compat/linux(32)→Linuxエミュレーション

  • LWP: Light Weight Process – sys/kern/kern_lwp.cに詳しい – 一つのプロセスが複数のLWPを持つ – 状態→LSONPROC(実行中), LSRUN(キューに入れられていて、もうすぐ実行される), LSIDL(実行可能状態), LSSUSPENDED(_lwp_suspend syscallによって他のLWPにorシャットダウンによってシステムに止められた状態), LSZOMB(忘れらされそうなOR忘れ去られた状態 余分なリソースを保持しているので取り戻せる), LSSLEEP(“sleep queue”に入った休眠状態), LSSTOP(終了、カーネル領域にいて(?)シグナルを受け取ってもユーザ領域に戻らない) – ロックについても書かれている
  • sys/sys/proc.h – いろいろ読んでいるうちに脱線してしまった – struct mdproc p_md → md = machine dependentなprocの情報 — mi = machine independent — arch/usermodeではmdprocはからっぽ

ELFローダ関係

*** Linux: x86

  • fs/binfmt_elf.c – load_elf_phdrs(): load ELF program headers – load_elf_interp() — ELFのタイプ・PT_LOADなセグメントがあるかどうかチェック — プログラムヘッダをループ、PT_LOADがあれば読んでいく — マップを更新 ?? 4,5行のコメントがある部分 — 一番後ろのセグメントの直後にbss領域を確保, 0で埋める
    • load_elf_binary(struct linux_binprm *bprm)
      • 長い — いろいろチェックしている —- マジック —- ファイルタイプ —- e_type(ET_DYN: Shared Object または ET_EXEC: Executable) —- アーキテクチャ→例えばarch/x86/include/asm/elf.hにチェック関数が定義されていて、x86のもの以外は動かせない —- mmapが使えるかどうかもチェック — 先にPT_INTERP (.interp) を読み込み← ELF interpreter, ld-linux.so.2とか(動的ローダ), コレ自体もELFバイナリ —- open_exec(elf_interpreter): 書き込めない状態にしてread-onlyでopen — PT_GNU_STACKなセグメントがexecutableであればスタックがexecutableになる (これの値は $ execstack ./binary で確認できる) —- たとえば、関数の中に関数を定義するとき(local function, nested function)にスタックをexecutableにする必要がある —- https://www.win.tue.nl/~aeb/linux/hh/protection.html — PT_LOPROC … PT_HIPROC: アーキ依存の処理に使えるよう予約されているのでアーキ依存部分の処理を呼び出し — ELF interpreterがある→マジック・アーキをチェックしてヘッダをロード — (arch_check_elf(): ダミー) — personalityをセット —- x86→何もしない —- MIPS→フレームポインタとNaN encodingの設定 arch/mips/(include/asm/elf.h, kernel/elf.c) — setup_new_exec(bprm) <読み直しここまで> --- install_exec_creds(bprm): credを準備、credはsecure context of taskで、uidやegidを持っている --- randomizationの処理 ---- randomize_stack_top() ---- load_bias --- プログラムヘッダをループ ---- PT_LOADを ---- ET_DYNとET_EXECでロードするアドレスを変えている←"./ld.so hogehoge"などとしたとき、ld.so自身によるhogehogeのロード先がld.soの領域と重複するから ---- elf_map()→mmapする --- load_elf_interp() --- (compat_)start_thread()する -- create_elf_tables() --- ELFテーブル、あんまりテーブルらしくない --- elf_infoにはELF interpreter infoが入っていて、ATのエントリが追加されていく --- スタックを伸ばしてから… --- __put_userマクロでkernel spaceからuser spaceにargc, argv, envpをコピー←NetBSDのcopyoutと同じ ---- argv, envp→文字列へのポインタをセット ---- __put_user_asmに実際のコピー処理がアセンブラで書かれている ---- strnlen_user() ← strnlen() + 1 (末尾のNULL文字を入れたいらしい) --- 最後にelf_infoをスタックに追加 -- gdbで確認してみる
 gdb-peda$ stack 0x300
 0000| 0x7fffffffdea0 --> 0x2 ← argc
 0008| 0x7fffffffdea8 --> 0x7fffffffe1f0 ("/home/sei0o/Devs/SecHack365/original_startup/first") ← argv
 0016| 0x7fffffffdeb0 --> 0x7fffffffe223 --> 0x41414141414141 ('AAAAAAA')
 0024| 0x7fffffffdeb8 --> 0x0  ← argvの終わり
 0032| 0x7fffffffdec0 --> 0x7fffffffe22b ("ANDROID_SDK=/home/sei0o/Android/Sdk") ← envpの始まり
 0040| 0x7fffffffdec8 --> 0x7fffffffe24f ("COLORFGBG=15;0")
 0048| 0x7fffffffded0 --> 0x7fffffffe25e ("COLORTERM=truecolor")
 ...
 0472| 0x7fffffffe078 --> 0x7fffffffef92 ("XDG_VTNR=7")
 0480| 0x7fffffffe080 --> 0x7fffffffef9d ("XMODIFIERS=@im=fcitx")
 0488| 0x7fffffffe088 --> 0x7fffffffefb2 ("XSESSION_IS_UP=yes")
 0496| 0x7fffffffe090 --> 0x0 ← envpの終わり
 0504| 0x7fffffffe098 --> 0x21 ('!') ← #define AT_SYSINFO_EHDR 33
 0512| 0x7fffffffe0a0 --> 0x7ffff7ffa000 (jg     0x7ffff7ffa047) ← vDSOへのポインタ
 0520| 0x7fffffffe0a8 --> 0x10 ← #define AT_HWCAP 16
 0528| 0x7fffffffe0b0 --> 0xbfebfbff ←AT_HWCAPの値
 0536| 0x7fffffffe0b8 --> 0x6 
 0544| 0x7fffffffe0c0 --> 0x1000 
 0552| 0x7fffffffe0c8 --> 0x11 
 0560| 0x7fffffffe0d0 --> 0x64 ('d')
 --More--(75/768)
 0640| 0x7fffffffe120 --> 0x0 
 0648| 0x7fffffffe128 --> 0x9 ('\t') ← #define AT_ENTRY 9
 0656| 0x7fffffffe130 --> 0x400630 (<_start>:    pop    rdi) ← エントリポイント
 0664| 0x7fffffffe138 --> 0xb ('\x0b')
 ...
 0864| 0x7fffffffe200 ("/SecHack365/original_startup/first")←argvの文字列
 0872| 0x7fffffffe208 ("365/original_startup/first")
 0880| 0x7fffffffe210 ("inal_startup/first")
 0888| 0x7fffffffe218 ("rtup/first")
 0896| 0x7fffffffe220 --> 0x4141414141007473 ('st')
 0904| 0x7fffffffe228 --> 0x4f52444e41004141 ('AA')
 ...
  • load_elf_interp() — load_elf_binaryと同じようなチェックをする — PT_LOADなセグメントを配置していき、直後にbssセクションを確保
  • include/linux/binfmts.h – struct linux_binprmの定義 — unsigned long p; /* current top of mem */
  • arch/x86/kernel/process_64.c – start_thread(regs, new_ip, new_sp)
  • fs/exec.c – setup_new_exec(bprm) — secureexec

  • AT: Auxiliary Table (Vector) 補助的な情報が入っている – AT_UIDやAT_RANDOM, AT_PHDR, AT_BASEなど
  • AUX: Auxiliary
  • randomize_layout → __attribute((randomize_layout))

*** NetBSD: x86 あんまりわからないので結局比較してみることにした

  • sys/kern/exec_elf.c, exec_subr.c, exec_elf32.c, core_elf32.c, kern_exec.cなどに分散
  • elf_check_header() – ここでもOS ABIのチェックはしていない
  • copyargs() – dp = argc,argv,envp,auxinfoを合わせた要素数 –
  • execve_runproc() → copyoutargs() → elf_copyargs() – copyargs() –
  • elf_load_interp()
  • exec_elf_makecmds() = ELFNAME2(exec,makecmds) = exec_elf32_makecmds – インタプリタ(interp)を探して、セグメントからパスを取り出す – elf_probe_funcはエミュレーションについてだからスキップ – elf_placedynexec(): 共有ライブラリが実行されたとき、最下位のアドレス(i386なら0)+max(ロードするセグメントのalignのうち最大のもの, ページサイズ)だけ各セグメント・エントリポイントを下にずらす … ? — PaX ASLRが有効になっていればoffsetもランダムにする(Linuxは標準でやってたような) – p_alignとかそのへんがよくわからないな ? – セグメントの読み込み: executableなセグメントのうち最大サイズのものを.text(それ以外は無視), executableでないものは.data/.bssとする – elf_load_interp(): インタプリタの読み込み, vmcmdsもここで追加される(はずなので、load後にチェックしている) – 追加されたvmcmdsのev_addr + interp_offset(interpをloadした場所?)をエントリポイントに再設定 – exec_setup_stack()にバトンタッチ
  • exec_setup_stack(): exec_packageの情報をプロセスに移していく – p_rlimit rlim_cur ? – PaX ASLRが有効になっていればpax_aslr_stack()でランダムにスタックをずらす – stackのためのvmcmdsをつくる、実際にバイナリがアクセスする領域(sp - max_stack_size以下)としない領域を0埋めしている、アクセスできないなら確保しなくてよいのでは ?
  • execsw構造体
  • exec_add()
  • exec_remove()
  • どこでロードが終わってプロセスが開始されるかわからない – run queueなるものにpushされて待つらしい – https://www.netbsd.org/docs/internals/en/chap-processes.html
  • sys_execve() → execve1() → execve_loadvm(), execve_runproc()
  • execve_loadvm() – credのチェック? – exec packageの初期化 – check_exec() – 引数のバッファを確保, copyinargs()で書き込み – stackのサイズを計算して保存
  • check_exec() – vnode(fs)のチェックをしてから読み込んでそれぞれのexecswで実際にmakecmdsしてみる – エントリポイントが変な場所にないかチェック
  • execve_runproc() – LWPが存在していればexitさせる(←この時点ではexecveで置き換わる前のプロセスがrunningしているから?) – vmspaceを準備 – vmcmdsというものを使ってバイナリからメモリにコピーなどしている – execve_dovmcmds() ? – pathexec() ? – copyoutargs(): 引数をnewstackに積む – openしているファイルハンドラを閉じる – PL_PPWAIT ? – credexec(): スティッキービット(suid,sgid)の処理 — ここの 926L周辺のifは何がしたい? – doexechook(): 事前に登録されたhookを動かすとなっているが、何も登録されない(マクロでNULLになっている) – setregs(): machdep.c, SPをnewstackにしたりレジスタを初期化したり — PCB ? – LWPのprivate data pointerをNULLにしている ? – PCU discard ? – signal trampoline code: ? – emulexec(): ? – PSL_TRACED|PSL_SYSCALL ? – p->p_sflag: PS_STOPEXEC = stop on exec, execすると止まるプロセス?
  • calcargs(): argv, envp, auxinfoのサイズを足して返している、auxinfoがスタックに積まれるのか
  • exec_fakearg: shell scriptの実行で使われている(のでELFには関係ない)
  • ktr*()もktraceが有効なときだけ動くので無視できる

略語メモ

  • vmspace: virtual memory space
  • ap: elf_args
  • epp: exec_package
  • ep_ssize: stack size
  • l: lwp
  • pb: pathbuf
  • STACK_GROW: アドレスを引いているだけ(スタックが上方伸長だと足す)
    • STACK_ALLOCも下方伸長なアーキでは同じ
  • LinuxのほうがELFの構造をそのまま使っている面ではわかりやすい気がする
  • NetBSDはexec_package, execsw, … なんかいろいろある
  • 本筋の処理の流れを理解するのは重要だが、structいっぱいでごちゃっとなったらそれを使った小さい関数でも読んでちょっと慣れてみるのもいいかも

Backlinks