11 abril 2012

Infección de ejecutables en Linux: ELF y código PIC (1/6)

Durante años, desde que los ordenadores han estado al alcance de las más calenturientas mentes de los hackers de antaño, multitud de extrañas ideas han pasado por sus cabezas, muchas de las cuales fueron llevadas a la práctica y de entre todas ellas, una que creó escuela (sobre todo allá por los años ochenta y principios de los noventa) fue la de infección de binarios. Escribir un programa que puede inocularse a otro, pegársele como un parásito y extenderse por ahí, suena casi a ciencia ficción, ¿cómo resistirse?

A lo largo de la historia de la computación, fueron múltiples las implementaciones prácticas de la infección de archivos ejecutables. Algunos veteranos recordarán las historias del paquistaní Brain, el Virdem, el Vienna, el WHALE y el más moderno CIH.

Todos estos son diversos tipos de virus de DOS/Windows que infectaban ejecutables tipo COM y EXE, y hay mucha literatura acerca de infección sobre este tipo de binarios. Sin embargo, ya no hay tanto (y menos en español) acerca de infección bajo Unix. Lo bueno -o malo, según se mire- de Unix es que de aquí a hace unos años existe un estándar de facto en lo que al tema de formato de ejecutables se refiere. Hablo, por supuesto del Executable and Linkable Format (ELF) cuyo éxito motiva el siguiente artículo.

Y es a partir de aquí cuando debo introducir el DISCLAIMER de rigor para que los geos no me hagan una desagradable visita a la puerta de mi casa. La única finalidad de esta publicación es ilustrar por cuántos sitios un ELF puede ser explotado para a) securizar nuestras aplicaciones incluyendo diversas comprobaciones al arranque o b) aplicar lo expuesto como alguna retorcida técnica a utilizar en algún CTF o como simple curiosidad para aquellos a los que nos gusta jugar con cosas así de raras. El hacer algo más allá ya no es que me limite a decir que es responsabilidad del que lo usa, que cae de cajón, si no que afirmo categóricamente que es mala idea. No es la primera vez que un bicho de estos se escapa de la cámara de incubación para la que ha sido diseñado, y sus desafortunadas leyendas perduran.

Y aclarado esto, saquemos nuestras herramientas de doctor Frankenstein. Vamos a pensar cómo se podría crear un monstruo.

Como este artículo toca tantas cosas, y probablemente me enfrento a un conjunto de lectores con muy diversos grados de conocimiento, he decidido dividirlo en 5 entregas para que cada cual salte a la que le interese:
  1. Introducción al formato ELF y el código PIC
  2. Técnicas de inyección.
  3. Técnicas de activación.
  4. Técnicas de armouring y antidebug.
  5. Securización.

Estas entregas van a ser en general bastante técnicas, por lo que sería bueno que el lector estuviese familiarizado con:
  • El lenguaje C y el compilador de C de GNU
  • Unix en general, y Linux en particular.
  • Programación en ensamblador para x86 (32 bits)
  • El inline assembly de GCC.
Hay mucho que escribir sobre este tema, y dedicar el humilde espacio que una entrada de blog puede darme me obligará a saltarme muchas cosas. Aún así, espero poder aproximarme a la realidad de la explotación de un binario ELF.

El formato ELF
El formato ELF es un estándar que nació en el seno de los Unix System Laboratories de AT&T Bell allá por el 1997 con la intención de superar las limitaciones del antiguo formato a.out. A día de hoy es usado por la mayoría de sistemas *nix como Linux, BSD o Solaris sobre procesadores tanto de 32 como de 64 bits, little endian o big endian, dando como resultado la existencia de dos grandes subconjuntos de formatos: El ELF de 32 bits y el ELF de 64 bits, cuyas cabeceras son ligeramente diferentes debido a las diferencias en el ancho de palabra.

El formato ha sido pensado tanto para ejecutables como para bibliotecas dinámicas, pasando por ficheros objeto y volcados de memoria, aunque este artículo va a estar enfocado sobre todo hacia el primero.

