Explore o bug do iOS com o Hopper

Olá! Meu nome é Alexander Nikishin, estou desenvolvendo aplicativos iOS no Badoo. No artigo, falarei sobre como investigamos um bug no UIKit, que a Apple não quis corrigir por seis meses.



Tudo começou em agosto de 2019 com as primeiras versões beta do iOS 13. Em seguida, encontramos um problema. Nas aplicações Badoo e Bumble, estamos constantemente trabalhando para melhorar as interfaces e, por exemplo, tentamos otimizar o processo de registro tedioso e não amado o máximo possível. As dicas de ferramentas preditivas sistemáticas acima do teclado são uma ótima maneira de reduzir o número de cliques do usuário ao inserir dados. No entanto, na nova versão do iOS, ficamos surpresos ao descobrir que as solicitações para inserir o número de telefone haviam desaparecido.



Quando a versão GM foi lançada, percebemos que o problema não estava resolvido. Pareceu-nos que um erro tão óbvio simplesmente não poderia ser desperdiçado pelos testes de regressão, que a Apple provavelmente possuía em seu arsenal, e começamos a esperar por uma correção no primeiro grande pacote de atualização. Outros desenvolvedores esperavam por isso . No entanto, com o lançamento da versão 13.1, nada mudou e não tivemos escolha a não ser abrir o radar, o que fizemos no início de outubro. O tempo passou, o iOS 13.2 e 13.3 saiu e o bug ainda não foi corrigido.

Em fevereiro, nossa equipe de registro conseguiu um tempo livre trabalhando no backlog e decidi investigar um pouco mais esse problema.

Antes de começar a cavar, você tinha que descobrir qual o caminho a fazer. Como as dicas continuaram a funcionar em alguns tipos de teclados, o primeiro pensamento foi estudar a hierarquia de suas visualizações em diferentes versões do iOS.


iOS 12 vs iOS 13

Imediatamente ficou claro que a Apple no iOS 13 refatorou a implementação do teclado e destacou os prompts em um controlador separado ( UIPredictionViewController). Obviamente, a tendência para a modularização e decomposição também a alcançou (a propósito, Artyom Loenko falou recentemente sobre nossa experiência em sua aplicação ). Provavelmente, por causa disso, houve uma regressão da funcionalidade. O círculo de buscas começou a diminuir.

Acho que a maioria dos desenvolvedores de iOS sabe que é fácil encontrar interfaces de classes de sistema privadas em domínio público: basta inserir uma consulta no mecanismo de pesquisa. No estudo de uma interface de classe UIPredictionViewController no olho, captura um de seus métodos:



parece que havia uma pista, que é bastante fácil de verificar, usando as boas e antigas funções de implementação de falsificação de ferramentas (Swizzling). Vamos usá-lo para que uma função "sob suspeita" sempre retorne o valor verdadeiro:

+(void)swizzleIsVisibleForInputDelegate {
    SEL targetSelector = sel_getUid("isVisibleForInputDelegate:inputViews:");
    Class targetClass = NSClassFromString(@”UIPredictionViewController”);
    if (targetClass == nil) {
        return;
    }
    if (![targetClass instancesRespondToSelector:targetSelector]) {
        return;
    }

    Method method = class_getInstanceMethod(targetClass, targetSelector);
    if (method == NULL) {
        return;
    }

    IMP originalImplementation = method_getImplementation(method);
    IMP newImp = imp_implementationWithBlock(^BOOL(id me, id delegate, id views) {
        //   ,        . 
        BOOL result = ((bool (*)(id,SEL,id,id))originalImplementation)(me, targetSelector, delegate, views);
        if ([delegate isKindOfClass:[UITextField class]] && [delegate keyboardType] == UIKeyboardTypePhonePad) {
            return YES;
        }
        return result;
    });

    method_setImplementation(method, newImp);
}

Reiniciando o projeto de teste, descobri que os prompts do telefone retornavam ao iOS 13 e estão funcionando "no modo normal". Isso poderia encerrar a investigação e, talvez, até com muito cuidado, usar esta solução de diretrizes da Apple perigosa e proibida no assembly de liberação com a capacidade de ativar / desativar remotamente para alguns usuários (para a opção de gerenciamento remoto de recursos, você pode ver a gravação do relatório da minha colega Katerina Trofimenko). No entanto, nunca deixei de me perguntar por que essa função retorna falsa ao usar o tipo de teclado do telefone.

