O menor Mach-O executável deve ter pelo menos 0x1000
bytes. Por causa da limitação do XNU, o arquivo deve ter pelo menos PAGE_SIZE
. Veja xnu-4570.1.46/bsd/kern/mach_loader.c
, por volta da linha 1600.
No entanto, se não contamos esse preenchimento e contamos apenas uma carga útil significativa, o tamanho mínimo do arquivo executável no macOS é de 0xA4
bytes.
Tem que começar com mach_header (ou fat_header
/ mach_header_64
, mas esses são maiores).
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 */
};
Seu tamanho é 0x1C
bytes.
magic
tem que ser MH_MAGIC
.
Eu vou estar usando, CPU_TYPE_X86
pois é um x86_32
executável.
filtetype
deve ser MH_EXECUTE
executável ncmds
e sizeofcmds
depender de comandos e deve ser válido.
flags
não são tão importantes e são muito pequenas para fornecer qualquer outro valor.
A seguir estão os comandos de carregamento. O cabeçalho deve estar exatamente em um mapeamento, com direitos RX - novamente, limitações do XNU.
Também precisaríamos colocar nosso código em alguns mapeamentos RX, então isso é bom.
Para isso, precisamos de um segment_command
.
Vamos olhar para a definição.
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
tem que ser LC_SEGMENT
e cmdsize
tem que ser sizeof(struct segment_command) => 0x38
.
segname
o conteúdo não importa, e usaremos isso mais tarde.
vmaddr
tem que ser um endereço válido (eu vou usar 0x1000
), vmsize
tem que ser válido e múltiplo de PAGE_SIZE
, fileoff
tem que ser 0
, filesize
tem que ser menor que o tamanho do arquivo, mas maior que mach_header
pelo menos ( sizeof(header) + header.sizeofcmds
é o que eu usei).
maxprot
e initprot
tem que ser VM_PROT_READ | VM_PROT_EXECUTE
. maxport
geralmente também tem VM_PROT_WRITE
.
nsects
são 0, pois não precisamos de nenhuma seção e elas aumentam de tamanho. Eu configurei flags
para 0.
Agora, precisamos executar algum código. Existem dois comandos de carregamento para isso: entry_point_command
e thread_command
.
entry_point_command
não nos convém: veja xnu-4570.1.46/bsd/kern/mach_loader.c
, por volta da linha 1977:
1977 /* kernel does *not* use entryoff from LC_MAIN. Dyld uses it. */
1978 result->needs_dynlinker = TRUE;
1979 result->using_lcmain = TRUE;
Portanto, usá-lo exigiria que o DYLD funcionasse, e isso significa que precisaremos de __LINKEDIT
vazio symtab_command
e dysymtab_command
, dylinker_command
e dyld_info_command
. Excesso para o arquivo "menor".
Então, vamos usar thread_command
, especificamente, uma LC_UNIXTHREAD
vez que também configura a pilha que precisaremos.
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
vai ser LC_UNIXTHREAD
, cmdsize
seria 0x50
(veja abaixo).
flavour
é x86_THREAD_STATE32
, e contagem é x86_THREAD_STATE32_COUNT
( 0x10
).
Agora o thread_state
. Precisamos do 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;
};
Portanto, são realmente 16 uint32_t
anos que seriam carregados nos registros correspondentes antes do início do encadeamento.
A adição de cabeçalho, comando de segmento e comando de thread nos fornece 0xA4
bytes.
Agora, é hora de criar a carga útil.
Digamos que queremos imprimir Hi Frand
e exit(0)
.
Convenção Syscall para macOS x86_32:
- argumentos passados na pilha, pressionados da direita para a esquerda
- empilhe 16 bytes alinhados (nota: 8 bytes alinhados parece estar bem)
- número syscall no registro eax
- ligar por interrupção
Veja mais sobre syscalls no macOS aqui .
Então, sabendo disso, aqui está nossa carga útil na montagem:
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 a linha antes da primeira int 0x80
.
segname
pode ser qualquer coisa, lembra? Para que possamos colocar nossa carga útil nela. No entanto, são apenas 16 bytes e precisamos de um pouco mais.
Então, em 14
bytes, colocaremos a jmp
.
Outro espaço "livre" são os registros de estado do encadeamento.
Podemos definir qualquer coisa na maioria deles e colocaremos o restante de nossa carga útil lá.
Além disso, colocamos nossa string __eax
e __ebx
, uma vez que é mais curta do que movê-las.
Assim, podemos usar __ecx
, __edx
, __edi
para ajustar o resto da nossa carga útil. Observando a diferença entre o endereço de thread_cmd.state.__ecx
e o final de segment_cmd.segname
, calculamos que precisamos colocar jmp 0x3a
(ou EB38
) nos últimos dois bytes de segname
.
Portanto, nossa carga útil montada é 53 50 31C0 89E7 6A08 57 6A01 50 B004
para a primeira parte, EB38
para jmp e CD80 6A00 B001 50 CD80
para a segunda parte.
E último passo - definindo o __eip
. Nosso arquivo é carregado em 0x1000
(lembre-se vmaddr
) e a carga começa no deslocamento 0x24
.
Aqui está o xxd
arquivo de resultado:
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 ....
Pad-lo com qualquer coisa até 0x1000
bytes, chmod + xe execute :)
PS Sobre os binários x86_64 - 64 bits, é necessário ter __PAGEZERO
(qualquer mapeamento com VM_PROT_NONE
proteção cobrindo a página em 0x0). IIRC eles [Apple] não exigiram isso no modo de 32 bits apenas porque alguns softwares herdados não o possuíam e têm medo de quebrá-lo.