Mach-O nhỏ nhất có thể chạy được


8

Mach-O có thể chạy được nhỏ nhất có thể chạy được trên x86_64 là gì? Chương trình không thể làm gì (thậm chí không trả lại mã trả về), nhưng phải là tệp thực thi hợp lệ (phải chạy không có lỗi).

Tôi thử

Trình biên dịch GNU ( null.s):

.text
.globl _main

_main:
    retq

Biên dịch & Liên kết:

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

Kích thước: 4248 byte

Nhìn vào các giá trị hex, có vẻ như có rất nhiều phần đệm bằng 0 có thể được gỡ bỏ, nhưng tôi không biết làm thế nào. Ngoài ra tôi không biết liệu có thể làm cho exectubale chạy mà không cần liên kết libSystem ...


1

1
@JanDvorak Có thậm chí là một phiên bản Mach-O của "teensy": osxbook.com/blog/2009/03/15/crafting-a-tiny-mach-o-executable
DepressedDaniel

GitHub Gist này cũng có thể trả về một ngoại lệ nhỏ bằng cách tự mình xác định tiêu đề Mach-O: gist.github.com/softboysxp/1084476
Martin M.

Câu trả lời:


8

Mach-O nhỏ nhất có thể chạy được ít nhất là 0x1000byte. Do giới hạn XNU, tệp phải có ít nhất là PAGE_SIZE. Xem xnu-4570.1.46/bsd/kern/mach_loader.c, xung quanh dòng 1600.

Tuy nhiên, nếu chúng ta không tính phần đệm đó và chỉ tính tải trọng có ý nghĩa thì kích thước tệp tối thiểu có thể chạy trên macOS là 0xA4byte.

Nó phải bắt đầu với mach_header (hoặc fat_header/ mach_header_64, nhưng những cái đó lớn hơn).

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

Kích thước của nó là 0x1Cbyte.
magiccó được MH_MAGIC.
Tôi sẽ sử dụng CPU_TYPE_X86vì nó là một x86_32thực thi.
filtetypephải được MH_EXECUTEthực thi ncmdssizeofcmdsphụ thuộc vào các lệnh và phải hợp lệ.
flagskhông quan trọng và quá nhỏ để cung cấp bất kỳ giá trị nào khác.

Tiếp theo là các lệnh tải. Tiêu đề phải chính xác trong một ánh xạ, với quyền RX - một lần nữa, giới hạn XNU.
Chúng tôi cũng cần đặt mã của chúng tôi trong một số ánh xạ RX, vì vậy điều này là tốt.
Cho rằng chúng ta cần a segment_command.

Hãy nhìn vào định nghĩa.

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

cmdphải có LC_SEGMENT, và cmdsizephải có sizeof(struct segment_command) => 0x38.
segnamenội dung không quan trọng, và chúng tôi sẽ sử dụng nó sau.

vmaddrphải là địa chỉ hợp lệ (tôi sẽ sử dụng 0x1000), vmsizephải hợp lệ & nhiều PAGE_SIZE, fileoffphải 0, filesizephải nhỏ hơn kích thước tệp, nhưng lớn hơn mach_headerít nhất ( sizeof(header) + header.sizeofcmdslà những gì tôi đã sử dụng).

maxprotinitprotphải được VM_PROT_READ | VM_PROT_EXECUTE. maxportthường cũng có VM_PROT_WRITE.
nsectslà 0, vì chúng tôi không thực sự cần bất kỳ phần nào và chúng sẽ thêm kích thước. Tôi đã đặt flagsthành 0.

Bây giờ, chúng ta cần thực thi một số mã. Có hai lệnh tải cho điều đó: entry_point_commandthread_command.
entry_point_commandkhông phù hợp với chúng tôi: xem xnu-4570.1.46/bsd/kern/mach_loader.c, khoảng dòng 1977:

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

Vì vậy, sử dụng nó sẽ đòi hỏi phải làm cho DỄ DÀNG hoạt động, và điều đó có nghĩa là chúng ta sẽ cần __LINKEDIT, trống symtab_commanddysymtab_command, dylinker_commanddyld_info_command. Quá mức cho tập tin "nhỏ nhất".

Vì vậy, chúng tôi sẽ sử dụng thread_command, đặc biệt LC_UNIXTHREADvì nó cũng thiết lập ngăn xếp mà chúng tôi cần.

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

cmdsẽ là LC_UNIXTHREAD, cmdsizesẽ là 0x50(xem bên dưới).
flavourx86_THREAD_STATE32, và đếm là x86_THREAD_STATE32_COUNT( 0x10).

Bây giờ thread_state. Chúng tôi cần 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;
};

Vì vậy, nó thực sự là 16 uint32_t'sẽ được tải vào các thanh ghi tương ứng trước khi luồng được bắt đầu.

Thêm tiêu đề, lệnh phân đoạn và lệnh luồng cho chúng ta 0xA4byte.

Bây giờ, thời gian để tạo ra trọng tải.
Hãy nói rằng chúng tôi muốn nó in Hi Frandexit(0).

