Pregunta:
¿Por qué el desmontaje no es una ciencia exacta?
Einar
2013-08-04 18:44:00 UTC
view on stackexchange narkive permalink

Aquí es un novato.

De Wikipedia

El desmontaje no es una ciencia exacta: en plataformas CISC con instrucciones de ancho variable, o en la presencia de código auto-modificable, es posible que un solo programa tenga dos o más desmontajes razonables. Determinar qué instrucciones se encontrarían realmente durante la ejecución del programa se reduce al problema de detención comprobado que no tiene solución.

  1. ¿Por qué el desensamblaje en CISC no es una ciencia exacta? Entiendo que reensamblar código desensamblado puede producir códigos de operación diferentes, pero similares para el misma instrucción, pero como son similares, no debería afectar el programa resultante. Y también, si no es una ciencia exacta, es decir, ensamblar (desensamblar (código de operación))! = Código de operación, ¿cómo puede la CPU determinar de qué manera interpretar el flujo de códigos de operación?

  2. En una nota al margen, ¿esto implica que el desmontaje en una plataforma RISC es una ciencia exacta?

Una razón más abstracta podría ser que la compilación no es 1 a 1. Cada declaración de alto nivel no se compila en un conjunto único de códigos de operación. Como en matemáticas, tienes un dominio y un rango. La compilación asigna el dominio al rango. Si el dominio es mucho más grande que el rango, la compilación no es 1 a 1. Así que no hay una función general (lo que significa que la solución no viene por necesidad, pero usted elige) inversa o función de recompilación / desensamblaje.
Cinco respuestas:
0xea
2013-08-04 18:52:54 UTC
view on stackexchange narkive permalink

Para responder a la primera pregunta. El mayor problema es que realmente no se pueden separar los datos del código. Básicamente, existen dos enfoques para el desarmado:

  1. Barrido lineal
  2. Recorrido recursivo

Los desarmadores que usan barrido lineal comienzan en alguna dirección y se desarman instrucciones una a una hasta el final, sin seguir saltos ni razonamientos sobre el código desmontado de ninguna manera. Por ejemplo, SoftICE y Windbg utilizan barrido lineal. Esto puede ser un problema porque no sabe cuándo detenerse. No sabe dónde terminan las instrucciones y dónde comienzan los datos. Debe confiar en los metadatos de formato ejecutable, como el tamaño de las secciones, para eso. Eso es lo que lo hace problemático.

Por otro lado, los algoritmos de recorrido recursivo toman en consideración los saltos y las llamadas, y el desensamblador sigue los saltos, solo desensamblando el código que realmente se ejecutará. Por ejemplo, IDA y OllyDBG utilizan este enfoque. Tiene la ventaja de que esencialmente sabe qué es código y qué datos. Pero tiene sus inconvenientes, obviamente. Primero, mediante un recorrido recursivo puro, no todo el código se desmontará. Por ejemplo, las funciones a las que no se hace referencia directamente, llamadas en tiempo de ejecución mediante el cálculo de su dirección, no se detectarán. Nuevamente, los motores tienen algunas heurísticas para evitar este problema.

Tomemos, por ejemplo, un programa que retrocede algunas instrucciones, pero en lugar de aterrizar en el comienzo de una instrucción previamente desmontada y ejecutada, salta en el medio de ella. ¿Cómo debería decidir el desmontador cuál mostrar? Ambos podrían ser válidos, si esa fuera la intención del codificador. El desensamblador transversal recursivo probablemente mostrará la primera versión desmontada y solo marcará un salto a la mitad de una función. Pero sin que inspeccione los bytes en detalle, es difícil saber qué se ejecutará realmente después del salto.

Hay muchos otros ejemplos.

Otro gran problema con ambos enfoques es el código de modificación automática. Simplemente no se puede hacer estáticamente.

La CPU en sí misma no tiene ninguno de estos problemas ya que está ejecutando código, en otras palabras, es dinámica.

Para responder a la segunda pregunta, no creo que esto sea un problema solo con las arquitecturas CISC, algunas de ellas también se pueden aplicar a los RISC.

De hecho, deberían decir arquitectura von Neumann (que permite la auto-modificación), no CISC.
@Ruslan: No tiene nada que ver con la auto-modificación. El punto es que las instrucciones de ancho variable permiten interpretaciones ambiguas. P.ej. podría colocar "JMP" en medio de una instrucción, lo que provocaría la lectura de una secuencia de instrucciones válida separada. Esto no es posible en conjuntos de instrucciones de ancho fijo, que RISC * (¿casi?) * Siempre lo son.
sí, el código de modificación automática no es el único problema como dije y como 0xC0000022L explica más
* "El código que se modifica automáticamente no es el único problema" * - Bueno, ni siquiera es un ** problema **. En una arquitectura de ancho fijo, no debería haber problemas para desensamblar el código de modificación automática. Por supuesto, el desensamblador no puede saber cómo se verá el ensamblado * después * de las modificaciones en tiempo de ejecución * (que es un problema indecidible) *, pero eso está fuera del alcance de desensamblar el binario.
OllyDBG utiliza barrido lineal?
Estaba bastante seguro de que sí, pero ahora no puedo encontrar la referencia para esa afirmación. Podría haberlo mezclado con SoftICE. Corregido en la respuesta. ¡Gracias!
debray
2013-08-04 20:26:15 UTC
view on stackexchange narkive permalink

