El ejecutable Mach-O ejecutable más pequeño posible


8

¿Cuál es el ejecutable Mach-O ejecutable más pequeño posible en x86_64? El programa no puede hacer nada (ni siquiera devolver un código de retorno), pero debe ser un ejecutable válido (debe ejecutarse sin errores).

Mi intento:

Ensamblador GNU ( null.s):

.text
.globl _main

_main:
    retq

Recopilación y vinculación:

as -o null.o null.s
ld -e _main -macosx_version_min 10.12 -o null null.o -lSystem

Tamaño: 4248 bytes

Mirando los valores hexadecimales parece que hay mucho relleno cero que tal vez se pueda eliminar, pero no sé cómo. Además, no sé si es posible ejecutar el exectubale sin vincular libSystem ...



1
@ JanDvorak Incluso hay una versión Mach-O de "teensy": osxbook.com/blog/2009/03/15/crafting-a-tiny-mach-o-executable
DepressedDaniel

Este GitHub Gist también podría devolver un pequeño ejemplo definiendo el encabezado Mach-O por sí solo: gist.github.com/softboysxp/1084476
Martin M.

Respuestas:


8

El Mach-O ejecutable más pequeño debe tener al menos 0x1000bytes. Debido a la limitación de XNU, el archivo debe ser al menos de PAGE_SIZE. Ver xnu-4570.1.46/bsd/kern/mach_loader.c, alrededor de la línea 1600.

Sin embargo, si no contamos ese relleno, y solo contamos una carga útil significativa, entonces el tamaño mínimo de archivo ejecutable en macOS es 0xA4bytes.

Tiene que comenzar con mach_header (o fat_header/ mach_header_64, pero esos son más grandes).

struct mach_header {
    uint32_t    magic;      /* mach magic number identifier */
    cpu_type_t  cputype;    /* cpu specifier */
    cpu_subtype_t   cpusubtype; /* machine specifier */
    uint32_t    filetype;   /* type of file */
    uint32_t    ncmds;      /* number of load commands */
    uint32_t    sizeofcmds; /* the size of all the load commands */
    uint32_t    flags;      /* flags */
};

Su tamaño es 0x1Cbytes.
magictiene que ser MH_MAGIC.
Lo usaré CPU_TYPE_X86ya que es un x86_32ejecutable.
filtetypetiene que ser MH_EXECUTEejecutable ncmdsy sizeofcmdsdepender de comandos, y debe ser válido.
flagsno son tan importantes y son demasiado pequeños para proporcionar cualquier otro valor.

Luego están los comandos de carga. El encabezado debe estar exactamente en un mapeo, con derechos de RX; nuevamente, limitaciones de XNU.
También necesitaríamos colocar nuestro código en alguna asignación de RX, por lo que está bien.
Para eso necesitamos a segment_command.

Veamos la definición.

struct segment_command { /* for 32-bit architectures */
    uint32_t    cmd;        /* LC_SEGMENT */
    uint32_t    cmdsize;    /* includes sizeof section structs */
    char        segname[16];    /* segment name */
    uint32_t    vmaddr;     /* memory address of this segment */
    uint32_t    vmsize;     /* memory size of this segment */
    uint32_t    fileoff;    /* file offset of this segment */
    uint32_t    filesize;   /* amount to map from the file */
    vm_prot_t   maxprot;    /* maximum VM protection */
    vm_prot_t   initprot;   /* initial VM protection */
    uint32_t    nsects;     /* number of sections in segment */
    uint32_t    flags;      /* flags */
};

cmdtiene que ser LC_SEGMENTy cmdsizetiene que ser sizeof(struct segment_command) => 0x38.
segnameel contenido no importa, y lo usaremos más tarde.

vmaddrtiene que ser una dirección válida (usaré 0x1000), vmsizetiene que ser válida y múltiplo de PAGE_SIZE, fileofftiene que ser 0, filesizetiene que ser menor que el tamaño del archivo, pero mayor que mach_headeral menos ( sizeof(header) + header.sizeofcmdses lo que he usado).

maxproty initprottiene que ser VM_PROT_READ | VM_PROT_EXECUTE. maxportpor lo general también tiene VM_PROT_WRITE.
nsectsson 0, ya que realmente no necesitamos ninguna sección y se sumarán al tamaño. Me puse flagsa 0.

Ahora, necesitamos ejecutar algo de código. Hay dos comandos de carga para eso: entry_point_commandy thread_command.
entry_point_commandno nos conviene: ver xnu-4570.1.46/bsd/kern/mach_loader.c, alrededor de la línea 1977:

1977    /* kernel does *not* use entryoff from LC_MAIN.  Dyld uses it. */
1978    result->needs_dynlinker = TRUE;
1979    result->using_lcmain = TRUE;

Por lo tanto, usarlo requeriría que DYLD funcione, y eso significa que necesitaremos __LINKEDIT, vaciar symtab_commandy dysymtab_command, dylinker_commandy dyld_info_command. Exageración para el archivo "más pequeño".

