はじめに
ありえるえりあの古のテクニックを見せようと思ったら最近の技術の前にあっさり敗北した話に、データ領域を実行することで、プログラムの自己書き換えをしたりする話がある。現在のほとんどの処理系ではメモリに実行領域とデータ領域の区別があり、データ領域にあるバイト列を命令として解釈して実行できない。
しかし、OSの設定によっては、データ領域の実行フラグチェックをスルーすることができる。というわけでちょっとデータ領域を実行して遊んでみる。ちなみにOSはCentOS release 6.6 (Final)。ここでの石はx86だが、別の石(例えばSPARC)でもできた。
既存関数のコピー
まずはオリジナルの記事同様、適当な関数があるコードを書いてみる。
# include <stdio.h>
int
func(int a){
return a+1;
}
int
main(void){
printf("%d\n",func(3));
}
これをコンパイルしてから、objdumbする。lessしてfunciで検索するとすぐに探せる。
$ g++ test.cc
$ objdump -d x ./a.out
(snip)
00000000004005a4 <_Z4funci>:
4005a4: 55 push %rbp
4005a5: 48 89 e5 mov %rsp,%rbp
4005a8: 89 7d fc mov %edi,-0x4(%rbp)
4005ab: 8b 45 fc mov -0x4(%rbp),%eax
4005ae: 83 c0 01 add $0x1,%eax
4005b1: c9 leaveq
4005b2: c3 retq
(snip)
これみると、funcという関数が15バイトで実現されていることがわかる。これを別のメモリ領域にコピーする。funcの先頭アドレスをchar*にキャストしてmemcpyを使う。そしてコピー先のポインタを再度関数として解釈して実行してみる。
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
int
func(int a){
return a+1;
}
int
main(void){
size_t s = 15;
char *mem = (char *)malloc(s);
memcpy(mem, (char *)(func),s);
int (*fp)(int ) = (int (*)(int))(mem);
printf("func %d\n",func(3));
printf("fp %d\n",fp(3));
}
実行してみると怒られる。
$ g++ test2.cc
$ ./a.out
func 4
zsh: segmentation fault (core dumped) ./a.out
これは、メモリ領域が保護されているから。で、どのくらい保護されているのか調べてみる。それには、/proc/sys/kernel/exec-shieldの値を調べる。
$ cat /proc/sys/kernel/exec-shield
1
「1」という値の意味は「データ実行を原則許可。execstack を用いて個別に禁止できる」なので、「一切禁止」にはなっていない。 個々のプログラムに対する設定はexecstacコマンドで設定できる。
$ execstack -s ./a.out; ./a.out
func 4
fp 4
できた。
中身の書き換え
関数の実体は単なるバイト列なので、そこを書き換えれば動作を変化させることができる。たとえばfuncの「1を加える」という動作の「1」は13バイト目に即値で入っているので、mem[12]を書き換えれば好きな値に変更できる。「2」を足すように動作を変えてみる。
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
int
func(int a){
return a+1;
}
int
main(void){
size_t s = 15;
char *mem = (char *)malloc(s);
memcpy(mem, (char *)(func),s);
int (*fp)(int ) = (int (*)(int))(mem);
mem[12] = 2; // Add 2 instead of 1
printf("func %d\n",func(3));
printf("fp %d\n",fp(3));
}
$ g++ test3.cc
$ execstack -s ./a.out; ./a.out
func 4
fp 5
無事に動作が変わった。12バイト目の「c0」を「e8」に変えればaddがsubになる。
(snip)
int
main(void){
size_t s = 15;
char *mem = (char *)malloc(s);
memcpy(mem, (char *)(func),s);
int (*fp)(int ) = (int (*)(int))(mem);
mem[11] = 0xe8; // Replace add with sub
printf("func %d\n",func(3));
printf("fp %d\n",fp(3));
}
$ g++ test4.cc
$ execstack -s ./a.out; ./a.out
func 4
fp 2
ちゃんと加算が減算になった。
直接バイト列を命令として解釈する。
関数の実体はバイト列なんだから、別の既存の関数からコピーしなくても、直接バイト列を与えて実行することもできる。
# include <stdio.h>
int
main(void){
char v[15] = {0x55,0x48,0x89,0xe5,0x89,0x7d,0xfc,0x8b,0x45,0xfc,0x83,0xc0,0x01,0xc9,0xc3};
int (*fp)(int a) = (int (*)(int))(v);
printf("fp(3)=%d\n",fp(3));
}
$ g++ test5.cc
$ execstack -s ./a.out; ./a.out
fp(3)=4
関数そのものを書き換えることができるか
関数のバイト列のコピーではなくて、関数そのものを書き換えてみる。
# include <stdio.h>
# include <stdlib.h>
int
func(int a){
return a+1;
}
int
main(void){
size_t s = 15;
char *mem = (char *)(func);
int (*fp)(int ) = (int (*)(int))(mem);
mem[12] = 2;
printf("fp %d\n",fp(3));
}
$ g++ test6.cc
$ ./a.out
zsh: segmentation fault (core dumped) ./a.out
これは怒られる。もちろんexecstackも効かない。execstackはあくまでデータ領域を実行させない仕組みであり、命令領域をread onlyにする機構はまた別だから。実際には命令領域は .text セグメント(code segment)に配置され、ロードされるときに読み込み専用のフラグがつけられてしまうから。あくまでOS側の仕組みなので、処理系や条件によっては実行できたりもする。
おわりに
データ領域をいじって遊んでみた。多分なんの役にも立たないけれど、プログラムもバイト列にすぎないことが実感できたし、exec-shiledや.text セグメントといったメモリ領域保護の仕組みとかわかって勉強にはなった。