Mach-O runnable terkecil yang dapat dieksekusi

8

Apa kemungkinan Mach-O runnable terkecil yang dapat dieksekusi pada x86_64? Program tidak dapat melakukan apa-apa (bahkan tidak mengembalikan kode kembali), tetapi harus dapat dieksekusi (harus dijalankan tanpa kesalahan).

Usaha saya:

GNU Assembler ( null.s):

.text
.globl _main

_main:
    retq

Kompilasi & Penautan:

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

Ukuran: 4248 byte

Melihat nilai hex sepertinya ada banyak zero padding yang mungkin bisa dihilangkan, tapi saya tidak tahu caranya. Saya juga tidak tahu apakah mungkin menjalankan exectubale tanpa menghubungkan libSystem ...

Martin M.
sumber
1
@JanDvorak Bahkan ada versi Mach-O dari "amat" ": osxbook.com/blog/2009/03/15/crafting-a-tiny-mach-o-executable
DepressedDaniel
GitHub Gist ini juga dapat mengembalikan exectubale kecil dengan mendefinisikan header Mach-O sendiri: gist.github.com/softboysxp/1084476
Martin M.

Jawaban:

8

Mach-O runnable terkecil harus sekurang-kurangnya 0x1000byte. Karena batasan XNU, file harus setidaknya PAGE_SIZE. Lihat xnu-4570.1.46/bsd/kern/mach_loader.c, sekitar baris 1600.

Namun, jika kami tidak menghitung padding itu, dan hanya menghitung payload yang berarti, maka ukuran file minimal yang bisa dijalankan pada macOS adalah 0xA4byte.

Itu harus dimulai dengan mach_header (atau fat_header/ mach_header_64, tetapi itu lebih besar).

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

Ukurannya adalah 0x1Cbyte.
magicharus MH_MAGIC.
Saya akan menggunakan CPU_TYPE_X86karena ini x86_32executable.
filtetypeharus MH_EXECUTEuntuk dieksekusi, ncmdsdan sizeofcmdsbergantung pada perintah, dan harus valid.
flagstidak begitu penting dan terlalu kecil untuk memberikan nilai lain.

Berikutnya adalah memuat perintah. Header harus tepat dalam satu pemetaan, dengan hak RX - lagi, batasan XNU.
Kami juga perlu menempatkan kode kami di beberapa pemetaan RX, jadi ini baik-baik saja.
Untuk itu diperlukan suatu segment_command.

Mari kita lihat definisi.

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

cmdharus LC_SEGMENT, dan cmdsizeharus sizeof(struct segment_command) => 0x38.
segnamekonten tidak masalah, dan kami akan menggunakannya nanti.

vmaddrharus alamat yang valid (saya akan menggunakan 0x1000), vmsizeharus valid & kelipatan PAGE_SIZE, fileoffharus 0, filesizeharus lebih kecil dari ukuran file, tetapi lebih besar dari mach_headersetidaknya ( sizeof(header) + header.sizeofcmdsadalah apa yang saya gunakan).

maxprotdan initprotharus VM_PROT_READ | VM_PROT_EXECUTE. maxportbiasanya juga sudah VM_PROT_WRITE.
nsectsadalah 0, karena kita tidak benar-benar membutuhkan bagian apa pun dan mereka akan menambahkan hingga ukuran. Saya telah menetapkan flagske 0.

Sekarang, kita perlu menjalankan beberapa kode. Ada dua perintah pemuatan untuk itu: entry_point_commanddan thread_command.
entry_point_commandtidak cocok untuk kita: lihat xnu-4570.1.46/bsd/kern/mach_loader.c, sekitar baris 1977:

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

Jadi, menggunakannya membutuhkan DYLD untuk bekerja, dan itu berarti kita perlu __LINKEDIT, kosong symtab_commanddan dysymtab_command, dylinker_commanddan dyld_info_command. Berlebihan untuk file "terkecil".

Jadi, kami akan gunakan thread_command, khususnya LC_UNIXTHREADkarena ia juga mengatur tumpukan yang akan kami butuhkan.

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

cmdakan menjadi LC_UNIXTHREAD, cmdsizeakan 0x50(lihat di bawah).
flavouradalah x86_THREAD_STATE32, dan hitung adalah x86_THREAD_STATE32_COUNT( 0x10).

Sekarang thread_state. Kami membutuhkan x86_thread_state32_talias _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;
};

Jadi, memang 16 uint32_t's yang akan dimuat ke register yang sesuai sebelum utas dimulai.

Menambahkan header, perintah segmen dan perintah utas memberi kita 0xA4byte.

Sekarang, waktu untuk menyusun payload.
Katakanlah kita ingin mencetak Hi Franddan exit(0).

