ptraceによる別プロセスのメモリ操作、備忘録

# 背景
[Turing Complete FM](https://turingcomplete.fm/)を聞いて低レイヤに興味を持ちました
私が低レイヤと聞いて思い浮かぶアプリに[strace](https://ja.wikipedia.org/wiki/Strace)や[gdb](https://www.gnu.org/software/gdb/)などがあります
これらアプリが[ptrace](http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/LDP_man-pages/man2/ptrace.2.html)の機能を使っていることを知ったので、とりあえずptraceについて勉強してみようと思いました
このページは、ptraceの機能を動かすまでにかかった疑問点などをまとめています

# はじめに
- このページ（ソースコードを含む）は[普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す - bkブログ]( http://0xcc.net/blog/archives/000077.html)の内容をベースにしています、オリジナルの内容ではありません
- 以下で示すコードは実行時にroot権限を必要とする箇所があります、個人の責任のうえで実施をお願いいたします
- 以下で示すコードは下記環境で動作確認を行っています。低レイヤの内容を扱っているため（と私の手抜きによって）他の環境では動作しない可能性があります。具体的には以下の違いが影響すると思われます
    - 32bitか64bitか
    - リトルエンディアンかビッグエンディアンか
- 下記環境で動作確認を実施しました

```shell
$ cat /etc/issue
Ubuntu 16.04.5 LTS \n \l

$ cat /proc/cpuinfo | grep model | head -n 2
model           : 78
model name      : Intel(R) Core(TM) i7-6600U CPU @ 2.60GHz
$ uname -m
x86_64
```

# 概要
ここでは`ptrace`の機能の一つである、別プロセスのメモリ書き換え実施してみます
説明の都合上、メモリが書き換えられるアプリを`tracee`、メモリを書き換えるアプリを`tracer`とします
※`tracee`と`tracer`という名前は[ここ](http://man7.org/linux/man-pages/man2/ptrace.2.html)から引用しています

# ソースコード
## Makefile
※Makefile内でインデントされている箇所はスペースからタブに変換してください
※具体的には12行目`$(CC)`直前、15行目`$(CC)`直前、19行目`$(RM)`手前です

```makefile:Makefile
CC = gcc
CFLAGS = -g3 -O0
TRACER = tracer
TRACER_SRC = tracer.c
TRACEE = tracee
TRACEE_SRC = tracee.c

.PHONY: all
all: $(TRACER) $(TRACEE)

$(TRACER): $(TRACER_SRC)
    $(CC) $^ -o $@ $(CFLAGS)

$(TRACEE): $(TRACEE_SRC)
    $(CC) $^ -o $@ $(CFLAGS)

.PHONY: clean
clean:
    $(RM) $(TRACER) $(TRACEE)
```

## tracee.c
メモリが書き換わるプロセスのソースです
通常なら7行目の関数によって**Hello World**と出力します
しかし別プロセスによってメモリが書き換えられ、異なる文字列が出力されます

```c:tracee.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const* argv[]) {
  unsigned int i = 0;
  while (1) {
    printf("%05u : Hello World\n", i);
    i++;
    if (i >= 100000) {
      i = 0;
    };
    sleep(1);
  }
  return 0;
}
```

## tracer.c
メモリを書き換えるプロセスのソースです
このプロセスによって`tracee`のメモリの内容を書き換えます
これにおり通常なら **Hello World** と出力されるところで別の文字列を`tracee`に出力させます

```c:tracer.c
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define WORD_PER_BYTE (__WORDSIZE / CHAR_BIT)

long int parse(const char *const ptr, int *idx) {
  long int ret = 0;
  for (int i = 0; i < WORD_PER_BYTE; ++i) {
    long int c = *(ptr + *idx);
    ++*idx;
    ret |= (c << (CHAR_BIT * i));
    if (c == 0x00) {
      break;
    }
  }
  return ret;
}

int main(int argc, char const *argv[]) {
  if (argc != 4) {
    fprintf(stderr, "Too few arguments.\n");
    fprintf(stderr, "Usage: %s <pid> <addr> <data>.\n", argv[0]);
    exit(EXIT_FAILURE);
  }
  pid_t pid = atoi(argv[1]);

  int ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  if (ret < 0) {
    fprintf(stderr, "PTRACE_ATTACH : %s\n", strerror(errno));
    exit(EXIT_FAILURE);
  }
  wait(NULL);

  void *addr = (void *)strtol(argv[2], NULL, 0);
  /* extra 2 is for new line and terminator char. */
  char *buf = (char *)malloc(strlen(argv[3]) + 2);
  strcpy(buf, argv[3]);
  strcat(buf, "\n\0");

  int index = 0;
  while (1) {
    int offset = index;
    void *word = (void *)parse(buf, &index);

    ret = ptrace(PTRACE_POKEDATA, pid, addr + offset, word);
    if (ret < 0) {
      int errsv = errno;
      fprintf(stderr, "PTRACE_POKEDATA error(%d)\n", ret);
      fprintf(stderr, "strerror(%d) = %s\n", errsv, strerror(errsv));
      exit(EXIT_FAILURE);
    }
    if (index >= strlen(buf)) {
      break;
    }
  }

  ret = ptrace(PTRACE_DETACH, pid, NULL, NULL);
  if (ret < 0) {
    fprintf(stderr, "DETACH_POKEDATA : %s\n", strerror(errno));
    exit(EXIT_FAILURE);
  }
  return 0;
}
```

# 実行方法
メモリ書き換え確認までの手順を説明します

## ビルド
上記３つのファイルをそれぞれ`Makefile`、`tracee.c`、`tracer.c`という名前で作成し、ひとつのディレクトリにまとめてください
その後`make`を実行すると２つの実行ファイル`tracee`、`tracer`が生成されます

```shell
$ ls
Makefile  tracee.c  tracer.c
$ make
gcc tracer.c -o tracer -g3 -O0
gcc tracee.c -o tracee -g3 -O0
$ ls
Makefile  tracee  tracee.c  tracer  tracer.c
```

## traceeの起動
`tracee`を実行します
数値とコロンに続いて **Hello World** という文字列が出力されます
後述する方法でこの文字列 **Hello World** を別の文字列に途中から変更させます

```shell
$ ./tracee
00000 : Hello World
00001 : Hello World
00002 : Hello World
00003 : Hello World
・・・以下略
```

※traceeプロセスは起動したまま放置してください
※新しい端末を開いてください。以降の作業は新しい端末で行います

## PIDの調査
`ptrace`の機能を使うためには監視対象となるプロセスのPID(Process ID)が必要です
以下コマンドで実行中の`tracee`プロセスのPIDを取得します
※この値は`tracee`を起動させるたびに変わります。`tracee`を再起動させたら再度、下記方法でPIDを取得してください
私の環境では以下のような結果が得られました
この結果から（今回の私のケースでは）現在起動している`tracee`プロセスのPIDは **1478** だと分かります

```shell
$ ps aux | head -n 1 ;ps aux | grep tracee | grep -v grep
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
n_hachi   1478  0.0  0.0   4356   680 pts/22   S+   20:41   0:00 ./tracee
```

## アドレス調査
`tracee`の何処に文字列 **Hello World** が格納されているかを調査します
以下に私の環境で出力された結果を示します
この内容から（今回の私のケースでは）文字列 **Hello World** が配置された先頭アドレスは **40063b** だとわかります

```shell
$ objdump -s -j .rodata tracee

tracee:     ファイル形式 elf64-x86-64

セクション .rodata の内容:
 400630 01000200 25303575 203a2048 656c6c6f  ....%05u : Hello
 400640 20576f72 6c640a00                     World..
```

## 出力文字変更
ここまでに得た２つの値、PIDと開始アドレスを使って動作中の`tracee`の出力内容を変更します
ここで２点注意があります

- root権限が必要
    - `ptrace`の実行にはroot権限が必要です、`sudo`をつけて実行してください
- 取得したアドレスに **0x** を付加する
    - これは内部で[strtol](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/strtol.3.html)を使っているためです
    
今回は出力される文字列を **Hello World** から **Good_Bye** に変更します
以下のようにコマンドを入力してください
※引数1478と0x40063bはここまでに取得した文字列に置き換えて入力してください
※引数の順番は `./tracker <PID> <Address> <String>`です

```shell
$ sudo ./tracer 1478 0x40063b Good_Bye
```

間違いがなければ`tracer`何も出力することなく、すぐに処理を終了します。
そして元の端末上で動作している`tracee`の出力文字列が途中から **Good_Bye** に変わるはずです


```shell
・・・途中略・・・
00012 : Hello World
00013 : Hello World
00014 : Hello World
00015 : Good_Bye
00016 : Good_Bye
00017 : Good_Bye
・・・以下略・・・
```

これによって`tracee`のメモリが書き換わたことが確認できます

# 疑問点
ここからは私が実装時に悩んだ内容についてまとめます
備忘録がわりなので、ここ以降は得るものはないかもしれません・・・（流し読み程度に思ってください）
また「これで解決だ」と断言できないものが大半なので、ご存知の方がいらっしゃればご教示ください

## ワード
ptraceについて調査している時、[Manpage of PTRACE](http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/LDP_man-pages/man2/ptrace.2.html#lbAH)で以下一文を発見しました

> 「ワード (word) 」の大きさは OS によって決まる。 (例えば、32 ビットの Linux では 32 ビットである、など。)

[ワード - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%AF%E3%83%BC%E3%83%89)にも書かれている通りOS毎に固定というわけではないです。
なので「どうやってこの値（自分の場合は64bitOSなのでおそらく64という数値）を取得すればいいのか？」という疑問が出ました。

### 暫定解決案
「定数はヘッダファイル`limits.h`にあるだろう」と適当に考えて`/usr/include/limits.h`を確認したところ`__WORDSIZE`という定数がどこかで定義されていることがわかりました。
ということで現状、この定数を使って動かしています。
この定数は`tracer.c`で以下のように使っています

```c
#define WORD_PER_BYTE (__WORDSIZE / CHAR_BIT)
```

そして`tracer.c`内の`parse`関数で文字列から別プロセスへ書き込む値を生成しています
もしも渡される文字列が`WORD_PER_BYTE`サイズよりも長い場合は、分割して作成されます

※正直これで良いのかわかりません・・・


# あとがき
以上が今回調査した内容です
別プロセスの内容をガッツリいじることができて、嬉しかったです
（まぁそうでないとgdbとかどうやって動いてるんだ？って話ですが）
今後はkernel内部の動作を見ていきたいです

# 参考資料（再掲含む）
- [普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す - bkブログ]( http://0xcc.net/blog/archives/000077.html)
    - 今回の資料のベースとなったページです
- [Turing Complete FM](https://turingcomplete.fm/)
    - 低レイヤを勉強したいと思うきっかけになったメディアです
- [Manpage of PTRACE](http://surf.ml.seikei.ac.jp/~nakano/JMwww/html/LDP_man-pages/man2/ptrace.2.html)
    - ptraceの日本語マニュアルです
- [ptrace(2) - Linux manual page](http://man7.org/linux/man-pages/man2/ptrace.2.html)
    - ptraceの英語マニュアルです、２つの実行ファイル`tracee`と`tracer`の名付け元とも言えます
- [Man page of STRTOL](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/strtol.3.html#lbAD)
    - strtolの日本語マニュアル、16進数の値の先頭に0xを付加した時の動作について書かれています
- [osx - default wordsize in UNIX/Linux - Unix & Linux Stack Exchange]( https://unix.stackexchange.com/questions/74648/default-wordsize-in-unix-linux)
    - wordsizeについて書かれているっぽい？
    - すいません力尽きてちゃんと読めてません・・・orz