Jailbreak do Switch 2: cadeia de exploits vs. Secure Boot
O jailbreak do Switch 2 encadeou um overflow no interpretador BASIC, um bug de caminho no fs e uma race condition no loader. Veja o que ensina.

Um console novo sai de fábrica com um microkernel endurecido, pointer authentication em cada endereço de retorno, memória somente de execução, control-flow integrity e uma cadeia de boot assinada. Duas semanas depois, um hobbyista está executando código nativo arbitrário como o serviço de sistema de arquivos, o processo mais confiável da máquina. Essa é a história do primeiro jailbreak público do Switch 2, e deveria deixar desconfortável todo engenheiro que constrói sistemas seguros. Não porque as mitigações falharam, já que a maioria funcionou exatamente como projetada, mas porque o exploit nunca precisou vencê-las de frente. Ele as contornou, um bug de lógica por vez.
Passei anos do lado defensivo da segurança de infraestrutura, e o padrão aqui é um que já vi em revisões de incidentes de produção mais vezes do que gostaria: uma cadeia de bugs individualmente pouco impressionantes, cada um conquistando privilégio suficiente para alcançar o próximo. O ponto de partida nem estava no Switch 2. Foi um bug de corrupção de memória em um jogo do Switch 1, o interpretador SmileBASIC, rodando no modo de compatibilidade retroativa. A partir daí, a cadeia subiu pelo serviço de sistema de arquivos, explorou uma race condition no carregador de programas e terminou com a capacidade de executar código nativo sob a identidade de qualquer sysmodule. Cada elo merece atenção, porque cada um corresponde a um erro que vi times cometerem em sistemas server-side também.
Por que a porta do navegador estava fechada e a porta do jogo não estava
O WebKit é o primeiro alvo clássico em jailbreaks de consoles, e o pesquisador conseguiu um ponto de apoio ali via captive portal e um n-day. Mas o caminho acabou rápido. A pilha de navegador do Switch 2 é cercada por três mitigações que se compõem de forma brutal: a memória somente de execução (XOM) impede que, mesmo com leitura e escrita arbitrárias, você localize gadgets de código úteis; a pointer authentication (PAC) impede forjar um ponteiro de código sem um oráculo de assinatura; e o CFI impede que até um ponteiro validamente assinado passe nas checagens de tipo na maioria dos pontos de chamada. Quebrar qualquer uma delas já é um projeto de pesquisa. Quebrar as três, encadeadas, a partir de um sandbox JavaScript, é uma carreira.
Então o ataque pivotou para o inquilino mais fraco da máquina: os jogos de terceiros do Switch 1. O raciocínio foi frio e correto. Os binários de jogos são feitos por estúdios, não pela equipe de segurança da Nintendo, então têm muito menos chance de serem compilados com a pilha completa de mitigações. E o Switch 1 nunca teve PAC, então tudo que roda no seu ambiente de compatibilidade herda essa fragilidade. Melhor ainda, a raiz de confiança do Switch 1 foi comprometida anos atrás, o que significa que o código dos jogos pode ser descriptografado e auditado offline. O SmileBASIC, um interpretador BASIC gigante em C++ que já tinha sido explorado no 3DS, era o alvo mole óbvio.
Há uma lição aqui que vai além de consoles: a postura de segurança do seu sistema é a postura de segurança do código menos endurecido que você concorda em executar. Compatibilidade retroativa é uma superfície de ataque que você herda, não uma que você escolhe. O modelo de segurança do Xbox aprendeu isso da pior forma nos anos 2000: exploits em jogos salvos, em títulos sem endurecimento, foram a cunha que abriu um kernel bem mais apertado, uma história que cobrimos em what the original Xbox hack teaches about hardware security models. Vinte anos depois, a mesma jogada ainda funciona.
O Integer Overflow que abriu a primeira porta
O bug do SmileBASIC é um integer overflow clássico em uma verificação de limites, do tipo que todo checklist de revisão de código seguro manda caçar, e que continua indo para produção mesmo assim. O comando de remoção de elementos de array do interpretador validava seus argumentos assim:
// intent: ensure the removed range is inside the array
if (position + count <= array.length) {
memmove(backing + position * 8, // start of the gap
backing + (position + count) * 8, // elements after the gap
(length - position) * 8); // bytes to copy
}
Chame REMOVE R, &H60000000, &H3FFFFFFF em um array de quatro elementos e position + count dá a volta e vira 0x9FFFFFFF, que, como valor de 32 bits com sinal, é profundamente negativo, ou seja, confortavelmente “menor” que o tamanho. As multiplicações por 8 também estouram, e você acaba executando memmove(backing, backing - 8, 32). O comando exibe um erro de falta de memória depois, mas a cópia já aconteceu. E aqui está a parte bonita: os 8 bytes imediatamente antes de qualquer array do SmileBASIC guardam um ponteiro para o próprio array. Depois da remoção, R[0] é interpretado como uma string cujo campo de tamanho é, na verdade, esse ponteiro, uma string astronomicamente longa pela qual você consegue ler e escrever. Leitura e escrita arbitrárias, entregues de presente pelo próprio layout de memória do interpretador.
Se você escreve C ou C++ profissionalmente, olhe essa checagem por um minuto. position + count <= length é o idioma de verificação de limites mais comum em código de sistemas, e está errado sempre que a soma pode estourar. A forma correta é position <= length && count <= length - position, ou aritmética com checagem. Já encontrei variantes desse bug em um parser de pacotes num emprego anterior e em um storage engine open source. Ele sobrevive à revisão de código porque parece obviamente certo. Essa é a lacuna entre especificação e implementação em miniatura: a especificação diz “valide o intervalo”, a implementação valida algo adjacente ao intervalo, e a diferença é explorável. Já escrevemos sobre como the specification-implementation gap is where bugs live; interpretadores de consoles são só o lugar onde esses bugs são encontrados por adolescentes motivados em vez de fuzzers.
Escalando até o serviço de sistema de arquivos com um overflow no tratamento de caminhos
Leitura e escrita em modo usuário dentro de um processo de jogo não rendem nada por si só, porque o design em microkernel do Switch faz com que cada serviço seja um processo separado com sua própria superfície de IPC. O alvo valioso era o fs, o sysmodule de sistema de arquivos embutido, por um motivo deliciosamente recursivo: o fs é o componente que entrega código descriptografado ao loader. Se você controla o fs, pode despejar o código de qualquer outro sysmodule para análise offline. O quartel-mestre da fortaleza entrega a você as plantas.
O bug era um heap overflow na normalização de caminhos. O comando IPC GetEntryType normaliza seu argumento de caminho, e caminhos contendo um componente de diretório pai com barra invertida, /..\, são encaminhados para um helper que aloca um buffer fixo de 0x301 bytes e então copia o caminho para dentro dele sem uma checagem de tamanho adequada. Um caminho suficientemente longo estoura o buffer sobre objetos vizinhos no heap. O comentário do próprio pesquisador ao descobrir isso foi uma palavra: “lol.”
Esse “lol” deveria incomodar, porque normalização de caminhos é exatamente o tipo de código de encanamento sem brilho que nunca recebe o escrutínio de segurança que a criptografia recebe. O exploit transformou o overflow em leitura e escrita arbitrárias corrompendo um objeto de cache vizinho cujo dirty bit redireciona I/O de arquivos por um ponteiro controlado pelo atacante, uma primitiva limpa de heap grooming. Depois, com varredura de memória, o exploit encontrou o objeto de conexão do seu próprio processo nas tabelas do fs e reescreveu o process ID armazenado para 1, concedendo ao processo do jogo “FullPermission” sobre o sistema de arquivos. Repare no que acabou de acontecer: nenhuma checagem criptográfica foi quebrada. O modelo de permissões era imposto por dados na memória, e o atacante conseguia escrever na memória.

