Menor executável Mach-O executável possível


8

Qual é o menor executável Mach-O executável possível no x86_64? O programa não pode fazer nada (nem mesmo retornar um código de retorno), mas deve ser um executável válido (deve ser executado sem erros).

Minha tentativa:

Assembler GNU ( null.s):

.text
.globl _main

_main:
    retq

Compilação e vinculação:

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

Tamanho: 4248 bytes

Olhando para os valores hexadecimais, parece que há muito preenchimento zero que talvez possa ser removido, mas não sei como. Também não sei se é possível fazer o exectubale rodar sem ligar o libSystem ...



1
@JanDvorak Existe até uma versão Mach-O de "teensy": osxbook.com/blog/2009/03/15/crafting-a-tiny-mach-o-executable
DepressedDaniel

Esse GitHub Gist também pode retornar um pequeno exectubale, definindo o cabeçalho do Mach-O por conta própria: gist.github.com/softboysxp/1084476
Martin M.

Respostas:


8

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.


2
Esta é uma resposta incrivelmente completa. Bem vindo ao site! :)
James

1
Eu usei truncate -s 4096 foo(com foo sendo o arquivo exectuable) para torná-lo apto 0x1000bytes e ele funciona perfeitamente :)
Martin M.

4

28 bytes, pré-compilados.

Abaixo está um despejo hexadecimal formatado do binário 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 inteiramente no cabeçalho e não precisa dos dados nem dos cmds. Este é, por natureza, o menor binário Mach-O possível. Pode não funcionar corretamente em qualquer hardware concebível, mas corresponde à especificação.

Eu forneceria o arquivo real, mas ele consiste inteiramente de caracteres não imprimíveis.


A primeira linha de descrição começa com "mais uma vez". Suponho que você os escreveu em uma ordem diferente.
Sparr

Leia de baixo para cima, como da esquerda para a direita. Sim, eu os escrevi nessa ordem.
ATaco 9/12

Isso, na verdade, não é executado em nenhum sentido significativo.
DepressedDaniel

Embora tecnicamente não seja executado em nenhum sentido significativo, é tecnicamente possível de executar. Supondo que a especificação esteja correta, esta é uma biblioteca sem nenhum dado. Ou, mais simplesmente, apenas o cabeçalho de uma biblioteca.
ATaco 9/12

Como isso pode ser executado? Colocá-lo em um arquivo binário e executá-lo gera um erro dizendo "Erro no formato Exec"
Martin M.

1

(uint) 0x00000007 é "I386" e "X86" (nome dependendo de onde você está procurando na especificação XNU, mas é o arco correto) (uint) 0x0x01000007 é X86_64

Teoricamente, você pode OU qualquer valor de CPU com 0x1000000 para torná-lo uma versão de 64 bits. XNU nem sempre os considera valores discretos; por exemplo, ARM 32 e 64 são 0x0000000C e 0x0100000C, respectivamente.

Ah, caramba, aqui está a lista que acabei tendo que descobrir alguns anos atrás, observe que a maioria deles é anterior ao 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
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.