Dans ce post je vais vous expliquer le concept de Trust Zone ainsi que les niveaux d’exception (Exception Levels) sur les processeurs de type aarch64. Ainsi je parlerai de KPP, de son extraction jusqu’à son analyse puis en expliquant quelques techniques utilisées pour bypass cette nouvelle sécurité d’Apple pour les appareils qui tournent sous iOS et tvOS. Pour finir je vais parler de KTRR la nouvelle sécurité hardware remplaçant KPP

Secure World

Avant de parler de KPP il faut définir ce qu’est le Secure World , la TrustZone et les niveaux d’exception des plateformes ARM.

Exception Levels

Si vous connaissez un peu l’architecture x86 vous connaissez surement les rings qui définissent les niveaux de privilèges imposés par l’architecture du processeur.

Ces mêmes niveaux sont appliqués aux processeurs ARM mais d’une façon un peu différente. Tout d’abord on appelle ça les niveaux d’exception (Exception Levels en anglais, ou EL).

img_from_quarkslab

D’après le schéma ci-dessus, voici à quoi correspondent les différents niveaux pour iOS:

  • EL0 : Applications et userland.
  • EL1 : le kernel et les différents services de l’OS ayant les mêmes privilèges.
  • EL2 : pas d’hyperviseur présent pour les appareils mobiles Apple donc EL2 n’est pas utilisé
  • EL3 : Secure monitor ou BootROM le premier composant iOS qui démarre.

Contrairement aux rings de l’architecture x86, le niveau le plus privilégié pour aarch64 est EL3. Seul le niveau 3, c’est à dire le Secure Monitor peut accéder au Secure World qui est en fait un espace mémoire protégé.

Trust Zone

La TrustZone est une technologie pour les Systèmes sur Puce (SoC) permettant une approche global au niveau de la sécurité des CPUs. C’est une sécurité hardware implémentée dans les SoC par les fabricants de puces. Le concept est simple: fournir un environnement d’exécution de confiance, en anglais Trusted Execution Environment (TEE), en séparant l’OS du Secure World.

La TrustZone va créer un environnement sécurisé et isolé du reste du système d’exploitation, puis vérifiera l’intégrité de l’OS.

Le système d’exploitation principal tourne en fait dans le normal world. C’est devenu un standard pour le renforcement de la sécurité des téléphones.

KPP

Avec l’arrivée d’iOS 9 en 2015, Apple a introduit une nouvelle fonctionnalité pour veiller à l’intégrité du noyau d’iOS : Watch Tower. Surnommé Kernel Patch Protection (ou KPP) par la communauté des développeurs de jailbreak iOS.

Watch Tower est une nouvelle protection contre les jailbreaks qui vérifie régulièrement les hashes des pages mémoire de type __TEXT et __DATA.__const dans le kernel.

KPP tourne au niveau d’exception 3 (EL3). Tout comme les applications en userland (EL0) ne peuvent pas accéder à la mémoire du kernel (EL1), le kernel ne peut donc pas accéder à la mémoire de KPP.

Kernel Patch Protection est présent uniquement sur les appareils 64 bits car ceux-ci possèdent une Trust Zone. Néanmoins sur les nouveaux appareils tels que l’iPhone 7, 8 et X il n’y a pas KPP.

Extraction de KPP

KPP est un fichier mach-o, c’est fichier exécutable pour ARM64. Ce fichier se trouve dans le kernelcache qui est au format img4.

A l’aide de joker on va pouvoir décompresser un kernelcache et y extraire KPP. Pour ce post je vais utiliser un kernelcache d’iPhone 5S sous iOS 11.0.

$ joker -dec kernelcache.release.iphone6
mmapped: 0x7f9ce7fee000
still HERE
Feeding me a compressed kernelcache, eh? That is fine, now. I can decompress! 
Compressed Size: 12988493, Uncompressed: 25411584. Unknown (CRC?): 0x59659f28, Unknown 1: 0x1
btw, KPP is at 12988928 (0xc63200)..And I saved it for you in /tmp/kpp
Got kernel at 436
got mem 0x7f9ce67b1010
mmapped: 0x7f9ce67b1010
This is a 64-bit kernel from iOS 11.x (b1+), or later (4570.2.5.0.0)
ARM64 Exception Vector is at file offset @0x83000 (Addr: 0xfffffff007087000)

