Conferencia DEFCON 18. Ingeniería inversa Trollim usando las matemáticas

Voy a hablar de trolling matemático. Estos no son algunos trucos de hackers de moda, más bien es una expresión artística, una tecnología inteligente y divertida para que la gente te considere un imbécil. Ahora comprobaré si mi informe está listo para mostrarse en la pantalla. Todo parece ir bien, así que puedo presentarme.



Mi nombre es Frank Tu, escrito frank ^ 2 y @franksquared en Twitter, porque Twitter también tiene algún tipo de spammer llamado "frank 2". Traté de aplicarles ingeniería social para que eliminaran su cuenta, porque técnicamente es spam y tengo el derecho de deshacerme de él como mi clon. Pero aparentemente, si los trata honestamente, no se corresponden, porque a pesar de mi solicitud de eliminar la cuenta de spammer, no hicieron nada con ella, así que envié este puto Twitter al infierno.

Mucha gente me reconoce por mi gorra. Trabajo en los grupos regionales DefCon DC949 y DC310. También trabajo con Rapid7, pero no puedo hablar de eso aquí sin usar lenguaje grosero, y mi gerente no quiere que lo jure. Entonces, he preparado esta presentación para DefCon y voy a cumplir el plazo de 15 minutos, aunque este es un tema bastante complicado. Esta es esencialmente una presentación estándar que se enfoca en ingeniería inversa y cosas divertidas relacionadas.

Al discutir este tema en Twitter, se formaron dos campos. Un tipo dijo: "No tengo idea de qué está hablando este jodido franco ^ 2, ¡pero es increíble!" El segundo chico de Reddit vio mis diapositivas y estaba molesto por los enlaces a cosas que no estaban relacionadas con el tema, estaba enojado porque un tema tan serio no estaba completamente cubierto, así que deseé que mi presentación tuviera "más contenido y menos basura".



Por lo tanto, quiero centrarme en esta cita. Nada personal, amigo de Reddit. Digo esto no solo en caso de que esté presente en esta sala, sino también porque fue una crítica justa. Porque una conversación que no contiene suficiente contenido útil es una conversación vacía.

El tema de mi conversación es una rutina estándar para los piratas informáticos, pero me parece que, de hecho, los oradores generalmente no intentan presentar su información de una manera entretenida, incluso cuando es posible, prefiriendo conclusiones secas y castradas. "Aquí está IP, aquí está ESP, así es como puedes realizar un exploit, aquí está mi" día cero ", ¡ahora aplaude!" - Y todos aplauden.

Gracias por los aplausos, lo agradezco! Me parece que hay muchos puntos interesantes en mi material, por lo que merece ser expresado de una manera algo entretenida, lo que intentaré hacer.

Verá una actitud excepcionalmente superficial hacia la informática y un humor completamente infantil, así que espero que aprecie lo que voy a mostrar aquí. Lo siento si viniste aquí buscando una conversación seria.

En la diapositiva, verá un análisis científico de mi último informe que compara la parte de un enfoque estrictamente científico y la parte de un "medicamento" que proporciona seguridad informática.



Usted ve que hay muchas más "drogas", pero no tiene que preocuparse, ahora la participación de la ciencia ha aumentado ligeramente.



Entonces, hace algún tiempo, mi amigo Merlin, sentado aquí a la vanguardia, escribió un increíble bot basado en el script IRC Python, que ocupa solo una línea.

Este es un ejercicio realmente increíble para aprender programación funcional, con el cual es muy divertido meterse. Simplemente puede agregar una función tras otra y obtener combinaciones de todo tipo de funciones diferentes, y todo esto se dibuja en la pantalla como una onda de arco iris, en general, esta es una de las cosas más estúpidas que puede hacer.



Pensé que si aplicas este principio a los archivos binarios? No sé de dónde vino esta idea, ¡pero resultó increíble! Sin embargo, quiero aclarar algunos conceptos básicos.

Es posible que su profesor de matemáticas haya presentado estas funciones mucho más complicadas de lo que realmente son.



Entonces, la fórmula f (x) tiene un significado muy simple, funciona como funciones ordinarias. Tienes X, tienes entrada, y luego obtienes X 7 veces, y eso es igual a tu valor. En Python puedes hacer una función (lambda x: x * 7). Si quieres trabajar con Java, lo siento, espero que nunca quieras hacer esto, entonces puedes hacer algo como:

public static int multiplyBySevenAndReturn(Integer x) { return x * 7; } 

