¿Cómo obtener código c para ejecutar código de máquina hexadecimal?

Resuelto Nosrettap asked hace 12 años • 7 respuestas

Quiero un método C simple para poder ejecutar código de bytes hexadecimal en una máquina Linux de 64 bits. Aquí está el programa en C que tengo:

char code[] = "\x48\x31\xc0";
#include <stdio.h>
int main(int argc, char **argv)
{
        int (*func) ();
        func = (int (*)()) code;
        (int)(*func)();
        printf("%s\n","DONE");
}

El código que estoy intentando ejecutar ( "\x48\x31\xc0") lo obtuve escribiendo este sencillo programa ensamblador (se supone que no debe hacer nada)

.text
.globl _start
_start:
        xorq %rax, %rax

y luego compilarlo y volcarlo para obtener el código de bytes.

Sin embargo, cuando ejecuto mi programa C aparece un error de segmentación. ¿Algunas ideas?

Nosrettap avatar Apr 01 '12 06:04 Nosrettap
Aceptado

El código de máquina debe estar en una página ejecutable. Estás char code[]en la sección de datos de lectura+escritura, sin permiso ejecutivo, por lo que el código no se puede ejecutar desde allí.

A continuación se muestra un ejemplo sencillo de cómo asignar una página ejecutable con mmap:

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

int main ()
{
  char code[] = {
    0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]
    0xC3                        //  ret
  };

  int (*sum) (int, int) = NULL;

  // allocate executable buffer                                             
  sum = mmap (0, sizeof(code), PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

  // copy code to buffer
  memcpy (sum, code, sizeof(code));
  // doesn't actually flush cache on x86, but ensure memcpy isn't
  // optimized away as a dead store.
  __builtin___clear_cache (sum, sum + sizeof(sum));  // GNU C

  // run code
  int a = 2;
  int b = 3;
  int c = sum (a, b);

  printf ("%d + %d = %d\n", a, b, c);
}

Consulte otra respuesta a esta pregunta para obtener detalles sobre __builtin___clear_cache.

 avatar Apr 01 '2012 12:04

Hasta las versiones recientes del kernel de Linux (en algún momento antes de la 5.4), simplemente se podía compilar gcc -z execstack, lo que haría que todas las páginas fueran ejecutables, incluidos los datos de solo lectura ( .rodata) y los datos de lectura y escritura ( .data) char code[] = "...".

Ahora -z execstacksolo se aplica a la pila real, por lo que actualmente solo funciona para matrices locales no constantes. es decir, mudarse char code[] = ...a main. Los sistemas modernos hacen que la menor cantidad de páginas ejecutables sea posible para protegerse contra exploits.


Consulte Comportamiento predeterminado de Linux en la sección `.data` para conocer el cambio del kernel y Permiso ejecutivo inesperado de mmap cuando se incluyen archivos ensamblados en el proyecto para conocer el comportamiento anterior: habilitar READ_IMPLIES_EXECel proceso de Linux para ese programa. (En Linux 5.4, esas preguntas y respuestas muestran que solo obtendrías READ_IMPLIES_EXECun faltante PT_GNU_STACK, como un binario muy antiguo; el GCC moderno -z execstackestablecería PT_GNU_STACK = RWXmetadatos en el ejecutable, lo que Linux 5.4 manejaría como si solo la pila fuera ejecutable. En algún momento antes de eso , PT_GNU_STACK = RWXresultó en READ_IMPLIES_EXEC.)

La otra opción es realizar llamadas al sistema en tiempo de ejecución para copiar en una página ejecutable, o cambiar los permisos en la página en la que se encuentra. Eso es aún más complicado que usar una matriz local para hacer que GCC copie el código en la memoria de pila ejecutable.

(No sé si hay una manera fácil de habilitarlo READ_IMPLIES_EXECen los kernels modernos. No tener ningún atributo de pila GNU en un binario ELF hace eso para el código de 32 bits, pero no para el de 64 bits).

Otra opción más es __attribute__((section(".text"))) const char code[] = ...;
el ejemplo práctico: https://godbolt.org/z/draGeh .
Si necesita que se pueda escribir en la matriz, por ejemplo, para el código shell que inserta algunos ceros en las cadenas, tal vez podría vincularlo con ld -N. Pero probablemente sea mejor usar -z execstack y una matriz local.


