背景
Turing Complete FMを聞いて低レイヤに興味を持ちました
私が低レイヤと聞いて思い浮かぶアプリにstraceやgdbなどがあります
これらアプリが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
とします
※tracee
とtracer
という名前はここから引用しています
ソースコード
Makefile
※Makefile内でインデントされている箇所はスペースからタブに変換してください
※具体的には12行目$(CC)
直前、15行目$(CC)
直前、19行目$(RM)
手前です
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と出力します
しかし別プロセスによってメモリが書き換えられ、異なる文字列が出力されます
#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
に出力させます
#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つのファイルをそれぞれMakefile
、tracee.c
、tracer.c
という名前で作成し、ひとつのディレクトリにまとめてください
その後make
を実行すると2つの実行ファイルtracee
、tracer
が生成されます
$ 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内部の動作を見ていきたいです
参考資料(再掲含む)
-
普通のやつらの下を行け: ptrace で実行中のプロセスにちょっかいを出す - bkブログ
- 今回の資料のベースとなったページです
-
Turing Complete FM
- 低レイヤを勉強したいと思うきっかけになったメディアです
-
Manpage of PTRACE
- ptraceの日本語マニュアルです
-
ptrace(2) - Linux manual page
- ptraceの英語マニュアルです、2つの実行ファイル
tracee
とtracer
の名付け元とも言えます
- ptraceの英語マニュアルです、2つの実行ファイル
-
Man page of STRTOL
- strtolの日本語マニュアル、16進数の値の先頭に0xを付加した時の動作について書かれています
-
osx - default wordsize in UNIX/Linux - Unix & Linux Stack Exchange
- wordsizeについて書かれているっぽい?
- すいません力尽きてちゃんと読めてません・・・orz