Ya sabes, las funciones matemáticas pueden ser mucho más complicadas, pero eso es todo lo que necesitamos saber sobre ellas en este momento.

Si observa el ensamblaje del código, notará que las instrucciones JMP y CALL no están vinculadas a valores específicos; funcionan con un desplazamiento. Si utiliza un depurador, puede ver que JMP00401000 se parece más a la instrucción de "saltar unos bytes hacia adelante" que a una instrucción específica para saltar 5 o 10 bytes. Lo mismo se aplica a la función CALL, excepto que empuja un montón de cosas a tu pila. La excepción es el caso cuando "pega" la dirección al registro, es decir, está accediendo a una dirección específica. Todo sucede aquí de una manera completamente diferente. Después de conectar la dirección al registro y hacer algo como CALL EAX, la función accede al valor específico en EAX. Lo mismo ocurre con CALL [EAX] o JMP [EAX]: simplemente elimina la referencia de EAX y se dirige a esa dirección. Al usar un depurador, es posible que no pueda determinar a qué dirección específica está accediendo CALL. Esto puede ser un problema, por lo que debe saberlo.
Veamos la función de salto corto JMP SHORT. Esta es una instrucción especial en la arquitectura x86 que le permite utilizar un desplazamiento de 1 byte en lugar de un desplazamiento de 4 bytes, lo que reduce el espacio de memoria utilizado. Esto importará más adelante para todas las manipulaciones que ocurrirán con instrucciones individuales. Es importante tener en cuenta que JMP SHORT tiene un rango de 256 bytes. Sin embargo, no existe un CALL SHORT.



Ahora considere la brujería de la informática. En medio de la creación de estas diapositivas, me di cuenta de que, de hecho, puede definir un ensamblaje como espacio cero, es decir, técnicamente hay espacio cero entre cada instrucción. Si observa las instrucciones individuales, verá que cada una se ejecuta una tras otra. Técnicamente, esto puede interpretarse como un salto incondicional a la siguiente instrucción. Esto nos proporciona un espacio entre cada instrucción de ensamblaje, mientras que cada instrucción se asocia correspondientemente con un salto incondicional.

Si observa este ejemplo de ensamblaje, por cierto, estas son cosas muy simples que recomiendo decodificar con ASCII, por lo que esto es solo un conjunto de instrucciones regulares.



Los 0 de JMP ubicados entre cada instrucción son saltos incondicionales que generalmente no se ven. Se siguen después de cada instrucción. Por lo tanto, es posible colocar cada instrucción de ensamblaje individual en una ubicación de memoria arbitraria si y solo si cada instrucción de unidad se acompaña de un salto incondicional a la siguiente instrucción. Porque si transfiere el ensamblaje y necesita usar el mismo código que antes, debe adjuntar un salto incondicional a cada instrucción.
Miremos más allá. Una matriz unidimensional puede interpretarse técnicamente como una matriz bidimensional, solo requiere un poco de matemáticas, filas o algo así, no lo diré con certeza, pero no es demasiado difícil. Esto nos permite interpretar la ubicación en la memoria en forma de una red (x, y). En combinación con la interpretación del espacio vacío entre instrucciones como saltos incondicionales que pueden relacionarse entre sí, podemos literalmente dibujar instrucciones. ¡Esto es asombroso!

Para implementar esto en la práctica, debe realizar los siguientes pasos:

  • desarme cada instrucción para averiguar cuál es el código;
  • Asigne un lugar en la memoria que sea mucho más grande que el tamaño del conjunto de instrucciones. Normalmente reservo 10 veces más memoria que el tamaño del código;
  • para cada instrucción, determine f (x);
  • establezca cada instrucción en la ubicación correspondiente (x, y) en la memoria;
  • adjunte un salto incondicional a la instrucción;
  • marque la memoria como ejecutable y ejecute el código.

Desafortunadamente, muchas preguntas surgen aquí. Es como con la gravedad, que funciona solo en teoría, pero en la práctica vemos algo completamente diferente. Debido a que en realidad x86 envía al infierno sus instrucciones JMP, las instrucciones CALL, distorsiona su código autorreferencial, código auto modificable que usa iteración.



Comencemos con las instrucciones JMP. Dado que las instrucciones JMP están sesgadas, cuando se colocan en un lugar arbitrario, ya no apuntan hacia donde crees que deberían apuntar. SHORT JMP se encuentran en una posición similar. Colocados accidentalmente por su función (x, y), no indicarán con qué está contando. Pero a diferencia de los JMP largos, los JMP cortos son más fáciles de solucionar, especialmente si se trata de una matriz unidimensional. SHORT JMP es fácil de convertir a JMP normal, pero luego debe averiguar en qué se ha convertido el nuevo desplazamiento.

