Aller au contenu


Photo

TegraRCM (aka ShofEL2 ou Fusée gelée) : plus d'explications sur l'exploit


  • Veuillez vous connecter pour répondre
1 réponse à ce sujet

Posté 07 mai 2018 - 12:42

#1
eliboa

eliboa

    Développeur

  • Members
  • PipPipPipPipPip
  • 2 112 messages
  • Sexe:Male

Salut. Voici un article que j'ai publié sur mon blog récemment et que je recopie ici pour ceux qui veulent en savoir plus sur le fonctionnement de l'exploit bootROM récemment rendu public.

 

Fin avril (2018) la team fail0verflow a liberé son exploit ShofEL2 permettant de lancer Linux sur une console Nintendo Switch (toutes versions de FW confondues). Le même jour, Kate Temkin de la team ReSwitched libérait le lanceur de payload nommé "Fusée Gelée". Ces deux exploit n'en sont en fait qu'un seul, ils utilisent même faille située dans la BootROM du SoC Tegra X1 de la Nintendo Switch (pour rappel la console utilise une puce Tegra de NVIDIA). Les deux teams ont liberé leur exploit suite au leak du code source (comme bien souvent).

 

L'exploit utilise une vulnérabilité du mode recovery de la Switch (appelé mode RCM) qui permet d'utiliser la technique, très répandue dans le monde du hack, du dépassement de tampon (buffer overflow). Voici quelques explications fournies par Kate Temkin que j'ai traduites et synthétisées.

 

Le coreboot du processeur Tegra X1 applique le pseudo code suivant au démarrage, obtenu après reverse-engineering de la bootROM  :

// If this is a warmboot (from "sleep"), restore the saved state from RAM.
if (read_scratch0_bit(1)) {
  restore_warmboot_image( & load_addr);
}
// Otherwise, bootstrap the processor.
else {
  // Allow recovery mode to be forced by a PMC scratch bit or physical straps.
  force_recovery = check_for_rcm_straps() || read_scratch0_bit(2);
  // Determine whether to use USB2 or USB3 for RCM.
  determine_rcm_usb_version( & usb_version);
  usb_ops = set_up_usb_ops(usb_version);
  usb_ops - > initialize();
  // If we're not forcing recovery, attempt to load an image from boot media.
  if (!force_recovery) {
    // If we succeeded, don't fall back into recovery mode.
    if (read_boot_configuration_and_images( & load_addr) == SUCCESS) {
      goto boot_complete;
    }
  }
  // In all other conditions
  if (read_boot_images_via_usb_rcm( < snip > , & load_addr) != SUCCESS) {
    /* load address is poisoned here */
  }
}
boot_complete:
  /* apply lock-outs, and boot the program at address load_address */ 

.

On peut voir que le mode RCM peut être activé sous certaines conditions :

- Si le processeur n'arrive pas à trouver une BTC (Boot Control Table) valide
- Si une combinaison de touche est activée (PCM scratch bit of physical straps)
- Si le processeur est rebooté alors qu'une valeur spécifique a été écrite dans le registre du PMC (Power Managment Controller)

 

Le mode recovery est présent dans toutes les appareils équipés d'une puce Tegra X1 (pas uniquement les Switch donc) et permet donc d'injecter dans la RAM un bout de code signé (appelé applet ou miniloader) présent sur un media USB.
Evidemment ce mode RCM n'accepte que des commandes qui sont chiffrées via RSA ou AES-CMAC :

// Significantly simplified for clarity, with error checking omitted where
unimportant.
while(1) {
  // Repeatedly handle USB standard events on the control endpoint EP0.
  usb_ops - > handle_control_requests(current_dma_buffer);
  // Try to send the device ID over the main USB data pipe until we succeed.
  if (rcm_send_device_id() == USB_NOT_CONFIGURED) {
    usb_initialized = 0;
  }
  // Once we've made a USB connection, accept RCM commands on EP1.
  else {
    usb_initialized = 1;
    // Read a full RCM command and any associated payload into a global buffer.
    // (Error checking omitted for brevity.)
    rcm_read_command_and_payload();
    // Validate the received RCM command; e.g. by checking for signatures
    // in RSA or AES_CMAC mode, or by trivially succeeding if we're not in
    // a secure mode.
    rc = rcm_validate_command();
    if (rc != VALIDATION_PASS) {
      return rc;
    }
    // Handle the received and validated command.
    // For a "load miniloader" command, this sanity checks the (validated)
    // miniloader image and takes steps to prevent re-use of signed data not
    // intended to be used as an RCM command.
    rcm_handle_command_complete(...);
  }
}