Dos problemas en la pregunta:

  • permiso exec en la página, porque usó una matriz que irá en la .datasección de lectura+escritura de noexec.
  • su código de máquina no termina con una retinstrucción , por lo que incluso si se ejecutara, la ejecución caería en lo que estuviera a continuación en la memoria en lugar de regresar.

Y por cierto, el prefijo REX es redundante. "\x31\xc0" xor eax,eax tiene exactamente el mismo efecto quexor rax,rax .


Necesita que la página que contiene el código de máquina tenga permiso de ejecución . Las tablas de páginas x86-64 tienen un bit separado para ejecución independiente del permiso de lectura, a diferencia de las tablas heredadas de 386 páginas.

La forma más sencilla de conseguir que las matrices estáticas estuvieran en la memoria read+exec era compilar con gcc -z execstack. (Se utiliza para hacer ejecutables la pila y otras secciones, ahora solo la pila).

typedef int (*intfunc_int)(int);

int main(void)
{
    unsigned char execbuf[] = {   // compile with -zexecstack
        0x8d, 0x47, 0x01,     // lea 0x1(%rdi),%eax
        0xc3                  // ret
    };
    // a string initializer like  char execbuf[] = "\xc3"; also works

    // Tell GCC we're about to run this data as code.  x86 has coherent I-cache,
    // but this also stops optimization from removing the initialization as dead stores.
    __builtin___clear_cache (execbuf, execbuf+sizeof(execbuf)-1);
    // Without this, the store disappears

    intfunc_int fptr = (intfunc_int) execbuf;  // cast to function pointer.
    int res = fptr(2);           // deref the function pointer
    
    return res;    // returns 3 on non-Windows ISAs where the first arg is in EDI
}

Se compila en un ensamblaje simple ( Godbolt ; también muestra que está roto sin él __builtin___clear_cache; se saltará la tienda y simplemente saltará al espacio de pila no inicializado). Esto se ejecuta correctamente con -z execstack, se producirá un error de segmentación sin él.

# GCC -O3 for x86-64
main:
    sub     rsp, 24              # GCC reserves 16 bytes more stack space than it needed
    mov     edi, 2               # function arg
    mov     DWORD PTR [rsp+12], -1023326323  # store 4 bytes of machine code
    lea     rax, [rsp+12]        # pointer into a register
    call    rax                  # call through the function pointer
    add     rsp, 24
    ret

Enlazador GNU más antiguo ldusado para hacer .rodataread+exec

Hasta hace poco (2018 o 2019), la cadena de herramientas estándar (binutils ld) colocaba la sección .rodataen el mismo segmento ELF que .text, por lo que ambos tendrían permiso de lectura+ejecución. Por lo tanto, el uso const char code[] = "...";fue suficiente para ejecutar bytes especificados manualmente como datos, sin execstack.

Pero en mi sistema Arch Linux con GNU ld (GNU Binutils) 2.31.1, ese ya no es el caso. readelf -amuestra que la .rodatasección entró en un segmento ELF con .eh_frame_hdry .eh_framey solo tiene permiso de lectura. .textva en un segmento con Read + Exec y .datava en un segmento con Read + Write (junto con y .got) .got.plt. (¿ Cuál es la diferencia entre sección y segmento en formato de archivo ELF ?)

Supongo que este cambio es para dificultar los ataques ROP y Spectre al no tener datos de solo lectura en páginas ejecutables donde secuencias de bytes útiles podrían usarse como "dispositivos" que terminan con los bytes de una instrucción reto jmp reg.

// See above for char code[] = {...} inside main with -z execstack, for current Linux

// This is broken on recent Linux, used to work without execstack.
#include <stdio.h>

// can be non-const if you use gcc -z execstack.  static is also optional
static const char code[] = {
  0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]       // retval = a+b;                    
  0xC3                        //  ret                                         
};

static const char ret0_code[] = "\x31\xc0\xc3";   // xor eax,eax ;  ret
                     // the compiler will append a 0 byte to terminate the C string,
                     // but that's fine.  It's after the ret.

int main () {
  // void* cast is easier to type than a cast to function pointer,
  // and in C can be assigned to any other pointer type.  (not C++)

  int (*sum) (int, int) = (void*)code;
  int (*ret0)(void) = (void*)ret0_code;

  // run code                                                                   
  int c = sum (2, 3);
  return ret0();
}

