簡単なデバッガを自作する

作るもの

プログラムを指定したアドレスで止め、その時のレジスタ値を表示するデバッガをC言語で開発します。入出力のイメージは以下の通りです(デバッガの名前をmdbとしています)。

$ mdb <program> <address>
Program stopped at xxxxxxxxxxxx
Register
  rbp    xxxxxxxxxxxx
  rbx    xxxxxxxxxxxx
  rax    xxxxxxxxxxxx
  rcx    xxxxxxxxxxxx
  .
  .
  .

開発環境

予備知識

ptrace

ptraceはあるプロセスから他のプロセスを制御するためのシステムコールです。ptraceを利用すると、プロセスの実行を制御したり、メモリやレジスタを読み書きすることができます。制御する側のプロセスをtracer、制御される側のプロセスをtraceeと呼びます。ptraceは以下のように呼び出します。

ptrace(request, pid, addr, data);

requestはptraceの振る舞いを決めるenumです。pidは制御するプロセスのIDです。addrとdataの役割はrequestによって異なります。いくつか実行例を見てみましょう。

ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);

PTRACE_PEEKTEXTはプロセスのメモリを読み取るrequestです。pidで指定したプロセスのaddr番地のメモリを読み出します。

ptrace(PTRACE_POKETEXT, pid, addr, data);

PTRACE_POKETEXTはプロセスのメモリに書き込むrequestです。pidで指定したプロセスのaddr番地にdataを書き込みます。

ptrace(PTRACE_ATTACH, pid, NULL, NULL);

PTRACE_ATTACHはpidで指定したプロセスをtraceeにするrequestです。ubuntuではデフォルトでPTRACE_ATTACHが実行できないようになっています。PTRACE_ATTACHされたプロセスは他のプロセスから様々な操作を実行できるようになるため、自由にPTRACE_ATTACHできるとセキュリティに問題が発生するためです。

ptrace(PTRACE_TRACEME, 0, NULL, NULL);

PTRACE_TRACEMEは他と違い、tracerではなくtraceeが実行するrequestです。親プロセス(= tracer)によってtraceされることを宣言します。PTRACE_TRACEMEを実行したプロセスに対して、tracerはPTRACE_PEEKTEXTやPTRACE_POKETEXTなど様々な操作を実行できるようになります。PTRACE_TRACEMEしたプロセスは、execveシステムコールを実行するたびにSIGTRAPで停止するようになります。

その他のrequestはubuntuのmanpageで調べることができます。 http://manpages.ubuntu.com/manpages/bionic/man2/ptrace.2.html

waitpid

waitpidは子プロセスが終了したり、シグナルによって停止・再開されるまで待つシステムコールです。tracerはwaitpidを使ってtraceeの状態変化を待ちます。

ASLR

ASLR(Address Space Layout Randomization)はOSが持つセキュリティ機能のひとつです。プログラムが実行されるたびにプロセスのメモリレイアウトがランダムに変化します。Linuxでは2005年にリリースされたkernel v2.6.12からASLRがデフォルトで有効化されました。webで見つけることができるデバッガ自作記事の中には、ASLRが有効化されていない環境を想定しているものがあります。そのような記事に記載されているサンプルコードは、ASLRが有効化されている環境ではうまく動きません。参考にするときは注意が必要です。

処理の流れ

開発するデバッガの処理の流れは以下の通りです。

  1. プロセスをforkする。
  2. 子プロセスがPTRACE_TRACEMEを実行する。
  3. 子プロセスがプログラムを実行する。
  4. 親プロセスは子プロセスの停止を待つ。
  5. 親プロセスは指定したアドレスで子プロセスが停止するようにメモリを書き換える(引数で指定されたアドレスをINT3命令に書き換える)。
  6. 親プロセスは子プロセスを再開しINT3で停止するまで待つ。
  7. 親プロセスは子プロセスのレジスタ値を標準出力する。
  8. 親プロセスは子プロセスの命令ポインタをひとつ戻す。
  9. 親プロセスは5で書き換えた子プロセスのメモリを元に戻す。
  10. 親プロセスは子プロセスを1ステップ進め、9で元に戻した命令を実行する。
  11. 5に戻る。

コード

デバッガ

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <sys/types.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>

