En tiempo de ejecución, el ensamblador o el código de la máquina (¿cuál es?) Debe estar en algún lugar de la RAM. ¿De alguna manera puedo acceder a él y leerlo o incluso escribirle?

Esto es solo para fines educativos.

Entonces, podría compilar este código. ¿Realmente me estoy leyendo aquí?

#include <stdio.h>
#include <sys/mman.h>

int main() {
    void *p = (void *)main;
    mprotect(p, 4098, PROT_READ | PROT_WRITE | PROT_EXEC);
    printf("Main: %p\n Content: %i", p, *(int *)(p+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(p+i)) );
    }
}

Aunque si agrego

*(int*)p =4;

Entonces es una falla de segmentación.


A partir de las respuestas, podría construir el siguiente código que se modifica durante el tiempo de ejecución:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment) {
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

// pattern is a 0-terminated string
char* find(char *string, unsigned int stringLen, char *pattern) {
    unsigned int iString = 0;
    unsigned int iPattern;
    for (unsigned int iString = 0; iString < stringLen; ++iString) {
        for (iPattern = 0;
            pattern[iPattern] != 0
            && string[iString+iPattern] == pattern[iPattern];
            ++iPattern);
        if (pattern[iPattern] == 0) { return string+iString; }
    }
    return NULL;
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }

    // Correct a part of THIS program directly in RAM
    char programSubcode[12] = {'H','e','l','l','o',
                                ' ','W','o','r','l','t',0};
    char *programCode = (char *)main;
    char *helloWorlt = find(programCode, 1024, programSubcode);
    if (helloWorlt != NULL) {
        helloWorlt[10] = 'd';
    }   
    printf("Hello Worlt\n");
    return 0;
}

¡Esto es increíble! ¡Gracias a todos!

-2
D. Rusin 8 sep. 2018 a las 21:57

4 respuestas

La mejor respuesta

El código de máquina se carga en la memoria. En teoría, puede leerlo y escribirlo como cualquier otra parte de la memoria a la que tenga acceso su programa.

Puede haber algunos obstáculos para hacer esto en la práctica. Los sistemas operativos modernos intentan limitar las secciones de datos de la memoria para operaciones de lectura / escritura pero sin ejecución, y las secciones de código de máquina de la memoria para leer / ejecutar pero no escribir. Esto es para tratar de limitar las posibles vulnerabilidades de seguridad que vienen con permitir la ejecución de lo que el programa se siente como poner en la memoria (como cosas aleatorias que podría extraer de Internet).

Linux proporciona la mprotect llamada al sistema para permitir cierta cantidad de personalización para proteger la memoria. Windows proporciona la SetProcessDEPPolicy llamada al sistema .

Editar para pregunta actualizada

Parece que estás intentando esto en Linux y estás usando mprotect. El código que publicó no verifica el valor de retorno de mprotect, por lo que no sabe si la llamada está teniendo éxito o no. Aquí hay una versión actualizada que verifica el valor de retorno:

#include <stdio.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdint.h>

void * alignptr(void * ptr, uintptr_t alignment)
{
    return (void *)((uintptr_t)ptr & ~(alignment - 1));
}

int main() {
    void *p = alignptr(main, 4096);
    int result = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);

    if (result == -1) {
        printf("Error: %s\n", strerror(errno));
    }
    printf("Main: %p\n Content: %i", main, *(int *)(main+2));
    unsigned int size = 16;
    for (unsigned int i = 0; i < size; ++i) {
        printf("%i ", *((int *)(main+i)) );
    }
}  

Tenga en cuenta los cambios en el parámetro de longitud pasado a mprotect y la función que alinea el puntero a un límite de página del sistema. Tendrá que investigar en su sistema específico. Mi sistema tiene una alineación de 4096 bytes (determinada ejecutando getconf PAGE_SIZE) y después de alinear el puntero y cambiar el parámetro de longitud a mprotect al tamaño de página, esto funciona y le permite escribir sobre su puntero a main .

Como han dicho otros, esta es una mala forma de cargar código dinámicamente. Las bibliotecas dinámicas, o complementos, son el método preferido.

4
bobshowrocks 8 sep. 2018 a las 22:26

La forma más directa y práctica de lograr esto es utilizar punteros de función. Puede declarar un puntero como:

void (*contextual_proc)(void) = default_proc;

Luego llámelo con la sintaxis contextual_proc();. También puede asignar una función diferente con la misma firma a contextual_proc, digamos contextual_proc = proc_that_logs;, y cualquier código que llame a contextual_proc() llamará (nuevo módulo de seguridad) al nuevo código.

Esto es muy similar al código auto modificable en efecto, pero es más fácil de entender, portátil y en realidad funciona en CPU modernas donde la memoria ejecutable no se puede escribir y las instrucciones se almacenan en caché.

