今読んで試しているのは「リンカ・ローダ実践開発テクニック」という書籍。
6章にリンカスクリプトを弄って、普通ならできないメモリの弄り方を実現するCのサンプルコードが動かなかったので、その原因と対処方法の備忘録。
勉強中なので間違った内容が書いてあったらすみません。また、そもそも書籍の6章自体が「実験」と言っているため、実用的なテクではないのに注意してください。
実行環境
$ uname -a
FreeBSD freebsd14.localdomain 14.2-RELEASE-p1 FreeBSD 14.2-RELEASE-p1 GENERIC amd64
対象のサンプルコード
書籍内のサンプルコードが以下。
普通に gcc overwrite.c -o errwrite でコンパイルして、 errwrite を実行すると、 Segmentation fault が出る。
memcpyを呼び出している箇所で、func1が .textセグメントに乗っていて、roな領域に書き込もうとするため。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int value;
char buffer[100];
void func1() { value += 1; }
void func2() { value += 2; }
void func3() { value += 4; }
int main() {
value = 0; func1(); func2(); func3();
printf("value = %d\n", value);
memcpy(func1, func2, (char *)func3 - (char *)func2);
memcpy(buffer, func3, (char *)main - (char *)func3);
value = 0; func1(); func2(); ((void(*)())buffer)();
printf("value = %d\n", value);
exit (0);
}
これに対して以下のsample.ldsを使って、各セクションをrweな場所に配置することで、overwrite.cを実行可能にするサンプルコードが掲載されている。
コンパイルの仕方としては、 gcc overwrite.c -o overwrite -Wall -T sample.lds -L/usr/lib/
/* sample.lds */
SECTIONS
{
. = 0x08048000 + SIZEOF_HEADERS;
.text : { *( .text) }
.rodata : { *( .rodata) }
.data : { *( .data) }
.bss : { *( .bss) }
}
しかし、これでもamd64な環境ではSegmentation Faultが発生する。
理由
sample.ldsを利用すると、func1, bufferの両方がrwxなメモリに配置される。これは書籍のサンプルの狙い通りのはず。memcpyの箇所も通り抜けることができる。
後述する調査で、エラーが発生しているのは、((void(*)())buffer)(); の内部であることがわかった。
amd64環境でoverwrite.cをコンパイルすると、func1,func2,func3のいずれもvalue変数への参照がインストラクションポインタからの相対位置となる様。言い換えると、func1,func2,func3ではvalueの位置を絶対参照していない。なので、func3の内容をbufferにコピーすると、もともとのvalue変数とは全然関係ないメモリの位置を参照する命令が出来上がり、実行するとセグフォが出ていた。
実験の趣旨 (リンカスクリプトを利用すると特定のメモリ領域を弄ることができるようになる) という点は達成できているが、セグフォを解消して実行できるようにしたいので、調べて試した。
調査方法
サンプルコードを以下の様に書き換えて、gcc -g3 overwrite.mod.c -o overwrite.mod -Wall -T sample.lds -L/usr/lib/ でコンパイルして、gdbで動作過程を調べられるようにする。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int value;
char buffer[100];
void func1() { value += 1; }
void func2() { value += 2; }
void func3() { value += 4; }
int main() {
printf("func1\t%#x\n", func1);
printf("func2\t%#x\n", func2);
printf("func3\t%#x\n", func3);
printf("main\t%#x\n", main);
printf("buffe\t%#x\n", buffer);
value = 0;
func1();
func2();
func3();
printf("value1 = %d\n", value);
memcpy(func1, func2, (char *)func3 - (char *)func2);
memcpy(buffer, func3, (char *)main - (char *)func3);
value = 0;
func1();
printf("value2_1 = %d\n", value);
func2();
printf("value2_2 = %d\n", value);
((void(*)())buffer)();
printf("value2_3 = %d\n", value);
exit (0);
}
mainにブレークポイントを打って、ステップ実行し、memcpyが完了したあとの、func1とfunc2の内容をdisassembleする。
そうするとfunc2では value(0x8048b20) が正しく参照できていることに対して、func1ではvalueを参照したいはずのところで、valueが参照できていない(0x8048b0a を参照している) のがわかる。
(なので、 printf("value2_1 = %d\n", value); の行でvalueの中身を見ても、正しく加算されていない。)
メモ: 0x00000000080482cc <+4>: mov 0x84e(%rip),%eax # 0x8048b20 <value> については次の行の、 0x00000000080482d2 + 0x84e が 0x8048b20 になっている
23 memcpy(func1, func2, (char *)func3 - (char *)func2);
(gdb) n
24 memcpy(buffer, func3, (char *)main - (char *)func3);
(gdb) n
25 value = 0;
(gdb) disass func2
Dump of assembler code for function func2:
0x00000000080482c8 <+0>: push %rbp
0x00000000080482c9 <+1>: mov %rsp,%rbp
0x00000000080482cc <+4>: mov 0x84e(%rip),%eax # 0x8048b20 <value>
0x00000000080482d2 <+10>: add $0x2,%eax
0x00000000080482d5 <+13>: mov %eax,0x845(%rip) # 0x8048b20 <value>
0x00000000080482db <+19>: nop
0x00000000080482dc <+20>: pop %rbp
0x00000000080482dd <+21>: ret
End of assembler dump.
(gdb) disass func1
Dump of assembler code for function func1:
0x00000000080482b2 <+0>: push %rbp
0x00000000080482b3 <+1>: mov %rsp,%rbp
0x00000000080482b6 <+4>: mov 0x84e(%rip),%eax # 0x8048b0a
0x00000000080482bc <+10>: add $0x2,%eax
0x00000000080482bf <+13>: mov %eax,0x845(%rip) # 0x8048b0a
0x00000000080482c5 <+19>: nop
0x00000000080482c6 <+20>: pop %rbp
0x00000000080482c7 <+21>: ret
End of assembler dump.
同様に、func3とbufferをdisassembleすると、bufferでも同じことが起きているのがわかる。
(gdb) disass func3
Dump of assembler code for function func3:
0x00000000080482de <+0>: push %rbp
0x00000000080482df <+1>: mov %rsp,%rbp
0x00000000080482e2 <+4>: mov 0x838(%rip),%eax # 0x8048b20 <value>
0x00000000080482e8 <+10>: add $0x4,%eax
0x00000000080482eb <+13>: mov %eax,0x82f(%rip) # 0x8048b20 <value>
0x00000000080482f1 <+19>: nop
0x00000000080482f2 <+20>: pop %rbp
0x00000000080482f3 <+21>: ret
End of assembler dump.
(gdb) disass buffer
Dump of assembler code for function buffer:
0x0000000008048b40 <+0>: push %rbp
0x0000000008048b41 <+1>: mov %rsp,%rbp
0x0000000008048b44 <+4>: mov 0x838(%rip),%eax # 0x8049382
0x0000000008048b4a <+10>: add $0x4,%eax
0x0000000008048b4d <+13>: mov %eax,0x82f(%rip) # 0x8049382
0x0000000008048b53 <+19>: nop
0x0000000008048b54 <+20>: pop %rbp
0x0000000008048b55 <+21>: ret
0x0000000008048b56 <+22>: add %al,(%rax)
特に、bufferの方の 0x8049382 に関しては、objdumpした結果の領域外になっている。
以下がobjdumpの結果。
$ /usr/local/bin/objdump -h -p overwrite.mod
overwrite.mod: file format elf64-x86-64-freebsd
Program Header:
PHDR off 0x0000000000000040 vaddr 0x0000000008048040 paddr 0x0000000008048040 align 2**3
filesz 0x0000000000000188 memsz 0x0000000000000188 flags r--
INTERP off 0x0000000000000505 vaddr 0x0000000008048505 paddr 0x0000000008048505 align 2**0
filesz 0x0000000000000015 memsz 0x0000000000000015 flags r--
LOAD off 0x0000000000000000 vaddr 0x0000000008048000 paddr 0x0000000008048000 align 2**12
filesz 0x0000000000000b00 memsz 0x0000000000000ba4 flags rwx
DYNAMIC off 0x00000000000008d8 vaddr 0x00000000080488d8 paddr 0x00000000080488d8 align 2**3
filesz 0x00000000000001d0 memsz 0x00000000000001d0 flags rw-
NOTE off 0x00000000000007ec vaddr 0x00000000080487ec paddr 0x00000000080487ec align 2**2
filesz 0x0000000000000048 memsz 0x0000000000000048 flags r--
EH_FRAME off 0x0000000000000738 vaddr 0x0000000008048738 paddr 0x0000000008048738 align 2**2
filesz 0x0000000000000014 memsz 0x0000000000000014 flags r--
STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4
filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw-
...
余談
memcpyしたあとにfunc1にstep inしても元の関数の中身しか見れない。デバッグ情報にfunc1はそういうものと記録されているのだと思うが未調査。
(gdb) n
26 func1();
(gdb) s
func1 () at overwrite.mod.c:8
8 void func1() { value += 1; }
(gdb) n
main () at overwrite.mod.c:27
27 func2();
(gdb) s
func2 () at overwrite.mod.c:9
9 void func2() { value += 2; }
(gdb) n
main () at overwrite.mod.c:28
28 ((void(*)())buffer)();
(gdb) n
Program received signal SIGSEGV, Segmentation fault.
Address not mapped to object.
0x0000000008048b44 in buffer ()
対処方法1 (関数のシグネチャを変える)
そもそも、valueがグローバルな変数であるために、func2もfunc3もインストラクションポインタからの相対位を利用して解決しているのがSegmentatil Faultの理由なので、大人しく、func1, func2, func3がvalueを引数に取るように変更したら問題なく動く。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int value;
char buffer[100];
void func1(int *value) { *value += 1; }
void func2(int *value) { *value += 2; }
void func3(int *value) { *value += 4; }
int main() {
printf("func1\t%#x\n", func1);
printf("func2\t%#x\n", func2);
printf("func3\t%#x\n", func3);
printf("main\t%#x\n", main);
printf("buffe\t%#x\n", buffer);
value = 0;
func1(&value);
printf("value1_1 = %d\n", value);
func2(&value);
printf("value1_2 = %d\n", value);
func3(&value);
printf("value1_3 = %d\n", value);
memcpy(func1, func2, (char *)func3 - (char *)func2);
memcpy(buffer, func3, (char *)main - (char *)func3);
value = 0;
func1(&value);
printf("value2_1 = %d\n", value);
func2(&value);
printf("value2_2 = %d\n", value);
((void(*)())buffer)(&value);
printf("value2_3 = %d\n", value);
exit (0);
}
対処方法2 (memcpyしたあとに更にメモリの内容を書き換える)
関数のシグネチャを変えたら動くが、valueはグローバル変数のままでコードを動かしたいので、こねくり回した結果が以下。
func1に関しては、func2の内容をコピーする前にfunc1の関数の内容の一部を対比しておいて、memcpyしたあと、元の位置にfunc1の内容を戻してやる。
bufferに関しては、もともとは空なので、gdbでbufferにfunc3の内容をコピーしたあと、valueの位置と手打ちでメモリの内容を書き換える。
その際に、bufferのメモリの位置とvalueのメモリの位置関係の都合が悪かったので、bufferの変数宣言とvalueの変数宣言の位置を入れ替えている。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char buffer[100];
int value;
char bkup1[6];
char bkup2[6];
void func1() { value += 1; }
void func2() { value += 2; }
void func3() { value += 4; }
int main() {
printf("func1\t%#x\n", func1);
printf("func2\t%#x\n", func2);
printf("func3\t%#x\n", func3);
printf("main\t%#x\n", main);
printf("buffe\t%#x\n", buffer);
value = 0; func1(); func2(); func3();
printf("value1 = %d\n", value);
memcpy(bkup1, func1 + 4, 6);
memcpy(bkup2, func1 + 13, 6);
memcpy(func1, func2, (char *)func3 - (char *)func2);
memcpy(func1+4, bkup1, 6);
memcpy(func1+13, bkup2, 6);
memcpy(buffer, func3, (char *)main - (char *)func3);
buffer[3 + 3] = 0x5A;
buffer[3 + 4] = 0;
memcpy(bkup1, buffer + 4, 6);
buffer[12 + 3] = 0x51;
buffer[12 + 4] = 0;
value = 0;
func1();
printf("value2_1 = %d\n", value);
func2();
printf("value2_2 = %d\n", value);
((void(*)())buffer)();
printf("value2_3 = %d\n", value);
exit (0);
}
buffer に差し込んでいる処理の内容は、func1を弄る際にbkup1とbkup2に退避した内容を参考に、bufferのアドレスを元に計算して手打ちした。
(gdb) p/x bkup1
$3 = {0x8b, 0x5, 0x28, 0xa, 0x0, 0x0}
(gdb) p/x bkup2
$4 = {0x89, 0x5, 0x1f, 0xa, 0x0, 0x0}
実行結果
特に面白みは無いけど、問題なく実行できる
$ ./overwrite
func1 0x80482b2
func2 0x80482c8
func3 0x80482de
main 0x80482f4
buffe 0x8048c80
value1 = 7
value2_1 = 2
value2_2 = 4
value2_3 = 8
対処方法3
他にも色々ありそう。結局、valueという変数への参照方法が問題なので、参照方法を変える方法があればなんとかなると思う。
自分は対処方法1と対処方法2を見つけて満足しているし、あまりいい方法が思いつかないので、ここらへんで切り上げる。
参考