// /proc{pid}/mapsからプロセスのベースアドレスを調べる関数です
long baseAddr(pid_t pid) {
  char maps[32] = "/proc/";
  char id[10];
  sprintf(id, "%d", pid);
  strcat(maps, id);
  strcat(maps, "/maps");

  FILE *fp = fopen(maps, "r");
  if (!fp) {
    printf("Error: Failed to open map.\n");
    exit(1);
  }

  char base[32];
  for (int i = 0;; ++i) {
    base[i] = fgetc(fp);
    if (base[i] == '-') {
      base[i] = '\0';
      break;
    }
  }

  fclose(fp);

  return strtol(base, NULL, 16);
}

long addBreakpoint(pid_t pid, long addr) {
  long orig = ptrace(PTRACE_PEEKTEXT, pid, (void *)addr, NULL);
  if (orig == -1 && errno != 0) {
    printf("Error: Failed to peek. %s\n", strerror(errno));
    exit(1);
  }
  ptrace(PTRACE_POKETEXT, pid, (void *)addr,
         (orig & 0xFFFFFFFFFFFFFF00) | 0xCC); // 0xCCはINT3命令です
  return orig;
}

void moveInstructionPointer(pid_t pid, long addr) {
  ptrace(PTRACE_POKEUSER, pid, sizeof(long) * RIP, addr);
}

void store(pid_t pid, long addr, long orig) {
  ptrace(PTRACE_POKETEXT, pid, (void *)addr, (void *)orig);
}

void continueProcess(pid_t pid) { ptrace(PTRACE_CONT, pid, NULL, NULL); }

void stepIn(pid_t pid) {
  ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
  int status;
  waitpid(pid, &status, 0);
}

void printRegister(pid_t pid) {
  struct user_regs_struct regs;
  ptrace(PTRACE_GETREGS, pid, 0, &regs);
  printf("Register\n");
  printf("  rbp    %llx\n", regs.rbp);
  printf("  rbx    %llx\n", regs.rbx);
  printf("  rax    %llx\n", regs.rax);
  printf("  rcx    %llx\n", regs.rcx);
  printf("  rdx    %llx\n", regs.rdx);
  printf("  rsi    %llx\n", regs.rsi);
  printf("  rdi    %llx\n", regs.rdi);
  printf("  rip    %llx\n", regs.rip);
  printf("  cs     %llx\n", regs.cs);
  printf("  eflags %llx\n", regs.eflags);
  printf("  rsp    %llx\n", regs.rsp);
  printf("  ss     %llx\n", regs.ss);
  printf("  ds     %llx\n", regs.ds);
  printf("  es     %llx\n", regs.es);
  printf("  fs     %llx\n", regs.fs);
  printf("  gs     %llx\n", regs.gs);
}

void tracer(pid_t pid, long addr) {
  int status = 0;
  waitpid(pid, &status, 0);
  addr = baseAddr(pid) + addr;

  while (1) {
    long orig = addBreakpoint(pid, addr);
    continueProcess(pid);
    waitpid(pid, &status, 0);
    printf("Program stopped at 0x%016lx\n", addr);
    printRegister(pid);
    moveInstructionPointer(pid, addr);
    store(pid, addr, orig);
    stepIn(pid);
  }
}

void tracee(char *prog) {
  ptrace(PTRACE_TRACEME, 0, 0, 0);
  execl(prog, prog, NULL);
}

int main(int argc, char **argv) {
  if (argc != 3) {
    printf("Usage: mdb <prog> <addr>\n");
    return 1;
  }

  char *prog = argv[1];
  long addr = strtol(argv[2], NULL, 0);

  pid_t pid = fork();
  if (pid > 0) {
    tracer(pid, addr);
  } else if (pid == 0) {
    tracee(prog);
  } else {
    printf("Error: Failed to fork.\n");
    return 1;
  }

  return 0;
}

デバッグするプログラム

#include <stdio.h>
#include <unistd.h>

// デバッガはこの関数を呼び出す直前でプログラムを停止させる
int increase(int num) { return num + 1; }

int main() {
  int count = 0;
  while (1) {
    count = increase(count);
    printf("%d\n", count);
    sleep(1);
  }
  return 0;
}

実行結果

$ gcc mdb.c -o mdb
$ gcc counter.c -o counter
$ nm counter | grep increase
000000000000068a T increase
$ ./mdb counter 0x68a
Program stopped at 0x00005560a1e3f68a
Register
  rbp    7ffe68a7d3f0
  rbx    0
  rax    0
  rcx    5560a1e3f6e0
  rdx    7ffe68a7d4e8
  rsi    7ffe68a7d4d8
  rdi    0
  rip    5560a1e3f68b
  cs     33
  eflags 202
  rsp    7ffe68a7d3d8
  ss     2b
  ds     0
  es     0
  fs     0
  gs     0
1

参考にした記事

