import ImageDirectoryEntry from './format/ImageDirectoryEntry.js'; import { binaryToString, cloneObject, copyBuffer, roundUp, stringToBinary, } from './util/functions.js'; function removeDuplicates(a) { return a.reduce(function (p, c) { return p.indexOf(c) >= 0 ? p : p.concat(c); }, []); } function readString(view, offset) { var length = view.getUint16(offset, true); var r = ''; offset += 2; for (var i = 0; i < length; ++i) { r += String.fromCharCode(view.getUint16(offset, true)); offset += 2; } return r; } function readLanguageTable(view, typeEntry, name, languageTable, cb) { var off = languageTable; var nameEntry = { name: name, languageTable: languageTable, characteristics: view.getUint32(off, true), dateTime: view.getUint32(off + 4, true), majorVersion: view.getUint16(off + 8, true), minorVersion: view.getUint16(off + 10, true), }; var nameCount = view.getUint16(off + 12, true); var idCount = view.getUint16(off + 14, true); off += 16; for (var i = 0; i < nameCount; ++i) { var nameOffset = view.getUint32(off, true) & 0x7fffffff; var dataOffset = view.getUint32(off + 4, true); // ignore if the offset refers to the next table if ((dataOffset & 0x80000000) !== 0) { off += 8; continue; } var name_1 = readString(view, nameOffset); cb(typeEntry, nameEntry, { lang: name_1, dataOffset: dataOffset }); off += 8; } for (var i = 0; i < idCount; ++i) { var id = view.getUint32(off, true) & 0x7fffffff; var dataOffset = view.getUint32(off + 4, true); // ignore if the offset refers to the next table if ((dataOffset & 0x80000000) !== 0) { off += 8; continue; } cb(typeEntry, nameEntry, { lang: id, dataOffset: dataOffset }); off += 8; } } function readNameTable(view, type, nameTable, cb) { var off = nameTable; var typeEntry = { type: type, nameTable: nameTable, characteristics: view.getUint32(off, true), dateTime: view.getUint32(off + 4, true), majorVersion: view.getUint16(off + 8, true), minorVersion: view.getUint16(off + 10, true), }; var nameCount = view.getUint16(off + 12, true); var idCount = view.getUint16(off + 14, true); off += 16; for (var i = 0; i < nameCount; ++i) { var nameOffset = view.getUint32(off, true) & 0x7fffffff; var nextTable = view.getUint32(off + 4, true); // ignore if no next table is available if (!(nextTable & 0x80000000)) { off += 8; continue; } nextTable &= 0x7fffffff; var name_2 = readString(view, nameOffset); readLanguageTable(view, typeEntry, name_2, nextTable, cb); off += 8; } for (var i = 0; i < idCount; ++i) { var id = view.getUint32(off, true) & 0x7fffffff; var nextTable = view.getUint32(off + 4, true); // ignore if no next table is available if (!(nextTable & 0x80000000)) { off += 8; continue; } nextTable &= 0x7fffffff; readLanguageTable(view, typeEntry, id, nextTable, cb); off += 8; } } function divideEntriesImplByID(r, names, entries) { var entriesByString = {}; var entriesByNumber = {}; entries.forEach(function (e) { if (typeof e.lang === 'string') { entriesByString[e.lang] = e; names.push(e.lang); } else { entriesByNumber[e.lang] = e; } }); var strKeys = Object.keys(entriesByString); strKeys.sort().forEach(function (type) { r.s.push(entriesByString[type]); }); var numKeys = Object.keys(entriesByNumber); numKeys .map(function (k) { return Number(k); }) .sort(function (a, b) { return a - b; }) .forEach(function (type) { r.n.push(entriesByNumber[type]); }); return 16 + 8 * (strKeys.length + numKeys.length); } function divideEntriesImplByName(r, names, entries) { var entriesByString = {}; var entriesByNumber = {}; entries.forEach(function (e) { var _a, _b; if (typeof e.id === 'string') { var a = (_a = entriesByString[e.id]) !== null && _a !== void 0 ? _a : (entriesByString[e.id] = []); names.push(e.id); a.push(e); } else { var a = (_b = entriesByNumber[e.id]) !== null && _b !== void 0 ? _b : (entriesByNumber[e.id] = []); a.push(e); } }); var sSum = Object.keys(entriesByString) .sort() .map(function (id) { var o = { id: id, s: [], n: [], }; r.s.push(o); return divideEntriesImplByID(o, names, entriesByString[id]); }) .reduce(function (p, c) { return p + 8 + c; }, 0); var nSum = Object.keys(entriesByNumber) .map(function (k) { return Number(k); }) .sort(function (a, b) { return a - b; }) .map(function (id) { var o = { id: id, s: [], n: [], }; r.n.push(o); return divideEntriesImplByID(o, names, entriesByNumber[id]); }) .reduce(function (p, c) { return p + 8 + c; }, 0); return 16 + sSum + nSum; } function divideEntriesImplByType(r, names, entries) { var entriesByString = {}; var entriesByNumber = {}; entries.forEach(function (e) { var _a, _b; if (typeof e.type === 'string') { var a = (_a = entriesByString[e.type]) !== null && _a !== void 0 ? _a : (entriesByString[e.type] = []); names.push(e.type); a.push(e); } else { var a = (_b = entriesByNumber[e.type]) !== null && _b !== void 0 ? _b : (entriesByNumber[e.type] = []); a.push(e); } }); var sSum = Object.keys(entriesByString) .sort() .map(function (type) { var o = { type: type, s: [], n: [] }; r.s.push(o); return divideEntriesImplByName(o, names, entriesByString[type]); }) .reduce(function (p, c) { return p + 8 + c; }, 0); var nSum = Object.keys(entriesByNumber) .map(function (k) { return Number(k); }) .sort(function (a, b) { return a - b; }) .map(function (type) { var o = { type: type, s: [], n: [] }; r.n.push(o); return divideEntriesImplByName(o, names, entriesByNumber[type]); }) .reduce(function (p, c) { return p + 8 + c; }, 0); return 16 + sSum + nSum; } function calculateStringLengthForWrite(text) { var length = text.length; // limit to 65535 because the 'length' field is uint16 return length > 65535 ? 65535 : length; } function getStringOffset(target, strings) { var l = strings.length; for (var i = 0; i < l; ++i) { var s = strings[i]; if (s.text === target) { return s.offset; } } throw new Error('Unexpected'); } /** (returns offset just after the written text) */ function writeString(view, offset, text) { var length = calculateStringLengthForWrite(text); view.setUint16(offset, length, true); offset += 2; for (var i = 0; i < length; ++i) { view.setUint16(offset, text.charCodeAt(i), true); offset += 2; } return offset; } function writeLanguageTable(view, offset, strings, data) { // characteristics view.setUint32(offset, 0, true); // timestamp view.setUint32(offset + 4, 0, true); // major version / minor version view.setUint32(offset + 8, 0, true); // name entries view.setUint16(offset + 12, data.s.length, true); // id entries view.setUint16(offset + 14, data.n.length, true); offset += 16; // name entries (not in specification) data.s.forEach(function (e) { var strOff = getStringOffset(e.lang, strings); view.setUint32(offset, strOff, true); view.setUint32(offset + 4, e.offset, true); offset += 8; }); // id entries data.n.forEach(function (e) { view.setUint32(offset, e.lang, true); view.setUint32(offset + 4, e.offset, true); offset += 8; }); return offset; } function writeNameTable(view, offset, leafOffset, strings, data) { // characteristics view.setUint32(offset, 0, true); // timestamp view.setUint32(offset + 4, 0, true); // major version / minor version view.setUint32(offset + 8, 0, true); // name entries view.setUint16(offset + 12, data.s.length, true); // id entries view.setUint16(offset + 14, data.n.length, true); offset += 16; data.s.forEach(function (e) { e.offset = leafOffset; leafOffset = writeLanguageTable(view, leafOffset, strings, e); }); data.n.forEach(function (e) { e.offset = leafOffset; leafOffset = writeLanguageTable(view, leafOffset, strings, e); }); data.s.forEach(function (e) { var strOff = getStringOffset(e.id, strings); view.setUint32(offset, strOff + 0x80000000, true); view.setUint32(offset + 4, e.offset + 0x80000000, true); offset += 8; }); data.n.forEach(function (e) { view.setUint32(offset, e.id, true); view.setUint32(offset + 4, e.offset + 0x80000000, true); offset += 8; }); return leafOffset; } function writeTypeTable(view, offset, strings, data) { // characteristics view.setUint32(offset, 0, true); // timestamp view.setUint32(offset + 4, 0, true); // major version / minor version view.setUint32(offset + 8, 0, true); // name entries view.setUint16(offset + 12, data.s.length, true); // id entries view.setUint16(offset + 14, data.n.length, true); offset += 16; var nextTableOffset = offset + 8 * (data.s.length + data.n.length); data.s.forEach(function (e) { e.offset = nextTableOffset; nextTableOffset += 16 + 8 * (e.s.length + e.n.length); }); data.n.forEach(function (e) { e.offset = nextTableOffset; nextTableOffset += 16 + 8 * (e.s.length + e.n.length); }); data.s.forEach(function (e) { var strOff = getStringOffset(e.type, strings); view.setUint32(offset, strOff + 0x80000000, true); view.setUint32(offset + 4, e.offset + 0x80000000, true); offset += 8; nextTableOffset = writeNameTable(view, e.offset, nextTableOffset, strings, e); }); data.n.forEach(function (e) { view.setUint32(offset, e.type, true); view.setUint32(offset + 4, e.offset + 0x80000000, true); offset += 8; nextTableOffset = writeNameTable(view, e.offset, nextTableOffset, strings, e); }); return nextTableOffset; } //////////////////////////////////////////////////////////////////////////////// /** Manages resource data for NtExecutable */ var NtExecutableResource = /** @class */ (function () { function NtExecutableResource() { /** The timestamp for resource */ this.dateTime = 0; /** The major version data for resource */ this.majorVersion = 0; /** The minor version data for resource */ this.minorVersion = 0; /** Resource entries */ this.entries = []; /** * The section data header of resource data (used by outputResource method). * This instance will be null if the base executable does not contain resource data. * You can override this field before calling outputResource method. * (Note that the addresses and sizes are ignored for output) */ this.sectionDataHeader = null; this.originalSize = 0; } NtExecutableResource.prototype.parse = function (section, ignoreUnparsableData) { if (!section.data) { return; } var view = new DataView(section.data); // --- First: Resource Directory Table --- // (off: 0 -- Characteristics (uint32)) this.dateTime = view.getUint32(4, true); this.majorVersion = view.getUint16(8, true); this.minorVersion = view.getUint16(10, true); var nameCount = view.getUint16(12, true); var idCount = view.getUint16(14, true); var off = 16; var res = []; var cb = function (t, n, l) { var off = view.getUint32(l.dataOffset, true) - section.info.virtualAddress; var size = view.getUint32(l.dataOffset + 4, true); var cp = view.getUint32(l.dataOffset + 8, true); if (off >= 0) { var bin = new Uint8Array(size); bin.set(new Uint8Array(section.data, off, size)); res.push({ type: t.type, id: n.name, lang: l.lang, codepage: cp, bin: bin.buffer, }); } else { if (!ignoreUnparsableData) { throw new Error('Cannot parse resource directory entry; RVA seems to be invalid.'); } res.push({ type: t.type, id: n.name, lang: l.lang, codepage: cp, bin: new ArrayBuffer(0), rva: l.dataOffset, }); } }; for (var i = 0; i < nameCount; ++i) { var nameOffset = view.getUint32(off, true) & 0x7fffffff; var nextTable = view.getUint32(off + 4, true); // ignore if no next table is available if (!(nextTable & 0x80000000)) { off += 8; continue; } nextTable &= 0x7fffffff; var name_3 = readString(view, nameOffset); readNameTable(view, name_3, nextTable, cb); off += 8; } for (var i = 0; i < idCount; ++i) { var typeId = view.getUint32(off, true) & 0x7fffffff; var nextTable = view.getUint32(off + 4, true); // ignore if no next table is available if (!(nextTable & 0x80000000)) { off += 8; continue; } nextTable &= 0x7fffffff; readNameTable(view, typeId, nextTable, cb); off += 8; } this.entries = res; this.originalSize = section.data.byteLength; }; /** * Parses resource data for `NtExecutable`. * This function returns valid instance even if * the executable does not have resource data. * @param exe `NtExecutable` instance * @param ignoreUnparsableData (default: false) specify true if skipping 'unparsable' (e.g. unusual format) data. * When true, the resource data may break on write operation. */ NtExecutableResource.from = function (exe, ignoreUnparsableData) { if (ignoreUnparsableData === void 0) { ignoreUnparsableData = false; } var secs = [] .concat(exe.getAllSections()) .sort(function (a, b) { return a.info.virtualAddress - b.info.virtualAddress; }); var entry = exe.getSectionByEntry(ImageDirectoryEntry.Resource); // check if the section order is supported // (not supported if any other sections except 'relocation' is available, // because the recalculation of virtual address is not simple) if (entry) { var reloc = exe.getSectionByEntry(ImageDirectoryEntry.BaseRelocation); for (var i = 0; i < secs.length; ++i) { var s = secs[i]; if (s.info.name === entry.info.name) { for (var j = i + 1; j < secs.length; ++j) { if (!reloc || secs[j].info.name !== reloc.info.name) { throw new Error('After Resource section, sections except for relocation are not supported'); } } break; } } } var r = new NtExecutableResource(); r.sectionDataHeader = entry ? cloneObject(entry.info) : null; if (entry) { r.parse(entry, ignoreUnparsableData); } return r; }; /** * Add or replace the resource entry. * This method replaces the entry only if there is an entry with `type`, `id` and `lang` equal. */ NtExecutableResource.prototype.replaceResourceEntry = function (entry) { for (var len = this.entries.length, i = 0; i < len; ++i) { var e = this.entries[i]; if (e.type === entry.type && e.id === entry.id && e.lang === entry.lang) { this.entries[i] = entry; return; } } this.entries.push(entry); }; /** * Returns all resource entries, which has specified type and id, as UTF-8 string data. * @param type Resource type * @param id Resource id * @returns an array of lang and value pair (tuple) */ NtExecutableResource.prototype.getResourceEntriesAsString = function (type, id) { return this.entries .filter(function (entry) { return entry.type === type && entry.id === id; }) .map(function (entry) { return [entry.lang, binaryToString(entry.bin)]; }); }; /** * Add or replace the resource entry with UTF-8 string data. * This method is a wrapper of {@link NtExecutableResource.replaceResourceEntry}. */ NtExecutableResource.prototype.replaceResourceEntryFromString = function (type, id, lang, value) { var entry = { type: type, id: id, lang: lang, codepage: 1200, bin: stringToBinary(value), }; this.replaceResourceEntry(entry); }; /** * Removes resource entries which has specified type and id. */ NtExecutableResource.prototype.removeResourceEntry = function (type, id, lang) { this.entries = this.entries.filter(function (entry) { return !(entry.type === type && entry.id === id && (typeof lang === 'undefined' || entry.lang === lang)); }); }; /** * Generates resource data binary for NtExecutable (not for .res file) * @param virtualAddress The virtual address for the section * @param alignment File alignment value of executable * @param noGrow Set true to disallow growing resource section (throw errors if data exceeds) * @param allowShrink Set true to allow shrinking resource section (if the data size is less than original) */ NtExecutableResource.prototype.generateResourceData = function (virtualAddress, alignment, noGrow, allowShrink) { if (noGrow === void 0) { noGrow = false; } if (allowShrink === void 0) { allowShrink = false; } // estimate data size and divide to output table var r = { s: [], n: [], }; var strings = []; var size = divideEntriesImplByType(r, strings, this.entries); strings = removeDuplicates(strings); var stringsOffset = size; size += strings.reduce(function (prev, cur) { return prev + 2 + calculateStringLengthForWrite(cur) * 2; }, 0); size = roundUp(size, 8); var descOffset = size; size = this.entries.reduce(function (p, e) { e.offset = p; return p + 16; }, descOffset); var dataOffset = size; size = this.entries.reduce(function (p, e) { return roundUp(p, 8) + e.bin.byteLength; }, dataOffset); var alignedSize = roundUp(size, alignment); var originalAlignedSize = roundUp(this.originalSize, alignment); if (noGrow) { if (alignedSize > originalAlignedSize) { throw new Error('New resource data is larger than original'); } } if (!allowShrink) { if (alignedSize < originalAlignedSize) { alignedSize = originalAlignedSize; } } // generate binary var bin = new ArrayBuffer(alignedSize); var view = new DataView(bin); var o = descOffset; var va = virtualAddress + dataOffset; this.entries.forEach(function (e) { var len = e.bin.byteLength; if (typeof e.rva !== 'undefined') { // RVA view.setUint32(o, e.rva, true); } else { va = roundUp(va, 8); // RVA view.setUint32(o, va, true); va += len; } // size view.setUint32(o + 4, len, true); // codepage view.setUint32(o + 8, e.codepage, true); // (zero) view.setUint32(o + 12, 0, true); o += 16; }); o = dataOffset; this.entries.forEach(function (e) { var len = e.bin.byteLength; copyBuffer(bin, o, e.bin, 0, len); o += roundUp(len, 8); }); var stringsData = []; o = stringsOffset; strings.forEach(function (s) { stringsData.push({ offset: o, text: s, }); o = writeString(view, o, s); }); writeTypeTable(view, 0, stringsData, r); // fill with 'PADDINGX' if (alignedSize > size) { var pad = 'PADDINGX'; for (var i = size, j = 0; i < alignedSize; ++i, ++j) { if (j === 8) { j = 0; } view.setUint8(i, pad.charCodeAt(j)); } } return { bin: bin, rawSize: size, dataOffset: dataOffset, descEntryOffset: descOffset, descEntryCount: this.entries.length, }; }; /** * Writes holding resource data to specified NtExecutable instance. * @param exeDest An NtExecutable instance to write resource section to * @param noGrow Set true to disallow growing resource section (throw errors if data exceeds) * @param allowShrink Set true to allow shrinking resource section (if the data size is less than original) */ NtExecutableResource.prototype.outputResource = function (exeDest, noGrow, allowShrink) { if (noGrow === void 0) { noGrow = false; } if (allowShrink === void 0) { allowShrink = false; } // make section data var fileAlign = exeDest.getFileAlignment(); var sectionData; if (this.sectionDataHeader) { sectionData = { data: null, info: cloneObject(this.sectionDataHeader), }; } else { sectionData = { data: null, info: { name: '.rsrc', virtualSize: 0, virtualAddress: 0, sizeOfRawData: 0, pointerToRawData: 0, pointerToRelocations: 0, pointerToLineNumbers: 0, numberOfRelocations: 0, numberOfLineNumbers: 0, characteristics: 0x40000040, // read access and initialized data }, }; } // first, set virtualAddress to 0 because // the virtual address is not determined now var data = this.generateResourceData(0, fileAlign, noGrow, allowShrink); sectionData.data = data.bin; sectionData.info.sizeOfRawData = data.bin.byteLength; sectionData.info.virtualSize = data.rawSize; // write as section exeDest.setSectionByEntry(ImageDirectoryEntry.Resource, sectionData); // rewrite section raw-data var generatedSection = exeDest.getSectionByEntry(ImageDirectoryEntry.Resource); var view = new DataView(generatedSection.data); // set RVA var o = data.descEntryOffset; var va = generatedSection.info.virtualAddress + data.dataOffset; for (var i = 0; i < data.descEntryCount; ++i) { var len = view.getUint32(o + 4, true); va = roundUp(va, 8); // RVA view.setUint32(o, va, true); va += len; o += 16; } }; return NtExecutableResource; }()); export default NtExecutableResource;