En plus de la décompression du kernelcache, joker va extraire KPP vers le répertoire /tmp . Vous devriez avoir deux nouveaux fichiers dans ce répertoire : kpp et kernel.

Il ne reste plus qu’à vérifier que kpp est bel et bien un exécutable mach-o à l’aide de jtool :

$ jtool -h kpp
Magic:	64-bit Mach-O
Type:	executable
CPU:	ARM64
Cmds:	8
size:	1120 bytes
Flags:	0x1

Analyse de KPP

A l’aide de jtool on liste les sections. KPP est chargé par l’iBoot à l’adresse mémoire 0x4100000000 .

$ jtool -l -v kpp | grep LC_SEGM
LC 00: LC_SEGMENT_64    Mem: 0x4100000000-0x4100006000	File: 0x0-0x6000	r-x/r-x	__TEXT
LC 01: LC_SEGMENT_64    Mem: 0x4100006000-0x410000b000	File: Not Mapped	rw-/rw-	__DATA
LC 02: LC_SEGMENT_64    Mem: 0x410000b000-0x410000b000	File: Not Mapped	rw-/rw-	__IMAGEEND
LC 03: LC_SEGMENT_64    Mem: 0x410000b000-0x410000b000	File: Not Mapped	r--/r--	__LINKEDIT

A son démarrage (_start(boot_args * args)), KPP va recevoir comme argument la structure boot_args définie dans pexpert/pexpert/arm64/boot.h de XNU :

typedef struct boot_args {
	uint16_t		Revision;			/* Revision of boot_args structure */
	uint16_t		Version;			/* Version of boot_args structure */
	uint64_t		virtBase;			/* Virtual base of memory */
	uint64_t		physBase;			/* Physical base of memory */
	uint64_t		memSize;			/* Size of memory */
	uint64_t		topOfKernelData;	/* Highest physical address used in kernel data area*/
	Boot_Video		Video;				/* Video Information */
	uint32_t		machineType;		/* Machine Type */
	void			*deviceTreeP;		/* Base of flattened device tree */
	uint32_t		deviceTreeLength;	/* Length of flattened tree */
	char			CommandLine[BOOT_LINE_LENGTH];	/* Passed in command line */
	uint64_t		bootFlags;			/* Additional flags specified by the bootloader */
	uint64_t		memSizeActual;		/* Actual size of memory */
} boot_args;

Puis WatchTower va ainsi réécrire son propre en-tête à 0x4100000000 à l’aide d’un trampoline qui va appeler la même fonction _start mais avec NULL comme argument. Cela lui permet d’installer deux gestionnaires d’exception :

  • sync_handler (+0x400)
  • irq_handler (+0x480)

Lorsqu’une exception se produit, le processeur se branche sur la table de vecteurs d’exception et exécute le gestionnaire (ou handler en anglais) correspondant. Avec l’architecture ARMv8, chaque niveau d’exception possède sa propre table de vecteurs

img_from_quarkslab2

Grâce à la fonction ci-dessous, dès le démarrage du noyau, KPP notifie le moniteur au niveau EL3. C’est à ce moment que KPP va s’initialiser et vérifiera l’intégrité du texte du noyau, des données en lecture seule ainsi que les pages mémoire du kernel.

KPP vérifie l’intégrité des segments de mémoire suivant :

  • __Text

  • __DATA_CONST

Mais aussi les tables de pages mémoire, ainsi que plusieurs registres systèmes.

void machine_lockdown(void)
{
#if CONFIG_KERNEL_INTEGRITY
#if KERNEL_INTEGRITY_WT
	/* Watchtower
	 *
	 * Notify the monitor about the completion of early kernel bootstrap.
	 * From this point forward it will enforce the integrity of kernel text,
	 * rodata and page tables.
	 */  
#ifdef MONITOR
	monitor_call(MONITOR_LOCKDOWN, 0, 0, 0);
#endif
#endif /* KERNEL_INTEGRITY_WT */
[...]
#endif /* CONFIG_KERNEL_INTEGRITY */
}