th0x4c.github.io naotechnology.hatenablog.com

2021-01-07

「イラストでわかるDockerとKubernetes」の4章コンテナランタイムとコンテナの標準仕様の概要を読み始めた。この日記を書いた後に布団の中で読み終わりそうだ。タイトルを見るとイラストが強調されているが、この本の特徴は文章のわかりやすさではないか。文章がわかりやすいのでイラストは不要と思いすらする。

MD5とBase62の概要を調べた。Base64はEncodeが楽(6bitごとに区切って文字列化)だけど、Base62はどうやるんだろうか。 ja.wikipedia.org en.wikipedia.org

あんちぽさんの記事を読み返してHabitifyを再開した。パフォーマンス向上だけでなく家事のリマインダーとしても使うつもり。ツールの言いなりになって洗濯やゴミ出しをしたい。 kentarokuribayashi.com

2021-01-06

「イラストでわかるDockerとKubernetes」の3章Kubernetesの概要を読み終わった。いよいよ次の章でコンテナランタイムとコンテナの標準仕様の概要を学ぶ。詳しく知らないので読むのが楽しみ。

昨年末から仕事中にポモドーロテクニックを実践している。ポモドーロテクニック用のツールはMarinara Pomodoro Assistantと物理ノートを使っている。物理ノートを使っているのが自分でもちょっと意外で、専用のツールやテキストファイルも試したんだけど自分に合うのはノートのようだ。休憩するたびにラップトップから手を離さなければいけないのが良い方向に効果しているのかもしれない。仕事が強制的に中断される。

chrome.google.com

夕方になるとタイマーをかけ忘れてダラダラと仕事をしてしまう癖を直したいなあ。意志力が消耗されるためか、夕方になると自己コントロールが弱くなる。「スタンフォードの自分を変える教室」を読み返して1日5~10分のマインドフルネスを始めてみたけど効果あるかしら。

www.daiwashobo.co.jp

2021-01-05

イラストでわかるDockerとKubernetesのDockerの章を読み終えて、Kubernetesの章を読み始めた。 Kubernetesの概要と特徴が紹介されていたが、公式ドキュメントや入門Kubernetesで既知の内容だった。 kubernetes.io www.oreilly.co.jp

System Design PrimerのSystem Design Interview Questionsに引き続き挑戦中。お題はpastebinの設計。学びが多く、あれこれ調べてるうちに1週間くらい使ってしまいそうだ。

System Designは以下のstepで進めると良いらしい。

  1. Outline use cases, constraints, and assumptions.
  2. Create a high level design.
  3. Design core components.
  4. Scale a design.

仕事だと 1 -> 2&4 -> 3 のように進めていたが、確かに上のように進めると課題を一つ一つ解いていけるので効率が良さそうだ。

問題を解いてみて、以下の様な質問にうまく答えられなそうなだとわかった。勉強しておこう。

  • URIのDBとしてSQLとNoSQLのどちらを使うか
  • textの格納先としてFile server/Object Storage/NoSQL document storeのどれを使うか
  • MD5/Base62/Base64の詳細
  • URIの長さをどのように決めればよいか
  • requestとresponseはどうすればよいか
  • DBのschemeはどのようにするか

System Designの課題を解くの面白いなー。仕事だとSystem Designする機会ってそんなにないので知識を得にくいのだけど、こういう課題を解くと素早くスキルアップできそうだ。良い教材を見つけた。

2021-01-04

イラストでわかる DockerとKubernetes を読み始めた。Docker・k8s・container runtimeと教えてくれるらしい。container runtimeを調べたことがなかったので読むのが楽しみ。 gihyo.jp

System Design PrimerのSystem Design Interview Questionsを解き始めた。System Design PrimerはFacebookでテックリードをしている方が作成した、システムデザインのナレッジをまとめたリポジトリ。テキストはお正月休みに一通り読んだので、腕試しに問題を解いている。 github.com

お正月休みに簡単なデバッガー(指定したアドレスでプログラムを止めて、そのときのレジスタを表示するだけ)を作った。OSのセキュリティ関係ではまったので、もう少し調べて記事を書きたい。

log

前職先輩のおうちにお呼ばれ。浜松民1人+元浜松民4人で餃子パーティーを開催。先輩のお子さんがまったく人見知りしないタイプだった。北欧メタルの話などをする。

帰宅後はGTDのWeekly Review。来週マネージメント関係でやらなければいけないことがあるのでその整理。散らかっていた部屋の片付け。

スタディサプリTOEICをやってCPUの創り方を読んだら寝る。