En cualquier caso, se suele decir que un fichero ELF se compone de una cabecera al principio del archivo con unos números mágicos apropiados e información general (del tipo "yo soy un ELF ejecutable para tal procesador y me ejecuto a partir de aquí") y de (hasta) dos tablas de cabeceras que sirven para dotar al fichero ELF de dos "vistas": una vista de segmentos (la cual es indexada mediante un array de estructuras llamado tabla de cabeceras de programa) y una vista de secciones (indexada por otro tipo de array de estructuras llamado tabla de cabeceras de sección). Si bien las dos sirven para definir de algún modo el tipo de información que alberga nuestro fichero ELF, la primera es necesaria para decirle al kernel (o al enlazador si nuestro binario es dinámico) qué porciones de nuestro fichero se cargan en qué partes del espacio de direcciones virtuales y con qué permisos (lectura / escritura / ejecución), y la segunda tiene como finalidad darle al enlazador información sobre qué bibliotecas necesita cargar, qué símbolos debe importar, qué símbolos exporta, etc.

Lo importante de ambas tablas es que no cubren todo el binario, y en la tabla de segmentos habrá zonas que incluso se solapen. Esto tiene como consecuencia que existirán zonas no referenciadas por ninguna de las dos tablas, existirán espacios vacíos o gaps que podríamos modificar a nuestro antojo de forma totalmente asintomática de cara al funcionamiento del binario (aquí probablemente ya haya alguno al que se le pongan los dientes largos).

