August 16, 2018

簡易 strace を作ってシステムコールを表示する

いつまで経っても親知らずが人間から失われないの、歯医者業界との癒着か?


strace は ptrace(2) でプロセスを監視することで呼び出されるシステムコールとその引数を表示している。
ここでは簡易的な strace を作成してシステムコールを表示してみる。

どのようにシステムコールを追いかけるか

今回は任意のプロセスにアタッチするのではなく、引数として与えた文字列を execvp してシステムコールを表示してみる。
システムコールを逐次表示していく手順としては以下のようになる。

  1. fork(2) して子プロセスの中で execvp する。
  2. 子プロセスでは ptrace(PTRACE_TRACEME, 0, NULL, NULL) によって親プロセスでトレースを行わせる。
  3. 親プロセスでは waitpid で状態が変化するまで待ち、ptrace(PTRACE_GETREGS, pid, NULL, &regs) でレジスタの状態を取得する。

基本的には waitpidptrace(PTRACE_GETREGS, pid, NULL, &regs) を繰り返していけばよい。

システムコールとレジスタの定義

ptrace(PTRACE_GETREGS, pid, NULL, &regs) すると regs.orig_rax にはシステムコール番号が入っている。
そして、 rdirsi にはそれぞれシステムコールに対する引数が入っている。

このあたりの話は以下の記事を参照されたい。

システムコール番号は環境によって異なるが、x86_64 の場合は ausyscall でも確認できる。

$ ausyscall x86_64 --dump
Using x86_64 syscall table:
0       read
1       write
2       open
3       close
4       stat
5       fstat
6       lstat
7       poll
8       lseek
(snip)

ausyscall による出力をいい感じに整形し、システムコールを表示するプログラムが以下。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/fcntl.h>
#include <syscall.h>