En sistemas Linux más antiguos: gcc -O3 shellcode.c && ./a.out(Funciona en constmatrices globales/estáticas)

En Linux anterior a 5.5 (más o menos)gcc -O3 -z execstack shellcode.c && ./a.out (funciona independientemente -zexecstackde dónde esté almacenado el código de máquina). Dato curioso: gcc permite -zexecstacksin espacio, pero clang solo acepta clang -z execstack.

Estos también funcionan en Windows, donde ingresan datos de solo lectura .rdataen lugar de archivos .rodata.

El compilador generado maintiene este aspecto (de objdump -drwC -Mintel). Puede ejecutarlo internamente gdby establecer puntos de interrupción codeyret0_code

(I actually used   gcc -no-pie -O3 -zexecstack shellcode.c  hence the addresses near 401000
0000000000401020 <main>:
  401020:       48 83 ec 08             sub    rsp,0x8           # stack aligned by 16 before a call
  401024:       be 03 00 00 00          mov    esi,0x3
  401029:       bf 02 00 00 00          mov    edi,0x2           # 2 args
  40102e:       e8 d5 0f 00 00          call   402008 <code>     # note the target address in the next page; that's where .rodata goes
  401033:       48 83 c4 08             add    rsp,0x8
  401037:       e9 c8 0f 00 00          jmp    402004 <ret0_code>    # optimized tailcall

O utilice llamadas al sistema para modificar los permisos de la página.

En lugar de compilar con gcc -zexecstack, puede utilizar mmap(PROT_EXEC)para asignar nuevas páginas ejecutables o mprotect(PROT_EXEC)cambiar las páginas existentes a ejecutables. (Incluidas las páginas que contienen datos estáticos). Por lo general, también desea al menos PROT_READy a veces PROT_WRITE, por supuesto.

Usarlo mprotecten una matriz estática significa que todavía estás ejecutando el código desde una ubicación conocida, lo que tal vez facilite establecer un punto de interrupción en él.

En Windows puedes usar VirtualAlloc o VirtualProtect.


Decirle al compilador que los datos se ejecutan como código

Normalmente, los compiladores como GCC suponen que los datos y el código están separados. Esto es como un alias estricto basado en tipos, pero incluso usarlo char*no lo deja bien definido para almacenarlo en un búfer y luego llamar a ese búfer como un puntero de función.

En GNU C, también es necesario usarlo __builtin___clear_cache(buf, buf + len)después de escribir bytes de código de máquina en un búfer , porque el optimizador no trata la desreferenciación de un puntero de función como una lectura de bytes de esa dirección. La eliminación del almacén muerto puede eliminar los almacenes de bytes de código de máquina en un búfer, si el compilador demuestra que nada lee el almacén como datos. https://codegolf.stackexchange.com/questions/160100/the-repetitive-byte-counter/160236#160236 y https://godbolt.org/g/pGXn3B tienen un ejemplo en el que gcc realmente hace esta optimización, porque gcc "sabe sobre" malloc. También el primer bloque de código en esta respuesta, donde usamos una matriz local en el espacio de pila ejecutable.

(Y en arquitecturas que no son x86 donde I-cache no es coherente con D-cache, en realidad realizará cualquier sincronización de caché necesaria. En x86 es puramente un bloqueador de optimización en tiempo de compilación y no se expande a ninguna instrucción en sí, porque un salto o una llamada es suficiente en papel para JIT o código automodificable, y en la práctica es completamente imposible observar código obsoleto después de un almacenamiento en CPU x86 reales).

Re: el nombre extraño con tres guiones bajos: es el __builtin_namepatrón habitual, pero namees __clear_cache.

Mi edición de la respuesta de @AntoineMathys agregó esto.

En la práctica, GCC/clang no "conoce" mmap(MAP_ANONYMOUS)la forma en que lo sabe malloc. Entonces, en la práctica, el optimizador asumirá que la memcpy en el búfer podría leerse como datos mediante la llamada a la función no en línea a través del puntero de función, incluso sin __builtin___clear_cache(). (A menos que haya declarado el tipo de función como __attribute__((const)).)

En x86, donde I-cache es coherente con los cachés de datos, que los almacenes se realicen en asm antes de la llamada es suficiente para que sea correcto. En otras ISA, __builtin___clear_cache()en realidad emitirá instrucciones especiales y garantizará el orden correcto en tiempo de compilación.

Es una buena práctica incluirlo al copiar código en un búfer porque no afecta el rendimiento y evita que futuros compiladores hipotéticos rompan su código. (por ejemplo, si entienden que eso mmap(MAP_ANONYMOUS)proporciona memoria anónima recién asignada a la que nada más tiene un puntero, como malloc).


Con GCC actual, pude provocar que GCC realmente hiciera una optimización que no queremos al usarla__attribute__((const)) para decirle que el optimizador sum()es una función pura (que solo lee sus argumentos, no la memoria global). GCC entonces sabe sum()que no puede leer el resultado de los memcpydatos.

Con otro memcpyen el mismo búfer después de la llamada, GCC realiza la eliminación del almacén inactivo solo en el segundo almacén después de la llamada. Esto da como resultado que no se almacene antes de la primera llamada, por lo que ejecuta los 00 00 add [rax], albytes, con un error de segmentación.

// demo of a problem on x86 when not using __builtin___clear_cache
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

int main ()
{
  char code[] = {
    0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]
    0xC3                        //  ret                                         
  };

  __attribute__((const)) int (*sum) (int, int) = NULL;

  // copy code to executable buffer                                             
  sum = mmap (0,sizeof(code),PROT_READ|PROT_WRITE|PROT_EXEC,
              MAP_PRIVATE|MAP_ANON,-1,0);
  memcpy (sum, code, sizeof(code));
  //__builtin___clear_cache(sum, sum + sizeof(code));

  int c = sum (2, 3);
  //printf ("%d + %d = %d\n", a, b, c);

  memcpy(sum, (char[]){0x31, 0xc0, 0xc3, 0}, 4);  // xor-zero eax, ret, padding for a dword store
  //__builtin___clear_cache(sum, sum + 4);
  return sum(2,3);
}

