ptraceを用いたプログラム実行時動的書き換え
概要
実行中のプロセスをptraceを使って別の関数に書き換えるプログラムを書いたので、その話をします
テスト方法
$ make
$ make test
livepatchについて
実行中のプロセス/カーネルを動的に書き換える
実行中プログラム書き換えができると楽しい
Linux カーネルでは実際に使われている
Linux kernel 向けにはUbuntuなどで実際に使われている
一方 ユーザーランドプロセス向けに使われているものはほとんどない
既存の実装
今回の実装にもっとも近い。ptraceを使って実行時書き換えをする
コンパイラオプション不要
x86 向け
Linux kernel に mmap 風の独自命令を追加した. ptraceの代わりに使う
高速だが、mmapが必要
SUSE が作成
gcc コンパイラにコンパイルオプション -fpatchable-function-entry をつけることで それぞれの関数に NOP 命令を埋め込む
置き換え対象プログラム起動時に LD_PRELOAD=/path/to/libpulp.so をつける必要あり
作成した実装
toy program に対して書き換えを行う
動作条件
sudo setcap CAP_SYS_PTRACE+ep rewrite
ASLR 無効 sysctl -w kernel.randomize_va_space=0
例
実行中のプロセス A の func2()を
code:func2.c
static int func2() {
puts("func2");
system("echo");
return 2000;
}
別のバイナリファイル B の func3() に置き換える
code:func3.c
static int func3_internal() __attribute__((noinline,noclone));
int func3_internal() {
return 200;
}
static int func3() __attribute__((noinline,noclone));
static int func3() {
puts("func3! injected code!!!");
system("ls");
return func3_internal();
}
動作方針
1. 実行中のプロセス A に ptrace (2) でアタッチ
Aのメモリやレジスタを操作できる
code:c
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) < 0) {
perror("ptrace attach");
exit(1);
}
2. mmap() を呼ぶ
3. ptrace を使い呼び出させるバイナリファイル B を丸ごと A のメモリ空間にコピー
https://scrapbox.io/files/624407470892c20022b3d56d.png
まずファイルを char* data に入れる
code:c
fp = fopen(filename, "r");
if (fp == NULL) {
perror("file open");
return 0;
}
if (fread(data, st.st_size, 1, fp) == 0) {
perror("fread");
}
次に PTRACE_PEEKDATA でデータを
8バイト単位で書き込むので、8バイト分で割り切れない分は mod として処理する
一旦 PEEKDATA で8バイト読み込み
割り切れない部分だけ入れる
code:c
for (size_t i=0;i<x;i ++) {
if (set_data(target_pid,(void*)( load_addr + i*8), (void*)lv) < 0) {
return -1;
}
}
if (mod > 0) {
lv = ptrace(PTRACE_PEEKDATA, target_pid, addr+x, 0);
memcpy(&lv, &tox*8, mod); set_data(target_pid, addr+x, (void*)lv);
}
4. B の GOTを A の GOT に書き換える
これで標準Cライブラリ ( puts など ) が正しく呼び出せるようになる
まず、GOTの情報を収集する
JUMP 情報を入れると良い
code:bash
runningexename=A // 旧ファイル
exename=B // 新しいファイル
func2=$(readelf -s ${runningexename} | grep func2 | awk '{print $2}')
readelf -r ${runningexename} | grep R_X86_64_JUMP_SLO | awk '{print $1,$5}' > hoge_data.txt
readelf -r ${exename} | grep R_X86_64_JUMP_SLO | awk '{print $1,$5}' > lib_data.txt
この hoge_data, lib_data を入れるコテやると
0x555555554000 を起点に書き込む
code:c
while (!feof(fp)) {
void* raddr = (void*)ptrace(PTRACE_PEEKDATA, target_pid, (void*)(0x555555554000 + before), NULL);
set_data(target_pid, load_addr + after, raddr);
}
5. func2() の先頭 from_addr を func3() の先頭 to_addr に書き換える
jmp <func3の相対アドレス> (callでも可)
ここで相対アドレスは to_addr - from_addr - 5 と計算
code:c
int jmp_relative = (long long)to_addr - (long long)from_addr -5;
code0 = 0xe9; // call or jmp memcpy(code+1, &jmp_relative, sizeof(int));
code に 引数を入れる
ret or nop を設定する
code:c
size_t ptr;
func2 の先頭に書き込んで実行する
code:c
memcpy(&ptr, code, 8);
ptrace(PTRACE_POKEDATA, target_pid, from_addr, ptr);
6. プロセスAからデタッチ
ptraceでシステムコールを呼ぶ手法
2. mmap (2) を A に呼び出させメモリ確保
レジスタをまず取得する
ここでプログラムは一旦停止
code:c
struct user_regs_struct regs, original_regs;
if (ptrace(PTRACE_GETREGS, target_pid, NULL, &original_regs) < 0) {
perror("ptrace getregs");
return 0;
}
regs = original_regs;
C言語の呼び出し規約では、rdi, rsi, rdx,... の順序で引数を入れていく
code:c
regs.rax = 9;
regs.rdi = load_addr;
regs.rsi = size;
regs.rdx = PROT_READ | PROT_WRITE | PROT_EXEC;
regs.r10 = MAP_PRIVATE | MAP_ANONYMOUS;
regs.r8 = -1;
regs.r9 = 0;
レジスタを設定する
code:c
if (ptrace(PTRACE_SETREGS, target_pid, NULL, ®s) < 0) {
perror("ptrace setregs");
return 0;
}
次の命令が mmap() になるように RIP を変更し実行する
code:c
// 古い値をバックアップ
void* raddr = (void*)ptrace(PTRACE_PEEKDATA, target_pid, regs.rip, NULL);
void* lv = (void*)0x050f; // pptraceの値
// 次の命令 ptrace を書き込む
if (ptrace(PTRACE_POKEDATA, target_pid, regs.rip, lv) < 0) {
perror("ptrace poke code");
return 0;
}
// ptrace実行
if (ptrace(PTRACE_SINGLESTEP, target_pid, NULL,NULL) < 0) {
perror("ptrace cont");
return 0;
}
戻り値を比較する. regs.rax に新しい値が入る
code:c
wait(NULL);
if (ptrace(PTRACE_GETREGS, target_pid, NULL, ®s) < 0) {
perror("ptrace getregs");
return 0;
}
//puts("get registers part 2");
if ((void*)(regs.rax) == MAP_FAILED) {
puts("mmap failed");
return 0;
}
旧来のレジスタをかき戻す
code:c
if (ptrace(PTRACE_SETREGS, target_pid, NULL, &original_regs) < 0) {
perror("ptrace setregs (restore)");
return 0;
}
プログラム
参考資料
謝辞
メンターの光成さんはじめ関係者の皆様に御礼申し上げます