自己書き換えコード(self-modifying code)
自己書き換えコードとは, 実行時に自分自身の命令を書き換えるコードのことである.
以下のコードでは, foo()
関数の i++ の命令を i += 2に自己書き換えしている.
順に説明していくと, まず mprotect()
関数でfoo関数の命令が書かれているページに読み, 書き, 実行の権限を与える. foo()
関数を書き換えるので, 書き換え権限を与えなければならない. 通常セキュリティーの観点から関数などのコードが書かれている領域は書き込み権限がない.
次に, 普通に foo()
を呼び出した後, foo()
関数の先頭から 18
byte進んだ位置のbyteに 0x2
を書き込んでいる. これは foo()
のアセンブラを見ればわかるように, 18
byteのところが add DWORD PTR [rbp-0x4],0x1
の 0x1
に対応しているので, そこを 0x2
に書き換えている.
これで自己書き換えができたので, その後に foo を呼び出すと出力結果が変わる.
プログラム
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/mman.h> void foo() { int i = 0; i++; printf("i: %d\n", i); } /* 000000000000072a <foo>: 72a: 55 push rbp 72b: 48 89 e5 mov rbp,rsp 72e: 48 83 ec 10 sub rsp,0x10 732: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0 739: 83 45 fc 01 add DWORD PTR [rbp-0x4],0x1 73d: 8b 45 fc mov eax,DWORD PTR [rbp-0x4] 740: 89 c6 mov esi,eax 742: 48 8d 3d 4b 01 00 00 lea rdi,[rip+0x14b] # 894 <_IO_stdin_used+0x4> 749: b8 00 00 00 00 mov eax,0x0 74e: e8 8d fe ff ff call 5e0 <printf@plt> 753: 90 nop 754: c9 leave 755: c3 ret */ int main() { int page_size = getpagesize(); void *foo_addr = (void*)foo; printf("foo_addr: %p\n", foo_addr); void* page_addr = (void*)((unsigned long)(foo_addr) & ~(page_size - 1)); printf("page_addr: %p\n", page_addr); mprotect(page_addr, page_size, PROT_READ|PROT_WRITE|PROT_EXEC); printf("Call original foo()\n"); foo(); // Change code // add DWORD PTR [rbp-0x4],0x1 => add DWORD PTR [rbp-0x4],0x2 ((unsigned char*)foo_addr)[18] = 0x2; printf("Call modified foo()\n"); foo(); return 0; }
出力結果
foo_addr: 0x555992c1672a page_addr: 0x555992c16000 Call original foo() i: 1 Call modified foo() i: 2