Red de conocimiento informático - Material del sitio web - ¿Cómo evitar eficazmente que se mire el código fuente de un programa Java?

¿Cómo evitar eficazmente que se mire el código fuente de un programa Java?

Otros pueden ver fácilmente el código fuente de un programa Java. Mientras haya descompilación, cualquiera puede analizar el código de otras personas. Este artículo analiza cómo proteger el código fuente mediante cifrado sin modificar el programa original: ¿por qué cifrar?

Para lenguajes tradicionales como C o C++, es fácil proteger el código fuente en la Web siempre que no se distribuya. Desafortunadamente, otras personas pueden ver fácilmente el código fuente de un programa Java. Mientras haya descompilación, cualquiera puede analizar el código de otras personas. La flexibilidad de Java hace que el código fuente sea fácil de robar, pero también hace que sea relativamente fácil proteger el código mediante cifrado. Lo único que necesitamos saber es Ja. Por supuesto, en el proceso de cifrado del objeto ClassLoader de va, el conocimiento sobre la Extensión de Criptografía Java (JCE) también es esencial.

Existen varias técnicas para ofuscar archivos de clases Java, lo que reduce en gran medida la efectividad del descompilador en el procesamiento de archivos de clases. Sin embargo, no es difícil modificar el descompilador para procesar estos archivos de clase ofuscados, por lo que es imposible garantizar la seguridad del código fuente basándose únicamente en la tecnología de ofuscación.

Podemos cifrar aplicaciones utilizando herramientas de cifrado populares como PGP (Pretty Good Privacy) o GPG (GNU Privacy Guard). En este punto, el usuario final debe descifrarlo antes de ejecutar la aplicación, pero después del descifrado, el usuario final tendrá un archivo de clase sin cifrar, lo que no es diferente a no cifrarlo de antemano.

El mecanismo mediante el cual el tiempo de ejecución de Java carga el código de bytes significa que el código de bytes se puede modificar. Cada vez que la JVM carga un archivo de clase, requiere un objeto llamado ClassLoader, que es responsable de cargar la nueva clase en la JVM en ejecución. La JVM le da a ClassLoader una cadena que contiene el nombre de la clase que se va a cargar (como java lang Object), y luego ClassLoader es responsable de encontrar el archivo de clase, cargar los datos originales y convertirlos en un objeto de clase.

Podemos modificar los archivos de clases antes de su ejecución personalizando el cargador de clases. Esta tecnología se utiliza ampliamente aquí. Su función es descifrar archivos de clase a medida que se cargan, por lo que se puede considerar como un descifrador sobre la marcha. Debido a que el archivo de código de bytes descifrado nunca se guarda en el sistema de archivos, a los ladrones les resulta difícil obtener el código descifrado.

Debido a que el proceso de convertir código de bytes sin formato en objetos de clase es responsabilidad exclusiva del sistema, no es difícil crear un objeto ClassLoader personalizado. Primero obtenga los datos sin procesar y luego realice cualquier transformación, incluido el descifrado.

Java simplifica hasta cierto punto la construcción de cargadores de clases personalizados. En Java, la implementación predeterminada de loadClass sigue siendo responsable de manejar todos los pasos necesarios, pero también llama a un nuevo método findClass para tener en cuenta el proceso de carga de varias clases personalizadas.

Esto nos proporciona un atajo para escribir un cargador de clases personalizado, lo que reduce los problemas al anular findClass en lugar de loadClass. Este método evita repetir todos los pasos comunes que debe realizar el cargador, ya que loadClass se encarga de todos ellos.

Sin embargo, el cargador de clases personalizado de este artículo no utiliza este método, por la sencilla razón de que se puede descubrir si el cargador de clases predeterminado busca primero el archivo de clase cifrado; está cifrado y no reconocerá que el proceso de carga de este archivo de clase fallará, por lo que tenemos que implementar loadClass nosotros mismos, lo que agrega un poco de carga de trabajo.

Dos cargadores de clases personalizados

Cada JVM en ejecución tiene un cargador de clases, el cargador de clases predeterminado, que se encuentra en el sistema de archivos local según el valor de la variable de entorno CLASSPATH Encuentre el apropiado archivo de código de bytes.

Aplicar un cargador de clases personalizado requiere una comprensión más profunda de este proceso. Primero tenemos que crear una instancia de la clase del cargador de clases personalizado y luego pedirle explícitamente que cargue otra clase. Esto obliga a la JVM a asociar esta clase y todas las clases que necesita con la lista del cargador de clases personalizado, mostrando cómo usar las cargas del cargador de clases personalizado. archivos de clase.

El manifiesto utiliza un cargador de clases personalizado para cargar archivos de clases.

Lo siguiente es un fragmento de cita.