Este estudio se centrará en el ELF ejecutable para x86, o sea que trabajaremos con ELFs de 32 bits y little endian. En teoría, esto se podría extrapolar a otros formatos (de hecho, me habría gustado probar esto en un UltraSPARC IIe a 64 bits, pero el router que lo saca a Internet ha muerto :( espero poder hacer un anexo más adelante cuando tenga el cacharro de nuevo en mis manos), pero de momento nos conformaremos con esta arquitectura, a día de hoy todavía muy extendida y bien conocida por muchos.

A lo largo de esta serie de entregas voy a referirme constantemente a las diversas estructuras que son contenidas por los ficheros ELF, así que a modo de referencia rápida incluiré una pequeña referencia de las mismas. Para trabajar con ellas desde C bastará con incluir el fichero de cabecera <elf.h>. La cabecera del ELF32 es tal que así:




Las cabeceras de programa empiezan en el desplazamiento e_phoff y es un array con e_phnum elementos del tipo:



A nosotros nos van a interesar sobre todo aquellos segmentos de tipo PT_LOAD, que son los que realmente se van a cargar en memoria. El resto de segmentos son meras subdivisiones de los anteriores explicitando el tipo de información que en ellos se guarda.

Las cabeceras de sección, por contra, empiezan en e_shoff y hay e_shnum cabeceras con el siguiente formato:




Hay muchos tipos de sección, cada uno de ellos haciendo referencia a diversos tipos de datos que podemos encontrar en el fichero ELF. Como mencioné antes, la utilidad que se le da es sobre todo información para el enlazador y depuración (ayuda muchísimo saber dónde empiezan los datos de sólo lectura de los datos normales, por ejemplo, algo que no se puede ver en las cabeceras de segmentos). Algunas secciones que encontraremos mucho en estos binarios son:
  • .text: aquí es donde se encuentra el código máquina ejecutable.
  • .data: donde podemos encontrar datos de lectura/escritura, normalmente variables estáticas o globales.
  • .bss: variables estáticas y globales sin inicializar (que por defecto son iguales a 0)
  • .rodata: datos de sólo lectura, normalmente cadenas y constantes.
  • .hash: tablas hash para la resolución rápida de símbolos.
  • .plt: trampolines para poder acceder a funciones externas importadas de bibliotecas dinámicas.

El procedimiento de carga
¿Cómo se carga un ELF en memoria? En la práctica se dan muchas situaciones diversas, pero en la mayoría de casos sólo nos debemos de preocupar de qué tipo de ELF se trata (¿es una biblioteca dinámica o un ejecutable?) y de si el ELF es estático o no (es decir, si depende o no de bibliotecas ajenas)

Se podría escribir todo un libro sólo sobre la carga de un ELF, pero voy a intentar resumirlo en los dos grandes casos que suelen darse en el mundo real: el ejecutable ELF estático -todo lo que el ELF necesita ya está codificado en el ELF y no usará nada más- y los ejecutables / bibliotecas ELF dinámicos -que importan / exportan objetos que otros módulos puedan necesitar-.

El binario ELF estático: es el más fácil de cargar (además de que suelen ser los más grandes y más fáciles de infectar). De entre los ejecutables y las bibliotecas, sólo los ejecutables pueden tener enlazado estático, y eso tiene sentido ya que las bibliotecas las usas para exportar dinámicamente símbolos. Este tipo de fichero es el que el kernel abre, se cerciona de que no existen cabeceras de tipo PT_INTERP o PT_DYNAMIC y que carga religiosamente cada PT_LOAD que encuentra si se lo puede permitir, saltando a ep_entry una vez que ha terminado la carga.

El binario ELF dinámico: este tiene un poco más de chicha, ya que bajo el nombre de ELF dinámico abarcamos tanto ejecutables como bibliotecas. En el caso de los ejecutables, tan pronto el kernel encuentra un segmento de tipo PT_INTERP, lee su contenido (que debe incluir una ruta absoluta a un "intérprete", normalmente algo como /lib/ld-linux.so) y lo carga en memoria en el mismo proceso que el binario dinámico que estamos cargando. El kernel pasa el control a este intérprete, que examina el binario cargado, analiza qué bibliotecas necesita, las intenta cargar, carga las bibliotecas que las otros bibliotecas necesita, y una vez que ha terminado de recorrer todo el árbol de dependencias comienza a resolver todos los símbolos que deban ser resueltos en el momento de carga. Una vez listo, salta al punto de entrada del ejecutable.

Nótese que en este último caso, las bibliotecas dinámicas no las carga el kernel, si no el propio intérprete mediante reads y mmaps. Los casos en los que el kernel carga directamente una biblioteca dinámica son tan concretos que los vamos a obviar. A la hora de infectar debemos tener en cuenta que hay muchas cosas que no se van a cargar en el mismo sitio, y esto suele complicar las cosas de cara a una infección exitosa. Sin embargo, este proceso tan complejo involucra la ejecución de constructores, destructores, funciones de inicialización y llamadas a punteros en general, lo cual nos abre la puerta a hacer llamadas al código inyectado.

Esta situación además nos dice algo interesante: es posible escribir código que pueda funcionar en cualquier punto de la memoria (puesto que una misma biblioteca puede cargarse en puntos distintos del espacio de direcciones) sin tener que recurrir a programar directamente en ensamblador. Y esto lo necesitaremos ya que en última instancia vamos a inyectar código en otro ejecutable, código que necesariamente se cargará en sitios distintos en función de donde lo inyectemos. Esta técnica, necesaria para que las bibliotecas dinámicas funcionen se llama PIC (Position-Independent Code), y nos lleva al siguiente punto:

El código independiente de la posición (PIC)
Esta es la clave del funcionamiento de las bibliotecas dinámicas. Un ejecutable suele escribirse para una determinada dirección de carga (en GNU/Linux-x86 suele ser a partir de 0x08048000), por lo que puede hacer las referencias absolutas que le plazca. Sin embargo, en una biblioteca ya no es así, porque entre que dos binarios pueden cargarla en puntos distintos, y que si tenemos ASLR activado entonces ya en un mismo binario se nos va a cargar en Cuenca, lo único que tenemos estable son las distancias relativas a cada punto del código. Y esto es precisamente con lo que juega PIC.

En x86, cuando GCC compila un binario con PIC (sea un ejecutable o una biblioteca), el binario resultante intenta llevar a cabo lo que se conoce como "direccionamiento relativo al EIP", concepto que a los programadores de x86-64 les sonará bastante. La idea viene a ser que, ya que no sabemos a priori dónde nos cargamos, podemos calcular la dirección en la que nos encontramos y sumarle (o restarle) un desplazamiento que será siempre el mismo para saber dónde se encuentran las funciones o variables que nos interesan.

Como dije antes, en x86-64 esta idea es familiar, y hay numerosas instrucciones que permiten acceder a datos tomando el puntero de instrucción (%rip) como base. En x86 (32 bits) tal direccionamiento no está tan pulido, y sólo puede utilizarse con instrucciones de salto siempre y cuando el punto al que queramos saltar no nos quede demasiado lejos. En el resto de casos, tenemos que llamar a una función que nos calcule la dirección en la que nos encontramos. Esta función GCC la llamará __i686.get_pc_thunk.bx, que meterá en %ebx la dirección de retorno desde la que se llama. Como consecuencia de este hack, tenemos una ligera pérdida de rendimiento a la hora de compilar para PIC:

Un inocente programa que muestra un mensaje, compilado bajo x86 con la opción -fPIC de GCC


El mismo programa compilado para x86-64, nótense los direccionamientos relativos al %rip


La razón por la que los chicos de GCC decidieron utilizar %ebx para este fin en una arquitectura en la que los registros de propósito general ya escasean, complicando mucho su uso como registro genérico teniendo en cuenta además que es donde se coloca el primer argumento de las syscalls, es bastante oscura (las directivas de inline assembly de GCC no nos van a dejar utilizar %ebx como registro de entrada), pero ahí está. En cualquier caso, esto quiere decir que si queremos que nuestro código PIC funcione, vamos a tener que incluir esa función en él. El problema es que esa función la pone directamente el compilador donde le cuadre y tampoco es un weak symbol (o sea que tampoco podremos sobreescribirla para colocarla donde nos guste). Veremos técnicas para medir nuestro código incluyendo esta molesta función en el siguiente punto:

Nuestro primer código PIC que se mide a sí mismo.
La mejor forma de estudiar hasta qué punto se puede explotar la infección de un binario es poniéndola en práctica nosotros mismos. Lo primero que necesitamos es poder inyectar código (y no datos) en un ejecutable. O sea, que nada de variables globales.

Esto debe hacerse así debido a que las variables globales para nuestros propósitos son un impedimento bastante gordo, estas suelen empezar mucho después del segmento de código (esto lo veremos luego, sobre todo por requerimientos de alineamiento de página del procesador, no podemos tener una página que sea una mitad ejecutable y la otra no) y además de que ocupan un espacio innecesario cuando no están siendo usadas.

Lo que vamos a inyectar debe ser puro segmento de código, y el resto de información debe ir incrustada en nuestras funciones como variables locales. Como las variables locales se reservan dinámicamente en la pila tan pronto la función que las contiene es llamada, no hay que reservar espacio previamente para ellas. La función correspondiente hace todo el trabajo.

Otra cosa muy importante es que necesitamos que el código a inyectar no dependa de bibliotecas externas. Esto es importantísimo ya que en tal caso necesitaríamos toda la información dinámica que el GCC generaría, la cual se encuentra principalmente en el segmento de datos -primera pega- y además tendría que ser visible al enlazador dentro de un binario infectado, lo cual es muy difícil por no decir imposible. Nuestro código ha de ser estático, y para reducir espacio no utilizaremos la libc. Trabajaremos directamente con syscalls.

Y por último, cómo no, que sea independiente de la posición.

Para poder inocular nuestro código, independientemente de cómo lo hagamos (que ya veremos técnicas, tranquilos :P) es saber cuánto medimos sin tener que incluir esa información a fuego en el binario. Que se puede, pero después de varias modificaciones sucesivas acaba ralentizando el desarrollo. Está claro que esto va a aumentar su tamaño, pero de momento servirá para su intención didáctica.

Primer intento (fallido)
Una primera aproximación podría ser esta: podemos definir un símbolo al principio de nuestro programa con la sección .text (siempre y cuando trabajemos con un único fichero y que es donde empieza nuestro código realmente, no el segmento de código, que ese empieza mucho antes e incluye hasta las cabeceras del ejecutable), y el final de nuestro código podemos medirlo con la dirección de _etext (que la define el compilador y marca donde acaba el código realmente, no sólo el segmento de datos). Esto parece buena idea, ya que esa __i686.get_pc_thunk.bx está dentro del .text. Tenemos algo como lo que sigue:




Aquí hay varias cosas que pueden llamar la atención. La primera, es que hay una función write que hace la llamada a la llamada al sistema correspondiente y una función long2hex. Además de la ausencia de main y una tal _start. Las dos primeras son consecuencia de la ausencia de la biblioteca estándar de C. Todo lo que necesitemos vamos a tener que codificarlo nosotros mismos si lo queremos realmente portable, en este caso la función write (para escribir por pantalla, por ejemplo) o long2hex (para convertir un número a cadena).

La función _start es el punto de entrada. Cuando al GCC enlaza contra la libc se añade una suerte de funciones extra al binario que hace inicializaciones previas (entre ellas, su propia _start) que se encargan, entre otras cosas, de llamar al main de C de toda la vida. Como nosotros no podremos disponer de la libc vamos a tener que escribir directamente a partir del punto de entrada, y ahí sí que no tenemos nada. Ni argumentos, ni variables de entorno arregladas, ni nada.

Este código, para cumplir todos los requisitos que hemos impuesto, debe compilarse de esta forma:
% gcc first.c -o first -nostdlib --static -fPIC
Donde --static sirve para generar un binario estático (sin dependencias de bibliotecas), -nostdlib evita enlazarnos la biblioteca estándar de C y -fPIC (quizá la más importante) obliga a GCC a generar código independiente de la posición. Ejecutándolo, cumple religiosamente su cometido:
% ./standalone
080480f8
08048241
00000149
^C
La primera dirección es donde empieza, la segunda donde acaba, y lo último es el tamaño (en hexadecimal).

Si desensamblamos lo que hemos generado, vemos esto:
% objdump -d first
first:     file format elf32-i386

Disassembly of section .text:
080480f8 <write>:
[...]
8048186:       e8 b2 00 00 00          call   804823d <__i686.get_pc_thunk.bx>
804818b:       81 c3 69 1e 00 00       add    $0x1e69,%ebx
[...]
8048195:       8b 83 fc ff ff ff       mov    -0x4(%ebx),%eax
[...]
8048240:       c3                      ret
Las direcciones cuadran con lo que nuestro binario ha dicho, podemos ver direcciones absolutas que realmente el código ignora, pero hemos cometido un tremendo FAIL. ¿Por qué? Lo que GCC suele hacer con %ebx es tan pronto tiene el %eip, calcular la dirección de la tabla de desplazamientos globales (GOT, la cual debería estar en el segmento de datos) relativa al %eip, para luego direccionar de nuevo a partir de la GOT. Esto en sí no es un problema, ya que el único papel que desempeña la GOT (que no copiaremos) es como una especie de "referencia imaginaria" a partir de la cual direccionar cosas. Normalmente, cuando se tiene que acceder al código, se suele sumar a %ebx un desplazamiento de una magnitud similar pero negativa, ya que el código está antes del segmento de datos y por tanto de la GOT.

El problema aparece cuando nuestro código accede realmente a la GOT, cosa que está pasando en 0x8048195 para leer en ella las direcciones de _stext y _etext. Esto quiere decir que el código depende del segmento de datos. Entonces no nos vale.

Segundo intento (este ya funciona mejor)
Ya no podíamos usar variables globales, ahora hemos descubierto que tampoco podemos utilizar los símbolos externos de tipo _etext, _stext ni nada que se le parezca. Tampoco vamos a codificar los límites del código a fuego. Por lo tanto, vamos a tener que ingeniárnoslas para calcular dichos límites. Una forma chapucera pero funcional es acotar nuestro código con un par de cadenas de bytes que hagan de marcadores que podamos identificar, cadenas que deben aparecen como bytes dentro del código ejecutable (siguen sin tratarse como datos, ojo). Allá donde encontremos dichos bytes sabremos que de ahí hacia adelante (o hacia atrás) no hay más código.

Aún así, esto implica una serie de problemas que debemos (y podremos) sortear. El primero es el de las colisiones, debemos tomar una secuencia de bytes lo suficientemente larga y extraña para que no aparezca en el código, y que el código que comprueba dichos bytes no provoque una colisión por el hecho de utilizar dicha información para comparar. El segundo es que no tenemos forma de asegurar desde nuestro código que dicho marcador se va a colocar exactamente al principio o al final. Aunque acotemos nuestro código con esas cadenas mediante el inline assembly, el enlazador del GCC puede recolocarlas después como le plazca. Y el tercero, ¿dónde se carga __i686.get_pc_thunk.bx? ¿Al principio, al final, en medio de nuestra sección .text?

Pues como dijo Jack el Destripador, "vamos por partes". La primera parte tiene fácil solución: tomamos un par de enteros de 32 bits como dicha secuencia de bytes, de forma que tenemos una marca de 4 bytes al principio y otra al final. Para que la comprobación no genere dichos enteros, podremos comprobar separadamente las dos mitades de 16 bits de cada una, de forma que no se generará una instrucción con exactamente la misma palabra que buscamos. Si 32 bits fuesen insuficientes (cosa que dudo) podríamos tomar dos enteros de 32 bits. Pero cuanto menos usemos, mejor.

Para lo segundo y lo tercero tenemos un remedio común: scripts del enlazador. Los scripts del enlazador nos permiten configurar aspectos más oscuros del binario que estamos generando, como por ejemplo qué hay en cada sección, que símbolos se exportan, en qué formato debe generarse el binario, en qué dirección se carga... lo que haremos será generar dos secciones, una para el marcador de inicio y otra para el marcador final, y haremos que se carguen justo antes y después de .text. Así matamos dos pájaros de un tiro, por una parte colocamos correctamente los marcadores, y por otra aseguramos que __i686.get_pc_thunk.bx queda ubicado entre ellos.

El plan de acción es este: definimos un par de secciones (.code_bottom y .code_top) que contendrán esos marcadores, mediante un script del enlazador forzamos a que se coloquen justo antes y después de la sección de código (.text) y escribimos una función llamada get_code_limits que se encargará de buscarlos.

He utilizado la cadena "KPAX" como marcador de inicio y final por dos razones. La primera es que, pese a tener sólo 4 bytes, es prácticamente imposible encontrarla en ninguna parte de la memoria de ningún binario, aunque bastaba con que no saliese en toda la extensión del código. He buscado esa cadena con grep en /bin, /usr/bin y /usr/lib y el número de coincidencias asciende a 0. La segunda, que me ha molado mucho esa película, o sea que a tomar por saco :P

El código funcional (second.c) es el siguiente:



Y el script del enlazador (second.lds) este:




Nuestro programa se compilará con:
% gcc second.c -o second -fPIC -static -nostdlib -Wl,-Tsecond.lds
Y al ejecutar veremos que se mide correctamente:
% ./second
080480d8
080482a4
000001cc
^C
Estos límites los podemos comprobar con objdump -sdx second. Además, no hay referencias al segmento de datos, o sea que todo bien.


Artículo cortesía de BatchDrake

9 comments :

NighterMan dijo...

Bienvenidos al primer capitulo de las locuras mentales de BatchDrake no aptas para personas que aun conserven la cordura jaja

Grande batch, eres grande!!!

Newlog dijo...

Oh yeah!

Ya tengo algo interesante que leer en clase.

Newlog dijo...

@NighterMan Queremos el código que prometiste en la Rooted :D

Ole - dijo...

Bravo Batch, me encantan estas cosas. Tengo un par de preguntas que te las dejo por aqui por si le surgen a mas gente.

- Cuando los programas ya estan cargados, ¿la distancia entre secciones es siempre la misma? Dices que se puede hacer y que es comun en x86-64 acceder a datos tomando como base el %rip. Dado que %rip apunta (normalmente :P) a algo en .text y que los datos suelen estar (normalmente tambien) en otras secciones (.data, .rodata, etc) entiendo que, independientemente de donde ser carguen debido a PIC y ASLR, la distancia entre cualesquiera dos secciones es la misma.

- Dado que en el codigo que vamos a inyectar no podemos utilizar la libc e invocaremos syscalls directamente, ¿por qué se incluye sys/syscall.h?, ¿qué contiene esa cabecera?, en el código de ejemplo ¿dónde se usa?.

- De nuevo debido a que no vamos a enlazar con libc, ¿por qué se incluye stdint.h?. Entiendo que no define e implementa funciones, sino que sólo define tipos, ¿no?.

- En el programa second veo que esa vez no se declaró la ".section .text", a diferencia de en el programa first (que luego al ejecutar llamastes standalone xD). ¿Cómo sabe el compilador que a partir de la función write() (justo lo que está despues de la declaración de ".section .code_top") es seccion .text y no seccion .code_top?

Saludos.

Gonzalo José Carracedo Carball dijo...

a) Pues sí, ya que las secciones sólo son subdivisiones que se hacen sobre la previa división en cabeceras de programa. Lo que realmente puede mover el kernel son los segmentos.

