Mach-O nhỏ nhất có thể chạy được ít nhất là 0x1000
byte. 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à 0xA4
byte.
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à 0x1C
byte.
magic
có được MH_MAGIC
.
Tôi sẽ sử dụng CPU_TYPE_X86
vì nó là một x86_32
thực thi.
filtetype
phải được MH_EXECUTE
thực thi ncmds
và sizeofcmds
phụ thuộc vào các lệnh và phải hợp lệ.
flags
khô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 */
};
cmd
phải có LC_SEGMENT
, và cmdsize
phải có sizeof(struct segment_command) => 0x38
.
segname
nội dung không quan trọng, và chúng tôi sẽ sử dụng nó sau.
vmaddr
phải là địa chỉ hợp lệ (tôi sẽ sử dụng 0x1000
), vmsize
phải hợp lệ & nhiều PAGE_SIZE
, fileoff
phải 0
, filesize
phải nhỏ hơn kích thước tệp, nhưng lớn hơn mach_header
ít nhất ( sizeof(header) + header.sizeofcmds
là những gì tôi đã sử dụng).
maxprot
và initprot
phải được VM_PROT_READ | VM_PROT_EXECUTE
. maxport
thường cũng có VM_PROT_WRITE
.
nsects
là 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 flags
thà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_command
và thread_command
.
entry_point_command
khô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_command
và dysymtab_command
, dylinker_command
và dyld_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_UNIXTHREAD
vì 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 */
/* ... */
};
cmd
sẽ là LC_UNIXTHREAD
, cmdsize
sẽ là 0x50
(xem bên dưới).
flavour
là x86_THREAD_STATE32
, và đếm là x86_THREAD_STATE32_COUNT
( 0x10
).
Bây giờ thread_state
. Chúng tôi cần 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;
};
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 0xA4
byte.
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 Frand
và exit(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
.
segname
có 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 14
byte 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
và __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.__ecx
và cuối của segment_cmd.segname
chú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 B004
cho phần đầu tiên, EB38
cho jmp và CD80 6A00 B001 50 CD80
cho 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à xxd
tậ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 0x1000
byte, 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_NONE
trang 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ó.