Slide 1

Slide 1 text

デバッガを自作してみよう Yutaro Oguri SWE Intern at AI / ML team M3, inc.

Slide 2

Slide 2 text

自己紹介 ● 名前: 小栗 悠太郎 (Yutaro Oguri) @irungo_ic ● 所属: 東京大学 工学部 電子情報工学科B3 ○ 現在、M3 AIチームにてインターンシップに参加 ● 趣味: 音楽(ヴァイオリン🎻)、お酒(🍺、🍶)

Slide 3

Slide 3 text

きっかけ 社員の方に昨日(!)誘われた。 今回のテーマは去年の 学科アドカレの記事に基づきます。

Slide 4

Slide 4 text

目次 ● デバッガとは ● 事前知識 ○ ptrace system call ○ ELF format ● 各部分の解説 ● 結果

Slide 5

Slide 5 text

デバッガとは 実行中のプログラムの特定の場所における状態を調べるツール 具体的には... 場所 = 機械語命令 状態 = 変数の中身、レジスタの中身、スタックの中身、など

Slide 6

Slide 6 text

printfデバッグ 1. バグの原因箇所と思われる場所に、”状態”を標準エラー出力するコードを差し込む 2. プログラムを(再ビルド+)再実行 3. エラー出力を見て試行錯誤 簡単だが、手間がかかる!

Slide 7

Slide 7 text

デバッガ 特定の場所にBreakPoint(BP)を設置し、 プロセスがBPに到達するたびに一時停止。 プログラマは状態の情報を得る。 これをFinishまで繰り返す。

Slide 8

Slide 8 text

デバッガの例: gdb 1. -gをつけてコンパイル 2. 起動 3. Breakpointを設置 4. 実行 5. 状態を観察 e.g.) レジスタの中身を出力 -> (gdb) info registers rax 0x7fffffffd310 140737488343824 rbx 0x401260 4199008 rcx 0x0 0 rdx 0x7fffffffd2f0 140737488343792 rsi 0x402004 4202500 rdi 0x402004 4202500 rbp 0x7fffffffd410 0x7fffffffd410 rsp 0x7fffffffd2f0 0x7fffffffd2f0 r8 0x0 0 r9 0x7ffff7fe0d60 140737354009952 r10 0x402004 4202500 r11 0x7ffff7de7c90 140737351941264 r12 0x401050 4198480 r13 0x7fffffffd500 140737488344320 r14 0x0 0 r15 0x0 0 rip 0x7ffff7de7d21 0x7ffff7de7d21 <__printf+145> eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0

Slide 9

Slide 9 text

今回作るデバッガ ● デバッグ対象はELF形式の実行可能(後ほど説明)ファイルとする ● 関数名を指定してBreakpointをおく ● 一時停止するたびにレジスタの情報を出力する ● これをFinishまで繰り返す

Slide 10

Slide 10 text

事前知識: ptrace ptrace: 実行中のプロセスの状態をRead/Writeできるシステムコール ptraceに何をさせるかをrequestにより指定 詳しくは `man ptrace` デバッガがやりたい ことと同じ!

Slide 11

Slide 11 text

ptrace request: 今回使うもの ● PTRACE_TRACEME このプロセスが親プロセスにtraceされるという関係を規定する ● PTRACE_PEEKTEXT / PTRACE_POKETEXT traceeのメモリにおいて、特定のAddressに対応するWordをRead / Write する

Slide 12

Slide 12 text

ptrace request: 今回使うもの ● PTRACE_GETREGS / PTRACE_SETREGS traceeのレジスタ値をRead / Write する ● PTRACE_CONT 停止していたtraceeの実行を再開する ● PTRACE_SINGLESTEP 停止していたtraceeの実行を次の命令まで進め、再び停止させる

Slide 13

Slide 13 text

ELF形式 LinuxなどのOSで広く採用されている実行形式 ELF Header: ファイルのメタデータ・Offset Program Header: 実行時に使う情報 Section Header: リンク時に使う情報 +------------------------+ | ELF header | +------------------------+ |+----------------------+| || Program header table || |+----------------------+| ||+--------------------+|| ||| ||| ||| data ||| ||| ||| ||+--------------------+|| |+----------------------+| || Setion header table || |+----------------------+| +------------------------ + source

Slide 14

Slide 14 text

ELF形式 LinuxなどのOSで広く採用されている実行形式 ELF Header: ファイルのメタデータ・Offset Program Header: 実行時に使う情報 Section Header: リンク時に使う情報 +------------------------+ | ELF header | +------------------------+ |+----------------------+| || Program header table || |+----------------------+| ||+--------------------+|| ||| ||| ||| data ||| ||| ||| ||+--------------------+|| |+----------------------+| || Setion header table || |+----------------------+| +------------------------ + source

Slide 15

Slide 15 text

実装: ELFのHandler構造体 ELFファイルの中身、 ヘッダ、Traceeの情報を 管理する構造体 #include #include ... typedef struct ElfHandler { Elf64_Ehdr *ehdr; // ELF header Elf64_Phdr *phdr; // program header Elf64_Shdr *shdr; // section header uint8_t *mem; // memory map of the executable char *exec_cmd; // exec command char *symbol_name; // symbol name to be traced Elf64_Addr symbol_addr; // symbol address struct user_regs_struct regs; // registers } ElfHandler_t;

Slide 16

Slide 16 text

実装: traceeの情報取得 コマンドライン引数から traceeの情報を取得 ElfHandler_t eh; eh.exec_cmd = strdup(argv[1]); eh.symbol_name = strdup(argv[2]);

