Debugger.godMode() – Hackear uma Aplicação JVM com o Depurador
Outras línguas: English Español 한국어 中文
Houve um tempo em que os jogos de computador eram diferentes. Não só a gráfica e a mecânica evoluíram desde então, mas também existe uma característica que não parece muito comum nos jogos de hoje: praticamente todos eles tinham códigos de trapaça.
Os códigos de trapaça eram sequências de teclas que lhe davam algo extraordinário, como munição infinita ou a capacidade de atravessar paredes. O mais comum e poderoso deles era o ‘modo deus’ – ele tornava você invencível.
É assim que seu personagem ficaria quando você digitasse IDDQD
– a combinação de teclas para o ‘modo deus’ no Doom.
Na verdade, essa sequência de teclas era tão popular que
se tornou um meme
e ganhou popularidade além do jogo em si.
Enquanto o ‘modo deus’ não é tão comum em jogos como era antigamente, e a era do meme IDDQD parece estar desaparecendo, podemos nos perguntar se existe um equivalente contemporâneo. Pessoalmente, tenho minha própria visão moderna do IDDQD. Embora não esteja necessariamente relacionado a jogos, evoca o mesmo sentido de ter superpoderes.
Space Invaders
Para ilustrar meu ponto, gostaria de trazer um cenário divertido aqui. Mesmo que você não esteja familiarizado com o Doom, você provavelmente já viu este jogo ainda mais antigo chamado Space Invaders. Como o Doom, seu enredo gira em torno do tema de combater invasores no espaço.
Meu amigo e colega, Eugene, escreveu um plugin IntelliJ IDEA, que permite você jogar este jogo diretamente no editor - uma ótima maneira de passar algum tempo enquanto espera pelo término do indexamento.
Não existe um modo de deus neste jogo, mas se estivermos muito determinados, podemos adicioná-lo nós mesmos? Vamos trazer de volta a tradição clássica de hackear programas com um depurador e descobrir!
Seja responsável! Obtive o consentimento de Eugene antes de mexer em seu programa. Se você está usando o depurador em um código que não é seu, certifique-se de que é ético. Caso contrário, não faça isso.
Prepare as ferramentas
Prepare-se para uma experiencia meta – vamos depurar o IntelliJ IDEA usando o seu próprio depurador.
Aqui temos um pequeno problema que precisamos resolver: para depurar o IntelliJ IDEA, precisamos suspendê-lo. Isso vai tornar a IDE irresponsiva. Por isso, precisamos de uma instância extra da IDE que permanecerá funcional e servirá como nossa ferramenta de depuração.
Para gerenciar várias instâncias da IDE, vou utilizar o JetBrains Toolbox. Esta é uma utilidade que organiza as IDEs instaladas da JetBrains. Com ele, você pode instalar várias versões ou criar atalhos para executá-las com diferentes conjuntos de opções VM.
Vamos instalar duas instâncias do IntelliJ IDEA:
Se você estiver usando a mesma versão da IDE para ambas as instâncias, certifique-se de especificar diferentes diretórios de sistema, configuração, e logs em Tool actions | Settings | Configuration. Nesta página, você também pode atribuir nomes às instâncias da IDE para conveniência.
Para poder depurar a instância ‘Space Invaders’, clique em Tool actions próximo a ela, depois vá para Settings | Edit JVM options. No arquivo que abre, cole a seguinte linha:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
Isso fará com que a JVM alvo seja executada com o agente de depuração e ouvirá as conexões do depurador
na porta 5005
.
Execute o jogo
Execute a instância Space Invaders
,
instale o jogo,
e inicie-o executando a ação Space Invaders.
Para encontrar a ação, pressione Shift duas vezes e comece a digitar Space Invaders
:
Vamos jogar por um tempo e observar o comportamento que queremos consertar: quando os míssies inimigos atingem a nave espacial, a barra de saúde no canto superior esquerdo da tela diminui.
Anexar e suspender
Nossa jornada de depuração começa abrindo a instância ‘Debug’ da IDE e configurando um novo projeto Kotlin. A principal razão pela qual precisamos deste projeto é que não seria possível lançar o depurador sem um.
Além disso, o IntelliJ IDEA inclui a biblioteca padrão Java/Kotlin no novo template de projeto, que podemos usar mais tarde. Explicarei o uso da biblioteca padrão nos capítulos subsequentes.
Depois de criar o projeto, vá para o menu principal e selecione Run | Attach to Process. Isso mostrará a lista de JVMs locais ouvindo solicitações de anexação do depurador. Selecione a outra IDE em execução da lista.
Deveríamos ver a seguinte mensagem no console confirmando que o depurador foi anexado com sucesso à VM alvo.
Connected to the target VM, address: 'localhost:5005', transport: 'socket'
Estamos chegando à parte interessante: como suspendemos a aplicação?
Normalmente, alguém definiria um ponto de interrupção no código da aplicação, mas neste caso, não temos as fontes da IntelliJ IDEA e nem do plugin Space Invaders. Isso não só nos impede de definir um ponto de interrupção, como também complica nossa compreensão de como o programa opera. À primeira vista, parece que não há nada para inspecionar ou avançar.
Felizmente para nós, o IntelliJ IDEA tem um recurso conhecido como Pause Program. Ele permite suspender o programa em qualquer ponto arbitrário no tempo, sem a necessidade de especificar a linha de código correspondente. Você pode encontrá-lo na barra de ferramentas do depurador ou no menu principal: Run | Debugging Actions | Pause Program.
A aplicação é suspensa. Isso nos dá um ponto de partida para a depuração.
Pause Program é uma técnica muito poderosa, especialmente útil em vários cenários avançados. Para saber mais, confira os artigos relacionados:
Encontre os objetos relevantes
Se analisarmos nosso objetivo em termos de programação, ele se resume a impedir que a saúde da nave espacial diminua. Vamos encontrar o objeto que mantém o estado correspondente.
Como não sabemos nada sobre o código do plugin, podemos inspecionar diretamente o heap usando a visualização Memory do depurador IntelliJ IDEA:
Este recurso fornece informações sobre todos os objetos que estão atualmente vivos.
Vamos digitar invaders
e ver se podemos encontrar algo:
Aparentemente, as classes do plugin estão sob o pacote com.github.nizienko.spaceinvaders
.
Dentro deste pacote, há uma classe GameState
com várias instâncias vivas.
Parece ser o que precisamos.
Ao clicar duas vezes em GameState
, são mostradas todas as instâncias desta classe:
Acontece que é um enum - que não é exatamente o que estávamos procurando.
Continuando nossa pesquisa, nos deparamos com uma única instância de Game
.
Expandir o nó nos permite inspecionar os campos da instância:
A propriedade health
parece ser a que nos interessa aqui.
Entre seus campos, podemos encontrar _value
.
No meu caso, era 100
, o que se correlaciona com
a barra de saúde estar cheia quando suspendi o jogo.
Portanto, é provável que seja o campo correto a considerar, e seu valor parece variar de 0
a 100
.
Vamos colocar esta hipótese à prova.
Clique com o botão direito em _value
, depois selecione Set Value.
Escolha um valor que seja diferente do seu atual. Por exemplo, escolhi 50
.
Nesta etapa, nos deparamos com um erro que diz Cannot evaluate methods after Pause action:
O problema surge porque usamos Pause Program em vez de pontos de interrupção, e esse recurso vem com algumas limitações. No entanto, podemos recorrer a um pequeno truque para contornar isso.
Eu o descrevi em um dos posts anteriores dedicado a Pause Program. Caso você tenha perdido lá, aqui está o que precisa ser feito: uma vez que a aplicação tenha pausado, execute uma ação de passo a passo, como Step Into ou Step Over. Fazer isso vai permitir o uso de recursos avançados como Set Value e Evaluate Expression.
Depois de aplicar o truque descrito acima, deveríamos ser capazes de definir o valor de health
.
Tente modificar o valor, depois retome a aplicação para ver se a barra de saúde exibe alguma alteração. De fato, ela exibe!
Então, localizamos o objeto que mantém o estado relevante. No mínimo, podemos reabastecer manualmente a barra de saúde de vez em quando. Embora isso não seja um sucesso completo ainda, já é um passo significativo à frente.
Etiquetas e expressões
Agora que identificamos o objeto a ser focado, seria útil marcá-lo. Para aqueles não familiarizados com etiquetas de depuração, é assim que um objeto marcado se parece:
As etiquetas podem ser benéficas de várias maneiras. No contexto deste artigo, marcar o objeto relevante garante que podemos usá-lo diretamente em recursos como Evaluate Expression sem depender do contexto de execução atual.
Infelizmente, não é possível marcar diretamente _value
, mas podemos marcar o objeto que o encapsula.
Para fazer isso, clique com o botão direito em health
, selecione Mark Object e dê um nome.
Agora podemos testar como a etiqueta funciona em outro lugar.
Abra a janela Evaluate Expression
e insira health_object_DebugLabel
como a expressão.
Como você pode ver, o objeto está acessível de qualquer lugar do programa através do diálogo Evaluate:
E quanto a mudar a saúde da nave espacial pelo Evaluate?
health_object_DebugLabel._value = 100
não funciona.
Ao mesmo tempo, _value
parece ser um campo de armazenamento de uma propriedade Kotlin.
Se for verdade, o Kotlin deve ter gerado um getter correspondente:
health_object_DebugLabel.getValue()
O diálogo Evaluate não acha que este é um código válido, mas vamos tentar de qualquer maneira:
A expressão retorna a saúde atual da nave espacial, então este método funciona! Razoavelmente, o setter deve funcionar também:
health_object_DebugLabel.setValue(100)
Depois de avaliar o setter, vamos retomar a aplicação e verificar se as alterações surtiram efeito. De fato, a barra de saúde está cheia!
Engate a expressão
O único passo restante para atingir nosso objetivo é automatizar a modificação do estado, para que o ‘refil de saúde’ aconteça nos bastidores, nos deixando desfrutar do jogo sem interrupções.
Isso pode ser feito usando pontos de interrupção não suspensos. Esse tipo de ponto de interrupção é comumente usado para logs; entretanto, a expressão de log não precisa necessariamente ser pura. Portanto, podemos introduzir o efeito colateral desejado dentro da expressão de log. No entanto, ainda não está claro onde definir esse ponto de interrupção, já que não temos o código fonte da aplicação.
Lembra que eu disse que poderíamos usar as fontes do Java standard library? Aqui está a ideia. O IntelliJ IDEA e seus plugins são escritos em Java/Kotlin, e eles usam Swing como o framework de UI. Portanto, Space Invaders certamente chamará código dessas dependências. Isso significa que podemos usar suas fontes para definir os pontos de interrupção.
Para manter a simplicidade, não especificamos uma versão do JDK em particular e iniciamos o projeto com a sugerida pelo IntelliJ IDEA. No entanto, para obter os melhores resultados, recomenda-se usar fontes que correspondam de perto à versão usada no runtime.
Existem inúmeros locais adequados para definir um ponto de interrupção.
Decidi definir um ponto de interrupção de método em java.awt.event.KeyListener::keyPressed
.
Isso desencadeará o efeito colateral toda vez que pressionarmos uma tecla:
Definir um ponto de interrupção com uma expressão em um código de execução frequente pode diminuir significativamente a velocidade do aplicativo alvo.
Vamos voltar ao Space Invaders e ver se o nosso IDDQD caseiro funciona. E funciona!
Conclusão
Neste artigo, usamos o depurador para descobrir como um aplicativo funciona internamente. Depois de entender isso, fomos capazes de navegar em sua memória e modificar sua funcionalidade, tudo isso sem acessar o código-fonte do aplicativo! Espero que minha comparação do depurador com o IDDQD não tenha sido muito ousada, e que você tenha aprendido algumas técnicas que irão empoderar você em seus desafios de depuração.
Gostaria de estender meus elogios a Eugene Nizienko por fazer o plugin Space Invaders e Egor Ushakov por constantemente compartilhar comigo uma abundância de criatividade em depuração e programação. Os computadores são duas vezes mais divertidos com pessoas assim ao redor.
Se você tem desafios de depuração em mente que quer que eu aborde nos próximos posts, me avise!
Bom hacking!