애플의 운영체제에서 실행되는 바이너리는 machO 파일 형식이다.
machO는 fat binary지만 본 문서는 x64를 기준으로 작성한다.

💡 참고
- 문서 내 MachO 구조체 코드는 /usr/include/mach-o/loader.h 에서 확인 가능
- XCode에서 Command+Shift+O 단축기를 활용하여 loader.h 열람 가능
Header의 첫 부분은 MachO의 매직 넘버인 FEEDFACF로 시작한다.
CPU 타입, 파일 타입, LoadCommand에 대한 정보 등을 확인 가능하다.
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
int32_t cputype; /* cpu specifier */
int32_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 */
uint32_t reserved; /* reserved */
};
flags 필드로 부가적인 내용을 확인할 수 있다. 그 중 하나인 MH_PIE 필드는 PIE 적용 여부를 의미한다. 기본적으로 지정되어 있으므로, 실행 시마다 주소가 변경된다.

Load Commands는 Header 뒤에 존재하며, 각 command에 대한 정보와 Segment의 offset 등을 담은 배열로 이루어져 있다.
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};

세그먼트에는 Mach-O 파일의 바이트 범위와, 정적 파일 내 오프셋(fileoff), 애플리케이션이 로드될 때 가상 메모리에 매핑되는 주소(vmaddr) 및 메모리 보호 속성(maxprot, initprot)이 정의되어 있다.
세그먼트는 항상 페이지의 경계에서 시작되며, 정적 파일의 세그먼트 크기가 페이지보다 적을 경우 남은 부분을 0으로 패딩한다. 세그먼트는 런타임에 더 많은 메모리를 필요로 하며, 메모리 내 segment 크기인 vmsize 필드 값은 정적 파일에서 segment 크기인 filesize와 동일하거나 더 크다.
각 세그먼트는 0 개 이상의 섹션을 포함한다.
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
int32_t maxprot; /* maximum VM protection */
int32_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
첫번째 세그먼트. rwx 모두 불가하다. 가상 메모리의 0에 위치하며, 세그먼트 오류를 유발하는 NULL 역참조를 트래핑한다. __PAGEZERO는 데이터가 없으므로 실행파일에서는 크기가 0이지만, 가상 메모리는 1페이지를 부여받는다
실행 코드와 읽기 전용 데이터들이 있다. TEXT 세그먼트가 메모리에 매핑되면, 모든 프로세서에서 접근이 가능하다. PAGEZERO는 데이터가 없으므로, 실 데이터가 포함된 첫 번째 세그먼트인 __TEXT가 페이징 시 Header와 Load Commands를 포함한다.
상수가 아닌 데이터, 즉 초기화 된 변수 등을 포함한다.
쓰기가 가능하므로, 특정 프로세스에서 공유 라이브러리의 __DATA 세그먼트에 쓰기를 수행할 시 copy-on-write가 적용되어 해당 페이지의 복사본을 가진다.
운영체제는 MachO 파일을 실행하기 전에 서명을 검증한다. 앱이 서명되면 Load Command의 가장 마지막에 LC_CODE_SIGNATURE이 생성되며, CodeSignature의 offset을 확인 할 수 있다.
otool -l [] | grep LC_CODE_SIGNATURE -B 1 -A 3

Apple의 opensource, llvm 등에서 CodeSign에 관련된 구조체를 확인 가능하다.

Blobs에 대한 정보를 담고 있는 구조체. Index에서 blob들의 타입과 offset 확인이 가능하다.
서명된 코드에 대한 정보를 담고 있는 Code Signature의 핵심 부분으로, 운영체제가 MachO의 무결성을 검증하는데 사용된다. 전체 앱을 해시하고 개인 키로 해시 값을 암호화한다. 무결성 검사 시 공개키를 사용하여 해시를 복호화 하고, 앱에서 계산한 해시 값과 비교한다.
사용된 알고리즘은 구조체 내 hashType 멤버의 값으로 확인 가능하며, 해시 타입이 여러개일 경우 CodeDirectory도 그에 따라 생성된다. 보통 SHA1과 SHA256이 사용된다.
typedef struct __CodeDirectory {
uint32_t magic; /* magic number (CSMAGIC_CODEDIRECTORY) */
uint32_t length; /* total length of CodeDirectory blob */
uint32_t version; /* compatibility version */
uint32_t flags; /* setup and mode flags */
uint32_t hashOffset; /* offset of hash slot element at index zero */
uint32_t identOffset; /* offset of identifier string */
uint32_t nSpecialSlots; /* number of special hash slots */
uint32_t nCodeSlots; /* number of ordinary (code) hash slots */
uint32_t codeLimit; /* limit to main image signature range */
uint8_t hashSize; /* size of each hash in bytes */
uint8_t hashType; /* type of hash (cdHashType* constants) */
uint8_t platform; /* platform identifier; zero if not platform binary */
uint8_t pageSize; /* log2(page size in bytes); 0 => infinite */
uint32_t spare2; /* unused (must be zero) */
...
/* followed by dynamic content as located by offset fields above */
} CS_CodeDirectory
codesign 명령어로 CodeDirectory에 대한 정보를 확인 가능하다.

codesign에서 -v 옵션을 추가하여 Page별 해시를 출력할 수 있다. 음수 인덱스는 Special Hash로 별도 후술한다.

첫번째 페이지의 오프셋은 hashOffset, 페이지의 개수는 nCodeSlots는 멤버에 저장된다.
아래 명령어를 사용하여 첫번째 페이지의 해시를 추출해보면, Code Slot 0의 값과 동일한걸 확인할 수 있다.
dd if=[appName].app/[appName] of=page1.bin bs=$(pagesize) count=1 skip=0

Code Signature의 다른 Blob들에 대한 무결성을 확인하는데 사용된다.
iOS 15 이상에서는 -7에 필수로 해시값이 있어야 한다.
| Index | Value |
|---|---|
| -1 | Hash of bundle Info.plist |
| -2 | Hash of embedded code signing requirements |
| -3 | Hash of _CodeSignature/CodeResources |
| -4 | App specific hash (usually not used) |
| -5 | Hash of entitlement embedded in the code signature |
| -6 | 최근 cs_blobs.h에서 확인 불가 |
| -7 | 최근 cs_blobs.h에서 확인 불가 |
ios 코드 서명에 대해서
So Macho - A look at Apple executable files
Low Level iOS
Inside Code Signing: Hashes
OS X ABI Mach-O File Format Reference