.

La principale vulnérabilité se trouve dans la fonction rcm_read_command_and_payload, qui permet de lire les commande RCM et les données du payload via le dispositif d'accès USB (endpoint).  Plusieurs bugs sont présents dans cette fonction mais un est particulièrement exploitable : 

uint32_t total_rxd = 0;
uint32_t total_to_rx = 0x400;
// Loop until we've received our full command and payload.
while (total_rxd < total_to_rx) {
  // Switch between two DMA buffers, so the USB is never DMA'ing into the same
  // buffer that we're processing.
  active_buffer = next_buffer;
  next_buffer = switch_dma_buffers();
  // Start a USB DMA transaction on the RCM bulk endpoint, which will hopefully
  // receive data from the host in the background as we copy.
  usb_ops - > start_nonblocking_bulk_read(active_buffer, 0x1000);
  // If we're in the first 680-bytes we're receiving, this is part of the RCM
  // command, and we should read it into the command buffer.
  if (total_rxd < 680) {
    /* copy data from the DMA buffer into the RCM command buffer until we've
    read a full 680-byte RCM command */
    // Once we've received the first four bytes of the RCM command,
    // use that to figure out how much data should be received.
    if (total_rxd >= 4) {
      // validate:
      // -- the command won't exceed our total RAM
      // (680 here, 0x30000 in upper IRAM)
      // -- the command is >= 0x400 bytes
      // -- the size ends in 8
      if (rcm_command_buffer[0] >= 0x302A8 u || rcm_command_buffer[0] < 0x400 u || (rcm_command_buffer[0] & 0xF) != 8) {
        return ERROR_INVALID_SIZE;
      } else {
        left_to_rx = * ((uint32_t * ) rcm_command_buffer);
      }
    }
  }
  /* copy any data _past_ the command into a separate payload
  buffer at 0x40010000 */
  /* -code omitted for brevity - */
  // Wait for the DMA transaction to complete.
  // [This is, again, simplified to convey concepts.]
  while (!usb_ops - > bulk_read_complete()) {
    // While we're blocking, it's still important that we respond to standard
    // USB packets on the control endpoint, so do that here.
    usb_ops - > handle_control_requests(next_buffer);
  }
}

.

Il est important de comprendre qu'une commande RCM est lue 1) dans un buffer global et 2) à l'adresse cible (target load adress) avant même qu'une quelconque signature ne soit vérifiée ce qui laisse à tout intrus une porte grande ouverte à une partie conséquente de la mémoire (non validée).

Pour trouver la vulnérabilité il faut s'enfoncer encore plus profondément dans le code, dans la partie qui gère les demandes de contrôle USB (handle_control_requests). Une "control request" est initiée lorsqu'un hôte envoie une commande dont la structure est décrite comme ceci :

 

tuto15.jpg

 

 

Par exemple, l'hôte peut envoyer une demande pour connaître le statut de l'appareil en envoyant une commande GET_STATUS à laquelle il se verra répondre par un paquet dont la taille maximale est définie par le champ "length". Selon la commande envoyée, l'appareil devrait donc retourner un paquet dont la taille maximale est soit la taille indiquée par ce champ soit la taille maximale de données disponibles qui correspondent à la réponse (logique). Admettons qu'il s'agisse d'un statut, ça ne devrait pas prendre plus d'un ou deux octets donc même si l'hôte indique 32b pour le champ "length", le réponse ne devrait pas être supérieure à 1b ou 2b.

 

L'implémentation de ce comportement dans le bootloader peut être décrite comme ceci :

