Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/unblob/handlers/executable/macho.py: 85%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

84 statements  

1import io 

2from enum import Enum, IntEnum 

3from pathlib import Path 

4 

5import attrs 

6 

7from unblob.file_utils import ( 

8 Endian, 

9 File, 

10 FileSystem, 

11 InvalidInputFormat, 

12 StructParser, 

13) 

14from unblob.models import ( 

15 ExtractResult, 

16 HandlerDoc, 

17 HandlerType, 

18 HexString, 

19 Reference, 

20 StructHandler, 

21 ValidChunk, 

22) 

23 

24 

25class MachOMagic(bytes, Enum): 

26 """Mach-O magics as they appear on disk (first 4 bytes). 

27 

28 Fat headers are always big-endian; a thin magic is byte-swapped ("cigam") 

29 when the binary's endianness is big. 

30 """ 

31 

32 FAT = b"\xca\xfe\xba\xbe" # fat / universal binary 

33 THIN_64_LE = b"\xcf\xfa\xed\xfe" # 64-bit little-endian (arm64, x86_64) 

34 THIN_32_LE = b"\xce\xfa\xed\xfe" # 32-bit little-endian (armv7, x86) 

35 THIN_64_BE = b"\xfe\xed\xfa\xcf" # 64-bit big-endian 

36 THIN_32_BE = b"\xfe\xed\xfa\xce" # 32-bit big-endian 

37 

38 

39# Header sizes in bytes 

40_HEADER_SIZE_32 = 28 

41_HEADER_SIZE_64 = 32 # adds a 4-byte reserved field 

42 

43# Maps (cputype, cpusubtype) to a human-readable name for fat binary slice filenames 

44_CPU_ARCH_NAMES: dict[tuple[int, int], str] = { 

45 (0xC, 5): "arm_v4t", 

46 (0xC, 6): "arm_v6", 

47 (0xC, 9): "arm_v7", 

48 (0xC, 11): "arm_v7s", 

49 (0xC, 12): "arm_v7k", 

50 (0x100000C, 0): "arm64", 

51 (0x100000C, 1): "arm64_v8", 

52 (0x100000C, 2): "arm64e", 

53 (0x7, 3): "i386", 

54 (0x1000007, 3): "x86_64", 

55 (0x12, 0): "ppc", 

56 (0x1000012, 0): "ppc64", 

57} 

58 

59# Single source of truth for every Mach-O structure, shared by the handler's struct parser 

60# and the fat binary extractor below. 

61C_DEFINITIONS = r""" 

62 typedef struct macho_header { 

63 uint32 magic; 

64 uint32 cputype; 

65 uint32 cpusubtype; 

66 uint32 filetype; 

67 uint32 ncmds; 

68 uint32 sizeofcmds; 

69 uint32 flags; 

70 } macho_header_t; 

71 

72 typedef struct macho_load_command { 

73 uint32 cmd; 

74 uint32 cmdsize; 

75 } macho_load_command_t; 

76 

77 // Covers both LC_SEGMENT (32-bit) and LC_SEGMENT_64 via a union. Only the fields up to 

78 // filesize are modelled (all we need to compute file extents), so the struct is 56 bytes 

79 // and never reads past the smaller 32-bit segment command. 

80 typedef struct macho_segment_command { 

81 uint32 cmd; 

82 uint32 cmdsize; 

83 char segname[16]; 

84 union { 

85 struct { 

86 uint32 vmaddr; 

87 uint32 vmsize; 

88 uint32 fileoff; 

89 uint32 filesize; 

90 } bits32; 

91 struct { 

92 uint64 vmaddr; 

93 uint64 vmsize; 

94 uint64 fileoff; 

95 uint64 filesize; 

96 } bits64; 

97 } seg; 

98 } macho_segment_command_t; 

99 

100 typedef struct fat_header { 

101 uint32 magic; // 0xcafebabe, always big-endian 

102 uint32 nfat_arch; // number of architecture slices 

103 } fat_header_t; 

104 

105 typedef struct fat_arch { 

106 uint32 cputype; // CPU architecture type 

107 uint32 cpusubtype; // CPU subtype 

108 uint32 offset; // file offset to this architecture's Mach-O slice 

109 uint32 slice_size; // size of this architecture's Mach-O slice 

110 uint32 align; // slice alignment as a power of 2 

111 } fat_arch_t; 

112""" 

113 

114_parser = StructParser(C_DEFINITIONS) 

115 

116 

117def _cpu_arch_name(cputype: int, cpusubtype: int) -> str: 

118 return _CPU_ARCH_NAMES.get( 

119 (cputype, cpusubtype), f"cputype_{cputype:#x}_{cpusubtype:#x}" 

120 ) 

121 

122 

123class LoadCommandType(IntEnum): 

124 SEGMENT = 0x1 

125 SEGMENT_64 = 0x19 

126 

127 

128@attrs.define(repr=False) 

129class MachOChunk(ValidChunk): 

130 """A fat (universal) Mach-O binary whose architecture slices are carved out on extraction.""" 

131 

132 def extract(self, inpath: Path, outdir: Path) -> ExtractResult | None: 

133 fs = FileSystem(outdir) 

134 with File.from_path(inpath) as file: 

135 fat_hdr = _parser.parse("fat_header_t", file, Endian.BIG) 

136 architectures = [ 

137 _parser.parse("fat_arch_t", file, Endian.BIG) 

138 for _ in range(fat_hdr.nfat_arch) 

139 ] 

140 for arch in architectures: 

141 if arch.slice_size == 0: 