En C ++, usaría subclases para esto; el envío estático lo implementará de la misma manera debajo del capó.

1
Davislor 8 sep. 2018 a las 23:26

En la mayoría de los sistemas operativos (Linux, Windows, Android, MacOSX, etc.), un programa no se ejecuta (directamente) en RAM pero tiene su espacio de direcciones virtuales y se ejecuta en él (estrictamente, el código no está -siempre o necesariamente- en RAM; puede tener un código que no está en RAM y que se ejecuta, después de algún error de página, tráigalo transparentemente en RAM). El sistema operativo administra (directamente) la RAM, pero su proceso solo ve su espacio de direcciones virtuales (inicializado en execve (2) hora y modificado con mmap (2), munmap , mprotect, mlock (2). ..). Utilice proc (5) y pruebe cat /proc/$$/maps en un shell de Linux para comprender más el espacio de direcciones virtuales de su proceso de shell. En Linux, puede consultar el espacio de direcciones virtuales de su proceso leyendo el archivo /proc/self/maps (secuencialmente, es un pseudoarchivo textual).

Lea Sistemas operativos: piezas fáciles para aprender Más información sobre los sistemas operativos.


En la práctica, si desea aumentar el código dentro de su programa (que se ejecuta en un sistema operativo común), será mejor que use complementos y las instalaciones de carga dinámica. En los sistemas Linux y POSIX, usará dlopen (3) (que usa mmap etc ...) luego con dlsym (3) obtendrá la dirección (virtual) de alguna función nueva y podría llamarla (almacenándola en algún puntero de función de su código C).

Realmente no define qué es un programa . Afirmo que un programa no solo es un ejecutable, sino que también está hecho de otros recursos (como bibliotecas específicas, tal vez fuentes o archivos de configuración, etc.) y es por eso que cuando instala algún programa , a menudo mucho más de lo que se mueve o copia el ejecutable (vea lo que make install hace para la mayoría de los programas de software gratuitos, incluso tan simple como GNU coreutils). Por lo tanto, un programa (en Linux) que genera algún código C (por ejemplo, en algún archivo temporal /tmp/genecode.c), compila ese código C en un complemento /tmp/geneplug.so (ejecutando gcc -Wall -O -fPIC /tmp/genecode.c -o /tmp/geneplug.so), luego { {X4}} que el plugin /tmp/geneplug.so está genuinamente modificándose a sí mismo. Y si codifica exclusivamente en C, esa es una forma sensata de escribir programas auto modificables.

En general, el código de su máquina se encuentra en el segmento de código, y ese segmento de código es de solo lectura ( y, a veces, incluso solo ejecutar; lea sobre el bit NX). Si realmente desea sobrescribir el código (y no extenderlo), necesitará usar instalaciones (quizás mprotect (2) en Linux) para cambiar esos permisos y habilitar la reescritura dentro del segmento de código.

Una vez que se puede escribir una parte de su segmento de código, puede sobrescribirlo.

Considere también algunas compilación JIT, como libgccjit o asmjit (y otros), para generar código de máquina en la memoria.

Cuando execve un nuevo ejecutable nuevo, la mayor parte de su código (todavía) no se encuentra en la RAM. Pero (desde el punto de vista del código de usuario en la aplicación) puede ejecutarlo (y el núcleo, de forma transparente, pero perezosa, traerá páginas de códigos a la RAM, a través de paginación de demanda). Eso es lo que trato de explicar al decir que su programa se ejecuta en su espacio de direcciones virtuales (no directamente en la RAM). Se necesita un libro completo para explicarlo más.

Por ejemplo, si tiene un gran ejecutable (por simplicidad, suponga que está vinculado estáticamente) de un gigabyte. Cuando inicia ese ejecutable (con execve), todo el gigabyte no se lleva a la RAM. Si su programa sale rápidamente, la mayoría de los gigabytes no se han introducido en la RAM y permanecen en el disco. Incluso si su programa se ejecuta durante mucho tiempo, pero nunca llama a una gran rutina de cien megabytes de código, esa parte del código (los 100Mbytes de la rutina nunca utilizada) no estará en la RAM.


Por cierto, stricto sensu, el código de auto modificación rara vez se usa en estos días (y los procesadores actuales no incluso maneje eso de manera eficiente, por ejemplo, debido a cachés y predictores de rama). Entonces, en la práctica, no modifica exactamente su código de máquina (incluso si eso fuera posible).

Y malware no tiene que modificar el código ejecutado actualmente. Podría (y a menudo lo hace) inyectar el código nuevo en la memoria y saltar de alguna manera (más precisamente, llamarlo a través de algún puntero de función). Entonces, en general, no sobrescribe el código "usado activamente" existente, crea un código nuevo en otro lugar y lo llama o pasa a él.

