Kleinstmögliche ausführbare Mach-O-ausführbare Datei

8

Was ist die kleinstmögliche ausführbare ausführbare Mach-O- Datei auf x86_64? Das Programm kann nichts tun (nicht einmal einen Rückkehrcode zurückgeben), sondern muss eine gültige ausführbare Datei sein (muss fehlerfrei ausgeführt werden).

Mein Versuch:

GNU Assembler ( null.s):

.text
.globl _main

_main:
    retq

Zusammenstellung & Verknüpfung:

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

Größe: 4248 Bytes

Wenn man sich die Hex-Werte ansieht, scheint es eine Menge Null-Polster zu geben, die vielleicht entfernt werden können, aber ich weiß nicht wie. Ich weiß auch nicht, ob es möglich ist, das Exectubale auszuführen, ohne libSystem zu verknüpfen ...

Martin M.
quelle
1
relevant / Antwort: muppetlabs.com/~breadbox/software/tiny/teensy.html
John Dvorak
1
@ JanDvorak Es gibt sogar eine Mach-O-Version von "teensy": osxbook.com/blog/2009/03/15/crafting-a-tiny-mach-o-executable
DepressedDaniel
Dieser GitHub Gist könnte auch ein winziges Exectubale zurückgeben, indem der Mach-O-Header selbst definiert wird: gist.github.com/softboysxp/1084476
Martin M.

Antworten:

8

Das kleinste ausführbare Mach-O muss mindestens 0x1000Bytes umfassen. Aufgrund der XNU-Einschränkung muss die Datei mindestens von sein PAGE_SIZE. Siehe xnu-4570.1.46/bsd/kern/mach_loader.cum die Linie 1600.

Wenn wir diese Auffüllung jedoch nicht zählen und nur sinnvolle Nutzdaten zählen, beträgt die unter macOS ausführbare minimale Dateigröße 0xA4Bytes.

Es muss mit mach_header beginnen (oder fat_header/ mach_header_64, aber diese sind größer).

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 */
};

Die Größe ist 0x1CBytes.
magicmuss sein MH_MAGIC.
Ich werde verwenden, CPU_TYPE_X86da es eine x86_32ausführbare Datei ist.
filtetypemuss sein , MH_EXECUTEfür ausführbare Datei, ncmdsund sizeofcmdshängen von Befehlen und haben ihre Gültigkeit.
flagssind nicht so wichtig und zu klein, um einen anderen Wert zu liefern.

Als nächstes folgen Ladebefehle. Der Header muss genau in einer Zuordnung mit RX-Rechten enthalten sein - wiederum XNU-Einschränkungen.
Wir müssten unseren Code auch in eine RX-Zuordnung einfügen, damit dies in Ordnung ist.
Dafür brauchen wir eine segment_command.

Schauen wir uns die Definition an.

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 */
};

cmdmuss sein LC_SEGMENTund cmdsizemuss sein sizeof(struct segment_command) => 0x38.
segnameInhalte spielen keine Rolle, und wir werden sie später verwenden.

vmaddrmuss eine gültige Adresse sein (ich werde sie verwenden 0x1000), vmsizemuss gültig sein und ein Vielfaches von PAGE_SIZE, fileoffmuss sein 0, filesizemuss kleiner als die Dateigröße sein, aber größer als mach_headermindestens ( sizeof(header) + header.sizeofcmdsist das, was ich verwendet habe).

maxprotund initprotmuss sein VM_PROT_READ | VM_PROT_EXECUTE. maxportin der Regel auch hat VM_PROT_WRITE.
nsectssind 0, da wir eigentlich keine Abschnitte benötigen und sie sich zu der Größe addieren. Ich habe flagsauf 0 gesetzt.

Jetzt müssen wir etwas Code ausführen. Dafür gibt es zwei Ladebefehle: entry_point_commandund thread_command.
entry_point_commandpasst nicht zu uns: siehe xnu-4570.1.46/bsd/kern/mach_loader.c, um Linie 1977:

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

Um es zu verwenden, müsste DYLD funktionieren, und das bedeutet, wir brauchen __LINKEDIT, leer symtab_commandund dysymtab_command, dylinker_commandund dyld_info_command. Overkill für "kleinste" Datei.

Wir werden es also verwenden thread_command, insbesondere LC_UNIXTHREADweil es auch einen Stapel erstellt, den wir benötigen.

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 */
    /* ... */
};

cmdwird sein LC_UNIXTHREAD, cmdsizewäre 0x50(siehe unten).
flavourist x86_THREAD_STATE32und count ist x86_THREAD_STATE32_COUNT( 0x10).

Nun die thread_state. Wir brauchen 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;
};

Es sind also tatsächlich 16 uint32_t, die in entsprechende Register geladen werden, bevor der Thread gestartet wird.

Durch Hinzufügen von Header, Segmentbefehl und Threadbefehl erhalten wir 0xA4Bytes.

Jetzt ist es Zeit, die Nutzlast herzustellen.
Nehmen wir an, wir möchten, dass es gedruckt wird Hi Frandund exit(0).