142 continue 

143 cpu_name = _cpu_arch_name(arch.cputype, arch.cpusubtype) 

144 fs.carve( 

145 Path(f"arch_{cpu_name}.macho"), file, arch.offset, arch.slice_size 

146 ) 

147 return ExtractResult(reports=fs.problems) 

148 

149 

150class MachOHandler(StructHandler): 

151 NAME = "macho" 

152 

153 EXTRACTOR = None 

154 

155 PATTERNS = [ 

156 # Thin Mach-O: magic (4) + [24] wildcards = full 28-byte common header guaranteed present, 

157 # avoiding false positives from partial matches and removing the need for truncation checks. 

158 HexString("CF FA ED FE [24]"), # 64-bit little-endian (arm64, x86_64) 

159 HexString("CE FA ED FE [24]"), # 32-bit little-endian (armv7, x86) 

160 HexString("FE ED FA CF [24]"), # 64-bit big-endian 

161 HexString("FE ED FA CE [24]"), # 32-bit big-endian 

162 # Fat (universal) binary: 

163 # CA FE BA BE — fat magic (always big-endian) 

164 # 00 00 00 (01-08) — nfat_arch 1-8; limits to <=8 architectures which 

165 # disambiguates from Java class files (major version ≥ 45) 

166 HexString("CA FE BA BE 00 00 00 (01|02|03|04|05|06|07|08)"), 

167 ] 

168 

169 C_DEFINITIONS = C_DEFINITIONS 

170 HEADER_STRUCT = "macho_header_t" 

171 

172 DOC = HandlerDoc( 

173 name="Mach-O", 

174 description="Mach-O (Mach Object) is the native executable and library binary format used by macOS, iOS, tvOS, and watchOS. It encodes load commands describing memory segments, dynamic libraries, and code signing, and supports multiple CPU architectures via fat/universal binaries.", 

175 handler_type=HandlerType.EXECUTABLE, 

176 vendor="Apple", 

177 references=[ 

178 Reference( 

179 title="Mach-O Programming Topics", 

180 url="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/MachOTopics/0-Introduction/introduction.html", 

181 ), 

182 ], 

183 limitations=[], 

184 ) 

185 

186 def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None: 

187 magic = file[start_offset : start_offset + 4] 

188 match magic: 

189 case MachOMagic.FAT: 

190 return self._calculate_fat_chunk(file, start_offset) 

191 case MachOMagic.THIN_64_LE: 

192 endian, is_64bit = Endian.LITTLE, True 

193 case MachOMagic.THIN_32_LE: 

194 endian, is_64bit = Endian.LITTLE, False 

195 case MachOMagic.THIN_64_BE: 

196 endian, is_64bit = Endian.BIG, True 

197 case MachOMagic.THIN_32_BE: 

198 endian, is_64bit = Endian.BIG, False 

199 case _: 

200 raise InvalidInputFormat(f"Unknown Mach-O magic: {magic.hex()}") 

201 return self._calculate_thin_chunk(file, start_offset, endian, is_64bit=is_64bit) 

202 

203 def _calculate_thin_chunk( 

204 self, file: File, start_offset: int, endian: Endian, *, is_64bit: bool 

205 ) -> ValidChunk: 

206 header = self.parse_header(file, endian) 

207 

208 # 64-bit headers have an extra 4-byte reserved field after flags 

209 hdr_size = _HEADER_SIZE_64 if is_64bit else _HEADER_SIZE_32 

210 file.seek(start_offset + hdr_size, io.SEEK_SET) 

211 

212 end_offset = start_offset + hdr_size + header.sizeofcmds 

213 

214 for _ in range(header.ncmds): 

215 lc_start = file.tell() 

216 lc = self._struct_parser.parse("macho_load_command_t", file, endian) 

217 

218 if lc.cmdsize < 8: 

219 raise InvalidInputFormat( 

220 f"Mach-O load command size too small: {lc.cmdsize}" 

221 ) 

222 

223 if lc.cmd in (LoadCommandType.SEGMENT, LoadCommandType.SEGMENT_64): 

224 file.seek(lc_start, io.SEEK_SET) 

225 command = self._struct_parser.parse( 

226 "macho_segment_command_t", file, endian 

227 ) 

228 seg = ( 

229 command.seg.bits64 

230 if lc.cmd == LoadCommandType.SEGMENT_64 

231 else command.seg.bits32 

232 ) 

233 if seg.filesize > 0: 

234 end_offset = max( 

235 end_offset, start_offset + seg.fileoff + seg.filesize 

236 ) 

237 

238 file.seek(lc_start + lc.cmdsize, io.SEEK_SET) 

239 

240 # Thin Mach-O binaries are leaf files: the handler has no extractor, so the carved 

241 # chunk is preserved as-is (see Handler.extract). 

242 return ValidChunk(start_offset=start_offset, end_offset=end_offset) 

243 

244 def _calculate_fat_chunk(self, file: File, start_offset: int) -> MachOChunk: 

245 # Fat headers are always big-endian regardless of the contained architectures 

246 fat_hdr = self._struct_parser.parse("fat_header_t", file, Endian.BIG) 

247 

248 end_offset = start_offset 

249 for _ in range(fat_hdr.nfat_arch): 

250 arch = self._struct_parser.parse("fat_arch_t", file, Endian.BIG) 

251 if arch.slice_size > 0: 

252 end_offset = max( 

253 end_offset, start_offset + arch.offset + arch.slice_size 

254 ) 

255 

256 return MachOChunk(start_offset=start_offset, end_offset=end_offset)