diff --git a/helpers/scanner/parsers/macho-node/constants.js b/helpers/scanner/parsers/macho-node/constants.js new file mode 100644 index 0000000..74880b5 --- /dev/null +++ b/helpers/scanner/parsers/macho-node/constants.js @@ -0,0 +1,308 @@ +// Adapted from https://github.com/indutny/macho/blob/master/lib/macho/constants.js + +export const constants = {} + +constants.cpuArch = { + mask: 0xff000000, + abi64: 0x01000000, + abi32: 0x02000000 +}; + +constants.cpuType = { + 0x01: 'vax', + 0x06: 'mc680x0', + 0x07: 'i386', + 0x01000007: 'x86_64', + 0x0a: 'mc98000', + 0x0b: 'hppa', + 0x0c: 'arm', + 0x0100000c: 'arm64', + 0x0200000c: 'arm64_32', + 0x0d: 'mc88000', + 0x0e: 'sparc', + 0x0f: 'i860', + 0x10: 'alpha', + 0x12: 'powerpc', + 0x01000012: 'powerpc64' +}; + +constants.endian = { + 0xffffffff: 'multiple', + 0: 'le', + 1: 'be' +}; + +constants.cpuSubType = { + mask: 0x00ffffff, + vax: { + 0: 'all', + 1: '780', + 2: '785', + 3: '750', + 4: '730', + 5: 'I', + 6: 'II', + 7: '8200', + 8: '8500', + 9: '8600', + 10: '8650', + 11: '8800', + 12: 'III' + }, + mc680x0: { + 1: 'all', + 2: '40', + 3: '30_only' + }, + i386: {}, + x86_64: { + 3: 'all', + 4: 'arch1' + }, + mips: { + 0: 'all', + 1: 'r2300', + 2: 'r2600', + 3: 'r2800', + 4: 'r2000a', + 5: 'r2000', + 6: 'r3000a', + 7: 'r3000' + }, + mc98000: { + 0: 'all', + 1: 'mc98601' + }, + hppa: { + 0: 'all', + 1: '7100lc' + }, + mc88000: { + 0: 'all', + 1: 'mc88100', + 2: 'mc88110' + }, + sparc: { + 0: 'all' + }, + i860: { + 0: 'all', + 1: '860' + }, + powerpc: { + 0: 'all', + 1: '601', + 2: '602', + 3: '603', + 4: '603e', + 5: '603ev', + 6: '604', + 7: '604e', + 8: '620', + 9: '750', + 10: '7400', + 11: '7450', + 100: '970' + }, + arm: { + 0: 'all', + 5: 'v4t', + 6: 'v6', + 7: 'v5tej', + 8: 'xscale', + 9: 'v7', + 10: 'v7f', + 11: 'v7s', + 12: 'v7k', + 14: 'v6m', + 15: 'v7m', + 16: 'v7em' + }, + arm64: { + 0: 'all', + 1: 'v8', + 2: 'e' + }, + arm64_32: { + 1: 'all' + } +}; + +function cpuSubtypeIntel(a, b, name) { + constants.cpuSubType.i386[a + (b << 4)] = name; +} + +[ + [3, 0, 'all'], + [4, 0, '486'], + [4, 8, '486sx'], + [5, 0, '586'], + [6, 1, 'pentpro'], + [6, 3, 'pentII_m3'], + [6, 5, 'pentII_m5'], + [7, 6, 'celeron'], + [7, 7, 'celeron_mobile'], + [8, 0, 'pentium_3'], + [8, 1, 'pentium_3_m'], + [8, 2, 'pentium_3_xeon'], + [9, 0, 'pentium_m'], + [10, 0, 'pentium_4'], + [10, 1, 'pentium_4_m'], + [11, 0, 'itanium'], + [11, 1, 'itanium_2'], + [12, 0, 'xeon'], + [12, 1, 'xeon_mp'] +].forEach(function(item) { + cpuSubtypeIntel(item[0], item[1], item[2]); +}); + +constants.fileType = { + 1: 'object', + 2: 'execute', + 3: 'fvmlib', + 4: 'core', + 5: 'preload', + 6: 'dylib', + 7: 'dylinker', + 8: 'bundle', + 9: 'dylib_stub', + 10: 'dsym', + 11: 'kext' +}; + +constants.flags = { + 0x1: 'noundefs', + 0x2: 'incrlink', + 0x4: 'dyldlink', + 0x8: 'bindatload', + 0x10: 'prebound', + 0x20: 'split_segs', + 0x40: 'lazy_init', + 0x80: 'twolevel', + 0x100: 'force_flat', + 0x200: 'nomultidefs', + 0x400: 'nofixprebinding', + 0x800: 'prebindable', + 0x1000: 'allmodsbound', + 0x2000: 'subsections_via_symbols', + 0x4000: 'canonical', + 0x8000: 'weak_defines', + 0x10000: 'binds_to_weak', + 0x20000: 'allow_stack_execution', + 0x40000: 'root_safe', + 0x80000: 'setuid_safe', + 0x100000: 'reexported_dylibs', + 0x200000: 'pie', + 0x400000: 'dead_strippable_dylib', + 0x800000: 'has_tlv_descriptors', + 0x1000000: 'no_heap_execution' +}; + +constants.cmdType = { + 0x80000000: 'req_dyld', + 0x1: 'segment', + 0x2: 'symtab', + 0x3: 'symseg', + 0x4: 'thread', + 0x5: 'unixthread', + 0x6: 'loadfvmlib', + 0x7: 'idfvmlib', + 0x8: 'ident', + 0x9: 'fmvfile', + 0xa: 'prepage', + 0xb: 'dysymtab', + 0xc: 'load_dylib', + 0xd: 'id_dylib', + 0xe: 'load_dylinker', + 0xf: 'id_dylinker', + 0x10: 'prebound_dylib', + 0x11: 'routines', + 0x12: 'sub_framework', + 0x13: 'sub_umbrella', + 0x14: 'sub_client', + 0x15: 'sub_library', + 0x16: 'twolevel_hints', + 0x17: 'prebind_cksum', + + 0x80000018: 'load_weak_dylib', + 0x19: 'segment_64', + 0x1a: 'routines_64', + 0x1b: 'uuid', + 0x8000001c: 'rpath', + 0x1d: 'code_signature', + 0x1e: 'segment_split_info', + 0x8000001f: 'reexport_dylib', + 0x20: 'lazy_load_dylib', + 0x21: 'encryption_info', + 0x80000022: 'dyld_info', + 0x80000023: 'dyld_info_only', + 0x24: 'version_min_macosx', + 0x25: 'version_min_iphoneos', + 0x26: 'function_starts', + 0x27: 'dyld_environment', + 0x80000028: 'main', + 0x29: 'data_in_code', + 0x2a: 'source_version', + 0x2b: 'dylib_code_sign_drs', + 0x2c: 'encryption_info_64', + 0x2d: 'linker_option' +}; + +constants.prot = { + none: 0, + read: 1, + write: 2, + execute: 4 +}; + +constants.segFlag = { + 1: 'highvm', + 2: 'fvmlib', + 4: 'noreloc', + 8: 'protected_version_1' +}; + +constants.segTypeMask = 0xff; +constants.segType = { + 0: 'regular', + 1: 'zerofill', + 2: 'cstring_literals', + 3: '4byte_literals', + 4: '8byte_literals', + 5: 'literal_pointers', + 6: 'non_lazy_symbol_pointers', + 7: 'lazy_symbol_pointers', + 8: 'symbol_stubs', + 9: 'mod_init_func_pointers', + 0xa: 'mod_term_func_pointers', + 0xb: 'coalesced', + 0xc: 'gb_zerofill', + 0xd: 'interposing', + 0xe: '16byte_literals', + 0xf: 'dtrace_dof', + 0x10: 'lazy_dylib_symbol_pointers', + 0x11: 'thread_local_regular', + 0x12: 'thread_local_zerofill', + 0x13: 'thread_local_variables', + 0x14: 'thread_local_variable_pointers', + 0x15: 'thread_local_init_function_pointers' +}; + +constants.segAttrUsrMask = 0xff000000; +constants.segAttrUsr = { + '-2147483648': 'pure_instructions', + 0x40000000: 'no_toc', + 0x20000000: 'strip_static_syms', + 0x10000000: 'no_dead_strip', + 0x08000000: 'live_support', + 0x04000000: 'self_modifying_code', + 0x02000000: 'debug' +}; + +constants.segAttrSysMask = 0x00ffff00; +constants.segAttrSys = { + 0x400: 'some_instructions', + 0x200: 'ext_reloc', + 0x100: 'loc_reloc' +}; + diff --git a/helpers/scanner/parsers/macho-node/parser.js b/helpers/scanner/parsers/macho-node/parser.js new file mode 100644 index 0000000..0bd7903 --- /dev/null +++ b/helpers/scanner/parsers/macho-node/parser.js @@ -0,0 +1,475 @@ +import Reader from 'endian-reader' + +import { constants } from './constants' + +export class Parser extends Reader { + constructor () { + super() + } + + execute (buf) { + var hdr = this.parseHead(buf); + if (!hdr) + throw new Error('File not in a mach-o format'); + + hdr.cmds = this.parseCommands(hdr, hdr.body, buf); + delete hdr.body; + + return hdr; + } + + mapFlags (value, map) { + var res = {}; + + for (var bit = 1; + (value < 0 || bit <= value) && bit !== 0; bit <<= 1) + if (value & bit) + res[map[bit]] = true; + + return res; + } + + parseHead(buf) { + if (buf.length < 7 * 4) + return false; + + var magic = buf.readUInt32LE(0); + var bits; + if (magic === 0xfeedface || magic === 0xcefaedfe) + bits = 32; + else if (magic === 0xfeedfacf || magic == 0xcffaedfe) + bits = 64; + else + return false; + + if (magic & 0xff == 0xfe) + this.setEndian('be'); + else + this.setEndian('le'); + + if (bits === 64 && buf.length < 8 * 4) + return false; + + var cputype = constants.cpuType[this.readInt32(buf, 4)]; + var cpusubtype = this.readInt32(buf, 8); + var filetype = this.readUInt32(buf, 12); + var ncmds = this.readUInt32(buf, 16); + var sizeofcmds = this.readUInt32(buf, 20); + var flags = this.readUInt32(buf, 24); + + // Get endian + var endian; + if ((cpusubtype & constants.endian.multiple) === constants.endian.multiple) + endian = 'multiple'; + else if (cpusubtype & constants.endian.be) + endian = 'be'; + else + endian = 'le'; + + cpusubtype &= constants.cpuSubType.mask; + + // Get subtype + var subtype; + if (endian === 'multiple') + subtype = 'all'; + else if (cpusubtype === 0) + subtype = 'none'; + else + subtype = constants.cpuSubType[cputype][cpusubtype]; + + // Stringify flags + var flagMap = this.mapFlags(flags, constants.flags); + + return { + bits: bits, + magic: magic, + cpu: { + type: cputype, + subtype: subtype, + endian: endian + }, + filetype: constants.fileType[filetype], + ncmds: ncmds, + sizeofcmds: sizeofcmds, + flags: flagMap, + + cmds: null, + hsize: bits === 32 ? 28 : 32, + body: bits === 32 ? buf.slice(28) : buf.slice(32) + }; + } + + parseCommands(hdr, buf, file) { + var cmds = []; + + var align; + if (hdr.bits === 32) + align = 4; + else + align = 8; + + for (var offset = 0, i = 0; offset + 8 < buf.length, i < hdr.ncmds; i++) { + var type = constants.cmdType[this.readUInt32(buf, offset)]; + var size = this.readUInt32(buf, offset + 4) - 8; + + var fileoff = offset + hdr.hsize; + offset += 8; + if (offset + size > buf.length) + throw new Error('Command body OOB'); + + var body = buf.slice(offset, offset + size); + offset += size; + if (offset & align) + offset += align - (offset & align); + + var cmd = this.parseCommand(type, body, file); + cmd.fileoff = fileoff; + cmds.push(cmd); + } + + return cmds; + } + + parseCStr(buf) { + for (var i = 0; i < buf.length; i++) + if (buf[i] === 0) + break; + return buf.slice(0, i).toString(); + } + + parseLCStr (buf, off) { + if (off + 4 > buf.length) + throw new Error('lc_str OOB'); + + var offset = this.readUInt32(buf, off) - 8; + if (offset > buf.length) + throw new Error('lc_str offset OOB'); + + return this.parseCStr(buf.slice(offset)); + } + + parseCommand(type, buf, file) { + if (type === 'segment') + return this.parseSegmentCmd(type, buf, file); + else if (type === 'segment_64') + return this.parseSegmentCmd(type, buf, file); + else if (type === 'symtab') + return this.parseSymtab(type, buf); + else if (type === 'symseg') + return this.parseSymseg(type, buf); + else if (type === 'encryption_info') + return this.parseEncryptionInfo(type, buf); + else if (type === 'encryption_info_64') + return this.parseEncryptionInfo64(type, buf); + else if (type === 'rpath') + return this.parseRpath(type, buf); + else if (type === 'dysymtab') + return this.parseDysymtab(type, buf); + else if (type === 'load_dylib' || type === 'id_dylib') + return this.parseLoadDylib(type, buf); + else if (type === 'load_weak_dylib') + return this.parseLoadDylib(type, buf); + else if (type === 'load_dylinker' || type === 'id_dylinker') + return this.parseLoadDylinker(type, buf); + else if (type === 'version_min_macosx' || type === 'version_min_iphoneos') + return this.parseVersionMin(type, buf); + else if (type === 'code_signature' || type === 'segment_split_info') + return this.parseLinkEdit(type, buf); + else if (type === 'function_starts') + return this.parseFunctionStarts(type, buf, file); + else if (type === 'data_in_code') + return this.parseLinkEdit(type, buf); + else if (type === 'dylib_code_sign_drs') + return this.parseLinkEdit(type, buf); + else if (type === 'main') + return this.parseMain(type, buf); + else + return { + type: type, + data: buf + }; + } + + parseSegmentCmd (type, buf, file) { + var total = type === 'segment' ? 48 : 64; + if (buf.length < total) + throw new Error('Segment command OOB'); + + var name = this.parseCStr(buf.slice(0, 16)); + + if (type === 'segment') { + var vmaddr = this.readUInt32(buf, 16); + var vmsize = this.readUInt32(buf, 20); + var fileoff = this.readUInt32(buf, 24); + var filesize = this.readUInt32(buf, 28); + var maxprot = this.readUInt32(buf, 32); + var initprot = this.readUInt32(buf, 36); + var nsects = this.readUInt32(buf, 40); + var flags = this.readUInt32(buf, 44); + } else { + var vmaddr = this.readUInt64(buf, 16); + var vmsize = this.readUInt64(buf, 24); + var fileoff = this.readUInt64(buf, 32); + var filesize = this.readUInt64(buf, 40); + var maxprot = this.readUInt32(buf, 48); + var initprot = this.readUInt32(buf, 52); + var nsects = this.readUInt32(buf, 56); + var flags = this.readUInt32(buf, 60); + } + + function prot(p) { + var res = { + read: false, + write: false, + exec: false + }; + if (p !== constants.prot.none) { + res.read = (p & constants.prot.read) !== 0; + res.write = (p & constants.prot.write) !== 0; + res.exec = (p & constants.prot.execute) !== 0; + } + return res; + } + + var sectSize = type === 'segment' ? 32 + 9 * 4 : 32 + 8 * 4 + 2 * 8; + var sections = []; + for (var i = 0, off = total; i < nsects; i++, off += sectSize) { + if (off + sectSize > buf.length) + throw new Error('Segment OOB'); + + var sectname = this.parseCStr(buf.slice(off, off + 16)); + var segname = this.parseCStr(buf.slice(off + 16, off + 32)); + + if (type === 'segment') { + var addr = this.readUInt32(buf, off + 32); + var size = this.readUInt32(buf, off + 36); + var offset = this.readUInt32(buf, off + 40); + var align = this.readUInt32(buf, off + 44); + var reloff = this.readUInt32(buf, off + 48); + var nreloc = this.readUInt32(buf, off + 52); + var flags = this.readUInt32(buf, off + 56); + } else { + var addr = this.readUInt64(buf, off + 32); + var size = this.readUInt64(buf, off + 40); + var offset = this.readUInt32(buf, off + 48); + var align = this.readUInt32(buf, off + 52); + var reloff = this.readUInt32(buf, off + 56); + var nreloc = this.readUInt32(buf, off + 60); + var flags = this.readUInt32(buf, off + 64); + } + + sections.push({ + sectname: sectname, + segname: segname, + addr: addr, + size: size, + offset: offset, + align: align, + reloff: reloff, + nreloc: nreloc, + type: constants.segType[flags & constants.segTypeMask], + attributes: { + usr: this.mapFlags(flags & constants.segAttrUsrMask, + constants.segAttrUsr), + sys: this.mapFlags(flags & constants.segAttrSysMask, + constants.segAttrSys) + }, + data: file.slice(offset, offset + size) + }); + } + + return { + type: type, + name: name, + vmaddr: vmaddr, + vmsize: vmsize, + fileoff: fileoff, + filesize: filesize, + maxprot: prot(maxprot), + initprot: prot(initprot), + nsects: nsects, + flags: this.mapFlags(flags, constants.segFlag), + sections: sections + }; + } + + + parseSymtab (type, buf) { + if (buf.length !== 16) + throw new Error('symtab OOB'); + + return { + type: type, + symoff: this.readUInt32(buf, 0), + nsyms: this.readUInt32(buf, 4), + stroff: this.readUInt32(buf, 8), + strsize: this.readUInt32(buf, 12) + }; + } + + parseSymseg(type, buf) { + if (buf.length !== 8) + throw new Error('symseg OOB'); + + return { + type: type, + offset: this.readUInt32(buf, 0), + size: this.readUInt32(buf, 4) + }; + } + + parseEncryptionInfo (type, buf) { + if (buf.length !== 12) + throw new Error('encryptinfo OOB'); + + return { + type: type, + offset: this.readUInt32(buf, 0), + size: this.readUInt32(buf, 4), + id: this.readUInt32(buf, 8), + }; + } + + parseEncryptionInfo64 (type, buf) { + if (buf.length !== 16) + throw new Error('encryptinfo64 OOB'); + + return this.parseEncryptionInfo(type, buf.slice(0, 12)); + }; + + parseDysymtab (type, buf) { + if (buf.length !== 72) + throw new Error('dysymtab OOB'); + + return { + type: type, + ilocalsym: this.readUInt32(buf, 0), + nlocalsym: this.readUInt32(buf, 4), + iextdefsym: this.readUInt32(buf, 8), + nextdefsym: this.readUInt32(buf, 12), + iundefsym: this.readUInt32(buf, 16), + nundefsym: this.readUInt32(buf, 20), + tocoff: this.readUInt32(buf, 24), + ntoc: this.readUInt32(buf, 28), + modtaboff: this.readUInt32(buf, 32), + nmodtab: this.readUInt32(buf, 36), + extrefsymoff: this.readUInt32(buf, 40), + nextrefsyms: this.readUInt32(buf, 44), + indirectsymoff: this.readUInt32(buf, 48), + nindirectsyms: this.readUInt32(buf, 52), + extreloff: this.readUInt32(buf, 56), + nextrel: this.readUInt32(buf, 60), + locreloff: this.readUInt32(buf, 64), + nlocrel: this.readUInt32(buf, 68) + }; + } + + parseLoadDylinker (type, buf) { + return { + type: type, + cmd: this.parseLCStr(buf, 0) + }; + } + + parseRpath (type, buf) { + if (buf.length < 8) + throw new Error('lc_rpath OOB'); + + return { + type: type, + name: this.parseLCStr(buf, 0), + }; + } + + parseLoadDylib (type, buf) { + if (buf.length < 16) + throw new Error('load_dylib OOB'); + + return { + type: type, + name: this.parseLCStr(buf, 0), + timestamp: this.readUInt32(buf, 4), + current_version: this.readUInt32(buf, 8), + compatibility_version: this.readUInt32(buf, 12) + }; + } + + parseVersionMin (type, buf) { + if (buf.length !== 8) + throw new Error('min version OOB'); + + return { + type: type, + version: this.readUInt16(buf, 2) + '.' + buf[1] + '.' + buf[0], + sdk: this.readUInt16(buf, 6) + '.' + buf[5] + '.' + buf[4] + }; + } + + parseLinkEdit (type, buf) { + if (buf.length !== 8) + throw new Error('link_edit OOB'); + + return { + type: type, + dataoff: this.readUInt32(buf, 0), + datasize: this.readUInt32(buf, 4) + }; + } + + // NOTE: returned addresses are relative to the "base address", i.e. + // the vmaddress of the first "non-null" segment [e.g. initproto!=0] + // (i.e. __TEXT ?) + parseFunctionStarts (type, + buf, + file) { + if (buf.length !== 8) + throw new Error('function_starts OOB'); + + var dataoff = this.readUInt32(buf, 0); + var datasize = this.readUInt32(buf, 4); + var data = file.slice(dataoff, dataoff + datasize); + + var addresses = []; + var address = 0; // TODO? use start address / "base address" + + // read array of uleb128-encoded deltas + var delta = 0, + shift = 0; + for (var i = 0; i < data.length; i++) { + delta |= (data[i] & 0x7f) << shift; + if ((data[i] & 0x80) !== 0) { // delta value not finished yet + shift += 7; + if (shift > 24) + throw new Error('function_starts delta too large'); + else if (i + 1 === data.length) + throw new Error('function_starts delta truncated'); + } else if (delta === 0) { // end of table + break; + } else { + address += delta; + addresses.push(address); + delta = 0; + shift = 0; + } + } + + return { + type: type, + dataoff: dataoff, + datasize: datasize, + addresses: addresses + }; + } + + parseMain (type, buf) { + if (buf.length < 16) + throw new Error('main OOB'); + + return { + type: type, + entryoff: this.readUInt64(buf, 0), + stacksize: this.readUInt64(buf, 8) + }; + } + +}