La fonction ci-dessous gère les erreurs de KPP. Dans le cas ou vous n’avez pas compilé le noyau vous-même ou que vous n’avez pas mis la main sur un kernel compilé en mode DEBUG ou DEVELOPMENT il ne vous sera pas possible de debugger KPP.

#if CONFIG_KERNEL_INTEGRITY
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
static void
kernel_integrity_error_handler(uint32_t esr, vm_offset_t far) {
#if defined(KERNEL_INTEGRITY_WT)
#if (DEVELOPMENT || DEBUG)
	if (ESR_WT_SERROR(esr)) {
		switch (ESR_WT_REASON(esr)) {
		case WT_REASON_INTEGRITY_FAIL:
			panic_plain("Kernel integrity, violation in frame 0x%016lx.", far);
		case WT_REASON_BAD_SYSCALL:
			panic_plain("Kernel integrity, bad syscall.");
		case WT_REASON_NOT_LOCKED:
			panic_plain("Kernel integrity, not locked.");
		case WT_REASON_ALREADY_LOCKED:
			panic_plain("Kernel integrity, already locked.");
		case WT_REASON_SW_REQ:
			panic_plain("Kernel integrity, software request.");
		case WT_REASON_PT_INVALID:
			panic_plain("Kernel integrity, encountered invalid TTE/PTE while "
				"walking 0x%016lx.", far);
		case WT_REASON_PT_VIOLATION:
			panic_plain("Kernel integrity, violation in mapping 0x%016lx.",
				far);
		case WT_REASON_REG_VIOLATION:
			panic_plain("Kernel integrity, violation in system register %d.",
				(unsigned) far);
		default:
			panic_plain("Kernel integrity, unknown (esr=0x%08x).", esr);
		}
	}
#else
	if (ESR_WT_SERROR(esr)) {
		panic_plain("SError esr: 0x%08x far: 0x%016lx.", esr, far);
	}
#endif
#endif
}

Dans tous les cas ces erreurs entraineront un kernel panic d’après la définition préprocesseur.

#define panic_plain(ex, ...)  (panic)(ex, ## __VA_ARGS__)

Avec un kernel de developpement, il vous suffit de l’ouvrir de un désassembleur, on voit que les chaines de caractères correspondent bien.

idakppcool

Ce noyau correspond à celui d’iOS 9.0 et KPP a beaucoup de similarités par rapport à iOS 11.0

KPP bypass

Premièrement il faut savoir que KPP ne stoppe pas les exploits mais les patches appliqués au noyau d’iOS. La raison pour laquelle KPP est si important c’est qu’il empêche d’appliquer les patches pour utiliser Cydia Substrate, le framwork permettant d’ecrire et d’appliquer des extensions pour iOS, aussi appelés tweaks.

Pangu

Le premier jailbreak d’iOS 9.0 a été publié par Pangu. La team Pangu n’a pas concrètement bypass KPP, mais l’a plutôt contourné. En effet ils ont décidé de ne pas patcher __TEXT mais plutôt certaines structures de données. Il leur a fallu appliquer les patches le plus tôt après le chargement du kernel en mémoire et avant le lancement de KPP.

Ces patches sont considérés comme “Data Only”.

Yalu

Le jailbreak de Luca Todesco, aka qwertyoruiop bypass entièrement KPP.

En effet le code injecté par son exploit remplace l’instruction MSR CPACR_EL1, X0 par un appel vers kppsh1. Ensuite la valeur actuelle du registre système TTBR1_EL1 est sauvegardée vers le registre X1. Puis charge la valeur originale dans X0 et écrase TTBR1_EL1 . La valeur de CPACR_EL1 est basculée en exécutant l’instruction écrasée et ainsi en invoquant KPP

Pour info Xn sont des registres de l’ABI iOS, l’équivalent de Rn pour les architectures x86.

Tout ça à l’aide de la fonction WriteAnywhere32 qui permet d’appeler les fonctions en kernel mode.