//Primero crea un cargador de clases de objetos ClassLoader my class loader = newmyclass loader(); //Utiliza un objeto ClassLoader personalizado//Carga el archivo de clase y conviértelo en un objeto de clase Class my Class = my Cargador de clases Load Class (mi paquete, mi clase); //Finalmente crea una instancia de esta clase, Object New Instance = My Class New Instance() //Tenga en cuenta que otras clases requeridas por MyClass se cargarán automáticamente a través de //personalizado; Cargador de clases?

Como se mencionó anteriormente, para personalizar ClassLoader solo es necesario obtener primero los datos del archivo de clase y luego pasar el código de bytes al sistema de ejecución, y el sistema de ejecución completará las tareas restantes.

El cargador de clases tiene varios métodos importantes. Al crear un cargador de clases personalizado, solo necesitamos anular uno de ellos, es decir, loadClass proporciona el código para obtener los datos del archivo de clase original. Este método toma dos parámetros: el nombre de la clase y un indicador que indica si la JVM necesita resolver el nombre de la clase (es decir, si debe cargar clases dependientes al mismo tiempo). Si este indicador es verdadero, solo necesitamos llamar a resolveClass antes de regresar a la JVM.

Enumere la implementación simple del cargador de clases loadClass()

A continuación se muestra un fragmento de referencia.