b) En sys/syscall.h tengo todos los códigos de las llamadas al sistema (cosas del tipo __NR_write y demás). Es realmente cómodo, e incluso creo que portable y todo. Allá donde veas un __NR_algo (en la propia implementación de write, por ejemplo) que sepas que es cortesía de ese fichero de cabecera.

c) Exacto, uint8_t, int32_t... esto es bueno porque suprime de lleno las ambigüedades que pueden existir al usar otros tipos como long (long en x86 es de 32 bits, pero en x86_64 es de 64, mientras que int es de 32 en ambas arquitecturas).

d) Pues porque esa directiva sólo funciona para el ensamblador. Todas las funciones de C se guardan en .text a menos que se especifique lo contrario con __attribute__ ((section (".sección_en_cuestión")))

Ole - dijo...

a) Entendiendo que con secciones te refieres a las .section, las divisiones echas en el ELF. Mientras que por segmento ya te refieres a los segments que usa la arquitectura i386, ¿no? (%cs, %ds, %es, %ss, %fs, %gs). En tal caso, el codigo es apuntado por %cs y los datos por %ds (por ejemplo), es decir, dos segmentos distintos, ¿podrían entonces estar a distinta distancia en distintas ejecuciones?. Tengo la sensacion de que tengo un concepto erroneo por algun lado...

d) Entonces imagino que en first pusistes la seccion .text explicitamente porque necesitabas poner el simbolo _stext al principio de la misma, ¿correcto?.

Gonzalo José Carracedo Carball dijo...

a) Vale, ahí me pasé de listo. Con segmentos me refiero a las porciones del binario que se referencian desde las cabeceras tipo PT_LOAD y que luego se cargan a partir de cierta dirección de memoria alineada a la página. Llamarlos segmentos es un poco abuso del lenguaje, lo utilizo porque hay una cabecera para todo lo que se puede ejecutar (en similitud con el segmento de texto) y otra para todos los datos (en similitud con el segmento de datos). Los registros de segmento olvídate de ellos xD muy rara vez los vas a ver en un binario.

d) Sep, exacto. Y lo cierto es que es un truco bastante patatero, pero bueno, como no iba a funcionar de todos modos ya pasé.

Gonzalo José Carracedo Carball dijo...

Jojojo, y huy lo que queda, ya verás el AWAKENING, cuando la bestia empiece a tomar consciencia de sí misma y los binarios arranquen con sorpresa.

vierito5 dijo...

Muy bueno el post BatchDrake!!! Tengo muchas ganas de leer el resto de la serie :)