WriteAnywhere32(kppsh+n, 0x580001e1); n+=4; // ldr	x1, #60
WriteAnywhere32(kppsh+n, 0x58000140); n+=4; // ldr	x0, #40
WriteAnywhere32(kppsh+n, 0xd5182020); n+=4; // msr	TTBR1_EL1, x0
WriteAnywhere32(kppsh+n, 0xd2a00600); n+=4; // movz	x0, #0x30, lsl #16
WriteAnywhere32(kppsh+n, 0xd5181040); n+=4; // msr	CPACR_EL1, x0
WriteAnywhere32(kppsh+n, 0xd5182021); n+=4; // msr	TTBR1_EL1, x1
WriteAnywhere32(kppsh+n, 0x10ffffe0); n+=4; // adr	x0, #-4
WriteAnywhere32(kppsh+n, isvad ? 0xd5033b9f : 0xd503201f); n+=4; // dsb ish (4k) / nop (16k)
WriteAnywhere32(kppsh+n, isvad ? 0xd508871f : 0xd508873e); n+=4; // tlbi vmalle1 (4k) / tlbi vae1, x30 (16k)
WriteAnywhere32(kppsh+n, 0xd5033fdf); n+=4; // isb
WriteAnywhere32(kppsh+n, 0xd65f03c0); n+=4; // ret
WriteAnywhere64(kppsh+n, ReadAnywhere64(ttbr0_real)); n+=8;
WriteAnywhere64(kppsh+n, physp); n+=8;
WriteAnywhere64(kppsh+n, physp); n+=8;

Cette méthode consiste simplement à appliquer une technique physique de “Copy on Write” : on laisse la page physique originale et non-modifiée pointée par le registre TTBR1_EL1 original. Puis on alloue une nouvelle page physique à modifier et pointée par le registre TTBR1_EL1 actuel.

Après ça le code de KPP qui tourne dans EL3 cherche à vérifier la valeur de TTBR1_EL1 et trouvera la valeur originale qui a été sauvegardée plus tôt.

Les tables de pages mémoire pointées par TTBR1_EL1 sont en fait, celles d’origine utilisées par le noyau lors de son démarrage, et n’ont pas été modifiées.

Cela permet ainsi de cacher de KPP les pages du noyau modifiées.

Apple n’a pas corrigé cette méthode puisqu’en soit ils ne peuvent pas. C’est une faille dans la conception de KPP donc il n’y a pas de manière viable de corriger ça pour si ce n’est mise à part une mise à jour materielle de l’iPhone.

KPP less

Début octobre, xerub à annoncé avoir ajouté une branche sur son fork du jailbreak extra_recipe nommé kppless.

Tout d’abord, contrairement à Yalu, ce n’est pas un bypass, mais juste une méthode pour patcher les zones du kernel non contrôlées par KPP.

De plus Apple peut ajouter des zones de contrôle via une simple mise à jour.

La méthode de fonctionnement de kppless est assez simple :

Lors que l’exploit est exécuté avec succès, celui-ci peut ainsi patcher le kernel avant que KPP ne fasse une quelconque vérification. Grâce à cette méthode, Watch Tower est toujours présent, mais ne détecte rien de mauvais dans le kernel.

Le fait est qu’avec cette branche, xerub monte un nouveau volume ou sont stockés les exécutables pour iOS ainsi qu’un serveur SSH.

Ce volume est monté dans le répertoire /Developer comme ceci : rv = mount("hfs", "/Developer", MNT_RDONLY, &args);.

Je vous passe les commandes d’initialisation.

Pour ce qui est de l’utilisation de Cydia et Substrate, il faut que ces technologies soient repensées. Elles ont maintenant 10 ans et il serait temps de les moderniser.


Il reste une dernière méthode pour contourner, que dis-je, pour désactiver KPP : un exploit dans la chaine de démarrage.

En imaginant que vous contrôlez l’iBoot, qui charge lui même le kernel XNU et KPP, vous pouvez l’empêcher de démarrer KPP ou bien même le modifier de façon à ce qu’il ne soit actif ou ne vérifie pas les pages que vous modifiez.

Ceci est possible car l’iBoot tourne au niveau d’exécution 3 (EL3), il a donc les même privilèges que KPP et peux accéder au Secure World dont nous avons parlé plus haut.