La clase pública Load Class (nombre de cadena Boolean Resolve) lanza ClassnotFoundException { try {//El objeto de clase que queremos crear Class clasz = null//Pasos necesarios si la clase ya está en el buffer del sistema/ / Ya no necesitamos cargar clasz = findLoadedClass(name); if (clasz!= null) return clasz//La siguiente es la parte personalizada byte classData[] = /*Obtener datos de código de bytes a través de algún método*/; classData! = null) {//Leí con éxito los datos del código de bytes, ahora conviértalos en un objeto de clase classz = define Class(name Class data Class data length);}//Los pasos necesarios para el error anterior//Intentamos utilizar el ClassLoader predeterminado se carga If (clasz = = null) clasz = findsystemclass (name); // Pasos necesarios Si es necesario, cargue clases relacionadas If (Resolve & clas. = null) resolve class (clasz); a la persona que llama, devolviendo clasz} catch(io excepción es decir){ throw new ClassNotFoundException(es decir toString());} catch(GeneralSecurityException GSE){ throw new ClassNotFoundException(GSE toString());} }?

El listado muestra que la mayor parte del código para una implementación loadClass simple es el mismo para todos los objetos ClassLoader, pero una pequeña porción (marcada con comentarios) es única. Durante el procesamiento, el objeto ClassLoader necesita utilizar varios otros métodos auxiliares.

FindLoadedClass se utiliza para verificar que la clase solicitada no existe actualmente y que el método loadClass debe llamarla primero.

Después de que defineClass obtenga los datos del código de bytes del archivo de clase original, llame a defineClass para convertirlo en un objeto de clase. Cualquier implementación de loadClass debe llamar a este método.

FindSystemClass proporciona soporte para el cargador de clases predeterminado.

Si el método personalizado utilizado para buscar una clase no puede encontrar la clase especificada (o no utiliza deliberadamente el método personalizado), puede llamar a este método para probar el método de carga predeterminado. Este método es muy útil, especialmente cuando se carga Java estándar desde el modo normal. Archivos JAR.

ResolveClass Cuando la JVM quiere cargar no solo la clase especificada, sino también todas las demás clases a las que hace referencia la clase, establece el parámetro de resolución de loadClass en verdadero. En este momento, primero debemos llamar a resolveClass y luego devolver el objeto de clase recién cargado a la persona que llama.

Tres cifrados y descifrados

Java Cryptography Extension (JCE para abreviar) es el software de servicio de cifrado de Sun, que incluye funciones de cifrado y generación de claves. JCE es una extensión de JCA (Java Cryptozoology Architecture).

JCE no especifica un algoritmo de cifrado específico, pero proporciona una implementación específica del algoritmo de cifrado del marco, que se puede agregar como proveedor de servicios. Además del marco JCE, el paquete JCE también incluye el proveedor de servicios SunJCE, que incluye muchos algoritmos de cifrado útiles como des (Data Encryption Standard) y Blowfish.

Para simplificar, en este artículo utilizaremos el algoritmo DES para cifrar y descifrar códigos de bytes. A continuación se detallan los pasos básicos que se deben seguir al cifrar y descifrar datos utilizando JCE.

Pasos para generar una clave de seguridad Antes de cifrar o descifrar cualquier dato, una clave es una pequeña porción de datos que se libera con la aplicación cifrada. El listado muestra cómo generar una clave. El manifiesto genera una clave.

Lo siguiente es un fragmento de cita.

//El algoritmo DES requiere una fuente de números aleatorios confiable, SecureRandom Sr = new SecureRandom(); //Genere un objeto KeyGenerator para el algoritmo DES que seleccionamos generador de claves kg = generador de claves getinstance(DES); Kilogram initial (Sr); //Generar clave secreta key = kg Generate key(); //Obtener los datos clave en bytes raw key data[] = key get encoded() /* Luego puedes cifrar o descifrar con la clave O guárdelo como un archivo para usarlo más adelante */haga algo (datos de clave sin procesar); cifrar datos Después de obtener la clave, puede usarla para cifrar los datos. Además del cargador de clases descifrado, suele haber un programa independiente que cifra la aplicación para su distribución (ver listado).

Lo siguiente es un fragmento de cita.

//El algoritmo DES requiere una fuente de números aleatorios confiable, SecureRandom Sr = new SecureRandom(); Byte rawKeyData[] = /*Obtener datos clave de alguna manera*/; los datos crean un objeto de especificación de escritorio escritoriospec dks = nuevo escritorio de especificación (datos de clave originales) //Crea una fábrica de claves y luego úsala para convertir DESKeySpec en //un objeto SecretKey Fábrica de claves secretas fábrica de claves = Fábrica de claves secretas GetInstance(DES);secreto); key key = key factory generate secret(dks); //El objeto de cifrado realmente completa la operación de cifrado cipher = cipher getinstance(des //Inicializa el objeto de contraseña con la clave Cipher init(Cipher encrypt _ mode key Sr); / Ahora obtenga los datos y cifrelos byte data[] = /*Obtenga los datos de alguna manera*//Realice la operación de cifrado byte encrypted data[]= cipher do final(data); (datos cifrados); cuando se ejecuta una aplicación cifrada, ClassLoader analiza y descifra archivos de clase. Los pasos se muestran en la lista, que utiliza una clave para descifrar los datos.

//El algoritmo DES requiere una fuente de números aleatorios confiable, SecureRandom Sr = new SecureRandom(); Byte rawKeyData[] = /*Obtener los datos de la clave original de alguna manera*/; los datos crean un objeto deskespec escritoriospec dks = nuevo escritoriospec (datos de clave originales) //Crea una fábrica de claves y luego úsala para convertir el objeto DESKeySpec en //un objeto SecretKey Fábrica de claves secretas fábrica de claves = Fábrica de claves secretas GetInstance (DES) ; clave secreta clave = fábrica de claves generar secreto (dks); // El objeto de contraseña realmente completa la operación de descifrado; cifrado cipher = contraseña getinstance(des) // Inicializa el objeto de contraseña con la clave Cipher init (Cipher decrypt _ mode key); Sr); //Ahora obtenga los datos y descifre los bytes encryptedData[] = /*Obtenga los datos cifrados*//Realice la operación de descifrado byte decrypted data[] = cipher do final(encrypted data); los datos hacen algo (datos descifrados);

Cuatro ejemplos de aplicaciones

La sección anterior presentó cómo cifrar y descifrar datos. Para implementar una aplicación cifrada, siga los pasos a continuación.

Pasos para crear una aplicación Nuestro ejemplo consta de una clase de aplicación principal y dos clases auxiliares (llamadas Foo y Bar respectivamente). Esta aplicación no tiene ninguna funcionalidad real, pero mientras podamos cifrarla, cifrar otras aplicaciones no es un problema.

Los pasos para generar una clave de seguridad son escribir la clave en el archivo % Java GenerateKey Key data usando la herramienta Generate Key (ver GenerateKey java) en la línea de comando.

La aplicación Step Encrypt utiliza la herramienta EncryptClasses para cifrar las clases de la aplicación en la línea de comando% Java Encrypt Classes Key Data App Class foo Class Bar Class (consulte EncryptClasses java).

Este comando reemplaza cada archivo de clase con su propia versión cifrada.

Pasos para ejecutar la aplicación cifrada El usuario ejecuta el programa DecryptStart de la aplicación cifrada a través del programa DecryptStart como se muestra en el listado. Enumere DecryptStart java para iniciar aplicaciones cifradas.

Lo siguiente es un fragmento de cita.

Importar Java io *; importar seguridad java *; importar lenguaje java reflejar *; importar cifrado javax *; importar especificación de cifrado javax *; el método set loadClass() los usará para descifrar la clave privada SecretKey de la clase // constructor establece el inicio de descifrado público del objeto (clave) lanza la excepción de seguridad general ioException { thiskey = algoritmo de cadena de clave = DESSecureRandom Sr = new SecureRandom(); err println( [DecryptStart: Crear contraseña]); cipher = Cipher getInstance (algoritmo); inicialización de contraseña (clave de modo de descifrado de contraseña Sr } // En el proceso principal, necesitamos leer la clave aquí Crear una instancia de DecryptStart/ /Este es nuestro ClassLoader personalizado //Lo usaremos para cargar la instancia de la aplicación después de configurar el ClassLoader//Finalmente llamamos al método principal de la instancia de la aplicación a través de la API de reflexión de Java static public void main(String Args[ ]) throwsException { String key filename = Args[]; string appName = args[]; // Estos son los parámetros pasados ​​a la aplicación misma String Real Args[] = New String[Args system arraycopy( args realArgs args length); /Leer error del sistema de claves println([inicio de descifrado: clave de lectura de bytes]); = Util readFile(nombre de archivo de clave); DES); clave secreta clave = fábrica de claves generar secreto (dks); //Crear cargador de clases descifrado decrypt start dr = new decrypt start(clave); //Crear una instancia de la clase principal de la aplicación // Cargar con ClassLoader ([descifrado); comienza: load + appname +]); clase classz = dr load class (appName); // Finalmente, llame al método main() de la instancia de la aplicación // a través de la API de reflexión // y obtenga un par de cadenas main(). proto[]= referencia de new String[]; clase mainArgs[]= {(new String[])getClass()}; método main = clasz get método(main mainArgs);//Crea un objeto de matriz de parámetros del método main(); matriz de argumentos[]= {argumentos reales}; error del sistema println([DecryptStart:running+appName+main()]); //Llamar a main()main invoke(matriz de argumentos nulos} clase de carga de clase pública (nombre de cadena resolución booleana); ) lanza classnotfoundexception { try {//El objeto de clase que queremos crear Clase

clasz = null // Pasos necesarios si la clase ya está en el buffer del sistema // Ya no necesitamos cargarla clasz = findLoadedClass(name); if (clasz!= null) return clasz // La siguiente es la costumbre; part try { // Leer el archivo de clase cifrado byteclassdata[]= utilreadfile(name+class); if (classData!= null){//Decryptedclassdata[]= cipher do final(class data);// Luego convertir a clase clasz = definir clase (nombre Longitud de la clase de descifrado); System err println( [DecryptStart: clase de descifrado + nombre +] } } catch (excepción de archivo no encontrado fnfe) // Pasos necesarios cuando lo anterior no tiene éxito // Intentamos usar el ClassLoader predeterminado para cargar If( clasz = = null) clasz = findsystemclass(name); // Pasos necesarios Si es necesario, cargue la clase relevante If (Resolve & amp; clas. = null) resolve class(clasz); la clase a la que llama O, devuelva clasz} catch(io excepción, es decir){ throw new ClassNotFoundException(es decir, toString());} catch(GeneralSecurityException GSE){ throw new ClassNotFoundException(GSE toString());}}} Para aplicaciones no cifradas , normal El método de ejecución es el siguiente:% javaappaargargargargarg.

Para aplicaciones cifradas, el modo de funcionamiento correspondiente es % Java descifrado clave de inicio datos aplicación argargarg.

DecryptStart tiene dos propósitos: una instancia de DecryptStart es un cargador de clases personalizado que implementa el descifrado sobre la marcha al mismo tiempo, DecryptStart también contiene un proceso principal que crea una instancia del descifrador y lo utiliza; para cargar y ejecutar una aplicación de muestra. El código de la aplicación está contenido en App java Foo java y Bar java. Util java es una herramienta de E/S de archivos. Este ejemplo utiliza su código completo en muchos lugares. Descárguelo desde el final de este artículo.

Cinco notas

Hemos visto que es fácil cifrar una aplicación Java sin modificar el código fuente, pero no existe ningún sistema completamente seguro en el mundo. Los métodos de cifrado descritos en este artículo proporcionan cierto grado de protección del código fuente, pero son vulnerables a ciertos ataques.

Aunque la aplicación en sí está cifrada, el iniciador DecryptStart no está cifrado. Un atacante puede descompilar y modificar el iniciador y guardar el archivo de clase descifrado en el disco. Una forma de reducir este riesgo es realizar una ofuscación de alta calidad del iniciador, o el iniciador también puede usar código compilado directamente en lenguaje de máquina, dándole al iniciador la seguridad de un formato de archivo ejecutable tradicional.

Además, es importante recordar que la mayoría de las JVM no son intrínsecamente seguras y un hacker astuto puede modificar la JVM para obtener el código de descifrado de un ClassLoader externo y guardarlo en el disco, evitando así el cifrado. técnicas en este artículo, y Java no proporciona un remedio realmente efectivo para esto.

Lishi Xinzhi/Article/program/Java/hx/201311/25751