Está en la página 1de 81

Reconstrucción de código I: estructuras de datos

I. Introducción y objetivos

II. Conceptos básicos sobre reconstrucción de código

III. Resumen

IV. Caso práctico con solución

V. Glosario

VI. Bibliografía
Lección 1 de 6

I. Introducción y objetivos

1.1. Introducción de la unidad

En esta unidad se hará un repaso de los tipos de datos más importantes y comunes en C/C++ desde un
punto de vista de implementación a código objeto. Por cada uno de ellos se verá su implementación en
varias arquitecturas: x86/32-64 bits y ARM.

El objetivo final del proceso de ingeniería inversa es obtener un conocimiento profundo del funcionamiento
total o parcial del objeto al que se le esté realizando. Este objetivo pasa por entender las estructuras y los
tipos de datos básicos que se implementan para que el procesador pueda realizar su trabajo.
En esta primera parte de reconstrucción de código se estudiarán los tipos de datos y las estructuras de
código básicos que se utilizan en todos los programas informáticos. Se prestará atención al proceso de
compilación comparando el código en un lenguaje de alto nivel, como C/C++, con el código ensamblador
generado por el proceso de compilación. Esto permitirá, de una forma fácil y visual, identificar dichas
estructuras en ambos extremos del proceso.

 Resulta importante remarcar que, de la misma forma que con diferentes


compiladores se genera diferente código a partir del mismo código de entrada, si la
arquitectura es diferente también se generará un código diferente. En este aspecto,
se verá la generación de código en distintas arquitecturas; en concreto, se mostrarán
las más importantes hoy en día, como son x86/32-64 bits y ARM.

El conjunto de la unidad sentará las bases para entender el código ensamblador generado por un compilador
y el funcionamiento total o parcial de un programa final que se quiera estudiar.

C O NT I NU A R

1.2. Objetivos de la unidad


Cuando el alumno finalice esta unidad, será capaz de:

1 Identificar tipos de datos en código ensamblador.

2 Identificar estructuras de datos.

3 Conocer el código ensamblador de las arquitecturas que se tratan.

4 Entender las diferencias entre el código generado para distintas arquitecturas.

5 Entender la transformación de un lenguaje de alto nivel a un lenguaje entendible por el


procesador.

6 Identificar objetos por distintos métodos en el código ensamblador.


Lección 2 de 6

II. Conceptos básicos sobre reconstrucción de


código

Uno de los requisitos de la presente unidad es comprender el proceso de compilación, que convierte el
código fuente, escrito en un lenguaje estructurado de alto nivel y fácilmente comprensible para una persona,
en código objeto, escrito en un lenguaje máquina para la arquitectura escogida y directamente ejecutable.

Ahora ya se conocen los conceptos básicos sobre diseño, análisis e implementación de lenguajes, así como
los detalles sobre las fases por las que pasa un compilador, las técnicas utilizadas para generar y optimizar
el código objeto y, en definitiva, todo lo relacionado con el proceso de compilación que genera código objeto
partiendo de código fuente.
Figura 1. Traducción de una proposición.
Fuente: elaboración propia.

Ya se sabe cómo el compilador convierte el código fuente a código intermedio, el cual se utiliza para
traducirlo a código máquina de manera directa, tal y como puede verse en la imagen.
Esto hace intuir que puede existir alguna forma de revertir el proceso; es decir, de generar código fuente a
partir del código objeto en lenguaje máquina. Esta labor sí es posible en gran medida, aunque se deben tener
en cuenta las limitaciones del campo de la ingeniería inversa explicadas en el apartado V de la unidad 1.

En este tema se empleará el término reconstrucción de código para definir el proceso inverso al
estudiado en la unidad anterior; es decir, el proceso de obtener el código fuente a partir del código objeto.

Debido a las limitaciones mencionadas, no será posible obtener comentarios ni los nombres de variables
tal y como las describió el desarrollador; puede que ni tan siquiera con el tipo o el tamaño exacto con el
que este lo hizo. Los motivos se explican en el apartado V de la unidad 1, ya mencionado.
No obstante, sí se va a poder obtener una estructura de código que cumple con bastante exactitud con el
comportamiento del programa fuente. Para ello es necesario conocer las estructuras que el compilador
maneja y con las que traduce el código fuente a código objeto. De esta forma, se podrán identificar estas
estructuras en el código objeto y traducirlas a código fuente.

Esta traducción depende de:

A R Q UI T E C T UR A O BJE T O O PT I M I Z A C I O N E S

Un mismo código fuente puede ser compilado para distintas arquitecturas, como pueden ser x86 en 32 bits
o 64 bits, ARM con Thumb o Thumb-2, MIPS con Big Endian o Little Endian, entre otras.

A R Q UI T E C T UR A O BJE T O O PT I M I Z A C I O N E S

Aunque el compilador sí tiene una traducción exacta de código fuente a código intermedio y de código
intermedio a código objeto, se llevan a cabo optimizaciones de código -incluso del objeto- que, dependiendo
del contexto local o global del código intermedio o del código objeto, son susceptibles de ser modificadas
tras aplicar técnicas de optimización.
Una vez traducidas estas estructuras, se podrá continuar con el proceso de ingeniería inversa analizando los
datos, dándoles nombres significativos a las variables, estructuras y funciones o incluso agregando
comentarios o anotaciones en el código reconstruido.
1

En este tema y en el siguiente se pretende mostrar los tipos de datos, las


estructuras e incluso los algoritmos utilizados comúnmente en el software de
tal forma que sea posible identificarlos y traducirlos de manera directa a
código fuente. Para ello se van a mostrar ejemplos en lenguaje C/C++ y el
código objeto generado para diferentes arquitecturas.
2

Se tratará de mostrar un número significativo de escenarios, así como su


metodología de análisis, para dotar al lector de autonomía suficiente como
para afrontar otros escenarios no contemplados aquí, como pueden ser otras
arquitecturas o incluso otros lenguajes fuente.
3

En este capítulo y en el siguiente los ejemplos del código objeto serán


principalmente código ensamblador x86/32 bits. Si las diferencias son
significativas y siempre que sea posible, se mostrarán paralelamente x86/64
bits y ARM u otras arquitecturas para mostrar claramente otras perspectivas.
En el caso de ARM, es posible compilar los ejemplos utilizando compiladores
cruzados (cross--compilers), que permiten compilar desde una máquina con
una arquitectura (por ejemplo, x86) a código objeto en otra arquitectura (en
este caso, ARM).
4

Para los códigos mostrados en este tema y el siguiente se ha utilizado el


compilador GCC (GNU Compiler Collection) y el depurador GDB (GNU
Debugger) en una plataforma Linux. Será labor del lector realizar estos
ejemplos con otros compiladores para comprobar por sí mismo las
diferencias y similitudes con las aquí expuestas.
5