Entonces, usaremos thread_command, específicamente LC_UNIXTHREADya que también configura la pila que necesitaremos.

struct thread_command {
    uint32_t    cmd;        /* LC_THREAD or  LC_UNIXTHREAD */
    uint32_t    cmdsize;    /* total size of this command */
    /* uint32_t flavor         flavor of thread state */
    /* uint32_t count          count of uint32_t's in thread state */
    /* struct XXX_thread_state state   thread state for this flavor */
    /* ... */
};

cmdva a ser LC_UNIXTHREAD, cmdsizesería 0x50(ver más abajo).
flavoures x86_THREAD_STATE32, y cuenta es x86_THREAD_STATE32_COUNT( 0x10).

Ahora el thread_state. Necesitamos x86_thread_state32_taka _STRUCT_X86_THREAD_STATE32:

#define _STRUCT_X86_THREAD_STATE32  struct __darwin_i386_thread_state
_STRUCT_X86_THREAD_STATE32
{
    unsigned int    __eax;
    unsigned int    __ebx;
    unsigned int    __ecx;
    unsigned int    __edx;
    unsigned int    __edi;
    unsigned int    __esi;
    unsigned int    __ebp;
    unsigned int    __esp;
    unsigned int    __ss;
    unsigned int    __eflags;
    unsigned int    __eip;
    unsigned int    __cs;
    unsigned int    __ds;
    unsigned int    __es;
    unsigned int    __fs;
    unsigned int    __gs;
};

Por lo tanto, son los 16 uint32_tlos que se cargarán en los registros correspondientes antes de que se inicie el subproceso.

Agregar encabezado, comando de segmento y comando de subproceso nos da 0xA4bytes.

Ahora, es hora de elaborar la carga útil.
Digamos que queremos que se imprima Hi Frandy exit(0).

Convención de syscall para macOS x86_32:

  • argumentos pasados ​​en la pila, empujados de derecha a izquierda
  • pila de 16 bytes alineados (nota: 8 bytes alineados parece estar bien)
  • número de syscall en el registro eax
  • llamar por interrupción

Vea más sobre syscalls en macOS aquí .

Entonces, sabiendo eso, aquí está nuestra carga útil en el ensamblaje:

push   ebx          #; push chars 5-8
push   eax          #; push chars 1-4
xor    eax, eax     #; zero eax
mov    edi, esp     #; preserve string address on stack
push   0x8          #; 3rd param for write -- length
push   edi          #; 2nd param for write -- address of bytes
push   0x1          #; 1st param for write -- fd (stdout)
push   eax          #; align stack
mov    al, 0x4      #; write syscall number
#; --- 14 bytes at this point ---
int    0x80         #; syscall
push   0x0          #; 1st param for exit -- exit code
mov    al, 0x1      #; exit syscall number
push   eax          #; align stack
int    0x80         #; syscall

Observe la línea antes del primero int 0x80.
segnamepuede ser cualquier cosa, ¿recuerdas? Para que podamos poner nuestra carga útil en él. Sin embargo, son solo 16 bytes, y necesitamos un poco más.
Entonces, en 14bytes colocaremos un jmp.

Otro espacio "libre" son los registros de estado de hilo.
Podemos configurar cualquier cosa en la mayoría de ellos, y pondremos el resto de nuestra carga allí.

Además, colocamos nuestra cadena __eaxy __ebx, dado que es más corta que moverlos.

Por lo tanto, podemos utilizar __ecx, __edx, __edipara encajar el resto de nuestra carga útil. Al observar la diferencia entre la dirección de thread_cmd.state.__ecxy el final de segment_cmd.segname, calculamos que necesitamos poner jmp 0x3a(o EB38) en los últimos dos bytes de segname.

Entonces, nuestra carga útil ensamblada es 53 50 31C0 89E7 6A08 57 6A01 50 B004para la primera parte, EB38para jmp y CD80 6A00 B001 50 CD80para la segunda parte.

Y último paso: configurar el __eip. Nuestro archivo se carga en 0x1000(recordar vmaddr), y la carga comienza en el desplazamiento 0x24.

Aquí está el xxdarchivo de resultados:

00000000: cefa edfe 0700 0000 0300 0000 0200 0000  ................
00000010: 0200 0000 8800 0000 0000 2001 0100 0000  .......... .....
00000020: 3800 0000 5350 31c0 89e7 6a08 576a 0150  8...SP1...j.Wj.P
00000030: b004 eb38 0010 0000 0010 0000 0000 0000  ...8............
00000040: a400 0000 0700 0000 0500 0000 0000 0000  ................
00000050: 0000 0000 0500 0000 5000 0000 0100 0000  ........P.......
00000060: 1000 0000 4869 2046 7261 6e64 cd80 6a00  ....Hi Frand..j.
00000070: b001 50cd 8000 0000 0000 0000 0000 0000  ..P.............
00000080: 0000 0000 0000 0000 0000 0000 2410 0000  ............$...
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000                                ....

Rellenar con cualquier cosa hasta 0x1000bytes, chmod + x y ejecutar :)

