6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

背景

Turing Complete FMを聞いて低レイヤに興味を持ちました
私が低レイヤと聞いて思い浮かぶアプリにstracegdbなどがあります
これらアプリがptraceの機能を使っていることを知ったので、とりあえずptraceについて勉強してみようと思いました
このページは、ptraceの機能を動かすまでにかかった疑問点などをまとめています

はじめに

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

ソースコード

Makefile

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

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と出力します
しかし別プロセスによってメモリが書き換えられ、異なる文字列が出力されます

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に出力させます

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;
}

実行方法

メモリ書き換え確認までの手順を説明します

ビルド

上記3つのファイルをそれぞれMakefiletracee.ctracer.cという名前で作成し、ひとつのディレクトリにまとめてください
その後makeを実行すると2つの実行ファイルtraceetracerが生成されます

$ 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 を別の文字列に途中から変更させます

$ ./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 だと分かります

$ 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 だとわかります

$ objdump -s -j .rodata tracee

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

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

出力文字変更

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

  • root権限が必要
    • ptraceの実行にはroot権限が必要です、sudoをつけて実行してください
  • 取得したアドレスに 0x を付加する
    • これは内部でstrtolを使っているためです

今回は出力される文字列を Hello World から Good_Bye に変更します
以下のようにコマンドを入力してください
※引数1478と0x40063bはここまでに取得した文字列に置き換えて入力してください
※引数の順番は ./tracker <PID> <Address> <String>です

$ sudo ./tracer 1478 0x40063b Good_Bye

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

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

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

疑問点

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

ワード

ptraceについて調査している時、Manpage of PTRACEで以下一文を発見しました

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

ワード - Wikipediaにも書かれている通りOS毎に固定というわけではないです。
なので「どうやってこの値(自分の場合は64bitOSなのでおそらく64という数値)を取得すればいいのか?」という疑問が出ました。

暫定解決案

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

#define WORD_PER_BYTE (__WORDSIZE / CHAR_BIT)

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

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

あとがき

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

参考資料(再掲含む)

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?