Skip to content

Instantly share code, notes, and snippets.

@rxwx
Last active July 16, 2022 20:15
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rxwx/55b486031fb44d94351174d8b340c8fe to your computer and use it in GitHub Desktop.
Save rxwx/55b486031fb44d94351174d8b340c8fe to your computer and use it in GitHub Desktop.
Mach-O Loader in Node.js (work in progress)
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