O menor Mach-O executável deve ter pelo menos 0x1000bytes. 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 0xA4bytes.
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 é 0x1Cbytes.
magictem que ser MH_MAGIC.
Eu vou estar usando, CPU_TYPE_X86pois é um x86_32executável.
filtetypedeve ser MH_EXECUTEexecutável ncmdse sizeofcmdsdepender de comandos e deve ser válido.
flagsnã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 */
};
cmdtem que ser LC_SEGMENTe cmdsizetem que ser sizeof(struct segment_command) => 0x38.
segnameo conteúdo não importa, e usaremos isso mais tarde.
vmaddrtem que ser um endereço válido (eu vou usar 0x1000), vmsizetem que ser válido e múltiplo de PAGE_SIZE, fileofftem que ser 0, filesizetem que ser menor que o tamanho do arquivo, mas maior que mach_headerpelo menos ( sizeof(header) + header.sizeofcmdsé o que eu usei).
maxprote initprottem que ser VM_PROT_READ | VM_PROT_EXECUTE. maxportgeralmente também tem VM_PROT_WRITE.
nsectssão 0, pois não precisamos de nenhuma seção e elas aumentam de tamanho. Eu configurei flagspara 0.
Agora, precisamos executar algum código. Existem dois comandos de carregamento para isso: entry_point_commande thread_command.
entry_point_commandnã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 __LINKEDITvazio symtab_commande dysymtab_command, dylinker_commande dyld_info_command. Excesso para o arquivo "menor".
Então, vamos usar thread_command, especificamente, uma LC_UNIXTHREADvez 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 */
/* ... */
};
cmdvai ser LC_UNIXTHREAD, cmdsizeseria 0x50(veja abaixo).
flavouré x86_THREAD_STATE32, e contagem é x86_THREAD_STATE32_COUNT( 0x10).
Agora o thread_state. Precisamos do 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;
};
Portanto, são realmente 16 uint32_tanos 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 0xA4bytes.
Agora, é hora de criar a carga útil.
Digamos que queremos imprimir Hi Frande 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.
segnamepode 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 14bytes, 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 __eaxe __ebx, uma vez que é mais curta do que movê-las.
Assim, podemos usar __ecx, __edx, __edipara ajustar o resto da nossa carga útil. Observando a diferença entre o endereço de thread_cmd.state.__ecxe 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 B004para a primeira parte, EB38para jmp e CD80 6A00 B001 50 CD80para 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 xxdarquivo 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é 0x1000bytes, chmod + xe execute :)
PS Sobre os binários x86_64 - 64 bits, é necessário ter __PAGEZERO(qualquer mapeamento com VM_PROT_NONEproteçã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.