Personne n’a encore parlé de cette méthode publiquement puisqu’en fait il y a très peu de chances qu’un exploit pour l’iBoot/LLB ou la BootROM soit publié, ces choses-là ont beaucoup trop de valeur.

Mais je suis sûr que ça a été utilisé en privé.

KTRR

L’iPhone 7 est arrivé avec une nouvelle parade qui, comme KPP, vérifie certaines pages mémoire du kernel mais empêche aussi d’écrire dans certaines région de la mémoire. Longtemps nommée AMCC ou SiDP en fait ces deux fonctionnalités n’ont rien à voir avec l’implémentation sur les nouveaux iPhones.

Cette mitigation remplace KPP dans la vérification de l’intégrité du kernel, celle-ci est maintenant contrôlée par le hardware sur les iPhone 7, 8 et X. Il semble que les capacités de protection de Watch Tower ai été atteintes.

Grâce à la publication du code source du noyau XNU pour iOS on a maintenant le vrai nom de cette mitigation : KTRR (Kernel Text Readonly Region ?)

L’image ci-dessous illustre KTRR tel qu’on l’a découvert à la sortie de l’iPhone 7

ktrr_from_ida

KTRR est un mechanisme hardware qui renforce l’intégrité du kernel. Ce mechanisme permet à l’appareil de fournir des régions de mémoire en lecture seule. C’est régions sont programmées en utilisant des registres hardwares spéciaux définis dans le header AMCC.h.

Cette nouvelle technologie verrouille la MMU en définissant 3 registres controleurs au niveau hardware.

Voici la fonction de KTRR ci-dessous. Comme vous le voyez, si compilez le kernel en mode DEVELOPMENT ou DEBUG vous pouvez désactiver KTRR en modifiant la variable de type booléen ktrr_disable .

void rorgn_lockdown(void)
{
	vm_offset_t ktrr_begin, ktrr_end;
	unsigned long plt_segsz, last_segsz;

#if DEVELOPMENT || DEBUG
	boolean_t ktrr_disable = FALSE;

	PE_parse_boot_argn("-unsafe_kernel_text", &ktrr_disable, sizeof(ktrr_disable));

	if (ktrr_disable) {
		/*
		 * take early out if boot arg present, since we may not have amcc DT entry present
		 * we can't assert that iboot hasn't programmed the RO region lockdown registers
		 */
		goto out;
	}
#endif /* DEVELOPMENT || DEBUG */

	assert_unlocked();

	/* [x] - Use final method of determining all kernel text range or expect crashes */

	ktrr_begin = (uint64_t) getsegdatafromheader(&_mh_execute_header, "__PRELINK_TEXT",         	&plt_segsz);
	assert(ktrr_begin && gVirtBase && gPhysBase);

	ktrr_begin = kvtophys(ktrr_begin);

	/* __LAST is not part of the MMU KTRR region (it is however part of the AMCC KTRR region) */
	ktrr_end = (uint64_t) getsegdatafromheader(&_mh_execute_header, "__LAST", &last_segsz);
	ktrr_end = (kvtophys(ktrr_end) - 1) & ~PAGE_MASK;

	/* ensure that iboot and xnu agree on the ktrr range */
	assert(rorgn_begin == ktrr_begin && rorgn_end == (ktrr_end + last_segsz));
	/* assert that __LAST segment containing privileged insns is only a single page */
	assert(last_segsz == PAGE_SIZE);

#if DEBUG
	printf("KTRR Begin: %p End: %p, setting lockdown\n", (void *)ktrr_begin, (void *)ktrr_end);
#endif

	/* [x] - ensure all in flight writes are flushed to AMCC before enabling RO Region Lock */

	assert_amcc_cache_disabled();

	CleanPoC_DcacheRegion_Force(phystokv(ktrr_begin),
		(unsigned)((ktrr_end + last_segsz) - ktrr_begin + PAGE_MASK));

	lock_amcc();

	lock_mmu(ktrr_begin, ktrr_end);

#if DEVELOPMENT || DEBUG
out:
#endif

	/* now we can run lockdown handler */
	ml_lockdown_run_handler();
}

