Cómo convertir archivos .class a archivos .java
Lo más frustrante es encontrar un error que no se puede solucionar sin el código fuente. Es esta razón la que llevó a la aparición de los descompiladores de Java, que convierten completamente el código de bytes compilado en código fuente. Aunque los descompiladores de código no son exclusivos del lenguaje Java, los desarrolladores de Java nunca han utilizado descompiladores de manera más abierta o amplia.
Lo opuesto a la descompilación es la ofuscación. Suponiendo que un descompilador pueda obtener fácilmente el código fuente del código compilado, proteger su código y sus valiosos secretos técnicos no es tan sencillo. A medida que los descompiladores de Java se vuelven más comunes, también lo hacen los fuzzers de Java. La descompilación y la ofuscación han provocado un debate en el mundo del desarrollo comercial, centrado principalmente en el lenguaje Java.
En este artículo, le presentaré cómo funcionan la descompilación y la ofuscación de código, discutiré las cuestiones teóricas de ambas técnicas y abordaré brevemente los debates que han provocado en el mundo de la programación comercial. También presentaré algunos de los descompiladores y fuzzers más conocidos (tanto comerciales como de código abierto) y crearé algunos ejemplos usándolos a medida que avance el artículo.
¿Qué es la descompilación?
La descompilación es el proceso de convertir código objeto en código fuente. Esto debe quedar claro ya que la compilación es un proceso de convertir el código fuente en código objeto. Pero ¿qué es el código objeto? En términos generales, el código objeto es un código expresado en un lenguaje que puede ser ejecutado directamente por una máquina real o virtual. Para lenguajes como C, el código objeto normalmente se ejecuta en una CPU de hardware, mientras que el código objeto Java normalmente se ejecuta en una máquina virtual.
Descompilar es difícil
Como se mencionó anteriormente, descompilar parece simple, pero en realidad es muy difícil; esencialmente, implica inferir el comportamiento de bajo nivel a partir de pequeñas escalas. Comportamiento avanzado a escala. Para entender esto intuitivamente, también podríamos imaginar un programa de computadora como una estructura organizacional corporativa compleja. Los altos directivos dan órdenes como "maximizar la productividad técnica" a los subordinados, quienes traducen estas órdenes en acciones más específicas, como instalar una nueva base de datos XML.
Como nuevo empleado de su empresa, podría preguntar a sus subordinados qué están haciendo y la respuesta sería "Estoy instalando una nueva base de datos XML". De esta frase es poco probable que se pueda inferir que el objetivo final sea maximizar la productividad tecnológica. Al fin y al cabo, el objetivo final varía y puede ser, por ejemplo, la separación de las cadenas de suministro o la acumulación de datos de los consumidores.
Sin embargo, si es una persona muy curiosa, puede hacer algunas preguntas más y que sus subordinados en diferentes niveles de la organización respondan sus preguntas. Al final, cuando se reúnen todas las respuestas, se podría adivinar que el objetivo más amplio es maximizar la productividad técnica.
Si piensa que la forma en que funciona un programa de computadora es similar a la estructura organizacional de una empresa, la analogía anterior le hará comprender inmediatamente por qué descompilar código no es trivial. Desde una perspectiva más teórica, Cristina Cifuentes, reconocida investigadora en la materia, describe el proceso de descompilación así:
Cualquier proyecto de conversión binaria requiere descompilar el código almacenado en el archivo binario. Teóricamente, separar datos y código en von Neumann es similar a un problema de detención, por lo que no es posible una traducción estática completa. Pero en la práctica, se pueden usar diferentes técnicas para aumentar la proporción de código que se puede traducir estáticamente o técnicas de traducción dinámicas que se pueden usar en tiempo de ejecución.
La conversión de código objeto a código fuente no es el único problema encontrado durante la descompilación. Los archivos de clase Java pueden contener muchos tipos diferentes de información. Comprender el tipo de información que puede contener un archivo de clase es importante para comprender cómo se puede explotar esta información y qué hacer con ella. Esto es lo que realmente hace el desensamblador de Java.
Volver al inicio
Desmontaje de archivos de clase
El formato binario real de un archivo de clase Java no es importante. Es importante comprender qué diferentes tipos de información contienen estos bytes. En este punto, usaremos la mayoría de las herramientas que vienen con el JDK: javap. javap es un desensamblador de código Java, que es diferente de un descompilador. El desensamblador convierte el código objeto en un formato legible por máquina (que se muestra en el Listado 1) en código legible por humanos (que se muestra en el Listado 2).
Listado 1. Contenido original del archivo de clase
000000 feca beba 0300 2d00 4200 0008 081f 3400
0000020 0008 073f 2c00 0007 0735 3600 0007 0737 p >
0000040 3800 0007 0a39 0400 1500 000a 0007 0a15
0000060 0800 1600 000a 0008 0a17 0800 1800 0009
...
Lista 2. Salida de javap
Variables locales del método void priv(int)
Foo this pc=0, length=35, slot=0
int argument pc= 0, longitud = 35, ranura = 1
Método void main(java.lang.String[])
Listado 2.lang.String[])
0 nuevo #4
3 invokespecial #10
6 retorno
Tenga en cuenta que lo que se muestra en el Listado 2 no es el código fuente. La primera parte de la lista enumera las variables locales del método; la segunda parte es el código ensamblador, que también es el código objeto legible por humanos.
Elementos en archivos de clase
javap se utiliza para desmontar o descomprimir archivos de clase. Aquí hay una breve lista de la información contenida en los archivos de clase Java que se pueden desensamblar usando javap:
Variables miembro. Cada archivo de clase contiene toda la información de nombre y tipo correspondiente a cada miembro de datos de la clase.
Método de desmontaje. Cada método de una clase está representado por una cadena de instrucciones de máquina virtual, acompañadas de su firma de tipo.
Número de línea. Cada sección dentro de cada método se asigna a una línea de código fuente y, cuando sea posible, a la línea de código fuente que generó esa sección. Esto permite que los sistemas y depuradores en tiempo real proporcionen seguimientos de pila para programas en ejecución.
Nombres de variables locales Una vez que se compila un método, las variables locales del método en realidad no necesitan nombres, pero se pueden incluir usando la opción -g del compilador javac. Esto también permite que el sistema en tiempo real y el depurador le ayuden.
Ahora que sabes un par de cosas sobre la estructura interna de un archivo de clase Java, veamos cómo convertir esta información para nuestros propósitos.
Volver al inicio
Usar un descompilador
Conceptualmente, usar un descompilador es muy simple. Es un descompilador: le das un archivo .class y te da un archivo de código fuente.
Algunos descompiladores más nuevos tienen interfaces gráficas sofisticadas. Pero en el ejemplo dado al principio, usaremos Mocha, que es el primer descompilador disponible públicamente. Al final de este artículo, hablaré sobre un descompilador más nuevo disponible bajo la GPL.
(Consulte Recursos para descargar Mocha y obtener una lista de descompiladores de Java)
Supongamos que tenemos un archivo de clase llamado Foo.class en un directorio. Para descompilarlo con Mocha simplemente escribe el siguiente comando:
$ java mocha.Decompiler Foo.class