Mach-O runnable terkecil harus sekurang-kurangnya 0x1000
byte. 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 0xA4
byte.
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 0x1C
byte.
magic
harus MH_MAGIC
.
Saya akan menggunakan CPU_TYPE_X86
karena ini x86_32
executable.
filtetype
harus MH_EXECUTE
untuk dieksekusi, ncmds
dan sizeofcmds
bergantung pada perintah, dan harus valid.
flags
tidak 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 */
};
cmd
harus LC_SEGMENT
, dan cmdsize
harus sizeof(struct segment_command) => 0x38
.
segname
konten tidak masalah, dan kami akan menggunakannya nanti.
vmaddr
harus alamat yang valid (saya akan menggunakan 0x1000
), vmsize
harus valid & kelipatan PAGE_SIZE
, fileoff
harus 0
, filesize
harus lebih kecil dari ukuran file, tetapi lebih besar dari mach_header
setidaknya ( sizeof(header) + header.sizeofcmds
adalah apa yang saya gunakan).
maxprot
dan initprot
harus VM_PROT_READ | VM_PROT_EXECUTE
. maxport
biasanya juga sudah VM_PROT_WRITE
.
nsects
adalah 0, karena kita tidak benar-benar membutuhkan bagian apa pun dan mereka akan menambahkan hingga ukuran. Saya telah menetapkan flags
ke 0.
Sekarang, kita perlu menjalankan beberapa kode. Ada dua perintah pemuatan untuk itu: entry_point_command
dan thread_command
.
entry_point_command
tidak 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_command
dan dysymtab_command
, dylinker_command
dan dyld_info_command
. Berlebihan untuk file "terkecil".
Jadi, kami akan gunakan thread_command
, khususnya LC_UNIXTHREAD
karena 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 */
/* ... */
};
cmd
akan menjadi LC_UNIXTHREAD
, cmdsize
akan 0x50
(lihat di bawah).
flavour
adalah x86_THREAD_STATE32
, dan hitung adalah x86_THREAD_STATE32_COUNT
( 0x10
).
Sekarang thread_state
. Kami membutuhkan x86_thread_state32_t
alias _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 0xA4
byte.
Sekarang, waktu untuk menyusun payload.
Katakanlah kita ingin mencetak Hi Frand
dan 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
.
segname
bisa apa saja, ingat? Jadi kita bisa menaruh muatan kita di dalamnya. Namun, ini hanya 16 byte, dan kami perlu lebih banyak.
Jadi, pada 14
byte 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 __eax
dan __ebx
, karena itu lebih pendek daripada memindahkannya.
Jadi, kita dapat menggunakan __ecx
, __edx
, __edi
agar sesuai dengan sisa payload kami. Melihat perbedaan antara alamat thread_cmd.state.__ecx
dan akhir segment_cmd.segname
kita 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 B004
untuk bagian pertama, EB38
untuk jmp, dan CD80 6A00 B001 50 CD80
untuk bagian kedua.
Dan langkah terakhir - pengaturan __eip
. File kami dimuat di 0x1000
(ingat vmaddr
), dan payload dimulai pada offset 0x24
.
Inilah xxd
file 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 0x1000
byte, chmod + x dan jalankan :)
PS Tentang x86_64 - binari 64bit harus memiliki __PAGEZERO
(setiap pemetaan dengan VM_PROT_NONE
halaman 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.