const char *syscalltable[] = {
  "read",
  "write",
  "open",
  "close",
  "stat",
  "fstat",
  "lstat",
  "poll",
  "lseek",
  "mmap",
  "mprotect",
  "munmap",
  "brk",
  "rt_sigaction",
  "rt_sigprocmask",
  "rt_sigreturn",
  "ioctl",
  "pread",
  "pwrite",
  "readv",
  "writev",
  "access",
  "pipe",
  "select",
  "sched_yield",
  "mremap",
  "msync",
  "mincore",
  "madvise",
  "shmget",
  "shmat",
  "shmctl",
  "dup",
  "dup2",
  "pause",
  "nanosleep",
  "getitimer",
  "alarm",
  "setitimer",
  "getpid",
  "sendfile",
  "socket",
  "connect",
  "accept",
  "sendto",
  "recvfrom",
  "sendmsg",
  "recvmsg",
  "shutdown",
  "bind",
  "listen",
  "getsockname",
  "getpeername",
  "socketpair",
  "setsockopt",
  "getsockopt",
  "clone",
  "fork",
  "vfork",
  "execve",
  "exit",
  "wait4",
  "kill",
  "uname",
  "semget",
  "semop",
  "semctl",
  "shmdt",
  "msgget",
  "msgsnd",
  "msgrcv",
  "msgctl",
  "fcntl",
  "flock",
  "fsync",
  "fdatasync",
  "truncate",
  "ftruncate",
  "getdents",
  "getcwd",
  "chdir",
  "fchdir",
  "rename",
  "mkdir",
  "rmdir",
  "creat",
  "link",
  "unlink",
  "symlink",
  "readlink",
  "chmod",
  "fchmod",
  "chown",
  "fchown",
  "lchown",
  "umask",
  "gettimeofday",
  "getrlimit",
  "getrusage",
  "sysinfo",
  "times",
  "ptrace",
  "getuid",
  "syslog",
  "getgid",
  "setuid",
  "setgid",
  "geteuid",
  "getegid",
  "setpgid",
  "getppid",
  "getpgrp",
  "setsid",
  "setreuid",
  "setregid",
  "getgroups",
  "setgroups",
  "setresuid",
  "getresuid",
  "setresgid",
  "getresgid",
  "getpgid",
  "setfsuid",
  "setfsgid",
  "getsid",
  "capget",
  "capset",
  "rt_sigpending",
  "rt_sigtimedwait",
  "rt_sigqueueinfo",
  "rt_sigsuspend",
  "sigaltstack",
  "utime",
  "mknod",
  "uselib",
  "personality",
  "ustat",
  "statfs",
  "fstatfs",
  "sysfs",
  "getpriority",
  "setpriority",
  "sched_setparam",
  "sched_getparam",
  "sched_setscheduler",
  "sched_getscheduler",
  "sched_get_priority_max",
  "sched_get_priority_min",
  "sched_rr_get_interval",
  "mlock",
  "munlock",
  "mlockall",
  "munlockall",
  "vhangup",
  "modify_ldt",
  "pivot_root",
  "_sysctl",
  "prctl",
  "arch_prctl",
  "adjtimex",
  "setrlimit",
  "chroot",
  "sync",
  "acct",
  "settimeofday",
  "mount",
  "umount2",
  "swapon",
  "swapoff",
  "reboot",
  "sethostname",
  "setdomainname",
  "iopl",
  "ioperm",
  "create_module",
  "init_module",
  "delete_module",
  "get_kernel_syms",
  "query_module",
  "quotactl",
  "nfsservctl",
  "getpmsg",
  "putpmsg",
  "afs_syscall",
  "tuxcall",
  "security",
  "gettid",
  "readahead",
  "setxattr",
  "lsetxattr",
  "fsetxattr",
  "getxattr",
  "lgetxattr",
  "fgetxattr",
  "listxattr",
  "llistxattr",
  "flistxattr",
  "removexattr",
  "lremovexattr",
  "fremovexattr",
  "tkill",
  "time",
  "futex",
  "sched_setaffinity",
  "sched_getaffinity",
  "set_thread_area",
  "io_setup",
  "io_destroy",
  "io_getevents",
  "io_submit",
  "io_cancel",
  "get_thread_area",
  "lookup_dcookie",
  "epoll_create",
  "epoll_ctl_old",
  "epoll_wait_old",
  "remap_file_pages",
  "getdents64",
  "set_tid_address",
  "restart_syscall",
  "semtimedop",
  "fadvise64",
  "timer_create",
  "timer_settime",
  "timer_gettime",
  "timer_getoverrun",
  "timer_delete",
  "clock_settime",
  "clock_gettime",
  "clock_getres",
  "clock_nanosleep",
  "exit_group",
  "epoll_wait",
  "epoll_ctl",
  "tgkill",
  "utimes",
  "vserver",
  "mbind",
  "set_mempolicy",
  "get_mempolicy",
  "mq_open",
  "mq_unlink",
  "mq_timedsend",
  "mq_timedreceive",
  "mq_notify",
  "mq_getsetattr",
  "kexec_load",
  "waitid",
  "add_key",
  "request_key",
  "keyctl",
  "ioprio_set",
  "ioprio_get",
  "inotify_init",
  "inotify_add_watch",
  "inotify_rm_watch",
  "migrate_pages",
  "openat",
  "mkdirat",
  "mknodat",
  "fchownat",
  "futimesat",
  "newfstatat",
  "unlinkat",
  "renameat",
  "linkat",
  "symlinkat",
  "readlinkat",
  "fchmodat",
  "faccessat",
  "pselect6",
  "ppoll",
  "unshare",
  "set_robust_list",
  "get_robust_list",
  "splice",
  "tee",
  "sync_file_range",
  "vmsplice",
  "move_pages",
  "utimensat",
  "epoll_pwait",
  "signalfd",
  "timerfd",
  "eventfd",
  "fallocate",
  "timerfd_settime",
  "timerfd_gettime",
  "accept4",
  "signalfd4",
  "eventfd2",
  "epoll_create1",
  "dup3",
  "pipe2",
  "inotify_init1",
  "preadv",
  "pwritev",
  "rt_tgsigqueueinfo",
  "perf_event_open",
  "recvmmsg",
  "fanotify_init",
  "fanotify_mark",
  "prlimit64",
  "name_to_handle_at",
  "open_by_handle_at",
  "clock_adjtime",
  "syncfs",
  "sendmmsg",
  "setns",
  "getcpu",
  "process_vm_readv",
  "process_vm_writev",
  "kcmp",
  "finit_module",
  "sched_setattr",
  "sched_getattr",
  "renameat2",
  "seccomp",
  "getrandom",
  "memfd_create",
  "kexec_file_load",
  "bpf",
  "execveat"
};


void die (const char *msg)
{
  perror(msg);
  exit(errno);
}

int main(int argc, char *argv[])
{
  int pid, result;
  struct user_regs_struct regs;
  const char *prog;

  if (argc < 2) {
    printf("usage: \n%s PROG [ARG]\n", argv[0]);
    return 0;
  }

  prog = argv[1];

  switch( (pid = fork()) ) {
    case -1:  die("Failed fork");
    case 0:
              // 親プロセスにトレースさせる
              ptrace(PTRACE_TRACEME, 0, NULL, NULL);
              result = execvp(prog, &argv[1]);
              if (result) {
                die("execvp");
                return result;
              }
              return 0;
  }

  while(1) {
    int st;
    // 子プロセスを再開する
    ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
    if (waitpid(pid, &st, __WALL) == -1) {
      break;
    }

    if (!(WIFSTOPPED(st) && WSTOPSIG(st) == SIGTRAP)) {
      break;
    }

    ptrace(PTRACE_GETREGS, pid, NULL, &regs);

    if (regs.rax == - ENOSYS) {
      continue;
    }

    printf("%s = %lld\n", syscalltable[regs.orig_rax], regs.orig_rax);
  }
  return 0;
}

まずは stracepwd を実行した結果を確認してみる。