Para chegar à verdade, precisamos do código fonte da função. Obviamente, a Apple não distribui o código do componente iOS para a direita e para a esquerda, portanto, não é possível fazer uma pesquisa no Google. Existe apenas uma maneira de fazer a engenharia reversa para descompilar o código binário. Antes, eu já ouvira falar de um produto como o Hopper várias vezes e lia vários artigos sobre seu uso para aprofundar o conhecimento das bibliotecas de sistemas, mas nunca o usei. E imediatamente fiquei agradavelmente surpreendido: verificou-se que, para brincar e aprender as ferramentas, nem é necessário comprar a versão completa. A versão demo inclui sessões de trabalho intermináveis ​​de 30 minutos, sem a capacidade de salvar o índice e fazer alterações nos arquivos binários, o que significa que é uma excelente plataforma para experimentação.

Tudo do mesmointerface privada publicada , você pode descobrir o que UIPredictionViewControllerfaz parte da estrutura UIKitCore. Tudo o que resta é encontrá-lo e enviá-lo para o Hopper. Arquivos de estrutura binária estão localizados nas profundezas do Xcode. Aqui, por exemplo, está o caminho completo para o UIKitCore que precisamos:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

Criamos um arquivo de arrastar e soltar no Hopper, confirmamos a ação na caixa de diálogo do sistema e aguardamos a conclusão da indexação (no caso de uma estrutura tão grande como o UIKit, isso pode levar de quatro a seis minutos).

A interface do programa é bastante simples, observarei apenas os principais elementos necessários para a minha pesquisa. No painel esquerdo da interface, você pode encontrar a linha de pesquisa através da qual o código é navegado. Ao pesquisar o nome da classe em que estamos interessados, obtemos rapidamente a função em estudo, seu código de assembler é aberto na janela principal.



Na barra de tarefas superior, existem botões para alternar os modos de exibição de código. Da esquerda para a direita:



  • Modo ASM - código do assembler;
  • Modo CFG - código do assembler na forma de um diagrama de blocos (árvore), onde os trechos de código são combinados em blocos e as transições são mostradas como ramificações;
  • Modo pseudo-código - o pseudo-código gerado (falaremos mais sobre isso abaixo);
  • O modo hexadecimal é uma representação hexadecimal de um arquivo binário, ou abracadabra, que não nos ajuda muito em nossa pesquisa.

Agora você precisa descobrir o que acontece dentro da função. Seu corpo é bastante longo, então apenas os gurus do ASM, a quem não posso me chamar, podem entender a lógica observando o código do assembler. Para fazer isso, o modo pseudo-código ajudará, no qual Hopper simplifica as operações de montagem o máximo possível, substituindo nomes de funções reais sempre que possível e usando nomes de registradores como variáveis. Parece assim:



Resta apenas passar pela lógica da função e entender em qual de seus ramos caímos. Pontos de interrupção simbólicos me ajudaram com isso., que pode ser instalado nas chamadas do sistema, imprimindo simultaneamente todas as variáveis ​​necessárias e os resultados das chamadas de função no console do Xcode. Usando essa maneira simples, descobri que a execução da função é interrompida por uma saída antecipada devido ao fato de uma das condições não funcionar no bloco de código de exemplo. Vamos descobrir o que está acontecendo aqui.

Um pouco de contexto: no registro rbx, um pouco mais alto no código (que eu omiti por simplicidade) é um link para o objeto que implementa o protocolo UITextInputTraits_Private(esta é uma versão estendida do protocolo público UITextInputTraits).

Portanto, a primeira das condições é verificar se os prompts não estão ocultos pela configuração do campo de entrada. Na depuração, você pode ver que está executando: propertyhidePredictionretorna falso. A segunda condição é verificar se o teclado não está no modo "dividido" (no seu iPad, pressione o botão inferior direito , apenas 2-3% dos usuários sabem disso). Com isso também tudo está em ordem.

