Depurando um app que não pode ser depurado
(bryce.co)- 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
ptraceou com uma chamada direta de sistema de mesmo efeito (svc #0x80), então não é possível capturá-lo apenas com um breakpoint simples emptrace - A chamada direta de sistema pode ser contornada encontrando no binário o padrão
mov w16, #26, colocando um breakpoint na posição desvce usandolldb jumppara 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 comthread returnno 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, executardebugservere então depurar o app a partir dolldbem outro computador - Ao anexar
debugservera este app da mesma forma, ocorre Segmentation fault e o attach falha - A causa está relacionada à requisição
PT_DENY_ATTACHdeptraceptraceé uma private API no iOS e uma public API no macOSPT_DENY_ATTACHdefine 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
ptracedelibsystem_kernel.dylibcomdlopenedlsym - esse método costuma ser relativamente fácil de contornar colocando um breakpoint em
ptracee pulando a chamada comthread return
- como é uma private API do iOS, na chamada real é preciso encontrar o símbolo
Por que o bypass fácil não funcionou
PT_DENY_ATTACHsó 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
debugserverdiretamente ao processo, é possível executá-lo e depois, nolldb, esperar a execução do app comprocess attach --name TopWidget --waitfor, fazendo o attach antes do código do app rodar - Mas neste app o breakpoint
b ptraceinicialmente 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
ptracemostra como parte central a chamada de sistemasvc #0x80x0recebe o valor31, que corresponde aPT_DENY_ATTACHx1,x2ex3recebem os argumentos não usados0x16recebe26, o número da chamada de sistema deptrace
- O app podia configurar esses mesmos registradores com inline assembly e executar
svc #0x80diretamente, sem chamar a funçãoptrace- isso evita lookups suspeitos de private APIs com
dlopenedlsym - também dificulta capturar o fluxo apenas com breakpoints na função comum
ptrace
- isso evita lookups suspeitos de private APIs com
- 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, #26oumov w16, #26, que é a visão de 32 bits do mesmo registrador- com armconverter.com foi possível obter os bytes
50 03 80 D2demov x16, #26para usar na busca binária mov x16, #26não retornou resultados, mas a busca pormov w16, #26retornou 4 resultados
- com armconverter.com foi possível obter os bytes
- 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, #0x1FMOV X1, #0MOV X2, #0MOV X3, #0MOV W16, #0x1ASVC 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
svcconfirmados no disassembler eram0x102A2BB14e0x102A2BB68 - No
lldb, foi usado-s TopWidgetao definir os breakpoints para converter endereços do binário em endereços reais carregadosbr s -a 0x102A2BB14 -s TopWidgetbr 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
- no exemplo, foi usado
- 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
lldbcontinuava anexado, foi possível ver que o processo recebiaSIGKILLe inspecionar o stacktrace - O stacktrace mostrava um fluxo de captura de conteúdo da tela
QuartzCorecomCARenderServerSnapshotUIKitCorecom_UISnapshotScreenWindowsRectAfterCommit- uma unnamed symbol interna de
TopWidget
- Com
lldb image lookup, o endereço de runtime foi convertido para o endereço do binário0x100041898, 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
- chamar
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.twrre 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 returnpara 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
0x1002027D4mostrava uma instruçãoBRKno disassembler, compatível com um crash intencional, como um force-unwrap denil - 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 Grouptambé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 Groupcom 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:deNSFileManager- quando o método original é chamado, passa a ser executado o método de substituição
- o método de substituição retorna
temporaryDirectoryem 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
Flexinjetado
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
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
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
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...
É 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, #26no código em vez desvc 0x80svc 0x80é a instrução que executa qualquer chamada de sistema, e qual chamada exata será executada depende do registrador x16Como 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
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
Foi bom você também disponibilizar uma versão em texto
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
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
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
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
.isodo Windows, e era isso mesmo; estava sendo usado no widget de teste de velocidade de redeExiste 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
procVocê 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?
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
É 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