Vencendo as checagens de assinatura trocando o sistema de arquivos em pleno voo
O elo mais esperto da cadeia é o que derrota a assinatura de código sem tocar em nenhuma chave. O carregador de programas (ldr) verifica duas assinaturas antes de executar qualquer coisa: a assinatura da Nintendo sobre os metadados de permissão do processo e a chave do desenvolvedor, e a assinatura do desenvolvedor sobre o hash do cabeçalho do arquivo de conteúdo. Em teoria, esse hash fixa transitivamente cada byte do executável. Na prática, existe uma lacuna: é trabalho do fs garantir que o sistema de arquivos que ele serve realmente corresponda ao hash que ele já reportou, e o atacante tinha leitura e escrita arbitrárias dentro do fs.
Então o exploit espera até que o loader tenha pedido o sistema de arquivos do executável e o fs já tenha entregado o hash e a assinatura válidos, e então troca o objeto de sistema de arquivos atrás do handle IPC do loader por um objeto forjado, que serve o arquivo de metadados genuinamente assinado, mas com um executável substituto arbitrário. As checagens de assinatura passam porque eram verificações sobre bytes que o atacante nunca precisou modificar. O timing é imposto reescrevendo o handle que o fs aguarda para a conexão do loader, apontando para um evento controlado pelo atacante a partir do processo do jogo, o que transforma uma race condition em uma interposição determinística.
A cadeia criptográfica era sólida do hash do cabeçalho até o conteúdo do executável, no papel. O único elo que existia apenas como suposição em tempo de execução, “o processo que serve os arquivos é honesto”, foi justamente o que quebrou.
Isso é um time-of-check/time-of-use bug disfarçado, e é a lição mais transferível de todo o texto. Sempre que o componente A valida um dado e o componente B age sobre ele depois, por meio de uma referência mutável, existe uma janela de troca. Certa vez revisei um sistema de deploy interno que validava o digest de uma imagem de container e depois puxava pela tag do registry no momento do rollout. Mesmo bug, outro chapéu. A correção nos dois casos é a mesma: valide no ponto de uso, ou entregue ao consumidor um objeto imutável e verificado, em vez de um handle para algo que ele precisa confiar que um terceiro manterá honesto. Um design defensivo que presume que todo intermediário está comprometido não é paranoia; numa máquina onde o atacante já consegue escrever parcialmente, é só modelagem precisa. Esse é o argumento central de defensive engineering for systems that resist catastrophe, e este exploit é um estudo de caso limpo.
Pegando um handle do kernel e corrigindo o fs por dentro
O truque final é pequeno e bonito. Para corrigir o código do próprio processo fs, o exploit precisava de um handle de kernel para ele. Conseguiu ao sobrescrever um campo de handle de 4 bytes na memória com o valor mágico 0xffff8001, que o kernel interpreta como “o handle do processo atual”. O processo do jogo chamou uma API inofensiva, recebeu de volta o próprio handle de processo, entregou-o a um processo malicioso rodando com as permissões do sysmodule de carregamento de código, e esse processo usou chamadas legítimas ao kernel para mapear e corrigir o código do fs. Cada operação individual era autorizada. A composição foi um comprometimento total do processo mais confiável do dispositivo.
Se isso soa familiar, é porque problemas de confused deputy estão em toda parte quando você começa a procurar: regras de sudo que permitem um binário inofensivo com um arquivo de configuração gravável, roles de IAM na nuvem que podem se passar por um serviço mais poderoso, sistemas de build que assinam tudo o que o job de CI lhes entrega. Cobrimos o lado de escalação de privilégios desse padrão em the history of Unix privilege escalation: os mecanismos mudam, a forma do bug não. Handles, capabilities e tokens são tão fortes quanto a integridade da memória que os armazena.
O que os projetistas de Secure Boot continuam errando
Recue um passo e o padrão ao longo de toda a cadeia é consistente, e é o mesmo padrão por trás da maioria das brechas corporativas sérias que já revisei: o núcleo verificado se manteve, e tudo ao redor dele não. O boot ROM, o esquema de assinatura e as mitigações do kernel da Nintendo estavam bons. O que caiu foi uma checagem de limites do interpretador, um buffer de normalização de caminho, um campo de permissão armazenado em memória gravável, uma janela de TOCTOU entre dois serviços confiáveis e uma tabela de handles que aceitava ponteiros de kernel de estruturas graváveis pelo userspace. Nenhum desses casos é exótico. Todos são o tipo de coisa que sobrevive à revisão porque cada um parece razoável isoladamente.
- Audite o código chato. Normalizadores de caminho, checagens de tamanho e tabelas de handles são onde as cadeias de exploit de fato começam, não na criptografia. Dedique tempo de revisão proporcional à alcançabilidade pelo atacante, não à sofisticação algorítmica.
- Trate cada lacuna de mitigação como um roteiro. Se um processo do sistema não tem PAC/CFI, seja uma camada de compatibilidade, um binário de terceiros ou um interpretador legado, presuma que ele é o ponto de entrada e modele o que ele consegue alcançar.
- Elimine a confiança baseada em posição. Um componente que valida dados e entrega um handle mutável a jusante criou uma janela de troca. Passe objetos imutáveis e autoverificáveis, ou verifique novamente no momento do uso.
- Presuma que memória gravável em qualquer lugar é memória gravável em todo lugar. Flags de permissão, campos de PID e valores de handle armazenados em memória acessível ao atacante fazem parte da sua superfície de ataque, não do seu mecanismo de imposição.
A outra conclusão desconfortável é sobre as ferramentas. O exploit foi desenvolvido com um agente de IA que tinha acesso físico ao hardware por meio de dispositivos KVM-over-IP: vídeo de entrada, input do controle via USB e um gadget de alavanca Bluetooth para pressionar o botão de energia após cada crash. Várias instâncias do agente trabalharam em superfícies de ataque diferentes em paralelo, o tempo todo, por duas semanas. Seja qual for sua opinião sobre pesquisa de segurança assistida por IA, a economia da descoberta de vulnerabilidades em hardware fechado acabou de mudar. Bugs que exigiam um especialista paciente agora exigem um especialista paciente que consegue supervisionar três assistentes incansáveis. Se o seu modelo de ameaças assumia que “ninguém vai se dar ao trabalho de fazer engenharia reversa desse sysmodule”, essa premissa venceu este mês.
A cadeia é o produto
É tentador ler um texto sobre jailbreak como uma lista dos erros da Nintendo. Leia-o, em vez disso, como um espelho. Cada sistema que já ajudei a defender tinha um equivalente do SmileBASIC (o componente legado que ninguém endureceu), um equivalente do buffer de caminho (o código de encanamento que cresceu além das suposições originais) e um equivalente da race do loader (a validação que confiou em um intermediário mutável). A vantagem dos atacantes não está na genialidade de nenhum passo isolado. Está no fato de que eles pensam em cadeias, enquanto os defensores pensam em componentes.
Então o exercício para o seu time esta semana não é “alguém conseguiria forjar nossas assinaturas?”. É: escolha o serviço interno mais confiável, presuma que o atacante já tem escrita arbitrária no processo menos endurecido que conversa com ele, e percorra a cadeia para frente. Onde a primeira escalada cai? O que essa identidade permite alcançar? Você vai encontrar o seu próprio sysmodule fs em breve. Melhor você do que alguém com uma KVM, uma alavanca Bluetooth e duas semanas de paciência.