Slide 17

Slide 17 text

実装: ELFファイルの読み込み ELFファイルの読み込み 巨大かもしれないので mmapで #include ... // read mode int fd = open(argv[1], O_RDONLY); // ファイルサイズの取得のためにstatを使用 struct stat st; fstat(fd, &st); // fdの内容をmapする (copy-on-write) eh.mem = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); // ヘッダを抽出 eh.ehdr = (Elf64_Ehdr *)eh.mem; eh.phdr = (Elf64_Phdr *)(eh.mem + eh.ehdr->e_phoff); eh.shdr = (Elf64_Shdr *)(eh.mem + eh.ehdr->e_shoff);

Slide 18

Slide 18 text

実装: BPを置きたいSymbolのAddrを特定 1. section header tableを総当たり. symbol tableに辿り着くまで. 2. symbol tableの実体を取ってくる. 3. Linkされているsymbol name tableを取ってくる. 4. symbol tableを総当たり. 目的のSymbol名に一致するEntryを返し, Addressを取 得 eh.symbol_addr = lookup_symbol_addr_by_name(&eh, eh.symbol_name);

Slide 19

Slide 19 text

実装: traceeの実行開始 fork/exec/waitで子プロセスを実行 PTRACE_TRACEMEによりtrace // process id int pid = fork(); ... // child executes the given program if (pid == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execve(eh.exec_cmd, args, envp); exit(EXIT_SUCCESS); } int status; wait(&status);

Slide 20

Slide 20 text

実装: Breakpointを設置 BPの設置 = Trap命令の差し込み Trap命令: ソフトウェア割り込みを生成 // trap命令のopcode (x86) #define OPCODE_INT3 0xcc ... // get original instruction const long original_inst = ptrace(PTRACE_PEEKTEXT, pid, eh.symbol_addr, NULL); // modify to trap instruction const long trap_inst = (original_inst & ~0xff) | OPCODE_INT3; ptrace(PTRACE_POKETEXT, pid, eh.symbol_addr, trap_inst);

Slide 21

Slide 21 text

実装: main loop Trap! ↓ レジスタ読み取り ↓ 元の命令/レジスタを復元し、 1つ前から再実行 ↓ trap命令を復元 while (1) { // resume process execution ptrace(PTRACE_CONT, pid, NULL, NULL); … if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { // get registers info and display them ptrace(PTRACE_GETREGS, pid, NULL, &eh.regs); display_registers(&eh); … // restore original instruction ptrace(PTRACE_POKETEXT, pid, eh.symbol_addr, original_inst); // single step to execute the original instruction eh.regs.rip -= 1; ptrace(PTRACE_SETREGS, pid, NULL, &eh.regs); ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL); … // restore trap instruction ptrace(PTRACE_POKETEXT, pid, eh.symbol_addr, trap_inst); } }

Slide 22

Slide 22 text

サンプルを実行してみる 足し算を3回する。 関数add(a, b, c)の引数と、 レジスタrdi, rsi, rdx*の値が 一致していればOK! *x86の呼び出し規約 int add (int a, int b, int c) { return a + b + c; } int main(int argc, char **argv, char **envp) { int a = 1; int b = 2; int c = 9; printf("a = %d, b = %d, c = %d\n", a, b, c); int d = add(a, b, 23); // 1(1) + 2(2) + 23(17) = 26(1a) printf("%d + %d + %d = %d\n", a, b, 23, d); int e = add(d, c, 54); // 26(1a) + 9(9) + 54(36) = 89(59) printf("%d + %d + %d = %d\n", d, c, 54, e); int f = add(e, 1, 7); // 89(59) + 1(1) + 7(7) = 97(61) printf("%d + %d + %d = %d\n", e, 1, 7, f); return 0; }

Slide 23

Slide 23 text

実行結果 add関数(symbol)にBPを設置 期待通りのレジスタ状態を観測できた $ ./debugger ./test_add add Tracing pid:43130 at symbol addr 401136 a = 1, b = 2, c = 9 %rax: 1 %rbx: 401260 %rcx: 2 %rdx: 17 // add関数の第3引数 %rsi: 2 // add関数の第2引数 %rdi: 1 // add関数の第1引数 %rbp: 7ffd2c1049d0 %rsp: 7ffd2c104988 ... %gs: 0 Please hit [ENTER] key to continue:

Slide 24

Slide 24 text

まとめ ● デバッガはptraceを使えば作れる ● 実行中プロセスにAttachするのも、ptraceで同様に可能(なはず) ● 車輪の再発明をして勉強になった

Slide 25

Slide 25 text

Reference ● はじめてのgdb, https://qiita.com/arene-calix/items/a08363db88f21c81d351 ● ELF Formatについて, https://www.hazymoon.jp/OpenBSD/annex/elf.html ● 最小限のELF, https://keens.github.io/blog/2020/04/12/saishougennoelf/ ● ptraceシステムコール入門 ― プロセスの出力を覗き見してみよう! , https://itchyny.hatenablog.com/entry/2017/07/31/090000 ● Ryan "elfmaster" O'Neill, Learning Linux Binary Analysis, 2016, Packt ● man page of MMAP, https://linuxjm.osdn.jp/html/LDP_man-pages/man2/mmap.2.html ● x86_64で関数の引数とレジスタの対応を確認する (アセンブラ), https://qiita.com/hara0219/items/6556ef17d00922536fa8 ● デバッガとは何ぞや, https://zenn.dev/satoru_takeuchi/articles/8de139a52af5c4

Slide 26

Slide 26 text

ありがとうございました!