Esta unidad se adentrará en la reconstrucción de código partiendo de las


estructuras de datos disponibles en el lenguaje C. Se tratarán desde los tipos
de datos más básicos, como son las variables, hasta los más complejos, como
pueden ser los objetos del paradigma de los lenguajes orientados a objetos
utilizados en C++.

Reconstrucción de código: estructuras de control de flujo de pr…   

  0:00 / 5:40 1x 
C O NT I NU A R

2.1. Variable
La estructura de datos más simple utilizada en C son las variables. Estas se pueden implementar de
diferentes maneras en función de diferentes factores:

1 Tamaño

El tamaño de una variable lo determinan el tipo con el que se declare y la arquitectura para la que se genere
el código objeto en 32 o 64 bits. En la siguiente tabla se muestran las implementaciones más comunes al
respecto en 32 bits (no se tratarán los tipos Float y Double debido al uso de instrucciones de coma flotante,
que están fuera del alcance de este curso):

Figura 2. Tipos de datos en 32 bits.


Fuente: elaboración propia.
En arquitecturas de 64 bits hay varias opciones, y cada compilador opta por una:

Figura 3. Diferentes implementaciones para tipos de datos en 64 bits.


Fuente: elaboración propia.

Estos detalles de implementación son especialmente interesantes a la hora de auditar código en busca de
vulnerabilidades, ya que puede suceder que un código fuente sea correcto desde el punto de vista de la
seguridad y, al compilarlo con dos compiladores diferentes -o con el mismo, pero en arquitecturas
diferentes-, se introduzcan vulnerabilidades; por ejemplo, al desbordarse por arriba o por abajo el tipo de
datos entero. De hecho, este es uno de los problemas más comunes en vulnerabilidades de
desbordamientos de búfer.

Ejemplo

En el siguiente ejemplo pueden verse variables declaradas con distintos


tipos. Para ello se muestra un código fuente con diferentes tipos de
variables locales y globales:.
Figura 4. Código fuente con distintas variables.
Fuente: elaboración propia.

Este programa de ejemplo simplemente declara una variable por cada tipo y la inicializa.

x86/32

La siguiente porción de código objeto en 32 bits, generado para este programa, inicializa las variables
locales.

Figura 5. Código objeto en 32 bits.

Fuente: elaboración propia.


Para identificar de manera más directa las diferentes variables se han inicializado con valores
claramente identificables en el código fuente. Estas variables locales se almacenan en la pila; por ello,
se utiliza el registro que apunta a la cima ESP como base para calcular su localización. Esto se verá más
claramente en el apartado de funciones.

Se utilizan diferentes directivas de tamaño del lenguaje ensamblador para acceder a cada variable local.
Como se puede ver en la tabla de la figura 2, el tipo char ocupa 8 bits = 1 byte; short en realidad se
traduce como short int, y es por esto por lo que ocupa 16 bits = word; int, long y long long son 32 bits =
dword. Los modificadores signed y unsigned no se tendrán en cuenta hasta que se acceda a los límites
de dichas variables o se realicen operaciones aritméticas o lógicas.

Variables globales

Por otro lado, pueden verse las variables globales gvar?, que son declaradas fuera de cualquier función e
inicializadas. Este tipo de variables globales son almacenadas en una sección de datos. En el caso del
compilador GCC, en la denominada “.data”. Si no estuvieran inicializadas, las hubiera almacenado en la
sección denominada “.bss”.
A continuación, se consulta el contenido de las variables globales con el debugger y se solicita el
contenido de memoria de la variable gvar1 (comando x/20x &gvar1).
Figura 6. Consulta del contenido de las variables globales con el debugger y con el comando x/20x &gvar1.

Fuente: elaboración propia.

Comparando con el ejemplo anterior es cuando se puede constatar la dificultad a la hora de reconstruir
variables, ya que sin símbolos de depuración no hay ninguna manera de acceder a la variable en sí.
Habría que ir a la porción de código que maneja esa supuesta variable y ver con qué directiva de tamaño
lo hace para saber si se trata de uno u otro tipo. Incluso así no se sabrá qué tipo fue en el código fuente,
sino tan solo que en esa porción de código ha manejado esa porción de la variable.

En esta imagen se observan los valores de inicialización, que se corresponden con 1 (byte), 2 (word) y 4
(dword) bytes de espacio cada uno, siendo rellenado con ceros a la izquierda el resto de espacio de la
variable.

Nótese que gvar3, siendo short, ocupa dos bytes, mientras que gvar4, siendo short, ocupa cuatro bytes.
Esto es debido a la alineación de memoria llevada a cabo por la optimización del compilador. Acceder a
direcciones de manera alineada incrementa el rendimiento al no tener que hacer operaciones para
calcular el espacio correcto.

x86/84 bits

Para el caso de 64 bits, se puede ver algo ligeramente diferente.

Figura 7. Código objeto en 64 bits.

Fuente: elaboración propia.


En este caso, además de la diferencia acerca de los registros -que se puede ver claramente en la figura 8
con el registro de ejemplo RAX-, se observa que hay una nueva directiva de tamaño para long long, que
son 64 bits = qword.

Figura 8. Registro de ejemplo RAX.

Fuente: elaboración propia.

Variables globales

En cuanto a las variables globales, ahora las variables gvar7, gvar8 y gvar9 ocupan ocho bytes.

Figura 9. Variables gvar7, gvar8 y gvar9 de ocho bytes.

Fuente: elaboración propia.


ARM 32 bits

En este otro caso con ARM se observa algo bastante parecido a lo anterior, salvando las distancias en
cuanto a la arquitectura, que modifica bastante la sintaxis, y no solo en cuanto a los registros.

Figura 10. ARM 32 bits.

Fuente: elaboración propia.