Compilado en el explorador del compilador Godbolt con GCC9.2 -O3

main:
        push    rbx
        xor     r9d, r9d
        mov     r8d, -1
        mov     ecx, 34
        mov     edx, 7
        mov     esi, 4
        xor     edi, edi
        sub     rsp, 16
        call    mmap
        mov     esi, 3
        mov     edi, 2
        mov     rbx, rax
        call    rax                  # call before store
        mov     DWORD PTR [rbx], 12828721    #  0xC3C031 = xor-zero eax, ret
        add     rsp, 16
        pop     rbx
        ret                      # no 2nd call, CSEd away because const and same args

Al pasar argumentos diferentes se habría obtenido otro call reg, pero incluso con __builtin___clear_cachelas dos sum(2,3)llamadas se puede hacer CSE . __attribute__((const))no respeta los cambios en el código de máquina de una función. No lo hagas. Sin embargo, es seguro si va a ejecutar JIT la función una vez y luego llamarla muchas veces.

Descomentar los primeros __clear_cacheresultados en

        mov     DWORD PTR [rax], -1019804531    # lea; ret
        call    rax
        mov     DWORD PTR [rbx], 12828721       # xor-zero; ret
       ... still CSE and use the RAX return value

La primera tienda está ahí gracias a __clear_cachela sum(2,3)llamada. (Eliminar la primera sum(2,3)llamada permite que se elimine el almacén muerto en todo el archivo __clear_cache).

El segundo almacén está ahí porque se supone que el efecto secundario en el búfer devuelto mmapes importante, y ese es el valor final mainque sale.

La opción de Godbolt ./a.outpara ejecutar el programa todavía parece fallar siempre (estado de salida de 255); ¿Quizás sea un espacio aislado para JITing? Funciona en mi escritorio __clear_cachey falla sin él.


mprotecten una página que contiene variables C existentes.

También puede otorgar permiso de lectura+escritura+ejecución a una única página existente. Esta es una alternativa a la compilación con-z execstack

No necesita __clear_cacheuna página que contenga variables C de solo lectura porque no hay una tienda para optimizar. Aún lo necesitarías para inicializar un búfer local (en la pila). De lo contrario, GCC optimizará el inicializador para este búfer privado al que una llamada de función no en línea definitivamente no tiene un puntero. (Análisis de fuga). No considera la posibilidad de que el búfer contenga el código de máquina para la función a menos que se lo indique a través de __builtin___clear_cache.

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