Konvensi Syscall untuk macOS x86_32:

  • argumen diteruskan pada tumpukan, didorong dari kanan ke kiri
  • tumpukan 16-byte selaras (catatan: 8-byte selaras tampaknya baik-baik saja)
  • nomor panggilan dalam register eax
  • panggilan dengan interupsi

Lihat lebih lanjut tentang syscalls di macOS di sini .

Jadi, mengetahui itu, inilah muatan kami dalam perakitan:

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

Perhatikan baris sebelumnya int 0x80.
segnamebisa apa saja, ingat? Jadi kita bisa menaruh muatan kita di dalamnya. Namun, ini hanya 16 byte, dan kami perlu lebih banyak.
Jadi, pada 14byte kita akan menempatkan a jmp.

Ruang "bebas" lainnya adalah register status utas.
Kami dapat mengatur apa saja di sebagian besar dari mereka, dan kami akan menaruh sisa muatan kami di sana.

Juga, kami menempatkan string kami di __eaxdan __ebx, karena itu lebih pendek daripada memindahkannya.

Jadi, kita dapat menggunakan __ecx, __edx, __ediagar sesuai dengan sisa payload kami. Melihat perbedaan antara alamat thread_cmd.state.__ecxdan akhir segment_cmd.segnamekita menghitung bahwa kita perlu memasukkan jmp 0x3a(atau EB38) dalam dua byte terakhir segname.

Jadi, payload kami yang dirakit adalah 53 50 31C0 89E7 6A08 57 6A01 50 B004untuk bagian pertama, EB38untuk jmp, dan CD80 6A00 B001 50 CD80untuk bagian kedua.

Dan langkah terakhir - pengaturan __eip. File kami dimuat di 0x1000(ingat vmaddr), dan payload dimulai pada offset 0x24.

Inilah xxdfile hasil:

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 dengan apa saja hingga 0x1000byte, chmod + x dan jalankan :)

PS Tentang x86_64 - binari 64bit harus memiliki __PAGEZERO(setiap pemetaan dengan VM_PROT_NONEhalaman perlindungan meliputi pada 0x0). IIRC mereka [Apple] tidak membuatnya diperlukan pada mode 32bit hanya karena beberapa perangkat lunak lama tidak memilikinya dan mereka takut untuk merusaknya.

stek29
sumber
2
Ini adalah jawaban yang sangat teliti. Selamat datang di situs ini! :)
James
1
Saya menggunakan truncate -s 4096 foo(dengan foo menjadi file exectuable) untuk membuatnya sesuai dengan 0x1000byte dan bekerja dengan sempurna :)
Martin M.
4

28 Bytes, Pra-dikompilasi.

Di bawah ini adalah dump hex yang diformat dari biner 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)

Terdiri sepenuhnya dari header, dan tidak memerlukan data atau cmds. Ini, secara alami, biner Mach-O terkecil yang mungkin. Mungkin tidak berjalan dengan benar pada perangkat keras apa pun yang mungkin, tetapi cocok dengan spesifikasinya.

Saya akan menyediakan file yang sebenarnya, tetapi seluruhnya terdiri dari karakter yang tidak dapat dicetak.

ATaco
sumber
Baris pertama deskripsi dimulai dengan "sekali lagi". Saya kira Anda menulisnya dalam urutan yang berbeda.
Sparr
Baca dari bawah ke atas, yang seperti kiri ke kanan. Ya, saya menulisnya dalam urutan itu.
ATaco
Ini sebenarnya tidak berjalan dalam arti yang berarti.
DepressedDaniel
Meskipun secara teknis tidak berjalan dalam arti yang berarti secara teknis dapat dijalankan. Dengan asumsi spesifikasi sudah benar, ini adalah perpustakaan tanpa data apa pun. Atau, lebih sederhana, hanya tajuk perpustakaan.
ATaco
Bagaimana ini bisa dijalankan? Menempatkannya dalam file biner dan mengeksekusinya melempar kesalahan yang mengatakan "Exec format error"
Martin M.
1

(uint) 0x00000007 adalah "I386" dan "X86" (nama tergantung di mana dalam spesifikasi XNU yang Anda cari, tetapi itu adalah lengkungan yang benar) (uint) 0x0x01000007 adalah X86_64

Secara teoritis, Anda dapat ATAU nilai CPU apa pun dengan 0x1000000 untuk menjadikannya versi 64bit. XNU tampaknya tidak selalu menganggapnya sebagai nilai diskrit; misalnya, ARM 32 dan 64 masing-masing adalah 0x0000000C dan 0x0100000C.

Ah, begini, daftar saya akhirnya harus mencari tahu beberapa tahun yang lalu, perhatikan bahwa sebagian besar sebelum 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
sumber