Trabajar con JMP basados ​​en registros sigue siendo un dolor de cabeza, y debido a que requieren compensaciones ajustadas y se pueden calcular en tiempo de ejecución, no hay una manera fácil de saber a dónde van. Para detectar automáticamente cada registro, debe utilizar un montón de conocimientos de la teoría de compilación. En tiempo de ejecución, puede haber punteros de función, punteros de clase y similares. Es cierto que si no desea hacer un trabajo extra para hacer todo esto, entonces no puede hacerlo. Las funciones f (x) funcionan en código real, no tan elegantemente como en papel. Si desea hacerlo correctamente, deberá hacer mucho trabajo.

Para definir punteros de clase y cosas por el estilo, necesita conjurar con C y C ++. Antes de guardar, durante el desmontaje, convierta su JMP CORTO en JMP normal, ya que tiene que lidiar con el sesgo, es bastante simple.

Intentar calcular los desplazamientos reales es un gran dolor de cabeza. Todas las instrucciones que encuentre tienen desplazamientos que se moverán cuando se mueva el código, y deben recalcularse. Esto significa que debe seguir las instrucciones y hacia dónde se mueven como objetivos. Es difícil para mí explicarte en las diapositivas, pero un ejemplo de cómo lograrlo está en el CD con los materiales de esta conferencia.

Después de haber colocado todas las instrucciones, reemplace las compensaciones antiguas con las nuevas compensaciones. Si no dañaste el desplazamiento, entonces todo saldrá bien. Ahora que se ha preparado, existe una oportunidad real de implementar la idea al más alto nivel. Para hacer esto, necesitas:

  • desmontar instrucciones;
  • preparar un búfer de memoria;
  • inicializar las constantes disponibles f (x);
  • iterar sobre f (x) y ciertos punteros de datos, de acuerdo con los cuales su código se escribirá mientras rastrea las malditas instrucciones;
  • Asignar instrucciones a los índices creados correspondientes;
  • arreglar todos los saltos condicionales;
  • marque la nueva partición de memoria como ejecutable;
  • ejecutar código

Si pones las cosas en su lugar, entonces obtenemos cosas extrañas: todo se confunde, las instrucciones saltan a lugares oscuros de memoria, y todo esto parece simplemente encantador.



¿Todo esto tiene algún significado práctico o es solo una actuación de circo? El valor aplicado de tales transformaciones es el siguiente. Aislar las instrucciones de ensamblaje y algunos pasos para calcular f (x) nos permite colocar estas instrucciones de ensamblaje en cualquier lugar del búfer sin ninguna interacción del usuario. Para confundir las rutas de ejecución de código, todo lo que tiene que hacer es escribir matemáticamente la función y los punteros en algún ensamblador, eligiéndolos al azar.

Esto simplifica enormemente las técnicas de codificación polimórficas. En lugar de escribir código cada vez que manipula su código de una manera determinada, puede escribir una serie de funciones que determinan aleatoriamente la posición de su código, y luego seleccionar estas funciones como aleatorias, etc.

Anti-reverse no es tan genial como la técnica anti-depuración.

La antiversión no se trata de la diversión que obtienes al imposibilitar el uso de la IDA, y no de cuánto estropearás la computadora del Reverser con las imágenes de GNAA Last Measure, aunque es muy divertido. Anti-reversión significa simplemente ser un imbécil, porque si, como el último imbécil, obtienes un Reverser, un tipo que rompe la protección de diferentes sistemas, simplemente se enojará, enviará este programa malicioso al infierno y se irá.

Mientras tanto, podrá vender todos sus bots a las redes comerciales rusas, porque con su software "baja" a todos los involucrados en la ingeniería inversa. Todos saben cómo encontrar técnicas anti-depuración en Google, pero no encontrarán soluciones a los problemas que surjan de cosas creativas allí. Los anti-revólveres más creativos harán que los reversos rompan los dedos del teclado y dejen agujeros del tamaño de un puño en las paredes. Los inversores hervirán de ira, no entenderán qué demonios hiciste, porque tu código lo estropeó todo.

Este es un tipo de juego nervioso, psicológico, si eres creativo en este asunto y creas un anti-reverso realmente impresionante, puedes estar orgulloso de ello. Pero sabes que, de hecho, solo estás tratando de alejarlos de tu código.