Además de los problemas descritos en las respuestas anteriores, hay otra forma en la que un programa puede tener más de un desmontaje. Considere el código que tiene la siguiente lógica (mis disculpas por la sintaxis-carnicería):

buf = ... alguna cadena ...
val = leer ()
si (val == 7) {
mprotect (buf, ..., PROT_EXEC); / * hacer que buf sea ejecutable * /
goto buf; / * ejecutar buf * /
}
más {
imprimir (buf);
}

En este caso, si buf es código (y por lo tanto debe desensamblarse) o datos (y por lo tanto no debe desensamblarse) depende de la entrada. Por tanto, el programa tiene múltiples desmontajes posibles. Tenga en cuenta que esto se debe a que el código es fundamentalmente indistinguible de los datos y no tiene nada que ver con RISC y CISC.

En este artículo hay una discusión deliciosa sobre cómo el código puede disfrazarse como datos de apariencia plausible:

J. Mason, S. Small, F. Monrose, G. MacManus. Shellcode en inglés. Actas de la 16ª Conferencia de ACM sobre seguridad informática y de comunicaciones (CCS), Chicago, IL. Noviembre de 2009. [PDF]

0xC0000022L
2013-08-04 19:02:27 UTC
view on stackexchange narkive permalink

Creo que lo que el comentario pretende describir es el hecho de que en CISC, tomando el x86 como ejemplo aquí, hay varias posibles representaciones razonables del desmontaje. Pero creo que esto también se puede decir en parte de las implementaciones típicas de RISC, donde, sin embargo, el ensamblador puede ofrecer algo así como un mnemónico similar a CISC representado por una combinación diferente de códigos de operación básicos (RISC).

Por ejemplo, el prefijo rep no tiene sentido en muchos códigos de operación y, sin embargo, se ha empleado en el pasado para hacerlo más difícil para los ingenieros inversos. Sin embargo, ¿cuál es la mejor representación para un humano que finalmente está mirando el desmontaje? El que tiene el prefijo rep o el que no tiene (que refleja más lo que hace la CPU).

Si el desensamblador se mantiene fiel a lo que encuentra, debe incluir el rep . Por otro lado, el trabajo del desensamblador es hacer que el código máquina sea legible para un humano. Por lo tanto, tiene mucho sentido hacer un esfuerzo adicional y asegurarse de que los prefijos redundantes se eliminen por brevedad. De manera similar, los códigos de operación de varios bytes que representan un nop (sin operación) se pueden eliminar por completo o representar de manera que se destaquen del resto (por ejemplo, como lo hace IDA con los bytes de alineación) o cada uno como nop respectivamente (para la versión de 1 y 5 bytes por igual).

Entonces, desde mi punto de vista, desensamblar el código de máquina es una ciencia exacta, ya que para cada código de operación hay es un mnemónico exacto (hagamos caso omiso de la dualidad jne / jnz aquí). De lo contrario, como señala, ¿cómo funcionaría la CPU y averiguaría qué hacer? Sin embargo, para dar una representación al ingeniero inverso humano, a veces (¿me atrevo a decir a menudo?) Se posprocesa para facilitar la lectura y la comprensión. Ahí es donde varían los desmontadores y desmontajes.


Otro problema es que para seguir todas las rutas de código posibles se deben usar heurísticas (y es una de las características distintivas de un desensamblador bueno ). De lo contrario, es difícil distinguir los datos del código. Pero que yo sepa, "CISC" no se puede destacar por ese rasgo, por lo que diría que este no es el motivo principal del comentario.

Codoka
2015-04-10 13:23:05 UTC
view on stackexchange narkive permalink

Un problema crítico en el desmontaje radica en separar con precisión el código de los datos en ejecutables binarios. Para ello, necesitará un análisis estático que pueda con precisión determinar los objetivos de los saltos indirectos (saltos a direcciones calculadas en tiempo de ejecución). Hay varios ejemplos de saltos indirectos, incluido el cálculo de destino de cambio y tablas virtuales en C ++.

Ese tipo de análisis estático, si existiera, sería capaz de resolver el problema de detención que se sabe que es indecidible. Por lo tanto, tal análisis estático no puede existir. En base a eso, las herramientas de desmontaje deben depender de heurísticas y / o aproximaciones para determinar objetivos de saltos indirectos. Se pueden encontrar más detalles al respecto en las respuestas a una pregunta similar aquí Solidez del desmontaje ARM. La tarea de la separación de código / datos se vuelve aún más difícil si los binarios están ofuscados y / o tienen código auto-modificable.