// can be non-const if you want, we're using mprotect
static const char code[] = {
  0x8D, 0x04, 0x37,           //  lea eax,[rdi+rsi]       // retval = a+b;                    
  0xC3                        //  ret                                         
};

static const char ret0_code[] = "\x31\xc0\xc3";

int main () {
  // void* cast is easier to type than a cast to function pointer,
  // and in C can be assigned to any other pointer type.  (not C++)
  int (*sum) (int, int) = (void*)code;
  int (*ret0)(void) = (void*)ret0_code;

   // hard-coding x86's 4k page size for simplicity.
   // also assume that `code` doesn't span a page boundary and that ret0_code is in the same page.
  uintptr_t page = (uintptr_t)code & -4095ULL;                  // round down
  mprotect((void*)page, 4096, PROT_READ|PROT_EXEC|PROT_WRITE);  // +write in case the page holds any writeable C vars that would crash later code.

  // run code                                                                   
  int c = sum (2, 3);
  return ret0();
}

Lo usé PROT_READ|PROT_EXEC|PROT_WRITEen este ejemplo para que funcione independientemente de dónde esté su variable. Si fuera un local en la pila y lo omitiera PROT_WRITE, callfallaría después de hacer que la pila solo lea cuando intentara enviar una dirección de retorno.

Además, PROT_WRITEle permite probar el código shell que se automodifica, por ejemplo, para editar ceros en su propio código de máquina u otros bytes que estaba evitando.

$ gcc -O3 shellcode.c           # without -z execstack
$ ./a.out 
$ echo $?
0
$ strace ./a.out
...
mprotect(0x55605aa3f000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit_group(0)                           = ?
+++ exited with 0 +++

Si comento mprotect, se produce un error de segmentación con versiones recientes de GNU Binutils ldque ya no colocan datos constantes de solo lectura en el mismo segmento ELF que la .textsección.

Si hiciera algo como ret0_code[4] = 0xc3;, después de eso necesitaría __builtin___clear_cache(ret0_code+2, ret0_code+2)asegurarme de que la tienda no esté optimizada, pero si no modifico las matrices estáticas, entonces no es necesario después mprotect. Es necesario después del almacenamiento mmap+ memcpyo manual, porque queremos ejecutar bytes que se han escrito en C (con memcpy).

Peter Cordes avatar Apr 28 '2019 19:04 Peter Cordes

Debe incluir el ensamblado en línea a través de una directiva de compilación especial para que termine correctamente en un segmento de código. Consulte esta guía, por ejemplo: http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html

QuantumMechanic avatar Mar 31 '2012 23:03 QuantumMechanic

Su código de máquina puede estar bien, pero su CPU se opone.

Las CPU modernas administran la memoria en segmentos. En funcionamiento normal, el sistema operativo carga un nuevo programa en un segmento de texto del programa y configura una pila en un segmento de datos . El sistema operativo le dice a la CPU que nunca ejecute código en un segmento de datos. Su código está en code[], en un segmento de datos. De ahí la falla de segmentación.

thb avatar Mar 31 '2012 23:03 thb

Esto requerirá algo de esfuerzo.

Su codevariable se almacena en la .datasección de su ejecutable:

$ readelf -p .data exploit

String dump of section '.data':
  [    10]  H1À

H1Àes el valor de tu variable.

La .datasección no es ejecutable:

$ readelf -S exploit
There are 30 section headers, starting at offset 0x1150:
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
[...]
  [24] .data             PROGBITS         0000000000601010  00001010
       0000000000000014  0000000000000000  WA       0     0     8

Todos los procesadores de 64 bits con los que estoy familiarizado admiten páginas no ejecutables de forma nativa en las tablas de páginas. La mayoría de los procesadores de 32 bits más nuevos (los que admiten PAE) proporcionan suficiente espacio adicional en sus tablas de páginas para que el sistema operativo emule páginas de hardware no ejecutables. Necesitará ejecutar un sistema operativo antiguo o un procesador antiguo para obtener una .datasección marcada como ejecutable.

Debido a que estos son solo indicadores en el ejecutable, debería poder configurarlos Xa través de algún otro mecanismo, pero no sé cómo hacerlo. Y es posible que su sistema operativo ni siquiera le permita tener páginas que se puedan escribir y ejecutar.

sarnold avatar Apr 01 '2012 00:04 sarnold