Ir em frente. No estágio seguinte, começam as manipulações keyboardType, o que sugere que a verdade está em algum lugar próximo. Primeiro, é verificado se o atual é keyboardTypemenor ou igual a 0xb (ou 11 no formato decimal). Se abrirmos a declaração no Xcode UIKeyboardType, veremos que existem 13 tipos de teclados e um deles (UIKeyboardTypeAlphabet) foi descontinuado e declarado como uma referência para outro tipo. Ou seja, existem 12 tipos de enum: se você começar de 0, o último terá um valor igual a 11. Em outras palavras, o código valida o valor na forma de uma verificação de estouro e, novamente, passa com êxito.

Além disso, vemos uma condição muito estranha if (!COND)e, durante muito tempo, não consegui entender o que estava verificando, já que a variável COND não foi declarada em nenhum outro lugar do código. Além disso, meus pontos de interrupção simbólicos mostraram que é precisamente a falha em cumprir essa condição que leva a uma saída antecipada da função. E aqui não temos escolha a não ser retornar ao modo ASM e estudar esta parte do código no formato assembler.

Para encontrar essa condição na listagem do ASM, você pode usar a opção “Sem duplicação de código”, que mostrará o pseudocódigo não na forma de código-fonte com condições if-else, mas na forma de blocos de código e transições exclusivos na forma de goto. Nesse caso, veremos a posição do início desses blocos no arquivo binário e usaremos esse ponteiro para pesquisar no modo ASM. Então, descobri que o bloco em que estamos interessados ​​está localizado em fe79f4:



Voltando ao modo ASM, podemos encontrar facilmente este bloco:



Chegamos à parte mais difícil, onde analisaremos as três linhas do código do montador.

Reconheci a primeira linha da minha memória (graças às lições dos montadores em meu MIET nativo, finalmente chegou o momento da minha vida em que o ensino superior foi útil!). Tudo é bem simples aqui: a constante 0x930 (100100110000 em formato binário) é colocada no registro ecx.

Na segunda linha, vemos a instrução bt executada nos registradores exce eax. O valor de um que já sabemos, o valor do segundo, pode ser visto na captura de tela anterior: rax = [rbx keyboardType]- contém o tipo atual de teclado. rax- este é o registro inteiro de 64 bits, eax- esta é sua parte de 32 bits.

Com os dados decididos, resta entender a lógica da equipe. O Google nos leva a esta descrição :
Selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset operand (second operand) and stores the value of the bit in the CF flag.

A instrução extrai um pouco do primeiro operando (constante 0x930) na posição especificada pelo segundo operando (tipo de teclado) e o coloca em CF ( sinalizador de transporte ). Ou seja, como resultado, o CF conterá 0 ou 1, dependendo do tipo de teclado.

Prosseguimos para a última operação jb, com a seguinte descrição :

Jump short if below (CF=1)

É fácil adivinhar que há uma transição para a função (saída antecipada) se CF for 1.

Nesse ponto, o quebra-cabeça começa a somar. Temos uma máscara de bits 100100110000 (contém 12 bits de acordo com o número de tipos de teclado disponíveis) e determina a condição para saída antecipada. Agora, se verificarmos a disponibilidade de dicas para todos os tipos de teclados em ordem crescente rawValue, tudo estará no lugar.



No iOS 12, não encontraremos essa lógica - as dicas funcionam para qualquer tipo de teclado. Eu suspeito que, no iOS 13, a Apple decidiu desativar as dicas para teclados digitais, o que é compreensível em princípio: não consigo criar um cenário em que o sistema precise solicitar números. Aparentemente, por engano, uma “mão quente” também atingiu UIKeyboardTypePhonePad, o que é muito semelhante a um teclado numérico comum. Ao mesmo tempo UIKeyboardTypeNamePhonePad, combinando um teclado QWERTY para procurar contatos telefônicos e exatamente o mesmo teclado digital desativado, ele continuou a mostrar avisos.

Para minha surpresa, trabalhar com Hopper acabou sendo muito agradável e divertido, por um longo tempo eu não recebi tanto fã. Compartilhei as descobertas com os engenheiros da Apple no meu relatório de erros e seu status mudou com o tempo para "Correção potencial identificada - para uma futura atualização do sistema operacional". Espero que a correção alcance os usuários em futuras atualizações. Ao mesmo tempo, o Hopper pode ser usado não apenas para encontrar as causas dos seus erros ou da Apple, mas também para detectar correções da Apple para programas de terceiros .

All Articles