#endif /* defined(KERNEL_INTEGRITY_KTRR)*/

Toutes les instructions exécutées depuis EL1 vers les zones protégées echoueront.

On sait clairement que KTRR verrouille la MMU, mais la vérification qui est mise en oeuvre par le contrôleur est encore flou.

KTRR Bypass

En théorie, si l’on trouve une faille dans le design de l’implémentation de KTRR, il devient impossible pour Apple de corriger le bug. Mais non.

Les primitives de KTRR viennent du composant hardware et ne sont pas modifiables. Mais il est possible de changer la façon dont elles sont utilisées ou appelées. C’est comme ça qu’Apple a corrigé le bypass de Luca Todesco avec la mise à jour d’iOS 10.2.

Il est tout à fait possible de faire un copy-on-write mais impossible d’injecter du code.

La technique appliquée par Todesco rend les patches “DATA Only”, ce qui veut dire que kloader ne peut pas être compatible pour iPhone 7 (pour l’instant).


Gal Beniamini du Google Project Zero a utilisé KTRR pour déterminer l’adresse physique de base du kernel qui est normalement protégée par KASLR. Il est possible de mapper les registres materiels car ceux-ci ne sont pas affectés par KASLR puisqu’ils sont définis sur des addresses mémoire fixes. On peut donc les utiliser pour déterminer l’addresse du noyau comme ceci :

# Finds the kernel's physical base address by reading the KTRR readonly-region registers.
def find_kernel_base(dart_offset):
    rorgn_begin_addr = PE_SOC_BASE_PHYS + RORGN_BEGIN_OFFSET
    rorgn_begin = read_host_dword(rorgn_begin_addr, dart_offset)
    kernel_base = (G_PHYS_BASE + (rorgn_begin << 14))
    return kernel_base 

La méthode kppless de xerub qui consiste à patcher le noyau XNU dans des zones non-vérifiées par KPP/KTRR fonctionne. Elle a permis de rendre le jailbreak extra_recipe fonctionnel pour l’iPhone 7 sous iOS 10.2.

KTRR est configurée pour protéger une partie de la mémoire physique, mais il est possible pour Apple de modifier KTRR via une mise à jour pour étendre le champ de vérification et modifier le kernel pour déplacer la mémoire non vérifiée vers une zone vérifiable.

A l’inverse des appareils pré-iPhone 7 un exploit iBoot ne peut pas désactiver KTRR. L’iBoot tourne désormais dans EL1 sur les nouveaux appareils et non plus dans EL3.

Conclusion

Pour conclure, tout ça c’est bien beau mais ces parades n’affectent que les utilisateurs de jailbreaks. En effet en prenant comme exemple le spyware Pegasus, il n’y a pas besoin de patches ni de bypass KPP.

Les malwares comme Pegasus ont uniquement besoin des données qui transitent en userland.

Des dizaines de failles sont dévoilées tous les ans pour les plus récentes versions d’iOS, avec des PoCs de type local privilege escalation mais sans appliquer les patches post-exploitation qui sont utiles uniquement pour les utilisateurs finaux de jailbreak.

Etonnement Apple n’a pas corrigé le KPP bypass de Luca Todesco sous une quelconque version d’iOS 10.

Pour l’instant la seule sécurité qui ai fait ses preuves c’est la sandbox.


Si vous avez besoin d’infos contactez-moi sur twitter: @matteyeux

Github : https://github.com/matteyeux


Sources :

iOSRE

https://elinux.org/Overwrite_detection_for_kernel_text_and_read-only_data

https://developer.arm.com/technologies/trustzone

http://infocenter.arm.com/help/topic/com.arm.doc.prd29-genc-009492c/PRD29-GENC-009492C_trustzone_security_whitepaper.pdf

https://github.com/apple/darwin-xnu

https://xerub.github.io/ios/kpp/2017/04/13/tick-tock.html

https://newosxbook.com

https://www.blackhat.com/docs/us-16/materials/us-16-Wang-Pangu-9-Internals.pdf

https://googleprojectzero.blogspot.fr/2017/10/over-air-vol-2-pt-3-exploiting-wi-fi.html