1 pontos por GN⁺ 2025-02-19 | 1 comentários | Compartilhar no WhatsApp
  • A engenharia reversa de apps iOS exige conseguir observar e manipular o app em execução, mas este app de widget usa ao mesmo tempo bloqueio de depurador, bloqueio de injeção de código e detecção de jailbreak
  • O bloqueio principal pode ser implementado com PT_DENY_ATTACH do ptrace ou com uma chamada direta de sistema de mesmo efeito (svc #0x80), então não é possível capturá-lo apenas com um breakpoint simples em ptrace
  • A chamada direta de sistema pode ser contornada encontrando no binário o padrão mov w16, #26, colocando um breakpoint na posição de svc e usando lldb jump para pular essa instrução
  • O comportamento de dar soft-reboot/respring no celular após detectar jailbreak estava ligado a uma função que chamava snapshotViewAfterScreenUpdates: em loop infinito, e foi contornado com thread return no início da função
  • O crash após injeção de código estava mais ligado à perda da permissão de App Group do que a uma checagem separada de framework em runtime, e com um swizzle que trocava containerURLForSecurityApplicationGroupIdentifier: por um diretório temporário o app passou a rodar também em dispositivos comuns

App de widget iOS com várias camadas de proteção

  • O alvo era um app de widget da App Store, com comportamentos defensivos mais fortes que os de um app de widget comum
    • bloqueio de attach de depurador
    • encerramento do app em caso de injeção de código
    • se executado em estado de jailbreak, o celular inteiro faz soft-reboot/respring
  • Não é raro que apps iOS incluam proteções como detecção de jailbreak ou ofuscação de código, mas este app combina vários métodos ao mesmo tempo
  • Outros comportamentos internos interessantes do app ficam para um texto posterior

Bloqueio de attach de depurador com PT_DENY_ATTACH

  • Em dispositivos com jailbreak, normalmente é possível conectar por ssh, executar debugserver e então depurar o app a partir do lldb em outro computador
  • Ao anexar debugserver a este app da mesma forma, ocorre Segmentation fault e o attach falha
  • A causa está relacionada à requisição PT_DENY_ATTACH de ptrace
    • ptrace é uma private API no iOS e uma public API no macOS
    • PT_DENY_ATTACH define uma flag que recusa rastreamento posterior pelo processo pai
    • se o processo já estiver sendo rastreado, o app termina com estado ENOTSUP
    • se o pai tentar rastrear um processo com essa flag ativada, ocorre uma segmentation violation no lado do processo pai
  • Uma implementação simples pode ser feita com a chamada ptrace(PT_DENY_ATTACH, 0, 0, 0)
    • como é uma private API do iOS, na chamada real é preciso encontrar o símbolo ptrace de libsystem_kernel.dylib com dlopen e dlsym
    • esse método costuma ser relativamente fácil de contornar colocando um breakpoint em ptrace e pulando a chamada com thread return

Por que o bypass fácil não funcionou

  • PT_DENY_ATTACH só bloqueia o depurador depois de ser chamado, então se o attach acontecer antes da execução do código do app é possível criar um ponto de bypass
  • Em vez de anexar debugserver diretamente ao processo, é possível executá-lo e depois, no lldb, esperar a execução do app com process attach --name TopWidget --waitfor, fazendo o attach antes do código do app rodar
  • Mas neste app o breakpoint b ptrace inicialmente nem era resolvido e, mesmo depois de resolvido, nunca era atingido antes do encerramento do app
  • Isso acontecia porque o app usava uma chamada direta de sistema com o mesmo efeito, em vez de chamar a função ptrace

Encontrando a posição da chamada direta de sistema

  • O disassembly da função ptrace mostra como parte central a chamada de sistema svc #0x80
    • x0 recebe o valor 31, que corresponde a PT_DENY_ATTACH
    • x1, x2 e x3 recebem os argumentos não usados 0
    • x16 recebe 26, o número da chamada de sistema de ptrace
  • O app podia configurar esses mesmos registradores com inline assembly e executar svc #0x80 diretamente, sem chamar a função ptrace
    • isso evita lookups suspeitos de private APIs com dlopen e dlsym
    • também dificulta capturar o fluxo apenas com breakpoints na função comum ptrace
  • Para contornar, foi preciso abrir o binário descriptografado do app em um disassembler e encontrar a posição da chamada de sistema de ptrace
  • O alvo da busca era mov x16, #26 ou mov w16, #26, que é a visão de 32 bits do mesmo registrador
    • com armconverter.com foi possível obter os bytes 50 03 80 D2 de mov x16, #26 para usar na busca binária
    • mov x16, #26 não retornou resultados, mas a busca por mov w16, #26 retornou 4 resultados
  • Entre eles, dois não batiam com o padrão esperado nas instruções ao redor, e no terceiro apareceu o seguinte formato de código
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • O quarto resultado era outro branch da mesma função, confirmando que essa era a posição usada para bloquear o attach do depurador

Pulando a instrução svc

  • Os endereços svc confirmados no disassembler eram 0x102A2BB14 e 0x102A2BB68
  • No lldb, foi usado -s TopWidget ao definir os breakpoints para converter endereços do binário em endereços reais carregados
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • Ao continuar a execução, o breakpoint era acionado na posição de svc #0x80
  • O bypass mais simples foi fazer jump para o endereço da próxima instrução e não executar a chamada de sistema
    • no exemplo, foi usado jump *0x10327bb18, endereço imediatamente seguinte à instrução atual
  • Com isso, foi possível entrar no app com o depurador anexado

O comportamento que fazia o celular dar soft-reboot

  • Mesmo após contornar o attach do depurador, o app ainda executava uma proteção que fazia o celular dar soft-reboot/respring
  • Desta vez, como o lldb continuava anexado, foi possível ver que o processo recebia SIGKILL e inspecionar o stacktrace
  • O stacktrace mostrava um fluxo de captura de conteúdo da tela
    • QuartzCore com CARenderServerSnapshot
    • UIKitCore com _UISnapshotScreenWindowsRectAfterCommit
    • uma unnamed symbol interna de TopWidget
  • Com lldb image lookup, o endereço de runtime foi convertido para o endereço do binário 0x100041898, e essa função foi analisada no disassembler
  • O decompile mostrou que a função apenas repetia as ações abaixo em loop infinito
    • chamar +[UIScreen mainScreen]
    • chamar snapshotViewAfterScreenUpdates: no objeto de tela retornado
    • ignorar o resultado e liberá-lo
  • snapshotViewAfterScreenUpdates: é uma public API para criar snapshots de views, mas o app a chamava infinitamente de forma intensiva em memória
  • No vídeo relacionado, foi confirmado que a origem dessa chamada estava ligada à notificação com.apple.tw.twrr e que o respring acontecia quando o celular não passava em uma checagem de risco
  • O bypass foi possível colocando um breakpoint no início dessa função e, quando atingido, usando thread return para pular sua execução

O crash causado pela injeção de código

  • Durante a depuração, utilitários mais complexos, como registro de informações de acessibilidade dos botões na tela, podem ser implementados em um framework injetado no app e chamados pelo depurador
  • Mesmo sem um dispositivo com jailbreak, ferramentas como Frida ou Flex permitem uma exploração inicial do app por injeção
  • Em geral, essa injeção é feita com ferramentas de resign do app; no exemplo foi usado o Sideloadly
  • Este app crashava imediatamente ao ser executado depois do resign
  • No topo da stack de crash, o endereço 0x1002027D4 mostrava uma instrução BRK no disassembler, compatível com um crash intencional, como um force-unwrap de nil
  • No código decompilado, a chamada suspeita era containerURLForSecurityApplicationGroupIdentifier:
    • esse método retorna a URL da pasta que pode ser acessada em conjunto por apps e extensões do mesmo grupo
    • App Group é definido no processo de assinatura de código
    • ao fazer injeção de código e resign, a assinatura original do app é descartada e a permissão de App Group também se perde
    • por isso, em vez de uma URL, o método provavelmente passava a retornar nil, e o app crashava ao fazer force-unwrap desse valor
  • Como apps de widget precisam compartilhar o App Group com a extensão responsável por exibir o widget na tela inicial, isso parecia mais um problema comum de assinatura de código do que uma proteção intencional

Contornando o problema de App Group

  • A solução mais fácil é não fazer resign do app
    • em dispositivos com jailbreak, é possível injetar o framework com um jailbreak tweak sem precisar fazer resign
    • também é possível executar código com assinatura inválida ou adicionar o app desejado ao grupo desejado
  • Mesmo sem um dispositivo com jailbreak e precisando fazer resign, ainda é possível contornar com um framework pequeno que faz swizzle do método
  • O código de exemplo substitui containerURLForSecurityApplicationGroupIdentifier: de NSFileManager
    • quando o método original é chamado, passa a ser executado o método de substituição
    • o método de substituição retorna temporaryDirectory em vez de um shared container
  • Essa substituição não reproduz exatamente o comportamento original que o app esperava
    • originalmente o app queria uma pasta compartilhada acessível tanto pelo app quanto pela extensão
    • um diretório temporário não é esse tipo de pasta compartilhada
  • Ainda assim, para fins de observar apenas o comportamento do app principal, isso pode ser suficiente
    • esse pequeno patch pode quebrar a app extension
    • se só o funcionamento básico do app principal for necessário, talvez isso não seja um problema
  • Um bypass maior seria criar um novo App Group, resignar o app principal e todas as extensões com esse grupo, e então fazer swizzle para que os métodos relevantes usem o novo identificador do grupo
    • é muito mais trabalhoso e, se não for realmente necessário, talvez seja melhor conseguir um dispositivo com jailbreak
  • Ao injetar esse framework junto com uma ferramenta como Flex, o app passou a rodar normalmente também em dispositivos comuns
  • Em dispositivos com jailbreak, ainda seria preciso contornar novamente o anti-debugging e a proteção de respring mencionados antes, mas depois disso já seria possível entrar no app com Flex injetado

Estado final

  • No fim, o app ficou em um estado em que eram possíveis attach do depurador, bypass da detecção de jailbreak e injeção de código
  • O que exatamente se queria observar dentro do app fica para o próximo texto

1 comentários

 
GN⁺ 2025-02-19
Comentários do Hacker News
  • Bryce Bostwick faz um trabalho realmente incrível e inspirador com depuração e engenharia reversa de apps
    Conheci pelo YouTube e, depois de ver um vídeo em que ele modifica o TikTok para mostrar só vídeos de gatos (https://youtu.be/YW3jL2gI9IE), tentei modificar o Instagram para deixar apenas o recurso de mensagens que eu uso e remover todo o resto
    Há tempos eu queria me aprofundar mais em modificação e engenharia reversa, no estilo de modificar o Windows com o Windhawk (https://windhawk.net/), e o Bryce mostra muito bem esse tipo de trabalho no iOS em vídeos em tempo real, passo a passo

    • Se houver alguém parecido no lado do Android, eu gostaria de aprender mais
      Já vi que dá para fazer coisas incríveis com o Revanced, mas parece haver poucos bons guias que ensinem como começar esse tipo de trabalho
    • Vou tentar fazer isso que ele explicou por conta própria
      Se alguém organizar o procedimento, tenho interesse
      Odeio ter que ser exposto ao Reels só para postar fotos ou conversar com amigos
  • Técnicas de anti-debug e até técnicas para bloquear o bloqueio do anti-debug já eram comuns há muito tempo no mundo DOS/Windows
    Se você olhar materiais antigos sobre cracking ou unpacking, esse assunto aparece tratado em vários níveis de profundidade
    O quanto o usuário consegue controlar facilmente o comportamento de um app é inversamente proporcional ao quanto a plataforma é hostil ao usuário
    PT_DENY_ATTACH parece literalmente um recurso criado para essa hostilidade ao usuário
    Pelo que sei, o Windows não tem algo assim, e em vez disso usam técnicas para fazer o app se anexar a si próprio
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • Exato. PT_DENY_ATTACH foi literalmente criado pela Apple no passado como parte da solução de DRM do iTunes
  • É meio surpreendente que a revisão da App Store da Apple não rejeite apps que fazem chamadas de sistema diretas
    As chamadas de sistema nas plataformas da Apple não têm ABI estável, então todas deveriam passar pela libSystem, e um app que faz chamadas diretas sem passar pela libSystem está claramente fazendo algo que não deveria
    Da mesma forma, também fiquei curioso por que o autor procurou mov w16, #26 no código em vez de svc 0x80

    • svc 0x80 é a instrução que executa qualquer chamada de sistema, e qual chamada exata será executada depende do registrador x16
      Como o app faria muitas chamadas de sistema não relacionadas, colocar breakpoint nisso não seria útil
      Pelo menos foi assim que explicaram no vídeo
    • Às vezes o compilador pode fazer inline de wrappers de chamada de sistema, então não é tão fácil verificar isso estaticamente
      Pelo mesmo motivo, se você procurar instruções SVC vai encontrar uma quantidade enorme de resultados
      Se encontrar o ID exato da chamada de sistema movido para o X16, você acha isso na hora
  • Sou o autor do post. Posso responder perguntas. Obrigado ao xmprt por compartilhar

    • Vi o vídeo no YouTube e achei interessante
      Foi bom você também disponibilizar uma versão em texto
    • Foi um post muito interessante, e eu sempre gostaria de ver mais textos desse tipo sobre engenharia reversa de celulares em baixo nível
      Queria perguntar algumas coisas ao autor: se a ferramenta comercial mais famosa é mesmo a Guardsquare, gostaria de saber se você acha que eles oferecem algo novo para impedir esse tipo de disassembly fácil
      Também queria saber se o TopWidgets usava alguma proteção parecida ou se era algo mais caseiro
    • Os vídeos são muito interessantes, e me surpreende que mais gente não tenha visto ou lido isso
      Pessoalmente uso Android, então isso não se aplica imediatamente para mim em termos práticos, mas ainda assim extraio muito valor de aprender como funciona o debugging de baixo nível no iOS
    • Fico curioso se no iOS existe algo como PTRACE_SYSCALL, que permita interceptar a entrada de chamadas de sistema para alterar valores de retorno ou detectar onde o SVC acontece
  • O vídeo do topo é uma das melhores coisas que já vi entre vídeos de programação
    O ritmo é rápido, assume exatamente o nível certo de conhecimento prévio e tem ótimas demonstrações sem quebrar o fluxo do vídeo

  • Excelente texto
    Fico realmente curioso se esse era um app normal excessivamente paranoico ou se já estava sendo depurado por suspeita de ser malware desde o começo
    Caso contrário, o esforço envolvido parece um tanto exagerado

    • Pelo meu julgamento, parece mais um app excessivamente paranoico
      Ele faz coisas bem legais com widgets e parece ter tentado proteger isso. Mas essas estratégias também já estão começando a vazar aos poucos
      Havia coisas interessantes no binário
      Em certo momento tentei descobrir por que estava vendo código que aparentemente baixava um .iso do Windows, e era isso mesmo; estava sendo usado no widget de teste de velocidade de rede
    • Ou talvez estivessem tentando provar violação de direitos autorais caso o app em si tivesse sido recompilado com outros logos e coisas do tipo
  • Existe um modo ainda mais difícil do que “contornar PT_DENY_ATTACH (modo difícil)”
    No passado, já fiz isso no macOS patchando o kernel para que o PT_DENY_ATTACH não fizesse nada
    No Mac, rodar um kernel modificado é até relativamente fácil na prática, mas no iOS isso provavelmente seria muito mais trabalhoso por causa de coisas como KTRR
    Embora o XNU seja tecnicamente open source, foi mais fácil aplicar o patch com um editor hexadecimal do que recompilar tudo

    • Também dá para usar a kernel task port para inverter bits na struct proc
      Você também pode permitir páginas de código não assinadas e RWX, tornando JIT possível
  • Se “executar com jailbreak ativado faz o telefone inteiro travar”, então isso não deveria ser denunciado na loja como malware?
    Fazer o telefone travar é claramente comportamento de malware, e é razoável suspeitar que haja outras ações maliciosas que ele também esteja tentando esconder
    Não era justamente esse o argumento do ecossistema fechado da Apple para impedir esse tipo de lixo?

    • “Com jailbreak ativado” significa fora do ecossistema fechado
      A Apple provavelmente não vai se importar muito se um app trava em um telefone com jailbreak
  • Fiquei muito curioso com a notificação com.apple.tw.twrr
    Por que começa com com.apple?
    Pelo que entendi, o app em questão não é da Apple, mas sim o Top Widgets

    • O nome da notificação é uma string arbitrária
      É costume usar esse tipo de “nome completo” para evitar conflitos
      Nesse caso, parece que o desenvolvedor apenas escolheu esse prefixo por acaso
  • Tirando as ferramentas usadas, isso tem exatamente a mesma cara de quebrar proteção anticópia de Apple II com boot tracing nos anos 1980
    Algumas coisas nunca mudam