Aquí se han necesitado dos instrucciones para realizar una asignación: una para
almacenar el literal en un registro (mov r3,#nn) y otra para almacenar el valor del
registro en una dirección de memoria apuntada por el registro r11 (también
denominado fp o frame pointer), más un desplazamiento negativo (str r3, [r11, #n]).
Como se puede observar, se utilizan también los mnemónicos strb y strh para
almacenar un byte o un word en lugar de un dword. O incluso en el caso de gvar9,
cuyo tamaño es qword, que utiliza dos registros r3 y r4 para inicializar las
direcciones de memoria contiguas [r11, #-36] y [r11,#32].

Variables globales

En el caso de las variables globales, se puede observar un caso similar al de 64 bits, solo que la única
variable de 64 bits = qword es gvar9.

Como se puede observar, gvar9 está inicializada como 0x0000000000000099, aunque en la imagen no lo
parezca. Esto es porque la memoria se gestiona en Little-endian y el debugger trata de traducir los words;
por eso hay una mezcla.

Figura 11. Variable gvar9 de 64 bits.

Fuente: elaboración propia.

Declaración de las variables globales

Para una mejor apreciación de estos detalles es posible compilar el código fuente como ensamblador en
cualquiera de las arquitecturas y ver más información, sobre todo de las variables globales. Para ello, se
utilizará el siguiente comando.

Figura 12. Variable global.

Fuente: elaboración propia.

Puede verse aquí la declaración de las variables globales de ARM y comprobarse que, efectivamente,
ocupa ocho bytes = qword.

Figura 13. Variables en ARM.

Fuente: elaboración propia.


C O NT I NU A R

2 Alcance

Indica desde qué parte del programa pueden ser accesibles determinadas variables. Para indicarlo se tiene
en cuenta dónde han sido declaradas. En C, se pueden declarar las variables hasta en cuatro lugares
distintos de la estructura del código fuente:

Fuera de todas las funciones del programa (variables globales), accesibles desde cualquier
parte.

Dentro de una función (variables locales), accesibles tan solo por la función en la que se
declaran.

Como parámetros a la función, accesibles de igual forma que si se declararan dentro de la


función.

Dentro de un bloque de código del programa, accesibles tan solo dentro del bloque donde se
declara. Esta forma de declaración puede interpretarse como una variable local del bloque
donde se declara. Esto solo está permitido a partir del estándar C99.

C O NT I NU A R
3 Almacenamiento

Se refiere a dónde se almacenará la variable dentro del programa objeto:

"S TAT I C " "R E G I S T E R "

Indica que la variable debe ser accesible en cualquier momento del programa, aunque no se esté
ejecutando la función que lo declaró; es decir, se utiliza para que una variable local perdure en el tiempo y se
pueda utilizar en cada invocación a la función, manteniéndose así su valor. Es por ello por lo que se
almacena en la sección de datos del binario, ya que, como se verá más adelante, las variables locales se
almacenan en la pila y estas se sobrescriben una vez se ha finalizado su ejecución.

"S TAT I C " "R E G I S T E R "

Asigna el valor a un registro del procesador. En el caso de que no dispusiese de registros disponibles para
su uso en esa zona de código, se omitiría este modificador. Los accesos a los registros son mucho más
rápidos que los accesos a la memoria. Es por esto por lo que, aunque el compilador trata de utilizar registros
siempre que puede en la fase de optimización, el desarrollador puede decidir que una variable se almacene
en un registro para mayor velocidad de cómputo.

Aunque ya se ha podido ver la diferencia de alcance entre las variables locales y las globales, a continuación
se muestra un ejemplo de código fuente que recopila los cuatro tipos de alcance y los dos tipos de
almacenamiento comentados anteriormente:
Figura 14. Código fuente que recopila los cuatro tipos de alcance.
Fuente: elaboración propia.

Cuya salida al ser ejecutado es:

Figura 15. Salida del código fuente una vez ejecutado.


Fuente: elaboración propia.

En dicho código se pueden ver las cuatro zonas distintas donde se pueden declarar las variables que se han
mencionado anteriormente. En el apartado de Tamaño se han visto ejemplos del almacenamiento global (en
la sección “.data” o “.bss”, dependiendo de si se han inicializado o no) y del local (en la pila). El único matiz
en este nuevo ejemplo (debido a las pocas diferencias entre 32 y 64 bits, se mostrará solo el de 32 bits) es
que, si se utiliza el modificador register en lugar de una dirección de la pila o de la sección de datos, se
empleará un registro, tal y como se puede ver en la siguiente imagen:

C O NT I NU A R

x36, 32 y 64 bits

Figura 16. Análisis con modificador register.


Fuente: elaboración propia.

La variable lvar1 inicializada con 0x11 se almacena en la pila (ya que se utiliza una dirección de memoria
basada en el registro ESP que apunta a la cima de la pila), mientras que lvar2 se inicializa en el registro EBX.
Esto hace que el compilador reserve ese registro para el uso de esa variable en el ámbito de la función. Esto
queda claro más adelante, en el Printf, que, al empujar los argumentos en la pila para invocar a la función
Printf, se empuja EBX (Main+60):

Figura 17. Función Printf.


Fuente: elaboración propia.

Esto quedará más claro en el apartado de las funciones. En el resto de código de la función Main se observa
el bucle for:
Figura 18. Código de la función Main.
Fuente: elaboración propia.

El alcance de las variables locales a un bucle -como es el caso de la variable i usada en el for de nuestro
código fuente- solo se preserva en el código del bucle. En la implementación (main+84) se ve como se
almacena en una variable de la pila [esp+0x1c], por lo que sería visible a toda la función; sin embargo, si se
tratara de acceder a ella desde fuera del bloque del for, daría un error de compilación por no estar declarada.

Por último, esta unidad se centrará en el modificador static, que se utiliza para dar alcance global, pero
restringiendo el acceso solo a la función que lo declaró. Esto se puede ver en el código de la función Foo():

Figura 19. Código de la función Foo().


Fuente: elaboración propia.

Aquí se observa como se almacena en el registro EDX una dirección de memoria de la sección “.data”
(comando: objdump -h a.out), la variable b del código fuente:

Figura 20. Almacenamiento en el registro EDX lde a dirección de memoria de la sección


“.data”.
Fuente: elaboración propia.

La sección “.bss” es una sección de datos no inicializados. Esto es así porque no es hasta el bucle
for cuando se inicializa por primera vez, tras haber ejecutado el código. A continuación, se almacena en el
registro EAX un valor pasado por argumento. Esto se sabe porque se hace referencia a una dirección
[ebp+0x08] cuya base es EBP -la base de la pila (se verá con más detalle en el apartado de las funciones); es
decir, la variable a del código fuente- y, posteriormente, se realiza la suma acumulativa (b += a;) en foo+15 y
foo+17 de la imagen anterior.

 Almacenar una variable local en la sección “.bss” impide que su contenido se pierda
al salir de la función y que se sobrescriban los datos con las variables locales de la
siguiente función invocada. Esto permite almacenar información persistente a la
ejecución del programa, pero de alcance restringido solo a la función.

C O NT I NU A R

ARM 32 bits

En la siguiente imagen se puede ver como el lvar1 se inicializa (#17=0x11) en una variable de la pila y
lvar2 en un registro, como en el ejemplo de la otra arquitectura:

Figura 21. Demostración de cómo lvar1 se inicializa (#17=0x11) en una variable de la pila.
Fuente: elaboración propia.

En el caso del bucle, se observa como también se inicializa con el valor #51 = 0x33 y se almacena en la pila
[r11, #-16]; es decir, en una variable local:
Figura 22. El bucle se inicializa con una variable local.
Fuente: elaboración propia.

Solo que, como en el caso anterior, si se pretende utilizar fuera del bloque del for, el compilador genera un
error de compilación.

Por último, en el caso del modificador static en la función Foo(), se puede ver en el siguiente código cómo se
lleva a cabo la suma acumulativa en una variable en la sección “.data”:

Figura 23. Código en función Foo().


Fuente: elaboración propia.

La suma se hace en foo+28. Si se observa hacia arriba, se ve como R3 contiene un valor pasado por
argumento (la dirección [r11, #-8] tiene como base R11, que en foo+4 obtiene el valor de la base de la pila).
R2 contiene la variable estática b, accedida en foo+16 y foo+20. Como se puede ver en foo+16, se obtiene el
valor de [pc, #48]: el registro PC es el puntero de control; es decir, indica la dirección que se está ejecutando.
Esto significa que, cuando ejecute esta instrucción, se almacenará en R3, delante, el valor de la dirección
#48 bytes, en concreto, en la dirección 0x9250 = foo+72.

En la imagen anterior aparece como instrucciones, pero en el apartado de funciones se verá cómo saber que
esta función acaba en foo+68 y que el resto son datos, no instrucciones. Para ver los datos se volcará esa
zona de memoria:
Figura 24. Volcado de la zona de memoria.
Fuente: elaboración propia.

Es posible comprobar que almacena una dirección de memoria que, efectivamente, si se consulta la sección
“.bss” (figura 25), se confirma que está ahí contenida:

0x00024260 <0x0002432c< (0x00024260 + 0x00000114 = 0x00024374).

Figura 25. Consulta de la sección “.bss”.


Fuente: elaboración propia.

Anotación

Además de estos modificadores, existen otros, como extern o const, que, a efectos de reconstrucción de
código, no son relevantes. Solo afectan al tiempo de compilación en cuanto a la política de acceso de las
variables. Por ello, esta unidad no se centrará en estos últimos.

C O NT I NU A R

2.2. Arrays
Ya se han expuesto los diferentes tipos de datos más básicos que pueden utilizarse y cómo se implementan
cada uno de sus modificadores según dónde se declare la variable. Ahora se aborda un tipo de datos
estructurados: los arrays.
Un array es una variable en la que cada elemento se almacena en memoria de manera consecutiva. Estos
pueden declararse con varias dimensiones. Las cadenas de caracteres en C se declaran un array
unidimensional (también conocido como vector) de caracteres. Los vectores constan de una serie de
variables del mismo tipo denominadas elementos o componentes del vector.

Otro tipo especial son los arrays de dos dimensiones, también conocidos como matrices. Al tener dos
dimensiones, se simula una tabla accedida por la tupla [fila][columna]. A los arrays de tres o más
dimensiones se accede de la misma forma que a las matrices, dependiendo del número de dimensiones:
[n1][n2][n3]…[nn].

Las cadenas de caracteres son arrays unidimensionales, donde cada elemento es del tipo char. Se pueden
inicializar elemento a elemento -{‘T’, ‘e’, ‘x’, ‘t, ‘o’, ‘\0’}-, finalizando con el carácter nulo, o todo junto entre
comillas dobles -{“texto de la cadena”}-, donde el compilador agregará el carácter nulo al final.

Para poder analizar bien este tipo de variable, se va a generar código objeto en distintas arquitecturas a partir

del siguiente código fuente:

Figura 26. Código fuente.


Fuente: elaboración propia.
C O NT I NU A R

2.2.1. x86 32 y 64 bits


En este caso, igual que en el anterior, la diferencia entre arquitecturas es casi inapreciable. Lo único que
varía es el tipo de registros y su utilización como argumentos a la hora de invocar una función, lo que se verá
más adelante.

Figura 27. Código en 32 bits.


Fuente: elaboración propia.
Figura 28. Código en 64 bits.
Fuente: elaboración propia.

VA R I A BLE S A N Á LI S I S

Este apartado se centrará en el código de 32 bits. Para ello, se debe comenzar por identificar sobre la
imagen (figura 29) las distintas variables y pasar a continuación a explicarlo con más detalle.

VA R I A BLE S A N Á LI S I S

Las variables var1 y var2 se inicializan como una lista de elementos, y es así como se implementan en el
código, moviendo el valor a la dirección de memoria pertinente. Después, cuando se trata de acceder al
elemento var2[1][2], se ve claramente como se accede directamente a la dirección de memoria para
moverla un registro (main+97).
En el caso de las cadenas de caracteres, var5 se comporta como var1 y var2, mientras que, con respecto a
var3, al inicializarla con una cadena de caracteres entre comillas dobles, el compilador sabe que es una
cadena de caracteres y utiliza valores de 32 bits (cuatro bytes) para copiar la cadena en la variable. Nótese
que al final agrega el carácter nulo (\x0) para finalizar la cadena de caracteres, mientras que en el código
fuente no se ha incluido. Esto se debe a que el compilador lo hace automáticamente al detectar las
comillas dobles.

Por último, se observa una inicialización especial en var4: la utilizada con un puntero a memoria. Aunque los
punteros se verán más adelante, simplemente mencionar que, al declararse como puntero de tipo char y
apuntar a una cadena entre comillas dobles, el compilador guarda la cadena en la sección “.rodata” en
tiempo de compilación, de tal forma que ahorra en tiempo de ejecución a la hora de inicializar la variable. En
main+146 se obtiene la cadena de caracteres de la dirección 0x08048577, que pertenece a la sección
“.rodata”, ya que 0x8048568 <= 0x8048577<= (0x8048568 + 0x1c = 0x8048584) (figura 30).

Figura 29. Variable de código en 32 bits.


Fuente: elaboración propia.

Figura 30. Consulta de la sección “.rodata”.


Fuente: elaboración propia.
C O NT I NU A R

2.2.2. ARM 32 bits

VA R I A BLE S A N Á LI S I S

En esta arquitectura se observa que el comportamiento es parecido, aunque las instrucciones, obviamente,
son diferentes. A continuación, se identificarán las variables en el siguiente código objeto (figura 31).

VA R I A BLE S A N Á LI S I S

Como se puede observar, hace referencia a direcciones del final de la función Main -
concretamente, desde main+160 hasta main+180- para almacenar los valores obtenidos de
la sección “.rodata”. Esto se sabe porque hace referencia a una dirección de memoria
anterior a Main (main+24) y, si se observa con el comando (obdump –x ./a.out), se ve que
esa dirección es “.rodata” (figura 32).
Figura 31. ARM 32 bits.
Fuente: elaboración propia.

Figura 32. Visualización del comando (obdump –x ./a.out).


Fuente: elaboración propia.

Para var1 lo que se hace es cargar la dirección de memoria de los datos de “.rodata” en el registro R3
(main+24) y apuntar el registro R2 al final de la función Main (main+20), donde se almacenarán los valores
inicializados. Luego se almacenan en R0, R1 y R2 los valores apuntados por el registro R2 (main+28) y se
almacena en R3 el contenido de R0, R1 y R2 (main+32).
Los casos de las variables var2, var3 y var5 son prácticamente iguales que var1, teniendo en cuenta que son
datos diferentes. Sin embargo, se puede ver como con var4, al ser una cadena, se opta por copiar el
contenido de la cadena en “.rodata”, hasta las variables alojadas después de Main (desde main+108 hasta
main+116).

C O NT I NU A R

2.3. Punteros
Ahora se tratará un tipo de datos muy importante en C: los punteros. Este tipo de datos es especial en C y no
se suele dar en otros lenguajes de programación. Los punteros son variables que apuntan a una dirección de
memoria a modo de apuntadores a otras variables y pueden apuntar a variables de cualquier tipo.

Aunque el puntero en sí ocupa 32 bits o 64 bits, dependiendo de la arquitectura, el compilador tiene esto en
cuenta para conocer la longitud del valor al que apunta.

Los arrays son, en realidad, un puntero a la dirección base del array -


es decir, a [0]-, y pueden utilizar la sintaxis de array o el puntero
indistintamente.

Partiendo del siguiente código fuente, se observa que se han declarado una cadena de caracteres y un par
de variables enteras (una de ellas, un puntero a entero):
Figura 33. Código fuente.
Fuente: elaboración propia.

A continuación, vamos a ver las diferentes implementaciones.

C O NT I NU A R

2.3.1. x86 32 y 64 bits

En el siguiente código generado se identifican las variables definidas, así como las operaciones sobre ellas,
en el código fuente:
Figura 34. Variable en el código fuente.
Fuente: elaboración propia.

Análisis

Por lo que respecta a la inicialización de la línea 5, como se puede ver, se obtiene una dirección de “.rodata”
(se puede comprobar utilizando Objdump, como en casos anteriores) y se almacena en la pila.

En la línea 10, se asigna al puntero p el contenido de la variable cadena. Lo que se pretende con esto es que
apunte al contenido y no a la variable que lo contiene, ya que, si se apunta a la variable cadena, el contenido
de p sería la dirección de la pila. Sin embargo, si se apunta a la cadena (que es lo que se pretende), el
contenido de p es una dirección de la sección “.rodata”.

Luego se incrementa el puntero. Como se puede ver en la imagen anterior, en el código de la línea 11 lo que
se hace es sumar 1 al contenido de la variable de la pila [ebp-0x8]; esto deja constancia de que se
incrementa su valor, lo que significa que ahora p apunta a un byte más adelante del inicio de la cadena. Esto
es especialmente útil cuando se quiere recorrer una cadena sin perder el apuntador que apunta al inicio de
la cadena o a la región de memoria.

En las variables enteras a y b el caso es más o menos parecido. Sin embargo, al incrementar b, lo que se
hace es aumentar su valor final, no el valor del puntero, lo que da como resultado 0x41414142.
C O NT I NU A R

2.3.2. ARM 32 bits

Este caso particular es bastante parecido al anterior, salvando las distancias en cuanto a los mnemónicos y
los tipos de registro.

Figura 35. ARM 32 bits.


Fuente: elaboración propia.

C O NT I NU A R

2.4. Estructuras
Las estructuras son tipos de datos parecidos a los arrays; la diferencia es que cada uno de sus elementos
puede ser de un tipo diferente. Las estructuras existen solo en el lenguaje fuente: el compilador trata cada
elemento como un objeto independiente sin relación con los demás elementos de la estructura. A
continuación, se exponen dos códigos fuente cuyo desensamblado muestra como se accede a los
elementos como si fueran variables independientes.

Figura 36. Código fuente 1. Figura 37. Código fuente 2.


Fuente: elaboración propia. Fuente: elaboración propia.
1

Escenario
En los dos códigos anteriores (figuras 36 y 37)se observa como se accede a los elementos como
variables locales dentro de la función.

En este escenario es imposible reconstruir el código para que resulte fiel al código fuente real, ya
que no hay ningún tipo de información que ayude a relacionar las variables. En ocasiones, por el
contexto del programa -es decir, conociendo el protocolo que se esté manejando y observando
los mensajes de error o de estado-, es posible relacionarlos, pero el código ensamblador no arroja
ningún dato al respecto.

Hay un caso muy común que ayuda a reconstruir una estructura, y es cuando se pasa una
estructura por referencia a una función. En este caso, la función declara como argumento un
puntero a la estructura. Dentro de la función, al acceder a cualquier elemento (los cuales están
dispuestos de manera contigua), el compilador almacena la base en un registro y lo utiliza junto
con un desplazamiento para acceder a los distintos elementos, como si se tratase de una cadena
de caracteres o un vector. De esta forma, sí se podrán identificar diferentes estructuras
analizando el código objeto.
2

Figura 38. Código fuente.

Fuente: elaboración propia.

La figura 38, muestra un código fuente donde se invoca a una función pasándole por referencia
una estructura y se observan los elementos para poder ver la implementación.

El código objeto, dependiendo de la arquitectura, se muestra a continuación.

C O NT I NU A R
2.4.1. x86 32 y 64 bits
En 64 bits, respecto al código fuente propuesto anteriormente, solo cambia el tipo de registro en cuestión. El
siguiente código sería el generado para 32 bits a partir del código fuente anterior:

Figura 39. x86 y 64 bits.


Fuente: elaboración propia.
Análisis

En la función Main, tal y como sucedió en los códigos fuente del ejemplo anterior, se accede como variables
locales; es decir, se utiliza el registro que apunta a la cima de la pila ESP. Sin embargo, en la función Foo, se
carga en el registro EBX el valor de la base de la estructura (foo+4) y después se accede a los elementos
utilizando el registro como base y un desplazamiento para cada elemento (main+25 hasta main+39). Si el
tipo del elemento fuera diferente (por ejemplo, short), la directiva de tamaño sería Word en lugar de Dword.
Esto, sin duda, ayuda a saber de qué tipo de datos es el elemento.

C O NT I NU A R

2.4.2. ARM 32 bits


En este ejemplo se puede ver igualmente como se utiliza R4 para almacenar la base de la estructura,
obtenida directamente del argumento de la función R0, como se puede ver en la instrucción foo+8. Luego se
va accediendo a cada elemento actualizando el desplazamiento con la instrucción (STR valor, [base, #n]):
Figura 40. ARM 32 bits en R4.
Fuente: elaboración propia.

En la función Main se accede de manera similar, pero utilizando el registro de pila SP:

Figura 41. ARM 32 bits en pila SP.


Fuente: elaboración propia.

Por último, se puede comentar que existe otro tipo de datos denominado union que se define prácticamente
igual que una estructura (cambiando struct por union). La diferencia es que, en lugar de que cada elemento
ocupe espacios de memoria consecutiva, en union todos los elementos ocupan el mismo espacio; es decir,
para acceder a ellos se hace desde la misma dirección de memoria, solo que la directiva de espacio del
código objeto será la indicada por ese elemento.
1

Figura 42. Datos de union de código fuente.

Fuente: elaboración propia.

Este código fuente muestra un ejemplo simple del tipo de datos union.
2

Figura 43. Acceso a las variables.

Fuente: elaboración propia.

Aquí se muestra su código objeto, donde se expone que los accesos a las
variables se hacen todos a la misma dirección [ESP+0x1C], aunque sean de
distinto tipo.
3

Figura 44. Salida en hexadecimal.

Fuente: elaboración propia.

Puede verse la salida en hexadecimal, donde se observa como se ha


sobrescrito “BBBB” = 4 bytes por 0xDEAD = 2 bytes.

C O NT I NU A R

3.5. Objetos
Los objetos de C++ son estructuras cuyos elementos no son solo los tipos de datos vistos hasta ahora, sino
que pueden ser un método (función) o un atributo (del tipo public, friend, etc.).
Los elementos de un objeto son procesados por el compilador como elementos normales de una estructura.
Las funciones no virtuales son invocadas por el offset de la estructura, ya que el código de la función no
está contenido en ella -solo la dirección al inicio de la función-.
Las funciones virtuales son

Funciones virtuales invocadas a través de un puntero


especial que apunta a la tabla
virtual (vtable) dentro del objeto.

Las funciones públicas son


Funciones públicas
llamadas por cualquier objeto.

Mientras que las funciones


privadas solo pueden ser
Funciones privadas
invocadas por el propio
objeto.
 Esta privacidad de métodos y atributos no afecta a la reconstrucción de código: solo
afecta al tiempo de compilación en cuanto a la política de acceso. Durante el tiempo
de compilación, se muestra un error que dice que no es posible acceder a ese
método o atributo desde fuera de algún método de la clase.

Para explicar de qué manera es posible relacionar ciertas variables en una estructura y, de esta forma,
reconstruir un objeto, es necesario definir un método (función de una clase) que acceda a otros métodos o
atributos de la clase. Aún no se ha explicado este tipo de estructuras; aunque sí se ha hecho referencia a los
registros de cima y de base de pila, no se ha profundizado en ellas. Es por esto por lo que el siguiente
ejemplo hará un uso sencillo de una función para mostrar cómo es posible identificar y reconstruir un objeto.

C O NT I NU A R

2.5.1. Estrategia

El siguiente código fuente inicializa variables locales a funciones y atributos de una clase:
Figura 45. Código fuente de una clase simple.
Fuente: elaboración propia.

La estrategia para identificar el objeto es ver que se pasa como argumento de manera implícita un registro
que apunta a la base del objeto y se utiliza dentro del método para acceder al resto de los atributos o
métodos de la clase.

Esto es así porque, para poder acceder a una estructura desde una función, es necesario pasarlo por
referencia; es decir, que el argumento de la función sea un puntero a la estructura que se pasa como
argumento. Esto provoca que se utilice ese puntero para acceder al resto de los elementos de la estructura.
Cuando esta estructura es un objeto, ese puntero se denomina this y se pasa de manera implícita; es decir,
no hace falta definirlo como argumento (el compilador lo pasa automáticamente en un registro). En el caso
de una estructura, sí es necesario pasarlo como argumento y definirlo con el nombre que se desee.

C O NT I NU A R

2.5.2. x86 32 y 64 bits


En esta ocasión tampoco hay gran diferencia entre 32 y 64 bits. Las diferencias se refieren a la manera de
invocar las funciones, pero esto se tratará de manera separada y en detalle más adelante, en otro apartado.

A continuación, se verá la función Main, que instancia una clase en el objeto c para más adelante acceder a
los diferentes atributos del objeto (a, b y c) y asignarles valores:

Figura 46. Función Main en clase en el objeto c.


Fuente: elaboración propia.

Como se puede ver, para acceder a ellos se utiliza el registro EBP como base -ya que el objeto es una
variable local- y la pila para almacenarlo. Es por esto por lo que no se diferencia en nada de una estructura
local o de tres variables enteras locales, tal y como se puede apreciar en el siguiente ejemplo:

Figura 47. Variables locales declaradas e inicializadas independientemente.


Fuente: elaboración propia.
Figura 48. Estructura local con elementos declarados e inicializados en forma de estructura.
Fuente: elaboración propia.

Figura 49. Código objeto idéntico generado por los dos códigos fuente.
Fuente: elaboración propia.

El resultado es idéntico para ambos códigos fuente y para la porción de código main+6 a main+20 del código
fuente anterior, donde se declara un objeto; es decir, que el contexto en cuanto a significado y relación a alto
nivel sobre las variables se ha perdido completamente: es solo una abstracción para el programador. Esto es
lo que hace imposible saber si se trata de variables, de una estructura o de un objeto.

Sin embargo, si se observa el método público Foo_public(), se puede apreciar una variación que ayuda a
identificar un objeto y no variables independientes:
Figura 50. Método público Foo_public().
Fuente: elaboración propia.

Si se contempla Foo_public+34, se puede ver como carga en EAX un argumento de función. Esto, como se
verá más adelante en el apartado de funciones, se detecta porque se usa el registro de base de pila y un
desplazamiento positivo. Este registro es importante, ya que a continuación se utiliza para inicializar unas
variables usando EAX y un desplazamiento. Esto indica que son variables relacionadas en función de una
dirección base. Lo que habrá que averiguar es si se trata de una estructura o de un objeto.

Por otro lado, si se observa el código, se demuestra que la función no es declarada con ningún argumento:

Figura 51. Muestra del código sin ningún argumento.


Fuente: elaboración propia.

Esto ya daría la pista de que no puede ser una estructura. Si el compilador actúa así, es para pasar el puntero
this a la función invocada, e indicaría que es un método de un objeto. Sin embargo, esto no puede saberse
sin el código fuente, ya que, si se pone el foco de atención en el código objeto, se ve claramente como se
pasa el valor del puntero como argumento:
Figura 52. Visualización del puntero como argumento dentro del código fuente.
Fuente: elaboración propia.

Y no se sabe si ha sido el programador o el compilador.

C O NT I NU A R

2.5.3. Identificación del objeto mediante el análisis de los métodos

Análisis de la dirección del puntero base



Hay otra manera de averiguar si se trata de un objeto, que es analizando la dirección del puntero base que
se pasa por referencia.

Figura 53. Identificación del objeto mediante el análisis de los métodos.


Fuente: elaboración propia.

Acceso al resto de variables



Como se puede ver, el puntero apunta directamente a la primera variable local inicializada, lo que relaciona
dicha variable con la función. Si se pasase solo un puntero de esa variable a la función, el hecho de que
dentro de la función se acceda al resto de variables contiguas a ese puntero (de igual forma que las
variables locales de Main) relaciona directamente esas variables contiguas con un puntero base mediante
una estructura:

Figura 54. Acceso al resto de variables.


Fuente: elaboración propia.

En este momento, ya se sabe que las variables y la función están relacionadas mediante una estructura. No
importa si se trata de un objeto o de una estructura, ya que, como se ha explicado, respecto al código objeto
son lo mismo.

C O NT I NU A R

2.5.4. Identificación del objeto mediante el constructor


No obstante, hay una forma bastante clara de identificar un objeto, que es cuando se inicializa e invoca al
constructor. El constructor de una función es una función especial cuyo nombre debe ser igual que el de la
clase y no devuelve ningún tipo. Se pueden declarar varios constructores con diferentes argumentos.

Aquí se muestra un ejemplo del código fuente anterior, al que se le ha agregado el constructor:
Figura 55. Código fuente con constructor.
Fuente: elaboración propia.

La única manera de invocar al constructor es mediante el operador new, que reserva memoria para
almacenar la estructura con el objeto y ejecuta el constructor correspondiente dependiendo de cómo se le
haya invocado. Como en este caso solo se dispone de un constructor, invoca a esta función; como el
operador new devuelve un puntero a la zona de memoria reservada, c se declara un puntero y a los atributos
y métodos se accede con -> en lugar de con un punto.
Si se observa el siguiente código objeto generado en 64 bits:
Figura 56. Código objeto generado en 64 bits.
Fuente: elaboración propia.

Esta utilización del operador new implementa el escenario de utilización de métodos, donde es necesario
acceder con un puntero base, solo que ahora la situación sucede desde el inicio y sin necesidad siquiera de
analizar los métodos.

C O NT I NU A R

2.5.5. Identificación del objeto mediante vtable


El último método para identificar un objeto es la identificación de vtable con el fin de saber que la estructura
analizada es efectivamente un objeto y no una secuencia de variables o una simple estructura.

Las vtables son referenciadas por un atributo implícito del tipo puntero; es decir, no viene declarado por el
programador, sino que es automáticamente agregado por el compilador, que apunta a una tabla con métodos
(funciones) virtuales:
Figura 57. Vtables.
Fuente: elaboración propia.

En programación orientada a objetos, las funciones virtuales se utilizan para implementar la sobrecarga de
manera correcta. Cuando una clase hereda de otra clase base, se puede querer aplicar un método ya
implementado por la clase base para modificar su comportamiento. Así, cuando se invoca el método, se
ejecuta el nuevo método implementado o el método de la clase base. Para permitir esto, esos métodos se
deben declarar como virtuales y se almacenan en la vtable dentro del objeto.

En el siguiente código fuente se ha declarado el método de la clase como virtual:


Figura 58. Código fuente con clase virtual.
Fuente: elaboración propia.

Y en el código objeto se puede ver lo siguiente:

Figura 59. Código objeto.


Fuente: elaboración propia.

El compilador ha creado automáticamente un constructor (paso 1). Dentro de este (paso 2), se copia un
puntero a la dirección de la vtable en el objeto (paso 3). Finalmente, el contenido de la vtable (paso 4) tiene
un puntero a la función virtual Foo_public (paso 5).
En el punto donde está parado el debugger <main()+41>, la instancia del objeto de la clase MyClass quedaría
así:

Figura 60. Instancia del objeto de la clase MyClass.


Fuente: elaboración propia.

Anotación

Debido a esta utilización de funciones virtuales, es posible reconstruir el objeto a partir del puntero a la base
utilizado por el compilador con el nombre this. Nótese que ha sido posible hacerlo sin llegar a analizar la
función Foo_public(), que también hace uso de this.

Por último, cabe comentar un caso bastante común en relación con las funciones virtuales, que se da
cuando se invocan mediante un registro cuyo valor es calculado en tiempo de ejecución.

 Esto es bastante importante a la hora de reconstruir el código, ya que dificulta en


cierta medida su reconstrucción al necesitar analizar dicho registro en lugar de visitar
directamente la dirección de la función a invocar.

Esto sucede cuando se instancia una clase heredada de otra cuyo método o métodos son virtuales, tal y
como se muestra en el código fuente del siguiente ejemplo:
Figura 61. Código fuente: método virtual con invocación por registro.
Fuente: elaboración propia.

La invocación a los métodos Foo_public() de la línea 40 y Foo_public2() de la línea 42 se lleva a cabo


mediante un salto basado en un registro, tal y como se puede ver en el siguiente código objeto:
Figura 62. Salto en el registro del código objeto.
Fuente: elaboración propia.

Esto supone un problema a la hora de construir un grafo de control de flujo (CFG) que muestre el código
como un todo en forma de grafo, cuyos nodos son los bloques básicos del código. Esto supone un límite a la
hora de revisar el código de manera estática y poder seguir los flujos de datos o de control.

Sin embargo, con lo aprendido anteriormente se podrá calcular el valor de EAX (extended ax) y el registro
correcto sin necesidad de ejecutar código. Siempre que se vea un CALLreg, atrás en el código se ha debido
calcular dicho registro. Para ese cálculo, así como para obtener la dirección de la vtable, el compilador parte
del puntero this y después modifica el desplazamiento para llegar hasta la función virtual en cuestión.
A continuación, se analizarán las instrucciones inmediatamente anteriores para tratar de obtener
el puntero a la vtable y se calculará el desplazamiento para dar finalmente con la función a la que
hará el salto el Call rax.
1

Figura 63. Salto al Call rax.

Fuente: elaboración propia.

Si se observa de nuevo la imagen anterior: en el primer bloque de código, de <main+45> hasta


<main+52>, se observa como se calcula el registro rax. Las dos instrucciones siguientes son el
argumento que se pasa; en este caso, solo this. Podría valer para obtener vtable, pero es
necesario saber el desplazamiento para determinar cuál de los métodos virtuales es.

En el primer caso no se calcula ningún desplazamiento, mientras que en el segundo sí hay un


desplazamiento en la dirección <main+82>. Nótese que los desplazamientos se llevan a cabo con
instrucciones de sumas.
2

Figura 64. Direcciones de los métodos invocados.

Fuente: elaboración propia.

Ahora que ya se sabe que el primer caso es el método, cuyo desplazamiento es


0, y el segundo caso es el desplazamiento 8 de la vtable, al estar en 64 bits se
habla de dos métodos contiguos. A continuación, se identifican las direcciones
de los métodos que se invocarán analizando el constructor.
3

Figura 65. Ejecución de la instrucción Call.

Fuente: elaboración propia.

Esto permite saber que el primer Call rax invoca a <MyClassNew::foo_public()> y el segundo a
<MyClassNew::foo_public2()>.

Para confirmarlo, se ejecutará cada instrucción call para ver qué valor tiene:
De esta forma, queda confirmado que el método utilizado para reconstruir la vtable y analizar
estáticamente el control de flujo es correcto.

C O NT I NU A R

2.5.6. ARM 32 bits

En este caso, la estructura del código es bastante similar, salvando las diferencias entre los tipos de
registros y la arquitectura. Se utilizan los mismos mecanismos en los objetos tanto a nivel de generación de
vtables para las funciones virtuales como para las instanciaciones de objetos con o sin constructor. Esto es
lógico, ya que en las fases de compilación el proceso tan solo se separa en la última etapa de generación de
código, donde simplemente se traduce el código intermedio a código objeto y se realizan algunas
optimizaciones en este último código generado.

Para no repetir todos los pasos, se mostrarán tan solo los ejemplos relacionados con la vtable, que contiene
los ejemplos más básicos vistos en los otros apartados.

A continuación, se muestra el código fuente expuesto anteriormente:


Figura 66. Código fuente.
Fuente: elaboración propia.

Y el código objeto generado:


Figura 67. Código objeto.
Fuente: elaboración propia.

Se observa como le pasa el puntero this al método en las direcciones <main+44> y <main+52>, donde el
código del método Foo_public() es el siguiente:

Figura 68. Código de método Foo_public().


Fuente: elaboración propia.
En la imagen anterior se observa cómo se accede a las variables locales y a los atributos de las líneas 10-15
del código fuente.

Por último, se muestra el código objeto del ejemplo más complejo de las vtables, mostrado en la figura 61:

Figura 69. Código objeto de Vtables.


Fuente: elaboración propia.

En las direcciones <+72> y <+112> se observan las invocaciones a métodos mediante un registro calculado y
cómo se utiliza la suma para calcular el desplazamiento (<+96>).

Así que, si se analiza el código del constructor (<+32>, cuya dirección es 0x8f74), se obtiene la dirección de
la vtable y se muestran sus punteros, se podrán conseguir las direcciones de los métodos utilizados en las
llamadas mediante el registro r3:
Figura 70. Registro r3.
Fuente: elaboración propia.

Con este profundo análisis sobre objetos es posible reconstruir el control de flujo y de datos de manera
eficiente.

Saber más

Este tipo de tareas están automatizadas para entornos como IDA. También se
recomienda la lectura de un gran artículo sobre este tema para el compilador
de visual C en entornos Windows:

Igorsk. Reversing Microsoft Visual C++ Part II: Classes, Methods and
RTTI. OpenRCE.org; 2006.
Lección 3 de 6

III. Resumen

Repasa los conocimientos adquiridos en la unidad

Esta unidad se ha centrado en las estructuras de datos. Los alumnos han aprendido cuáles son los tipos
de datos más importantes y comunes en los lenguajes de programación C y C++. Además, han aprendido
a implementar estos datos en varias arquitecturas, principalmente en x86/32-64bit y ARM. Con estos
conocimientos, los alumnos serán capaces de identificar estructuras de datos en código ensamblador
de diferentes arquitecturas empleando diferentes recursos y técnicas.
Sin duda, ser capaces de identificar los tipos de datos básicos usados en un programa permite entender
los datos sobre los que se trabaja y cómo se modifican, que es el primer paso para entender su
funcionamiento. En esta unidad se han introducido estos tipos de datos básicos y se ha mostrado cómo
identificarlos en distintas arquitecturas.

Una vez identificados los tipos de datos básicos, el siguiente paso es entender que cualquier programa
moderno define sus propios tipos de datos. Ahí entra la importancia de, en primer lugar, las estructuras
de datos, usadas extensamente en los lenguajes de programación C y C++, y, en segundo lugar, los
objetos, propios de lenguajes como C++. Este tipo de organización de los datos y el código es el más
habitual en cualquier programa real, y entender cómo se organizan y se usan en las distintas
arquitecturas sentará las bases para entender el funcionamiento de cualquier programa.
Lección 4 de 6

IV. Caso práctico con solución

Aplica los conocimientos adquiridos en esta unidad

SE PIDE

Reconstruir el siguiente código objeto a código fuente en C:


Figura 1. Ejercicio del caso práctico 1.

Fuente: elaboración propia.


VER SOLUCIÓN

SOLUCIÓN

Para realizar la comprobación de este ejercicio se debe:

Deducir el código fuente como se pide en el enunciado y compilarlo.


Comparar el resultado obtenido con el código objeto del enunciado.

Así, se podrá comprobar si el código fuente es el correcto.


Lección 5 de 6

V. Glosario

El glosario contiene términos destacados para la


comprensión de la unidad

“Array”

Variable en la que cada elemento se almacena en memoria de manera consecutiva.

Estructura

Son tipos de datos algo parecidos a los arrays; la diferencia es que cada uno de sus elementos puede ser
de un tipo diferente. Las estructuras existen solo en el lenguaje fuente.

Objetos

En C++, son estructuras cuyos elementos pueden ser métodos o atributos.

Punteros

Variables que apuntan a una dirección de memoria a modo de apuntadores a otras variables.

Unión

Tipo de datos que se definen prácticamente igual que una estructura; la diferencia es que, en lugar de que
cada elemento ocupe espacios de memoria consecutiva, todos los elementos ocupan el mismo espacio.
Lección 6 de 6

VI. Bibliografía

Rupprecht, T. et al. DSIbin: Indentifying Dynamic Data Structures in C/C++ Binaries. ASE:
Proceedings of the 32nd IEEE/ACM International Conference on Automated Software
Engineering; 2017.

Deitel, H. M.; Deitel, P. J. Reversing: Secrets of Reverse Engineering; 2011.

Stroustrup, B. The C++ Programming Language, 4.ª ed. Addison-Wesley; 2013.

Stroustrup, B. A Tour of C++, 2.ª ed. Addison-Wesley; 2018.

Kernighan, B. W.; Ritchie, D. M. The C Programming Language, 2.ª ed. Prentice Hall; 2011.

Igorsk. Reversing Microsoft Visual C++ Part II: Classes, Methods and RTTI. OpenRCE.org; 2006.

También podría gustarte