1"""Checks the validity of MachO binary signatures 2 3MachO binaries sometimes include a LC_CODE_SIGNATURE load command 4and corresponding section in the __LINKEDIT segment that together 5work to "sign" the binary. This script is used to check the validity 6of this signature. 7 8Usage: 9 ./code-signature-check.py my_binary 800 300 0 800 10 11Arguments: 12 binary - The MachO binary to be tested 13 offset - The offset from the start of the binary to where the code signature section begins 14 size - The size of the code signature section in the binary 15 code_offset - The point in the binary to begin hashing 16 code_size - The length starting from code_offset to hash 17""" 18 19import argparse 20import collections 21import hashlib 22import itertools 23import struct 24import sys 25import typing 26 27 28class CodeDirectoryVersion: 29 SUPPORTSSCATTER = 0x20100 30 SUPPORTSTEAMID = 0x20200 31 SUPPORTSCODELIMIT64 = 0x20300 32 SUPPORTSEXECSEG = 0x20400 33 34 35class CodeDirectory: 36 @staticmethod 37 def make( 38 buf: memoryview, 39 ) -> typing.Union[ 40 "CodeDirectoryBase", 41 "CodeDirectoryV20100", 42 "CodeDirectoryV20200", 43 "CodeDirectoryV20300", 44 "CodeDirectoryV20400", 45 ]: 46 _magic, _length, version = struct.unpack_from(">III", buf, 0) 47 subtype = { 48 CodeDirectoryVersion.SUPPORTSSCATTER: CodeDirectoryV20100, 49 CodeDirectoryVersion.SUPPORTSTEAMID: CodeDirectoryV20200, 50 CodeDirectoryVersion.SUPPORTSCODELIMIT64: CodeDirectoryV20300, 51 CodeDirectoryVersion.SUPPORTSEXECSEG: CodeDirectoryV20400, 52 }.get(version, CodeDirectoryBase) 53 54 return subtype._make(struct.unpack_from(subtype._format(), buf, 0)) 55 56 57class CodeDirectoryBase(typing.NamedTuple): 58 magic: int 59 length: int 60 version: int 61 flags: int 62 hashOffset: int 63 identOffset: int 64 nSpecialSlots: int 65 nCodeSlots: int 66 codeLimit: int 67 hashSize: int 68 hashType: int 69 platform: int 70 pageSize: int 71 spare2: int 72 73 @staticmethod 74 def _format() -> str: 75 return ">IIIIIIIIIBBBBI" 76 77 78class CodeDirectoryV20100(typing.NamedTuple): 79 magic: int 80 length: int 81 version: int 82 flags: int 83 hashOffset: int 84 identOffset: int 85 nSpecialSlots: int 86 nCodeSlots: int 87 codeLimit: int 88 hashSize: int 89 hashType: int 90 platform: int 91 pageSize: int 92 spare2: int 93 94 scatterOffset: int 95 96 @staticmethod 97 def _format() -> str: 98 return CodeDirectoryBase._format() + "I" 99 100 101class CodeDirectoryV20200(typing.NamedTuple): 102 magic: int 103 length: int 104 version: int 105 flags: int 106 hashOffset: int 107 identOffset: int 108 nSpecialSlots: int 109 nCodeSlots: int 110 codeLimit: int 111 hashSize: int 112 hashType: int 113 platform: int 114 pageSize: int 115 spare2: int 116 117 scatterOffset: int 118 119 teamOffset: int 120 121 @staticmethod 122 def _format() -> str: 123 return CodeDirectoryV20100._format() + "I" 124 125 126class CodeDirectoryV20300(typing.NamedTuple): 127 magic: int 128 length: int 129 version: int 130 flags: int 131 hashOffset: int 132 identOffset: int 133 nSpecialSlots: int 134 nCodeSlots: int 135 codeLimit: int 136 hashSize: int 137 hashType: int 138 platform: int 139 pageSize: int 140 spare2: int 141 142 scatterOffset: int 143 144 teamOffset: int 145 146 spare3: int 147 codeLimit64: int 148 149 @staticmethod 150 def _format() -> str: 151 return CodeDirectoryV20200._format() + "IQ" 152 153 154class CodeDirectoryV20400(typing.NamedTuple): 155 magic: int 156 length: int 157 version: int 158 flags: int 159 hashOffset: int 160 identOffset: int 161 nSpecialSlots: int 162 nCodeSlots: int 163 codeLimit: int 164 hashSize: int 165 hashType: int 166 platform: int 167 pageSize: int 168 spare2: int 169 170 scatterOffset: int 171 172 teamOffset: int 173 174 spare3: int 175 codeLimit64: int 176 177 execSegBase: int 178 execSegLimit: int 179 execSegFlags: int 180 181 @staticmethod 182 def _format() -> str: 183 return CodeDirectoryV20300._format() + "QQQ" 184 185 186class CodeDirectoryBlobIndex(typing.NamedTuple): 187 type_: int 188 offset: int 189 190 @staticmethod 191 def make(buf: memoryview) -> "CodeDirectoryBlobIndex": 192 return CodeDirectoryBlobIndex._make( 193 struct.unpack_from(CodeDirectoryBlobIndex.__format(), buf, 0) 194 ) 195 196 @staticmethod 197 def bytesize() -> int: 198 return struct.calcsize(CodeDirectoryBlobIndex.__format()) 199 200 @staticmethod 201 def __format() -> str: 202 return ">II" 203 204 205class CodeDirectorySuperBlob(typing.NamedTuple): 206 magic: int 207 length: int 208 count: int 209 blob_indices: typing.List[CodeDirectoryBlobIndex] 210 211 @staticmethod 212 def make(buf: memoryview) -> "CodeDirectorySuperBlob": 213 super_blob_layout = ">III" 214 super_blob = struct.unpack_from(super_blob_layout, buf, 0) 215 216 offset = struct.calcsize(super_blob_layout) 217 blob_indices = [] 218 for idx in range(super_blob[2]): 219 blob_indices.append(CodeDirectoryBlobIndex.make(buf[offset:])) 220 offset += CodeDirectoryBlobIndex.bytesize() 221 222 return CodeDirectorySuperBlob(*super_blob, blob_indices) 223 224 225def unpack_null_terminated_string(buf: memoryview) -> str: 226 b = bytes(itertools.takewhile(lambda b: b != 0, buf)) 227 return b.decode() 228 229 230def main(): 231 parser = argparse.ArgumentParser() 232 parser.add_argument( 233 "binary", type=argparse.FileType("rb"), help="The file to analyze" 234 ) 235 parser.add_argument( 236 "offset", type=int, help="Offset to start of Code Directory data" 237 ) 238 parser.add_argument("size", type=int, help="Size of Code Directory data") 239 parser.add_argument( 240 "code_offset", type=int, help="Offset to start of code pages to hash" 241 ) 242 parser.add_argument("code_size", type=int, help="Size of the code pages to hash") 243 244 args = parser.parse_args() 245 246 args.binary.seek(args.offset) 247 super_blob_bytes = args.binary.read(args.size) 248 super_blob_mem = memoryview(super_blob_bytes) 249 250 super_blob = CodeDirectorySuperBlob.make(super_blob_mem) 251 print(super_blob) 252 253 for blob_index in super_blob.blob_indices: 254 code_directory_offset = blob_index.offset 255 code_directory = CodeDirectory.make(super_blob_mem[code_directory_offset:]) 256 print(code_directory) 257 258 ident_offset = code_directory_offset + code_directory.identOffset 259 print( 260 "Code Directory ID: " 261 + unpack_null_terminated_string(super_blob_mem[ident_offset:]) 262 ) 263 264 code_offset = args.code_offset 265 code_end = code_offset + args.code_size 266 page_size = 1 << code_directory.pageSize 267 args.binary.seek(code_offset) 268 269 hashes_offset = code_directory_offset + code_directory.hashOffset 270 for idx in range(code_directory.nCodeSlots): 271 hash_bytes = bytes( 272 super_blob_mem[hashes_offset : hashes_offset + code_directory.hashSize] 273 ) 274 hashes_offset += code_directory.hashSize 275 276 hasher = hashlib.sha256() 277 read_size = min(page_size, code_end - code_offset) 278 hasher.update(args.binary.read(read_size)) 279 calculated_hash_bytes = hasher.digest() 280 code_offset += read_size 281 282 print("%s <> %s" % (hash_bytes.hex(), calculated_hash_bytes.hex())) 283 284 if hash_bytes != calculated_hash_bytes: 285 sys.exit(-1) 286 287 288if __name__ == "__main__": 289 main() 290