El Mach-O ejecutable más pequeño debe tener al menos 0x1000
bytes. 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 0xA4
bytes.
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 0x1C
bytes.
magic
tiene que ser MH_MAGIC
.
Lo usaré CPU_TYPE_X86
ya que es un x86_32
ejecutable.
filtetype
tiene que ser MH_EXECUTE
ejecutable ncmds
y sizeofcmds
depender de comandos, y debe ser válido.
flags
no 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 */
};
cmd
tiene que ser LC_SEGMENT
y cmdsize
tiene que ser sizeof(struct segment_command) => 0x38
.
segname
el contenido no importa, y lo usaremos más tarde.
vmaddr
tiene que ser una dirección válida (usaré 0x1000
), vmsize
tiene que ser válida y múltiplo de PAGE_SIZE
, fileoff
tiene que ser 0
, filesize
tiene que ser menor que el tamaño del archivo, pero mayor que mach_header
al menos ( sizeof(header) + header.sizeofcmds
es lo que he usado).
maxprot
y initprot
tiene que ser VM_PROT_READ | VM_PROT_EXECUTE
. maxport
por lo general también tiene VM_PROT_WRITE
.
nsects
son 0, ya que realmente no necesitamos ninguna sección y se sumarán al tamaño. Me puse flags
a 0.
Ahora, necesitamos ejecutar algo de código. Hay dos comandos de carga para eso: entry_point_command
y thread_command
.
entry_point_command
no 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_command
y dysymtab_command
, dylinker_command
y dyld_info_command
. Exageración para el archivo "más pequeño".
Entonces, usaremos thread_command
, específicamente LC_UNIXTHREAD
ya 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 */
/* ... */
};
cmd
va a ser LC_UNIXTHREAD
, cmdsize
sería 0x50
(ver más abajo).
flavour
es x86_THREAD_STATE32
, y cuenta es x86_THREAD_STATE32_COUNT
( 0x10
).
Ahora el thread_state
. Necesitamos x86_thread_state32_t
aka _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_t
los 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 0xA4
bytes.
Ahora, es hora de elaborar la carga útil.
Digamos que queremos que se imprima Hi Frand
y 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
.
segname
puede 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 14
bytes 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 __eax
y __ebx
, dado que es más corta que moverlos.
Por lo tanto, podemos utilizar __ecx
, __edx
, __edi
para encajar el resto de nuestra carga útil. Al observar la diferencia entre la dirección de thread_cmd.state.__ecx
y 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 B004
para la primera parte, EB38
para jmp y CD80 6A00 B001 50 CD80
para 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 xxd
archivo 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 0x1000
bytes, chmod + x y ejecutar :)
PS About x86_64: se requieren binarios de 64 bits __PAGEZERO
(cualquier mapeo con VM_PROT_NONE
pá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.