Quy ước tòa nhà cho macOS x86_32:

  • các đối số được truyền vào ngăn xếp, đẩy từ phải sang trái
  • xếp chồng 16 byte được căn chỉnh (lưu ý: 8 byte được căn chỉnh có vẻ ổn)
  • số tòa nhà trong đăng ký eax
  • gọi bằng cách ngắt

Xem thêm về các tòa nhà chọc trời trên macOS tại đây .

Vì vậy, biết rằng, đây là tải trọng của chúng tôi trong lắp ráp:

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

Chú ý dòng trước int 0x80.
segnamecó thể là bất cứ điều gì, nhớ không? Vì vậy, chúng tôi có thể đặt tải trọng của chúng tôi trong đó. Tuy nhiên, nó chỉ có 16 byte và chúng tôi cần thêm một chút.
Vì vậy, tại 14byte chúng ta sẽ đặt a jmp.

Một không gian "miễn phí" khác là các thanh ghi trạng thái luồng.
Chúng tôi có thể đặt bất cứ thứ gì trong hầu hết chúng, và chúng tôi sẽ đặt phần còn lại của mình vào đó.

Ngoài ra, chúng tôi đặt chuỗi của chúng tôi vào __eax__ebx, vì nó ngắn hơn so với việc di chuyển chúng.

Vì vậy, chúng ta có thể sử dụng __ecx, __edx, __ediđể phù hợp với phần còn lại của tải trọng của chúng tôi. Nhìn vào sự khác biệt giữa địa chỉ thread_cmd.state.__ecxvà cuối của segment_cmd.segnamechúng tôi tính toán rằng chúng tôi cần đặt jmp 0x3a(hoặc EB38) vào hai byte cuối cùng của segname.

Vì vậy, tải trọng của chúng tôi được lắp ráp là 53 50 31C0 89E7 6A08 57 6A01 50 B004cho phần đầu tiên, EB38cho jmp và CD80 6A00 B001 50 CD80cho phần thứ hai.

Và bước cuối cùng - thiết lập __eip. Tệp của chúng tôi được tải tại 0x1000(nhớ vmaddr) và tải trọng bắt đầu ở mức bù 0x24.

Đây là xxdtập tin kết quả:

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 nó với bất cứ thứ gì lên đến 0x1000byte, chmod + x và chạy :)

PS Giới thiệu về x86_64 - Cần có nhị phân 64 bit __PAGEZERO(bất kỳ ánh xạ nào có VM_PROT_NONEtrang bảo vệ ở 0x0). IIRC họ [Apple] đã không yêu cầu nó ở chế độ 32 bit chỉ vì một số phần mềm cũ không có nó và họ sợ phá vỡ nó.


2
Đây là một câu trả lời cực kỳ kỹ lưỡng. Chào mừng đến với trang web! :)
James

1
Tôi đã sử dụng truncate -s 4096 foo(với foo là tệp có thể xuất hiện) để làm cho nó phù hợp với 0x1000byte và nó hoạt động hoàn hảo :)
Martin M.

4

28 byte, được biên soạn trước.

Dưới đây là một kết xuất hex hình thành của nhị phân 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)

Bao gồm hoàn toàn tiêu đề và không cần dữ liệu cũng như cmds. Về bản chất, đây là nhị phân Mach-O nhỏ nhất có thể. Nó có thể không chạy chính xác trên bất kỳ phần cứng có thể hiểu được, nhưng nó phù hợp với đặc điểm kỹ thuật.

Tôi sẽ cung cấp tệp thực tế, nhưng nó hoàn toàn bao gồm các ký tự không thể in được.


Dòng mô tả đầu tiên bắt đầu bằng "một lần nữa". Tôi đoán bạn đã viết chúng theo một thứ tự khác.
Sparr

Đọc từ dưới lên trên, giống như từ trái sang phải. Vâng, tôi đã viết chúng theo thứ tự đó.
ATaco

Điều này không thực sự chạy trong bất kỳ ý nghĩa có ý nghĩa, mặc dù.
DepressionDaniel

Mặc dù về mặt kỹ thuật, nó không chạy theo bất kỳ ý nghĩa nào là khả năng chạy kỹ thuật. Giả sử đặc tả là chính xác, đây là một thư viện không có bất kỳ dữ liệu nào. Hay đơn giản hơn, chỉ là tiêu đề của một thư viện.
ATaco

Làm thế nào điều này có thể được chạy? Đặt nó trong một tệp nhị phân và thực thi nó sẽ đưa ra một lỗi thông báo "Lỗi định dạng Exec"
Martin M.

1

(uint) 0x00000007 là "I386" và "X86" (tên tùy thuộc vào vị trí trong thông số XNU mà bạn đang tìm, nhưng đó là vòm chính xác) (uint) 0x0x01000007 là X86_64

Về mặt lý thuyết, bạn có thể HOẶC bất kỳ giá trị CPU nào với 0x1000000 để biến nó thành phiên bản 64 bit. XNU dường như không phải lúc nào cũng coi chúng là các giá trị rời rạc; ví dụ: ARM 32 và 64 lần lượt là 0x0000000C và 0x0100000C.

Ôi trời, đây là danh sách mà tôi nghĩ ra phải tìm ra vài năm trước, lưu ý rằng hầu hết những OS / X có trước này:

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.