Entonces, ¿qué voy a hacer? Voy a tomar las funciones de ofuscación y ofuscarlas. Luego usaré la segunda versión de ofuscación de funciones enredadas y aplicaré nuevamente ofuscación. Entonces, saquemos el código. Este es un ejemplo de trolling matemático, que tomé como ejemplo.



Entonces, entro en el comando "confundir por fórmula" en la ventana que se abre.



A continuación, verá las instrucciones de ensamblaje que hacen su trabajo. Tenga en cuenta que uso C ++ aquí, aunque a la menor oportunidad trato de evitar esto.



Aquí se resalta la función activa CALL EAX, seguida de la instrucción de salto que se aplicará, verá un montón de todo tipo de cosas diferentes en el búfer, y todo esto se hace con cada instrucción individual.





Ahora rebobino el programa hasta el final, y verá el resultado. Entonces, el código aún se ve genial, aquí se compilan un montón de instrucciones JMP, se ve confuso, y en realidad es confuso.



La siguiente diapositiva muestra una representación gráfica de cómo se ve la pila.



Cada vez que esto sucede, genero una fórmula de onda sinusoidal aleatoria que tiene una forma arbitraria, ves aquí un montón de formas diferentes, y eso es genial. Creo que el código comienza en algún lugar en la esquina superior izquierda, pero no recuerdo exactamente. Entonces él tuerce todo, no solo puedes hacer sinusoides, sino también torcer las espirales.



Aquí solo funcionan dos fórmulas, que incluí en el código fuente. Basado en esto, puede hacer tantas cosas creativas como desee, esencialmente es solo DIFF desde el búfer de inicio al búfer de finalización.

El problema es que este ejemplo de código usa saltos incondicionales, lo que en realidad es malo, porque el código debería ser exactamente el mismo que antes, es decir, los saltos incondicionales siguen solo en una dirección. Por lo tanto, debe ir desde el punto de entrada hasta el final de la misma manera, deshacerse de las instrucciones de salto y listo, ¡obtuvo su código! Que hacer Es necesario convertir los saltos incondicionales en condicionales. Los saltos condicionales se realizan en dos direcciones, es mucho mejor, podemos decir que es un 50% mejor.

Aquí tenemos un dilema interesante: si necesitamos saltos condicionales, entonces todavía necesitamos usar saltos incondicionales ... ¿qué demonios? Entonces, ¿qué hacemos? ¡Los predicados opacos nos salvarán! Para aquellos que no saben, un predicado opaco es esencialmente una declaración booleana que siempre se ejecuta para una versión en particular, independientemente de cualquier cosa.
Entonces, veamos la extensión del espacio cero que mencioné anteriormente. Si tiene un conjunto de instrucciones y tienen saltos incondicionales, transiciones entre cada instrucción, se deduce que una serie de instrucciones de ensamblaje que no afectan directamente las instrucciones que necesitamos pueden preceder o seguir una sola instrucción.
Por ejemplo, si escribió instrucciones muy específicas que no cambian el ensamblaje principal de lo que está tratando de confundir, es decir, intenta no involucrarse con los registros siempre que mantenga el estado de cada instrucción de ensamblaje. Y esto es aún más sorprendente.
Puede considerar cada instrucción de ensamblaje, que puede confundirse, como el preámbulo, los datos de ensamblaje y la posdata. El preámbulo es lo que precede a la instrucción de ensamblaje, y la posdata es lo que le sigue. El preámbulo generalmente se usa o se puede usar para dos cosas:

  • corrección de las consecuencias del predicado opaco del preámbulo anterior;
  • fragmentos de código anti-depuración.

Pero el preámbulo es esencialmente limitado porque no puedes hacer demasiado.
Postscript es una cosa más divertida. Se puede usar para:

  • predicados opacos y saltos intrincados a las siguientes secciones de código;
  • anti-depuración y ofuscación de la ejecución general del código;
  • cifrado y descifrado de varios fragmentos de código en el propio programa.

En este momento estoy trabajando en la posibilidad de cifrar y descifrar cada instrucción individual para que cuando se ejecute cada instrucción, descifre la siguiente sección, la siguiente sección, la siguiente, etc. La siguiente diapositiva muestra un ejemplo de esto.



Las líneas de preámbulo y la llamada del depurador se resaltan en verde. Todo lo que hace esta llamada es verificar si tenemos un depurador, después de lo cual vamos a una sección arbitraria del código.

