-
-
Save rxwx/55b486031fb44d94351174d8b340c8fe to your computer and use it in GitHub Desktop.
Mach-O Loader in Node.js (work in progress)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const ffi = require('ffi-napi'); | |
const ref = require("ref-napi"); | |
const Struct = require("ref-struct-di")(ref); | |
const fs = require('fs') | |
const ArrayType = require('ref-array-napi') | |
var Union = require('ref-union-napi'); | |
/* | |
Dependencies: | |
$ npm install ffi-napi | |
$ npm install ref-napi | |
$ npm install ref-struct-di | |
$ npm install ref-array-napi | |
$ npm install ref-union-napi | |
*/ | |
// Structs: | |
const entry_point_command = Struct({ | |
cmd: ref.types.uint, // type of load command | |
cmdsize: ref.types.uint, // total size of command in bytes | |
entryoff: ref.types.uint64, // file (__TEXT) offset of main() | |
stacksize: ref.types.uint64 // if not zero, initial stack size | |
}); | |
const lc_str = new Union({ | |
offset: ref.types.uint32, | |
ptr: ArrayType('char') | |
}); | |
const dylibType = Struct({ | |
name: lc_str, /* library's path name */ | |
timestamp: ref.types.uint, /* library's build time stamp */ | |
current_version: ref.types.uint, /* library's current version number */ | |
compatibility_version: ref.types.uint, /* library's compatibility vers number*/ | |
}); | |
const dylib_command = Struct({ | |
cmd: ref.types.uint, /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,LC_REEXPORT_DYLIB */ | |
cmdsize: ref.types.uint, /* includes pathname string */ | |
dylib: dylibType, /* the library identification */ | |
}); | |
const DummyUnion = new Union({ | |
n_strx: ref.types.uint32, | |
}) | |
const nlist_64 = Struct({ | |
n_un: DummyUnion, | |
n_type: ref.types.uint8, // type flag, see below | |
n_sect: ref.types.uint8, // section number or NO_SECT | |
n_desc: ref.types.uint16, // see <mach-o/stab.h> | |
n_value: ref.types.uint64, // value of this symbol (or stab offset) | |
}); | |
const segment_command_64 = Struct({ | |
cmd: ref.types.uint, // LC_SEGMENT_64 | |
cmdsize: ref.types.uint, // includes sizeof section_64 structs | |
segname: ArrayType(ref.types.char, 16), // segment name [16] | |
vmaddr: ref.types.uint64, // memory address of this segment | |
vmsize: ref.types.uint64, // memory size of this segment | |
fileoff: ref.types.uint64, // file offset of this segment | |
filesize: ref.types.uint64, // amount to map from the file | |
maxprot: ref.types.uint, // maximum VM protection | |
initprot: ref.types.uint, // initial VM protection | |
nsects: ref.types.uint, // number of sections in segment | |
flags: ref.types.uint, // flags | |
}); | |
const symtab_command = Struct({ | |
cmd: ref.types.uint32, // LC_SYMTAB | |
cmdsize: ref.types.uint32, // sizeof(struct symtab_command) | |
symoff: ref.types.uint32, // symbol table offset | |
nsyms: ref.types.uint32, // number of symbol table entries | |
stroff: ref.types.uint32, // string table offset | |
strsize: ref.types.uint32, // string table size in bytes | |
}); | |
const load_command = Struct({ | |
cmd: ref.types.uint, // type of load command | |
cmdsize: ref.types.uint, // total size of command in bytes | |
}); | |
const mach_header_64_ = Struct({ | |
cputype: ref.types.uint, // cpu specifier | |
cpusubtype: ref.types.uint, // machine specifier | |
filetype: ref.types.uint, // type of file | |
ncmds: ref.types.uint, // number of load commands | |
sizeofcmds: ref.types.uint, // the size of all the load commands | |
flags: ref.types.uint, // flags | |
reserved: ref.types.uint // reserved | |
}); | |
const mach_header_64 = Struct({ | |
magic: ref.types.uint, // mach magic number identifier | |
cputype: ref.types.uint, // cpu specifier | |
cpusubtype: ref.types.uint, // machine specifier | |
filetype: ref.types.uint, // type of file | |
ncmds: ref.types.uint, // number of load commands | |
sizeofcmds: ref.types.uint, // the size of all the load commands | |
flags: ref.types.uint, // flags | |
reserved: ref.types.uint // reserved | |
}); | |
const LC_SEGMENT_64 = 0x19; | |
const LC_ID_DYLIB = 0xd; | |
const LC_SYMTAB = 0x2; | |
const LC_MAIN = 0x80000028; | |
const EXECUTABLE_BASE_ADDR = 0x100000000; | |
const DYLD_BASE = 0x00007fff5fc00000; | |
const NSLINKMODULE_OPTION_NONE = 0x0; | |
const NSLINKMODULE_OPTION_BINDNOW = 0x1; | |
const NSLINKMODULE_OPTION_PRIVATE = 0x2; | |
const NSLINKMODULE_OPTION_RETURN_ON_ERROR = 0x4; | |
const libsys = ffi.Library("/usr/lib/libSystem.B.dylib", { | |
'chmod': [ 'int', ['int64', 'int' ]], // don't use chmod | |
'chdir': [ 'int', ['int64']], // chdir is safer | |
'pthread_attr_init': ['int', ['void*']], | |
'pthread_attr_setdetachstate': ['int', ['void*', 'int']], | |
'pthread_create': ['void*', ['void*', 'void*', 'int64', 'void*']], | |
}); | |
const libdyld = ffi.Library("/usr/lib/system/libdyld.dylib", { | |
'NSCreateObjectFileImageFromMemory': ['int', ['void*', 'int', 'void*']], | |
'NSLinkModule': [ 'void*', [ 'void*', 'string', 'uint' ]], | |
'NSLookupSymbolInModule': ['void*', [ 'void*', 'string' ]], | |
'NSAddressOfSymbol': ['uint64', [ 'void*' ]] | |
}); | |
function is_sierra() { | |
// returns 1 if running on Sierra, 0 otherwise | |
// this works because /bin/rcp was removed in Sierra | |
return fs.existsSync('/bin/rcp');; | |
} | |
function find_macho(addr, increment) { | |
console.log("[*] Searching with chdir syscall"); | |
while(addr < 0x0007FFFFFFFFF000) { | |
//console.log("[*] Checking: 0x" + addr.toString(16)); | |
libsys.chdir(addr); | |
var errno = ffi.errno(); | |
if (errno == 2) { | |
var buf = ref.alloc('int64'); | |
ref.writeInt64LE(buf, 0, addr); | |
var ptr = ref.readPointer(buf, 0, 4); | |
//console.log("[*] Found candiate at 0x" + addr.toString(16)); | |
//console.log("[*] Bytes: " + ptr.readUInt32LE().toString(16)); | |
if (ptr.readUInt32LE() == 0xfeedfacf) { | |
//console.log("[+] Found valid address: 0x" + addr.toString(16)); | |
return addr; | |
} | |
} | |
addr += increment; | |
} | |
return 1; | |
} | |
function addrToStruct(addr, type) { | |
var addrPtr = ref.alloc('int64'); | |
ref.writeInt64LE(addrPtr, 0, addr); | |
var structPtr = ref.reinterpret(ref.readPointer(addrPtr, 0, 8), type.size, 0); | |
structPtr.type = type; | |
return structPtr.deref(); | |
} | |
function addrToString(addr) { | |
type = ref.types.CString; | |
var addrPtr = ref.alloc('int64'); | |
ref.writeInt64LE(addrPtr, 0, addr); | |
var structPtr = ref.reinterpret(ref.readPointer(addrPtr, 0, 8), type.size, 0); | |
return ref.readCString(structPtr); | |
} | |
function resolveSymbols(base, funcMap) { | |
var machHeader = addrToStruct(base, mach_header_64); | |
console.log("[*] Mach header .."); | |
console.log("[*] magic: 0x" + machHeader.magic.toString(16)); | |
console.log("[*] cputype: " + machHeader.cputype); | |
console.log("[*] cpusubtype: " + machHeader.cpusubtype); | |
console.log("[*] filetype: " + machHeader.filetype); | |
console.log("[*] ncmds: " + machHeader.ncmds); | |
console.log("[*] sizeofcmds: " + machHeader.sizeofcmds); | |
console.log("[*] flags: 0x" + machHeader.flags.toString(16)); | |
var lcAddr = (base + mach_header_64.size); | |
var lcHeader = addrToStruct(lcAddr, load_command); | |
console.log("[*] Load commands .."); | |
var symtab = 0; | |
var linkedit = 0; | |
var text = 0; | |
for (var i = 0; i < machHeader.ncmds; i++) { | |
//console.log("[*] cmd: 0x" + lcHeader.cmd.toString(16)); | |
//console.log("[*] cmdsize: " + lcHeader.cmdsize); | |
switch (lcHeader.cmd) | |
{ | |
case LC_SYMTAB: | |
console.log("[*] Found LC_SYMTAB at: 0x" + lcAddr.toString(16)); | |
// symtab = (struct symtab_command *)lc; | |
symtab = addrToStruct(lcAddr, symtab_command); | |
break; | |
case LC_SEGMENT_64: | |
//console.log("[*] Found LC_SEGMENT_64"); | |
// sc = (struct segment_command_64 *)lc; | |
var seg = addrToStruct(lcAddr, segment_command_64); | |
var segname = Buffer.from(seg.segname).toString(); | |
if (segname.startsWith("__TEXT")) { | |
text = seg; | |
console.log("[*] Found __TEXT at: 0x" + (base + seg.vmaddr).toString(16)); | |
} | |
else if (segname.startsWith("__LINK")) { | |
linkedit = seg; | |
console.log("[*] Found __LINKEDIT at: 0x" + (base + seg.vmaddr).toString(16)); | |
} | |
break; | |
case LC_ID_DYLIB: | |
// TODO: continue until we find this! | |
var dlb = addrToStruct(lcAddr, dylib_command); | |
var nameAddr = lcAddr + dlb.dylib.name.offset; | |
var nameStr = addrToString(nameAddr); | |
console.log("[*] dylib: " + nameStr); | |
if (!nameStr.startsWith("/usr/lib/system/libdyld.dylib")) { | |
return; | |
} | |
console.log("[+] Found libdyld.dylib") | |
break; | |
} | |
if (linkedit && symtab && text) | |
break; | |
// cmd = (load_command_t*)((byte*)cmd + cmd->cmdsize); | |
lcAddr = lcAddr + lcHeader.cmdsize; | |
lcHeader = addrToStruct(lcAddr, load_command); | |
} | |
// should have segments now | |
if (!linkedit || !symtab || !text) { | |
console.log("[!] Couldn't load segments"); | |
return -1; | |
} | |
console.log("[*] text->vmaddr: 0x" + text.vmaddr.toString(16)); | |
console.log("[*] text->vmsize: 0x" + text.vmsize.toString(16)); | |
console.log("[*] linkedit->vmaddr: 0x" + linkedit.vmaddr.toString(16)); | |
console.log("[*] linkedit->vmsize: 0x" + linkedit.vmsize.toString(16)); | |
console.log("[*] linkedit->fileoff: " + linkedit.fileoff); | |
console.log("[*] symtab->cmd: " + symtab.cmd); | |
console.log("[*] symtab->cmdsize: " + symtab.cmdsize); | |
console.log("[*] symtab->symoff: " + symtab.symoff); | |
console.log("[*] symtab->nsyms: " + symtab.nsyms); | |
console.log("[*] symtab->stroff: " + symtab.stroff); | |
console.log("[*] symtab->strsize: " + symtab.strsize); | |
var file_slide = linkedit.vmaddr - text.vmaddr - linkedit.fileoff; | |
console.log("[*] file_slide: 0x" + file_slide.toString(16)); | |
var strtab = (base + file_slide + symtab.stroff); | |
console.log("[*] strtab: 0x" + strtab.toString(16)); | |
for (var i = 0; i < symtab.nsyms; i++) { | |
var nlAddr = base + file_slide + symtab.symoff + (i * nlist_64.size) | |
//console.log("[*] nlist_64 address: 0x" + nlAddr.toString(16)); | |
var nl = addrToStruct(nlAddr, nlist_64); | |
var nameAddr = strtab + nl.n_un.n_strx; | |
var name = addrToString(nameAddr); | |
//console.log("[*] name address: 0x" + nameAddr.toString(16)); | |
//console.log("[*] symbol: " + name); | |
const foundFunc = Object.keys(funcMap).find(v => name.includes(v)); | |
if (foundFunc) { | |
if (is_sierra()) { | |
funcs[foundFunc] = base + nl.n_value; | |
} | |
else { | |
funcs[foundFunc] = base + nl.n_value - text.vmaddr; | |
} | |
console.log("[+] Found symbol " + name + " at 0x" + funcs[foundFunc].toString(16)); | |
} | |
} | |
return funcMap; | |
} | |
function find_epc(base) { | |
// for some reason we have to skip first 4 bytes or we get a read AV | |
// hence the mach_header_64_ struct which skips over the magic field | |
var machHeader = addrToStruct(base+4, mach_header_64_); | |
var lcAddr = (base + 4 + mach_header_64_.size); | |
var lcHeader = addrToStruct(lcAddr, load_command); | |
for (var i = 0; i < machHeader.ncmds; i++) { | |
if (lcHeader.cmd === LC_MAIN) { | |
return addrToStruct(lcAddr, entry_point_command); | |
} | |
lcAddr = lcAddr + lcHeader.cmdsize; | |
lcHeader = addrToStruct(lcAddr, load_command); | |
} | |
return -1; | |
} | |
async function sleep() { | |
console.log(1); | |
await doSleep(30000); | |
console.log(2); | |
} | |
function doSleep(ms) { | |
return new Promise((resolve) => { | |
setTimeout(resolve, ms); | |
}); | |
} | |
function loadFromMemory() { | |
const FUDGE_FACTOR = 0x4cd4000; | |
//const FUDGE_FACTOR = 0x7ff80ae1c000; | |
var binary, dyld; | |
if (is_sierra()) { | |
dyld = find_macho(DYLD_BASE, 0x1000) | |
} | |
else { | |
binary = find_macho(EXECUTABLE_BASE_ADDR, 0x1000); | |
console.log("[+] Found our load address: 0x" + binary.toString(16)); | |
// TODO: this needs improving as it could be higher, or we may miss it completely! | |
dyld = find_macho(binary + FUDGE_FACTOR, 0x1000); | |
console.log("[+] Found dyld address: 0x" + dyld.toString(16)); | |
} | |
//resolve symbols for: NSCreateObjectFileImageFromMemory, NSLinkModule, NSLookupSymbolInModule, NSAddressOfSymbol | |
funcs = {'NSCreateObjectFileImageFromMemory': 0, 'NSLinkModule': 0, 'NSLookupSymbolInModule': 0 , 'NSAddressOfSymbol': 0} //, '___chmod': 0} | |
var funcMap = resolveSymbols(dyld, funcs); | |
if (funcMap == -1 || !Object.values(funcs).every((v) => v !== 0)) { | |
console.log("[!] Couldn't resolve all symbols"); | |
return; | |
} | |
// map function pointers | |
var voidPtr = ref.refType(ref.types.ulong); | |
var funcPtr = ref.alloc('pointer'); | |
ref.writeInt64LE(funcPtr, 0, funcMap['NSCreateObjectFileImageFromMemory']); | |
const NSCreateObjectFileImageFromMemory = ffi.ForeignFunction( | |
funcPtr.deref(), ref.types.uint, [ voidPtr, ref.types.uint, voidPtr] | |
); | |
ref.writeInt64LE(funcPtr, 0, funcMap['NSLinkModule']); | |
const NSLinkModule = ffi.ForeignFunction( | |
funcPtr.deref(), ref.refType(ref.types.ulong), [ ref.refType(ref.types.ulong), ref.types.CString, ref.types.uint32 ] | |
); | |
ref.writeInt64LE(funcPtr, 0, funcMap['NSLookupSymbolInModule']); | |
const NSLookupSymbolInModule = ffi.ForeignFunction( | |
funcPtr.deref(), ref.refType(ref.types.ulong), [ ref.refType(ref.types.ulong), ref.types.CString ] | |
); | |
} | |
function main() { | |
var data = fs.readFileSync(process.argv[2]); | |
// patch MH_BUNDLE | |
var saved = data[12]; | |
console.log("[*] Saved bundle type: " + saved); | |
data[12] = 0x08; | |
var image = ref.alloc('void*'); | |
var imagePtr = image.ref(); | |
var ret = libdyld.NSCreateObjectFileImageFromMemory(data, data.length, imagePtr); | |
if (ret != 1) { // NSObjectFileImageSuccess | |
console.log("[!] NSCreateObjectFileImageFromMemory failed: " + ret); | |
return; | |
} | |
console.log("[*] NSObjectFileImageSuccess: " + ret); | |
var module = libdyld.NSLinkModule(imagePtr.deref(), "", (NSLINKMODULE_OPTION_PRIVATE | NSLINKMODULE_OPTION_BINDNOW)); | |
var symbol = libdyld.NSLookupSymbolInModule(module, "__mh_execute_header"); | |
var symAddr = libdyld.NSAddressOfSymbol(symbol); | |
console.log("[*] Symbol: 0x" + symAddr.toString(16)); | |
var funcPtr = ref.alloc('void*'); | |
ref.writeInt64LE(funcPtr, 0, symAddr); | |
// now find the entrypoint | |
var epc = find_epc(symAddr); | |
console.log("[*] Entrypoint address: 0x" + (symAddr + epc.entryoff).toString(16)); | |
// create pthread | |
//var attr = ref.alloc('void*'); | |
//var thread = ref.alloc('void*'); | |
//ret = libsys.pthread_attr_init(attr); | |
//ret = libsys.pthread_attr_setdetachstate(attr, 2); | |
//libsys.pthread_create(thread, attr, symAddr + epc.entryoff, ref.NULL); | |
var funcPtr = ref.alloc('pointer'); | |
ref.writeInt64LE(funcPtr, 0, symAddr + epc.entryoff); | |
const EntryPointFunc = ffi.ForeignFunction( | |
funcPtr.deref(), ref.types.void, [ ref.types.int, ArrayType(ref.types.CString), ArrayType(ref.types.CString), ArrayType(ref.types.CString)] | |
); | |
var argv = process.argv.slice(2); | |
var argc = argv.length; | |
argv.push(ref.NULL); | |
var envp = ref.NULL; | |
var apple = ref.NULL; | |
console.log('[*] Calling entrypoint ..'); | |
EntryPointFunc(argc, argv, envp, apple); | |
} | |
main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment