ELFにおいて、共有libraryのlinkはOSでなくheader内で指定されてlinkerが行う。 このlinkerを指定する文字列を書き換え、その挙動を確認した。

準備

ELFのprogram headerのtypeとして、PT_INTERPがある。 これは単一のbinary中に高々$1$つまで存在し、そのsegment内の文字列としてinterpreterを指定する。 interpreterが指定されているとき、本体がloadされるより先にそのinterpreterがloadされる。 用途としては共有libraryの準備であり、その場合interpreterが本体プログラムをloadする。 INTERP segmentはたいてい.interp sectionを唯一のsegmentとして含む(ただしsection名は必ずしも.interpである必要はない)。

準備として、普通のプログラムを用意する。 例え陽にlibcの関数を呼んでいなかったとしても(例えば__libc_start_mainなどのために) libcは動的linkされていて.interp sectionが存在する。 今回、pathは/lib64/ld-linux-x86-64.so.2であった。

#include <stdio.h>
int main(void) {
    printf("Hello, world!\n");
    return 0;
}
$ gcc helloworld.c -o helloworld

$ ./helloworld
Hello, world!

$ objdump -s helloworld | grep interp -A 2
Contents of section .interp:
 400238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 400248 7838362d 36342e73 6f2e3200           x86-64.so.2. 

上書き

interpreterとして指定するプログラムの処理内容は(指定するだけなら)なんでもよい。

この例ではinterruptedと表示して終了するプログラムを使用する。Hello, world!の代わりにこれが表示されれば成功である。

#include <stdio.h>
int main(int argc, char **argv) {
    printf("interrupted\n");
    return 1;
}

ただし再帰的にinterpreterを要求するのは許されないようで、静的linkする必要がある。

$ gcc -static interp.c -o interp

適当に.interpを編集する。null終端の文字列が認識されるので、後ろにゴミを残してもよい。

$ objdump -s helloworld | grep interp -A 1
Contents of section .interp:
 400238 2f746d70 2f696e74 65727000 6e75782d  /tmp/interp.nux-
 400248 7838362d 36342e73 6f2e3200           x86-64.so.2.

このような準備の元で、./helloworldを叩くとHello, world!でなくinterruptedと表示される。 これは期待される挙動である。

$ cp interp /tmp

$ ./helloworld
interrupted

引数とかもちゃんと渡ってきてたりする。(ld-linuxはargv = NULLでも仕事をするのでこれを読んでいるのではない。)

$ cat interp.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
    for (int i = 0; i < argc; ++ i) {
        printf("argv[%d] : %s\n", i, argv[i]);
    }
    scanf("%*c");
}
$ ./helloworld foo bar
argv[0] : ./helloworld
argv[1] : foo
argv[2] : bar
^Z

$ ps aux | grep '[h]elloworld\|[i]nterp'
user     10136  0.0  0.0   1120     8 pts/18   T    00:57   0:00 ./helloworld foo bar

$ gdb -p `pidof helloworld`
...
gdb-peda$ vmmap
Start              End                Perm	Name
0x00400000         0x004c9000         r-xp	/tmp/interp
0x006c8000         0x006cb000         rw-p	/tmp/interp
0x006cb000         0x006cd000         rw-p	mapped
0x01510000         0x01533000         rw-p	[heap]
0x00007fff59965000 0x00007fff59987000 rw-p	[stack]
0x00007fff599f6000 0x00007fff599f8000 r--p	[vvar]
0x00007fff599f8000 0x00007fff599fa000 r-xp	[vdso]
0xffffffffff600000 0xffffffffff601000 r-xp	[vsyscall]