TL;DR
ビルド済みのバイナリの関数を後から差し替える方法のまとめ
※本記事の方法は共有ライブラリの関数のみ有効
差し替え対象コード
差し替え対象のサンプルとしてptintfとfprintfのそれぞれ可変長引数ありとなしを用意する。
#include <stdio.h>
int main(int argc, char const* argv[])
{
printf("printf no args\n");
printf("printf %d\n", 1);
fprintf(stdout, "fprintf no args\n");
fprintf(stdout, "fprintf %d\n", 1);
return 0;
}
ビルドと実行結果は以下になる。
$ gcc print.c
$ ./a.out
printf no args
printf 1
fprintf no args
fprintf 1
差し替えコード
次にLD_PRELOADにて差し替えを行う用のコードを用意する。
可変長引数の有無にかかわらず基本的には差し替えたい関数の定義を持ってきて再定義すれば良い。
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) {
const char *c = "override fwrite\n";
write(1, c, strlen(c)); // stdout
return 0;
}
int fprintf (FILE *__restrict __stream, const char *__restrict __format, ...) {
const char *c = "override fprintf\n";
write(1, c, strlen(c)); // stdout
return 0;
}
int puts(const char *s) {
const char *c = "override puts\n";
write(1, c, strlen(c)); // stdout
return 0;
}
int printf (const char *__restrict __format, ...) {
const char *c = "override printf\n";
write(1, c, strlen(c)); // stdout
return 0;
}
これをLD_PRELOADで指定して実行すれば、定義を後から差し替えることができる。
$ gcc -shared -fPIC override.c -o liboverride.so
$ LD_PRELOAD=./liboverride.so ./a.out
override puts
override printf
override fwrite
override fprintf
ただし、GCCでは printf
と fprintf
で可変長引数の要素が0の場合、コンパイル時にそれぞれ puts
と fwrite
に置き替えられてしまう。(GCCは-O0でも置き替えは有効、clang 3.6では-O0ではprintfのまま置き換えなし、-O1以上で置き換え)
$ gcc -S print.c
$ cat print.s
.file "print.c"
.section .rodata
.LC0:
.string "printf no args"
.LC1:
.string "printf %d\n"
.LC2:
.string "fprintf no args\n"
.LC3:
.string "fprintf %d\n"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl $.LC0, %edi
call puts <- printf が puts になっている
movl $1, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
movq stdout(%rip), %rax
movq %rax, %rcx
movl $16, %edx
movl $1, %esi
movl $.LC2, %edi
call fwrite <- fprintf が fwrite になっている
movq stdout(%rip), %rax
movl $1, %edx
movl $.LC3, %esi
movq %rax, %rdi
movl $0, %eax
call fprintf
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.2-19ubuntu1) 4.8.2"
.section .note.GNU-stack,"",@progbits
GCCでこの置き替えをもし無効化したい場合は -fno-builtin-printf
-fno-builtin-fprintf
をつけてビルドすれば良い
$ gcc -fno-builtin-printf -fno-builtin-fprintf print.c
$ LD_PRELOAD=./liboverride.so ./a.out
override printf
override printf
override fprintf
override fprintf
注意点
当然といえば当然だけれども、差し替えた関数内で再度同じ関数を呼ぶと再帰呼び出しになり抜けられなくなる。
例えばprintfを差し替える場合は差し替え処理で使用したライブラリ内で使われていたりもするため、別途気をつける必要がある。
int puts(const char *s) {
const char *c = "override puts\n";
write(1, c, strlen(c)); // stdout
puts("recursive call?"); // ※putsから抜けられなくなる
return 0;
}
参考書籍
『BINARY HACKS』 #HACK 60 : LD_PRELOADで共有ライブラリを差し替える
関連
実行時の関数全ての in/out をトレースする - Qiita
参考
Linux 共有ライブラリ(.so)の動作を変える。LD_PRELOADで楽しく遊ぶ:Garbage In Garbage Out
LD_PRELOADで関数フックしてみよう! - バイナリの歩き方
LD_PRELOADで動的ライブラリ関数を上書きする | Siguniang's Blog