// Temporary, automatic variables, located on the stack.
uint16_t status;
void * data_to_tx;
// The amount of data available to transmit.
uint16_t size_to_tx = 0;
// The amount of data the USB host requested.
uint16_t length_read = setup_packet.length;
/* Lots of handler cases have omitted for brevity. */
// Handle GET_STATUS requests.
if (setup_packet.request == REQUEST_GET_STATUS) {
  // If this is asking for the DEVICE's status, respond accordingly.
  if (setup_packet.recipient == RECIPIENT_DEVICE) {
    status = get_usb_device_status();
    size_to_tx = sizeof(status);
  }
  // Otherwise, respond with the ENDPOINT status.
  else if (setup_packet.recipient == RECIPIENT_ENDPOINT) {
    status = get_usb_endpoint_status(setup_packet.index);
    size_to_tx = length_read; // <-- This is a critical error!
  } else {
    /* ... */
  }
  // Send the status value, which we'll copy from the stack variable 'status'.
  data_to_tx = & status;
}
// Copy the data we have into our DMA buffer for transmission.
// For a GET_STATUS request, this copies data from the stack into our DMA buffer.
memcpy(dma_buffer, data_to_tx, size_to_tx);
// If the host requested less data than we have, only send the amount requested.
// This effectively selects min(size_to_tx, length_read).
if (length_read < size_to_tx) {
  size_to_tx = length_read;
}
// Transmit the response we've constructed back to the host.
respond_to_control_request(dma_buffer, length_to_send); 

.

Dans la plupart des cas, le système limite correctement la taille des réponses à la taille maximale de données disponibles selon la demande. Mais, dans ces cas précis, la taille maximale est systématiquement définie par la taille fournie par l'hôte (champ "length") :
- Lors d'une demande GET_CONFIGURATION avec un contexte (recipient) à "DEVICE"
- Lors d'une demande GET_INTERFACE avec un contexte à "INTERFACE"
- Lors d'une demande GET_STATUS avec un contexte à "ENDPOINT"

 

Il s'agit là d'une faille majeure de sécurité car dans ces conditions l'hôte peut recevoir jusqu'à 65,535 octects par "control request" ! Dans les cas où cette taille max est définie directement dans "size_to_tx" alors cette valeur décrit la taille du memcpy qui va s'appliquer et pourra donc copier jusqu'à 65535b  directement dans le buffer DMA. Et le fait que les buffers DMA utilisés par la stack USB sont -comparativement- beaucoup plus petits va provoquer des dépassements de tampon (buffer overflow).

 

Et voilà! Je ne m'attarde pas sur la technique du dépassement de tampon car on la retrouve dans beaucoup d'exploit mais c'est bien cette méthode qui permet de charger notre payload qui comprend donc les commandes RCM, les valeurs qui iront écraser la stack USB et le payload final (programme) à charger !

 

Mais revenons aux deux versions de notre exploit...

Alors que Fusée Gelée se "contente" de lancer un payload (du code binaire pur, sans headers ni metadonnées), ShofEL2 de fail0verflow offre une bootchain complète permettant de lancer le kernel Linux : BootROM Exploit → coreboot loader → coreboot → ARM trusted firmware → coreboot → u-boot → Linux.

 

Cette bootchain est basée sur celle du  Pixel C et est libre à 99%. Plus d'info ici => https://fail0verflow...g/2018/shofel2/

 

 linux_on_nintendo_switch_fail0verflow-10

Plus d'info sur Fusée Gelée par Kate Temkin


Modifié par eliboa, 07 mai 2018 - 13:24.

Tuto Switch : Bloquer les maj | Supprimer les maj téléchargées | Lancer Linux | Lancer des payloads

switch-h4x0r |`FW max conseillé sur Switch => 4.1

 

  • Retour en haut

Posté 08 mai 2018 - 01:04

#2
Erko

Erko

    Sunriseur elite

  • Members
  • PipPipPipPip
  • 1 421 messages
  • Sexe:Male
  • Lieu:Belgique, Liège

salut 

 

tu nous a sortie l’artillerie lourde la niveau explication, c’est complet et très instructif pour ceux qui ne sont pas calé comme moi sur se domaine 

super blog aussi au passage, je garde le lien dans mon dossier dédié à la switch ;) au cas ou 

 

merci pour se partage 


PS4 Fat OFW 5.05, PS3 Slim (DECH) CFW rebug 4.81.2 DEX, PS Vita Slim 3.60 HENkaku, Nintendo New 3DS XL CFW Luma3ds-Boot9strap, Nintendo Switch 4.0.1

  • Retour en haut




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

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