A continuación tenemos un predicado opaco muy simple. Si mantiene el valor Eax en la posdata de la instrucción superior, siga el operador XOR, por lo que su JZ piensa: "OK, obviamente puedo ir hacia la izquierda o hacia la derecha, creo que es mejor que vaya hacia la derecha, porque hay 0". Luego se ejecuta POP EAX, su EAX retrocede, después de lo cual se procesa la siguiente instrucción, y así sucesivamente.

Obviamente, esto crea problemas mucho mayores que nuestra estrategia básica, como los efectos residuales y la complicación de generar diferentes conjuntos de instrucciones. Por lo tanto, será muy difícil determinar cómo una instrucción afecta a otra instrucción. Puedes arrojarme zapatillas, porque aún no he terminado este increíble programa, pero puedes seguir el progreso del desarrollo en mi blog.



Observo que nuestras fórmulas f (x) no tienen que calcularse iterativamente, por ejemplo f (1), f (2), ... f (n). Nada impide que se calculen al azar. Si es inteligente, puede determinar cuántas instrucciones necesita y luego asignar, por ejemplo, f (27), f (54), f (9), y este será el lugar donde se colocarán sus instrucciones al azar. Cuando haces esto, dependiendo de cómo escribiste tu código, puedes detenerlo por adelantado, y el código aún vinculará tus instrucciones al azar.

Si su código se genera en base a una fórmula predecible, se deduce que el punto de entrada también es predecible, por lo que puede tomar un nivel más antes de terminar de recibir el código y confundir significativamente el punto de entrada en un grado u otro. Por ejemplo, tome 300 instrucciones de ensamblaje provenientes de un único punto de entrada.

Ahora hablemos de las deficiencias.



Este método requiere una compilación cuidadosa del código, principalmente usando GCC o, Dios no lo quiera, usando C ++. C ++ es en realidad un lenguaje bastante bueno por varias razones, pero sabes que todos los compiladores apestan. Entonces, lo principal en este asunto es una compilación competente hecha a mano, porque si tu intento de confundir tu propio ensamblaje causará la aprobación de la pandilla que inventó el gusano Conficker, entonces la cagaste.

Necesitará una gran cantidad de memoria. Recuerda la imagen con ondas sinusoidales. El rojo es el código, y el azul es la memoria necesaria para que funcione, y debería ser suficiente para que todo funcione como debería.

Probablemente estará lidiando con un conjunto de datos gigantesco después de completar el código. Y aumentará significativamente si desea confundir más de una función.

Los punteros de función se comportan de manera impredecible, a veces correctamente, a veces no. Depende de lo que esté haciendo, y definitivamente habrá un problema porque no puede predecir dónde y cuándo se dispara el puntero de función en su ensamblaje.

Mientras más complicado genere ofuscación y manipule el ensamblaje en el preámbulo y la posdata, más difícil será arreglarlo y depurarlo. Entonces, escribir dicho código es como equilibrar entre "está bien, insertaré cuidadosamente uno o dos JMP aquí" y "¿cómo diablos puedo resolverlo en poco tiempo"? Por lo tanto, solo tiene que insertar instrucciones y luego descubrir durante varios meses lo que ha hecho.

Espero que hayas aprendido algo útil hoy. En mi opinión, realmente me emborraché y, por lo tanto, realmente no entiendo lo que sucedió ahora. La siguiente diapositiva muestra mis contactos de Twitter, mi blog y sitio web, así que visítame o escribe.



Eso es todo, ¡gracias por venir!



Gracias por quedarte con nosotros. ¿Te gustan nuestros artículos? ¿Quieres ver más materiales interesantes? Apóyenos haciendo un pedido o recomendándolo a sus amigos, un descuento del 30% para los usuarios de Habr en un análogo único de servidores de nivel de entrada que inventamos para usted: toda la verdad sobre VPS (KVM) E5-2650 v4 (6 núcleos) 10GB DDR4 240GB SSD 1Gbps de $ 20 o cómo dividir el servidor? (las opciones están disponibles con RAID1 y RAID10, hasta 24 núcleos y hasta 40GB DDR4).

VPS (KVM) E5-2650 v4 (6 núcleos) 10GB DDR4 240GB SSD 1Gbps hasta enero de forma gratuita al pagar por un período de seis meses, puede ordenar aquí .

Dell R730xd 2 veces más barato? ¡Solo tenemos 2 x Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 TV desde $ 249 en los Países Bajos y los Estados Unidos! Lea sobre Cómo construir un edificio de infraestructura. clase utilizando servidores Dell R730xd E5-2650 v4 que cuestan 9,000 euros por un centavo?

Source: https://habr.com/ru/post/es433280/


All Articles