Syscall-Konvention für macOS x86_32:

  • Argumente, die auf dem Stapel übergeben wurden, wurden von rechts nach links verschoben
  • Stapel 16 Bytes ausgerichtet (Hinweis: 8 Bytes ausgerichtet scheint in Ordnung zu sein)
  • Syscall-Nummer im eax-Register
  • Anruf durch Interrupt

Weitere Informationen zu Syscalls unter macOS finden Sie hier .

Da wir das wissen, ist hier unsere Nutzlast in der Montage:

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

Beachten Sie zuerst die Zeile int 0x80.
segnamekann alles sein, erinnerst du dich? So können wir unsere Nutzlast hineinlegen. Es sind jedoch nur 16 Bytes, und wir brauchen etwas mehr.
Bei 14Bytes platzieren wir also a jmp.

Ein weiterer "freier" Speicherplatz sind Thread-Statusregister.
Wir können in den meisten von ihnen alles einstellen, und wir werden den Rest unserer Nutzlast dort ablegen.

Außerdem legen wir unsere Saite in __eaxund __ebx, da sie kürzer ist als das Bewegen.

So können wir nutzen __ecx, __edx, __ediden Rest unserer Nutzlast zu passen. Wenn wir den Unterschied zwischen der Adresse thread_cmd.state.__ecxund dem Ende von betrachten segment_cmd.segname, berechnen wir, dass wir die letzten zwei Bytes von eingeben müssen jmp 0x3a(oder EB38) segname.

Unsere zusammengebaute Nutzlast ist also 53 50 31C0 89E7 6A08 57 6A01 50 B004für den ersten Teil, EB38für jmp und CD80 6A00 B001 50 CD80für den zweiten Teil.

Und letzter Schritt - Einstellen der __eip. Unsere Datei wird um 0x1000(erinnern vmaddr) geladen und die Nutzlast beginnt mit dem Offset 0x24.

Hier ist die xxdErgebnisdatei:

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                                ....

Füllen Sie es mit bis zu 0x1000Bytes, chmod + x und führen Sie es aus :)

PS Über x86_64 - 64-Bit-Binärdateien sind erforderlich __PAGEZERO(jede Zuordnung mit VM_PROT_NONESchutzdeckblatt bei 0x0). IIRC sie [Apple] haben es nicht im 32-Bit-Modus erforderlich gemacht, nur weil einige ältere Software es nicht hatte und sie Angst haben, es zu brechen.

stek29
quelle
2
Dies ist eine unglaublich gründliche Antwort. Willkommen auf der Seite! :)
James
1
Ich habe verwendet truncate -s 4096 foo(wobei foo die ausführbare Datei ist), um sie an 0x1000Bytes anzupassen, und sie funktioniert perfekt :)
Martin M.
4

28 Bytes, vorkompiliert.

Unten ist ein formatierter Hex-Dump der Mach-O-Binärdatei.

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)

Besteht vollständig aus dem Header und benötigt weder die Daten noch die Cmds. Dies ist von Natur aus die kleinstmögliche Mach-O-Binärdatei. Es läuft möglicherweise auf keiner denkbaren Hardware korrekt, entspricht jedoch der Spezifikation.

Ich würde die eigentliche Datei liefern, aber sie besteht vollständig aus nicht druckbaren Zeichen.

Ein Taco
quelle
Die allererste Beschreibungszeile beginnt mit "noch einmal". Ich vermute, Sie haben sie in einer anderen Reihenfolge geschrieben.
Sparr
Lesen Sie von unten nach oben, wie von links nach rechts. Ja, ich habe sie in dieser Reihenfolge geschrieben.
ATaco
Dies gilt nicht wirklich laufen in irgendeiner sinnvollen Sinn, though.
DepressedDaniel
Obwohl es technisch nicht in einem sinnvollen Sinne ausgeführt wird, ist es technisch ausführbar. Unter der Annahme, dass die Spezifikation korrekt ist, handelt es sich um eine Bibliothek ohne Daten. Oder einfacher nur der Header einer Bibliothek.
ATaco
Wie kann das laufen? Wenn Sie es in eine Binärdatei einfügen und ausführen, wird der Fehler "Exec format error" ausgegeben
Martin M.
1

(uint) 0x00000007 ist "I386" und "X86" (Name abhängig davon, wo in der XNU-Spezifikation Sie suchen, aber es ist der richtige Bogen) (uint) 0x0x01000007 ist X86_64

Theoretisch können Sie mit 0x1000000 einen beliebigen CPU-Wert ODER eine 64-Bit-Version erstellen. XNU scheint sie nicht immer als diskrete Werte zu betrachten. Beispielsweise sind ARM 32 und 64 0x0000000C bzw. 0x0100000C.

Zum Teufel, hier ist die Liste, die ich vor ein paar Jahren herausfinden musste. Beachten Sie, dass die meisten davon älter sind als 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
David Beveridge
quelle