Si desea crear un nuevo código en otra parte de C, las instalaciones de complementos (por ejemplo, dlopen y dlsym en Linux), o las bibliotecas JIT, son más que suficientes.

Tenga en cuenta que la mención de "cambiar su programa" o "escribir código" es muy ambigua en su pregunta.

Es posible que solo desee extender el código de su programa (y luego usar técnicas de complementos, o bibliotecas de compilación JIT, es relevante). Tenga en cuenta que algunos programas (por ejemplo, SBCL) pueden generar código de máquina en cada interacción del usuario.

Puede cambiar el código existente de su programa, pero luego debe explicar lo que eso significa exactamente (¿qué significa "código" para usted exactamente ? ¿Es solo el ejecutado actualmente? instrucción de máquina o es el segmento de código completo de su programa?). ¿Piensa en código auto modificable, en generar código nuevo, en actualización dinámica de software?

¿De alguna manera puedo acceder a él y leerlo o incluso escribirle?

Por supuesto que sí. Debe cambiar la protección en su espacio de dirección virtual para su código (por ejemplo, con mprotect) y luego escribir muchos bytes en alguna parte del "código antiguo". Por qué querrías hacer eso es una historia diferente (y no has explicado por qué). No veo ningún propósito educativo al hacerlo: es probable que bloquee su programa bastante rápido (a menos que tome un lote de precauciones para escribir un código de máquina lo suficientemente bueno en la memoria).

Soy un gran admirador de metaprogramación pero generalmente genero algunos nuevos código y saltar a él. En nuestras máquinas actuales, no veo ningún valor en sobrescribir el código existente. Y (en Linux), mi manydl.c demuestra que podría generar código C, compilar y vincular dinámicamente más de un millón de complementos (y dlopen todos) en un solo programa. En la práctica, en las computadoras portátiles o de escritorio actuales, puede generar una gran cantidad de código nuevo (antes de preocuparse por los límites). Y C es lo suficientemente rápido (tanto en tiempo de compilación como en tiempo de ejecución) que podría generar miles de líneas C en cada interacción del usuario (varias veces por segundo), compilarlo y cargarlo dinámicamente (lo hice hace diez años en mi difunto proyecto GCC MELT).

Si desea sobrescribir archivos ejecutables en el disco (no veo ningún valor al hacerlo, es mucho más simple para crear ejecutables fresh ), debe comprender profundamente su estructura. Para Linux, sumérjase en las especificaciones de ELF.


En la pregunta editada, olvidó probar contra el fallo de mprotect. Probablemente esté fallando (porque 4098 no es una potencia de 2 y una página múltiple). Entonces, al menos codifique:

int c = mprotect(p, 4096, PROT_READ | PROT_WRITE | PROT_EXEC);
if (c) { perror("mprotect"); exit(EXIT_FAILURE); };

Incluso con el 4096 (en lugar del 4098) es probable que mprotect falle con EINVAL, porque main probablemente no esté alineado con una página 4K. (No olvide que su ejecutable también contiene el código crt0).

Por cierto, con fines educativos, debe agregar el siguiente código cerca del inicio de su main:

 char cmdbuf[80];
 snprintf (cmdbuf, sizeof(cmdbuf), "/bin/cat /proc/%d/maps", (int)getpid());
 fflush(NULL);
 if (system(cmdbuf)) 
   { fprintf(stderr, "failed to run %s\n", cmdbuf); exit(EXIT_FAILURE));

Y podría agregar un fragmento de código similar cerca del final. Puede reemplazar la cadena de formato snprintf para cmdbuf con "pmap %d".

0
Basile Starynkevitch 9 sep. 2018 a las 06:33

¡En principio es posible, en la práctica su sistema operativo se protegerá de su código peligroso!

El código de auto-modificación puede haber sido considerado como un "truco ingenioso" en los días en que las computadoras tenían muy pequeños recuerdos (en la década de 1950). Más tarde (cuando ya no era necesario) llegó a considerarse una mala práctica, lo que resultó en un código que era difícil de mantener y depurar.

En sistemas más modernos (a fines del siglo XX) se convirtió en un comportamiento indicativo de virus y malware. Como consecuencia, todos los sistemas operativos de escritorio modernos no permiten la modificación del espacio de código de un programa y también impiden la ejecución del código inyectado en el espacio de datos. Los sistemas modernos con una MMU pueden marcar regiones de memoria como de solo lectura y no ejecutables, por ejemplo.

La pregunta más simple de cómo obtener la dirección del espacio de código es simple. Un valor de puntero de función, por ejemplo, generalmente es la dirección de la función:

int main()
{
    printf( "Address of main() = %p\n", (void*)main ) ;
}

Tenga en cuenta también que en un sistema moderno, esta dirección será una dirección virtual en lugar de física.

6
Clifford 8 sep. 2018 a las 19:44