機械語を配列に格納して関数として実行するだけの簡単なお勉強を CentOS 5 で using mprotect()
配列にマシン語を直接格納して関数として実行する、っていうのをバリケンさんの『バリケンのRuby日記 - バイナリアン的C言語入門(2) - 整数の配列で関数を作ってみる』を見ながらやってみたが、見事にSEGVで死んでしまうのでいろいろ試行錯誤した記録。3時間くらいかかった。死んだ。
結論:
Binary Hacksを読めばいい。持ってないので明日買いに行く。
説明
バリケンさんのエントリに書かれていることは、Linuxのカーネル2.4系やWindowsXPでは問題なく実行できる。だけど、CentOS に搭載されているLinux カーネル 2.6から、PaX という機能がカーネルにマージされ、ハードウェアのNXビット(もしくはそのエミュレーション機能)が実装されている。
このため、プロセスの実行イメージのデータセグメントにあるデータを関数として実行しようとすると、SEGVでプロセスが殺されてしまう。そこで、これはシステムコールのmprotect(2)を使えばこの制限を解除することができ、めでたく(?)配列を関数として実行することができる。
手順とコード
まず、intへのポインタを受け取って、その値をインクリメントする関数succを定義しよう。
void succ(int* a) { ++*a; }
これを、gcc の -c オプションでオブジェクトファイルにする。これを succ.o として、このオブジェクトファイルからsuccの機械語での定義を取り出そう。
$ objdump -D succ.o | grep "succ>:" -A 10 00000000 <succ>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 8b 45 08 mov 0x8(%ebp),%eax 6: 8b 00 mov (%eax),%eax 8: 8d 50 01 lea 0x1(%eax),%edx b: 8b 45 08 mov 0x8(%ebp),%eax e: 89 10 mov %edx,(%eax) 10: 5d pop %ebp 11: c3 ret
ここでの-A 10という数字はコンパイラの種類・バージョン、OSの種類・バージョン等によってさまざまに異なると思うので、まぁ多めの数値を指定して問題ない。隣(次)の関数も見えてしまっても別に害はないし。
さて、これでsuccの機械語の定義が手に入ったので、これを使って以下のCコードを書いてコンパイルして実行すればOKなはず!
#include<stdio.h> #include<string.h> #include<limits.h> #include<sys/mman.h> #include<stdlib.h> #include<unistd.h> #include<errno.h> typedef unsigned char uchar; void succ(int* a) { ++*a; } void dump_memory(void* _p, int len) { uchar* p = (uchar*) _p; int i; for(i=0; i<len; i++) { printf("%02x ", p[i]); } printf("\n"); } int main(int argc, char* argv[]) { int a = 0; uchar func[] = { 0x55, /* push %ebp */ 0x89, 0xe5, /* mov %esp,%ebp */ 0x8b, 0x45, 0x08, /* mov 0x8(%ebp),%eax */ 0x8b, 0x00, /* mov (%eax),%eax */ 0x8d, 0x50, 0x01, /* lea 0x1(%eax),%edx */ 0x8b, 0x45, 0x08, /* mov 0x8(%ebp),%eax */ 0x89, 0x10, /* mov %edx,(%eax) */ 0x5d, /* pop %ebp */ 0xc3, /* ret */ }; /* Binary Hacks に載ってる(らしい) */ long pagesize = sysconf(_SC_PAGESIZE); char* p = (void*)(((int) func + pagesize-1) & ~(pagesize-1)); if(mprotect(p, pagesize*10L, PROT_READ | PROT_WRITE | PROT_EXEC)) { fprintf(stderr, "mprocet(2) failed. errno = %d\n", errno); exit(1); } dump_memory((void*)&succ, sizeof(func)); dump_memory(func,sizeof(func)); succ(&a); printf("%d\n", a); ((void(*)(int*))func)(&a); printf("%d\n", a); return 0; }
これを実行すれば、
$ gcc -c -Wall succ.c && ./a.out 55 89 e5 8b 45 08 8b 00 8d 50 01 8b 45 08 89 10 5d c3 55 89 e5 8b 45 08 8b 00 8d 50 01 8b 45 08 89 10 5d c3 1 2
となるはず。関数を強制的にcharの配列とみなしてダンプした結果と、当たり前だが同じ内容になっている。
うわぁ・・コンピューターの中・・ バイナリ
Binary Hacks ―ハッカー秘伝のテクニック100選 | |
高林 哲 オライリー・ジャパン 2006-11-14 売り上げランキング : 8870 おすすめ平均 ハードコア?なソフトウエア 大工さんにおける電動工具の紹介本 当然教科書ではない。でも、とても参考になります。 Amazonで詳しく見る by G-Tools |