vagrant@ubuntu-xenial:~/shared/sandbox/ptrace$ strace pwd
execve("/bin/pwd", ["pwd"], [/* 21 vars */]) = 0
brk(NULL)                               = 0x19fc000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=45283, ...}) = 0
mmap(NULL, 45283, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd1abe21000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0P\t\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1868984, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1abe20000
mmap(NULL, 3971488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fd1ab83e000
mprotect(0x7fd1ab9fe000, 2097152, PROT_NONE) = 0
mmap(0x7fd1abbfe000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7fd1abbfe000
mmap(0x7fd1abc04000, 14752, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fd1abc04000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1abe1f000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fd1abe1e000
arch_prctl(ARCH_SET_FS, 0x7fd1abe1f700) = 0
mprotect(0x7fd1abbfe000, 16384, PROT_READ) = 0
mprotect(0x606000, 4096, PROT_READ)     = 0
mprotect(0x7fd1abe2d000, 4096, PROT_READ) = 0
munmap(0x7fd1abe21000, 45283)           = 0
brk(NULL)                               = 0x19fc000
brk(0x1a1d000)                          = 0x1a1d000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=1668976, ...}) = 0
mmap(NULL, 1668976, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fd1abc86000
close(3)                                = 0
getcwd("/home/vagrant/shared/sandbox/ptrace", 4096) = 36
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
write(1, "/home/vagrant/shared/sandbox/ptr"..., 36/home/vagrant/shared/sandbox/ptrace
) = 36
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

続いて上記プログラムで pwd を実行した結果は以下のようになる。

vagrant@ubuntu-xenial:~/shared/sandbox/ptrace$ ./a.out pwd
execve = 59
brk = 12
access = 21
access = 21
open = 2
fstat = 5
mmap = 9
close = 3
access = 21
open = 2
read = 0
fstat = 5
mmap = 9
mmap = 9
mprotect = 10
mmap = 9
mmap = 9
close = 3
mmap = 9
mmap = 9
arch_prctl = 158
mprotect = 10
mprotect = 10
mprotect = 10
munmap = 11
brk = 12
brk = 12
open = 2
fstat = 5
mmap = 9
close = 3
getcwd = 79
fstat = 5
/home/vagrant/shared/sandbox/ptrace
write = 1
close = 3
close = 3

システムコールに関しては strace と同様の結果が取得できている。

引数を表示する

strace ではシステムコールに加えて引数も表示できている。

getcwd("/home/vagrant/shared/sandbox/ptrace", 4096) = 36

前述した通り、引数はレジスタに格納されているので、取得すること自体は ptrace(PTRACE_PEEKDATA, ... で可能だが、当然システムコールごとに引数は異なっているので色々難しい。
「strace はどうやっているのだろう…何もわからない、俺たちは雰囲気で strace を使っている…」と思いながら見てみたらシステムコールごとに出力する実装を用意している。
例えば getcwd の実装は これ 。 人間は時として泥臭いことをしないといけないことがある。皆さんは泥臭いですか?

さて、とりあえずここでは getcwd だけでも表示してみる。
かなり雑だが、getcwd の引数を表示する print_getcwd() を書く。

レジスタ rdi には第一引数である絶対パス名 buf が、rsi には第二引数である size が格納されている。
bufPTRACE_PEEKDATA で取得して表示している。

void print_getcwd(int pid, long long addr, long long size)
{
  // %rdi = char *buf, %rsi = unsigned long size	
  int i;
  char* bytes = malloc(size + sizeof(long));
  for (i = 0; i < size; i += sizeof(long)) {
    long data = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
    if (data == -1) {
      printf("failed peekdata");
      free(bytes);
      return;
    }
    memcpy(bytes + i, &data, sizeof(long));
  }
  printf("getcwd(\"%s\", %lld) = 79\n", bytes, size);
}

ここもかなり雑だが、getcwd (システムコール番号が79) のときに print_getcwd を呼び出すようにする。

    if (regs.orig_rax == 79) {
      print_getcwd(pid, regs.rdi, (int)regs.rsi);
      continue;
    }

結果は以下のようになり、ちゃんと引数を表示できていることがわかる。

vagrant@ubuntu-xenial:~/shared/sandbox/ptrace$ ./a.out pwd
execve = 59
brk = 12
access = 21
access = 21
open = 2
fstat = 5
mmap = 9
close = 3
access = 21
open = 2
read = 0
fstat = 5
mmap = 9
mmap = 9
mprotect = 10
mmap = 9
mmap = 9
close = 3
mmap = 9
mmap = 9
arch_prctl = 158
mprotect = 10
mprotect = 10
mprotect = 10
munmap = 11
brk = 12
brk = 12
open = 2
fstat = 5
mmap = 9
close = 3
getcwd("/home/vagrant/shared/sandbox/ptrace", 4096) = 79 # 表示できている
fstat = 5
/home/vagrant/shared/sandbox/ptrace
write = 1
close = 3
close = 3
このエントリーをはてなブックマークに追加

© Kouhei Morita 2018