Habiendo extraído aproximaciones de código y datos de binarios. Entonces es necesario abordar el problema de adjuntarles semántica, que también es difícil. Por ejemplo, una estructura {int i; int j;} se vería similar a un int arr [2] cuando se acceda mediante instrucciones. Por lo tanto, es difícil adjuntar semántica única al código ejecutado sin depender de información externa como símbolos de depuración.

Una matriz más grande se puede distinguir de una estructura debido a cómo se accede a sus elementos, y quizás incluso una tan pequeña como su `arr [2]`.
@Jongware está de acuerdo. Es solo un ejemplo simple para ilustrar la idea de tener múltiples significados válidos en el desmontaje.
perror
2017-06-20 22:03:54 UTC
view on stackexchange narkive permalink

Comentarios preliminares

En primer lugar, debemos acordar qué significa un desmontaje correcto de un programa binario. Propondría la siguiente definición:

Un desmontaje correcto de un programa binario dará el conjunto de todas las posibles instrucciones que puede ejecutar el programa cualquiera que sea la entrada toma.

Otra forma de expresarlo sería decir que exponemos las instrucciones de todas las posibles ejecuciones del programa en cada entrada que pueda tomar.

Deteniendo Problema

Aquí ya podemos hacer un paralelo con el problema de detención en una máquina de Turing que se puede definir como sigue ( Wikipedia):

El problema de detención es el problema de determinar, a partir de una descripción de un programa de computadora arbitrario y una entrada, si el programa terminará de ejecutarse o continuará ejecutándose para siempre.

Este problema (aparentemente) muy simple ha sido demostrado indecidiblemente por Turing, lo que significa que, incluso si podemos manejar una cierta cantidad de casos automáticamente con un programa, algunos casos patológicos siempre escaparán de nuestro programa que no dirá si sí o no, la máquina / programa se detendrá en la entrada dada.

Y, por supuesto, tales casos patológicos son infinitos (por lo tanto, no hay esperanza de enumerarlos uno tras uno como casos especiales).

¡Volvamos a nuestro problema de desmontaje!

Explorar todas las rutas posibles de un programa es de hecho indecidible debido al problema de detención !

De hecho, una forma dual de formular el problema de la detención es el problema de accesibilidad , en el que desea saber si hay una entrada que le permita llegar a un punto específico del programa. Y, saber si el programa puede alcanzar un lugar específico en la memoria e interpretarlo como una instrucción ( es decir, el puntero de instrucción toma el valor de esta dirección en algún momento) es un problema de accesibilidad.

Entonces, el desmontaje es indecidible .

Pero, ¿en el mundo real?

Lo sé, lo sé, esto es solo matemáticas ... No es la realidad ... La mayoría de las ofuscaciones (voluntarias o no) se pueden resolver y eliminar automáticamente del código binario ...

Bueno, esto se debió esencialmente a que las personas que hicieron estas ofuscaciones no estaban acostumbradas a indecidibles problemas ...

Imagine que inserta en su programa el cálculo de un problema indecidible, o incluso, simplemente algo lo suficientemente difícil y complejo que romperá cualquier razonamiento automatizado que se le aplique.

Para dar un ejemplo, tomemos la secuencia de Collatz ( Wikipedia), se conjetura que esta secuencia siempre termina en 1 después de algún tiempo. Pero, el problema aritmético detrás de esto es tan tremendamente complejo que esta conjetura se mantiene desde aproximadamente un siglo ... ¡Este es un predicado opaco perfecto para usar! Por supuesto, podría ser que la prueba de tal conjetura exista, pero este problema es lo suficientemente complejo como para comenzar a construir sobre él y confundir a la computadora al explorar el espacio de estados de un programa.

De hecho, esta es la actual la dirección de la investigación en una fuerte ofuscación hoy en día ... Casi hemos terminado con los pequeños trucos que se usaban antes y la gente comienza a construir cosas sobre problemas mejor fundamentados. Incluso si todavía perdemos un equivalente de un Shannon (padre de la teoría de la información) en materia de ofuscación del software para compararlo con la criptología.

Palabras finales

Entonces, vimos que el problema de desmontaje está fuertemente vinculado al problema de detención . Y, también, que el uso de problemas muy complejos podría ser el siguiente paso en la ofuscación del software moderno .

Solo quisiera decir una última palabra sobre el hecho de que las herramientas de desmontaje actuales probablemente están muy por detrás de lo que podríamos hacer si tuviéramos que ceñirnos al estado del arte en técnicas de desmontaje. Siempre lloro de dolor cuando veo cuán prehistóricas son las herramientas actuales ... pero poner en práctica todas las técnicas modernas requeriría tanto esfuerzo de desarrollo y mantenimiento que nadie parece estar listo para hacerlo (pero esto es solo mi humilde opinión).



Esta pregunta y respuesta fue traducida automáticamente del idioma inglés.El contenido original está disponible en stackexchange, a quien agradecemos la licencia cc by-sa 3.0 bajo la que se distribuye.
Loading...