PS About x86_64: se requieren binarios de 64 bits __PAGEZERO(cualquier mapeo con VM_PROT_NONEpágina de cobertura de protección en 0x0). IIRC ellos [Apple] no lo hicieron necesario en el modo de 32 bits solo porque algunos software heredados no lo tenían y tienen miedo de romperlo.


2
Esta es una respuesta increíblemente completa. Bienvenido al sitio! :)
James

1
Utilicé truncate -s 4096 foo(siendo foo el archivo ejecutable) para que se ajuste a 0x1000bytes y funcione perfectamente :)
Martin M.

4

28 bytes, precompilados.

A continuación se muestra un volcado hexadecimal formateado del binario Mach-O.

00 00 00 00 FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
|---------| |---------| |---------| |---------| |---------| |---------| |---------/
|           |           |           |           |           |           +---------- uint32_t        flags;          // Once again redundant, no flags for safety.
|           |           |           |           |           +---------------------- uint32_t        sizeofcmds;     // Size of the commands. Not sure the specifics for this, yet it doesn't particularly matter when there are 0 commands. 0 is used for safety.
|           |           |           |           +---------------------------------- uint32_t        ncmds;          // Number of commands this library proivides. 0, this is a redundant library.
|           |           |           +---------------------------------------------- uint32_t        filetype;       // Once again, documentation is lacking in this department, yet I don't think it particularly matters for our useless library.
|           |           +---------------------------------------------------------- cpu_subtype_t   cpusubtype;     // Like cputype, this suggests what systems this can run on. Here, 0 is ANY.
|           +---------------------------------------------------------------------- cpu_type_t      cputype;        // Defines what cpus this can run on, I guess. -1 is ANY. This library is definitely cross system compatible.
+---------------------------------------------------------------------------------- uint32_t        magic;          // This number seems to be provided by the compiling system, as I lack a system to compile Mach-O, I can't retrieve the actual value for this. But it will always be 4 bytes. (On 32bit systems)

Consiste completamente en el encabezado y no necesita los datos ni los cmds. Este es, por naturaleza, el binario Mach-O más pequeño posible. Es posible que no se ejecute correctamente en cualquier hardware concebible, pero coincide con la especificación.

Proporcionaría el archivo real, pero consta completamente de caracteres no imprimibles.


La primera línea de descripción comienza con "una vez más". Supongo que los escribiste en un orden diferente.
Sparr

Lea de abajo hacia arriba, que es como de izquierda a derecha. Sí, los escribí en ese orden.
ATaco

Esto en realidad no funciona en ningún sentido significativo, sin embargo.
DepressedDaniel

Aunque técnicamente no se ejecuta en ningún sentido significativo, es técnicamente ejecutable. Asumiendo que la especificación es correcta, esta es una biblioteca sin ningún dato. O, más simplemente, solo el encabezado de una biblioteca.
ATaco

¿Cómo se puede ejecutar esto? Ponerlo en un archivo binario y ejecutarlo arroja un error que dice "Error de formato de ejecución"
Martin M.

1

(uint) 0x00000007 es "I386" y "X86" (el nombre depende de en qué parte de la especificación XNU está buscando, pero es el arco correcto) (uint) 0x0x01000007 es X86_64

Teóricamente, puede O cualquier valor de CPU con 0x1000000 para convertirlo en una versión de 64 bits. XNU parece no siempre considerarlos valores discretos; por ejemplo, ARM 32 y 64 son 0x0000000C y 0x0100000C, respectivamente.

Ah, diablos, aquí está la lista que terminé teniendo que descubrir hace unos años, tenga en cuenta que la mayoría de estos son anteriores a OS / X:

VAX       =          1,    // Little-Endian
ROMP      =          2,    // Big-Endian -- 24bit or 32bit
NS32032   =          4,    // Hybrid -- Treat as Little Endian -- First 32b procs on the market
NS32332   =          5,    // Hybrid -- Treat as Little Endian -- These introduced a 20 byte "instruction cache"
MC680x0   =          6,    // Big-Endian
X86       =          7,    // Little-Endian
I386      =          7,    // alias for X86 and gets used interchangeably
MIPS      =          8,    // Big-Endian
NS32532   =          9,    // Hybrid -- Treat as Little Endian -- These ran from 20MHz up to a stunning 30MHz
MC98000   =         10,    // Big-Endian
HPPA      =         11,    // Big-Endian
ARM       =         12,    // Both! -- will treat as Little-Endian by default
MC88000   =         13,    // Big-Endian
SPARC     =         14,    // Big-Endian
I860      =         15,    // Little-Endian
ALPHA     =         16,    // Big-Endian -- NB, this is a 64-bit CPU, but seems to show up w/o the ABI64 flag . . . 
POWERPC   =         18,    // Big-Endian
X86_64    =   16777223,    // Little-Endian
POWERPC64 =   16777234,    // Big-Endian
ARM_64    = 0x0100000C     // Both! -- wil treat as Little-Endian by default
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.