Das kleinste ausführbare Mach-O muss mindestens 0x1000
Bytes umfassen. Aufgrund der XNU-Einschränkung muss die Datei mindestens von sein PAGE_SIZE
. Siehe xnu-4570.1.46/bsd/kern/mach_loader.c
um 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 0xA4
Bytes.
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 0x1C
Bytes.
magic
muss sein MH_MAGIC
.
Ich werde verwenden, CPU_TYPE_X86
da es eine x86_32
ausführbare Datei ist.
filtetype
muss sein , MH_EXECUTE
für ausführbare Datei, ncmds
und sizeofcmds
hängen von Befehlen und haben ihre Gültigkeit.
flags
sind 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 */
};
cmd
muss sein LC_SEGMENT
und cmdsize
muss sein sizeof(struct segment_command) => 0x38
.
segname
Inhalte spielen keine Rolle, und wir werden sie später verwenden.
vmaddr
muss eine gültige Adresse sein (ich werde sie verwenden 0x1000
), vmsize
muss gültig sein und ein Vielfaches von PAGE_SIZE
, fileoff
muss sein 0
, filesize
muss kleiner als die Dateigröße sein, aber größer als mach_header
mindestens ( sizeof(header) + header.sizeofcmds
ist das, was ich verwendet habe).
maxprot
und initprot
muss sein VM_PROT_READ | VM_PROT_EXECUTE
. maxport
in der Regel auch hat VM_PROT_WRITE
.
nsects
sind 0, da wir eigentlich keine Abschnitte benötigen und sie sich zu der Größe addieren. Ich habe flags
auf 0 gesetzt.
Jetzt müssen wir etwas Code ausführen. Dafür gibt es zwei Ladebefehle: entry_point_command
und thread_command
.
entry_point_command
passt 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_command
und dysymtab_command
, dylinker_command
und dyld_info_command
. Overkill für "kleinste" Datei.
Wir werden es also verwenden thread_command
, insbesondere LC_UNIXTHREAD
weil 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 */
/* ... */
};
cmd
wird sein LC_UNIXTHREAD
, cmdsize
wäre 0x50
(siehe unten).
flavour
ist x86_THREAD_STATE32
und count ist x86_THREAD_STATE32_COUNT
( 0x10
).
Nun die thread_state
. Wir brauchen 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;
};
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 0xA4
Bytes.
Jetzt ist es Zeit, die Nutzlast herzustellen.
Nehmen wir an, wir möchten, dass es gedruckt wird Hi Frand
und 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
.
segname
kann alles sein, erinnerst du dich? So können wir unsere Nutzlast hineinlegen. Es sind jedoch nur 16 Bytes, und wir brauchen etwas mehr.
Bei 14
Bytes 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 __eax
und __ebx
, da sie kürzer ist als das Bewegen.
So können wir nutzen __ecx
, __edx
, __edi
den Rest unserer Nutzlast zu passen. Wenn wir den Unterschied zwischen der Adresse thread_cmd.state.__ecx
und 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 B004
für den ersten Teil, EB38
für jmp und CD80 6A00 B001 50 CD80
fü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 xxd
Ergebnisdatei:
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 0x1000
Bytes, chmod + x und führen Sie es aus :)
PS Über x86_64 - 64-Bit-Binärdateien sind erforderlich __PAGEZERO
(jede Zuordnung mit VM_PROT_NONE
Schutzdeckblatt 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.