@shockpkg/dir-projector
Version:
Package for creating Shockwave Director projectors
538 lines (496 loc) • 14.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.peResourceReplace = peResourceReplace;
exports.peVersionInts = peVersionInts;
exports.windowsLauncher = windowsLauncher;
exports.windowsPatch3dDisplayDriversSize = windowsPatch3dDisplayDriversSize;
var _portableExecutableSignature = require("portable-executable-signature");
var _resedit = require("@shockpkg/resedit");
var _util = require("../util.js");
// IMAGE_DATA_DIRECTORY indexes.
const IDD_RESOURCE = 2;
const IDD_BASE_RELOCATION = 5;
// IMAGE_SECTION_HEADER characteristics.
const IMAGE_SCN_CNT_CODE = 0x00000020;
const IMAGE_SCN_CNT_INITIALIZED_DATA = 0x00000040;
const IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080;
/**
* PE Resource replace config.
*/
/**
* Parse PE version string to integers (MS then LS bits) or null.
*
* @param version Version string.
* @returns Version integers ([MS, LS]) or null.
*/
function peVersionInts(version) {
const parts = version.split(/[,.]/);
const numbers = [];
for (const part of parts) {
const n = /^\d+$/.test(part) ? +part : -1;
if (n < 0 || n > 0xffff) {
return null;
}
numbers.push(n);
}
return numbers.length ? [
// eslint-disable-next-line no-bitwise
((numbers[0] || 0) << 16 | (numbers[1] || 0)) >>> 0,
// eslint-disable-next-line no-bitwise
((numbers[2] || 0) << 16 | (numbers[3] || 0)) >>> 0] : null;
}
/**
* Assert the given section is last section.
*
* @param exe NtExecutable instance.
* @param index ImageDirectory index.
* @param name Friendly name for messages.
*/
function exeAssertLastSection(exe, index, name) {
const section = exe.getSectionByEntry(index);
if (!section) {
throw new Error(`Missing section: ${index}:${name}`);
}
const allSections = exe.getAllSections();
let last = allSections[0].info;
for (const {
info
} of allSections) {
if (info.pointerToRawData > last.pointerToRawData) {
last = info;
}
}
const {
info
} = section;
if (info.pointerToRawData < last.pointerToRawData) {
throw new Error(`Not the last section: ${index}:${name}`);
}
}
/**
* Removes the reloc section if exists, fails if not the last section.
*
* @param exe NtExecutable instance.
* @returns Restore function.
*/
function exeRemoveReloc(exe) {
const section = exe.getSectionByEntry(IDD_BASE_RELOCATION);
if (!section) {
return () => {};
}
const {
size
} = exe.newHeader.optionalHeaderDataDirectory.get(IDD_BASE_RELOCATION);
exeAssertLastSection(exe, IDD_BASE_RELOCATION, '.reloc');
exe.setSectionByEntry(IDD_BASE_RELOCATION, null);
return () => {
exe.setSectionByEntry(IDD_BASE_RELOCATION, section);
const {
virtualAddress
} = exe.newHeader.optionalHeaderDataDirectory.get(IDD_BASE_RELOCATION);
exe.newHeader.optionalHeaderDataDirectory.set(IDD_BASE_RELOCATION, {
virtualAddress,
size
});
};
}
/**
* Update the sizes in EXE headers.
*
* @param exe NtExecutable instance.
*/
function exeUpdateSizes(exe) {
const {
optionalHeader
} = exe.newHeader;
const {
fileAlignment
} = optionalHeader;
let sizeOfCode = 0;
let sizeOfInitializedData = 0;
let sizeOfUninitializedData = 0;
for (const {
info: {
characteristics,
sizeOfRawData,
virtualSize
}
} of exe.getAllSections()) {
// eslint-disable-next-line no-bitwise
if (characteristics & IMAGE_SCN_CNT_CODE) {
sizeOfCode += sizeOfRawData;
}
// eslint-disable-next-line no-bitwise
if (characteristics & IMAGE_SCN_CNT_INITIALIZED_DATA) {
sizeOfInitializedData += Math.max(sizeOfRawData, (0, _util.align)(virtualSize, fileAlignment));
}
// eslint-disable-next-line no-bitwise
if (characteristics & IMAGE_SCN_CNT_UNINITIALIZED_DATA) {
sizeOfUninitializedData += (0, _util.align)(virtualSize, fileAlignment);
}
}
optionalHeader.sizeOfCode = sizeOfCode;
optionalHeader.sizeOfInitializedData = sizeOfInitializedData;
optionalHeader.sizeOfUninitializedData = sizeOfUninitializedData;
}
/**
* Replace all the icons in all icon groups.
*
* @param rsrc NtExecutableResource instance.
* @param iconData Icon data.
*/
function rsrcPatchIcon(rsrc, iconData) {
const {
byteOffset,
byteLength
} = iconData;
const ico = _resedit.Data.IconFile.from(iconData.buffer.slice(byteOffset, byteOffset + byteLength));
for (const iconGroup of _resedit.Resource.IconGroupEntry.fromEntries(rsrc.entries)) {
_resedit.Resource.IconGroupEntry.replaceIconsForResource(rsrc.entries, iconGroup.id, iconGroup.lang, ico.icons.map(icon => icon.data));
}
}
/**
* Update strings if present for all the languages.
*
* @param rsrc NtExecutableResource instance.
* @param versionStrings Version strings.
*/
function rsrcPatchVersion(rsrc, versionStrings) {
for (const versionInfo of _resedit.Resource.VersionInfo.fromEntries(rsrc.entries)) {
// Get all the languages, not just available languages.
const languages = versionInfo.getAllLanguagesForStringValues();
for (const language of languages) {
versionInfo.setStringValues(language, versionStrings);
}
// Update integer values from parsed strings if possible.
const {
FileVersion,
ProductVersion
} = versionStrings;
if (FileVersion) {
const uints = peVersionInts(FileVersion);
if (uints) {
const [ms, ls] = uints;
versionInfo.fixedInfo.fileVersionMS = ms;
versionInfo.fixedInfo.fileVersionLS = ls;
}
}
if (ProductVersion) {
const uints = peVersionInts(ProductVersion);
if (uints) {
const [ms, ls] = uints;
versionInfo.fixedInfo.productVersionMS = ms;
versionInfo.fixedInfo.productVersionLS = ls;
}
}
versionInfo.outputToResourceEntries(rsrc.entries);
}
}
/**
* Replace resources in Windows PE file.
*
* @param data File data.
* @param options Replacement options.
* @returns Modified data.
*/
function peResourceReplace(data, options) {
const {
iconData,
versionStrings,
removeSignature
} = options;
// Read EXE file and remove signature if present.
const signedData = removeSignature ? null : (0, _portableExecutableSignature.signatureGet)(data);
let exeData = (0, _portableExecutableSignature.signatureSet)(data, null, true, true);
// Parse EXE.
const exe = _resedit.NtExecutable.from(exeData);
// Remove reloc so rsrc can safely be resized.
const relocRestore = exeRemoveReloc(exe);
// Remove rsrc to modify.
exeAssertLastSection(exe, IDD_RESOURCE, '.rsrc');
const rsrc = _resedit.NtExecutableResource.from(exe);
exe.setSectionByEntry(IDD_RESOURCE, null);
if (iconData) {
rsrcPatchIcon(rsrc, iconData);
}
if (versionStrings) {
rsrcPatchVersion(rsrc, versionStrings);
}
// Update resources.
rsrc.outputResource(exe, false, true);
// Add reloc back.
relocRestore();
// Update sizes.
exeUpdateSizes(exe);
// Generate EXE.
exeData = exe.generate();
// Add back signature if not removing.
if (signedData) {
exeData = (0, _portableExecutableSignature.signatureSet)(exeData, signedData, true, true);
}
// Return updated EXE file.
return new Uint8Array(exeData);
}
/**
* Get Windows launcher for the specified type.
*
* @param type Executable type.
* @param resources Data to optionally copy resources from.
* @returns Launcher data.
*/
async function windowsLauncher(type, resources = null) {
let data;
switch (type) {
case 'i686':
{
data = await (0, _util.launcher)('windows-i686');
break;
}
default:
{
throw new Error(`Invalid type: ${type}`);
}
}
// Check if copying resources.
if (!resources) {
return data;
}
// Read resources from file.
const rsrc = _resedit.NtExecutableResource.from(_resedit.NtExecutable.from(await resources(), {
ignoreCert: true
}));
// Find the first icon group for each language.
const resIconGroups = new Map();
for (const iconGroup of _resedit.Resource.IconGroupEntry.fromEntries(rsrc.entries)) {
const known = resIconGroups.get(iconGroup.lang) || null;
if (!known || iconGroup.id < known.id) {
resIconGroups.set(iconGroup.lang, iconGroup);
}
}
// List the groups and icons to be kept.
const iconGroups = new Set();
const iconDatas = new Set();
for (const [, group] of resIconGroups) {
iconGroups.add(group.id);
for (const icon of group.icons) {
iconDatas.add(icon.iconID);
}
}
// Filter out the resources to keep.
const typeVersionInfo = 16;
const typeIcon = 3;
const typeIconGroup = 14;
rsrc.entries = rsrc.entries.filter(entry => entry.type === typeVersionInfo || entry.type === typeIcon && iconDatas.has(entry.id) || entry.type === typeIconGroup && iconGroups.has(entry.id));
// Remove signature if present.
const signedData = (0, _portableExecutableSignature.signatureGet)(data);
let exeData = (0, _portableExecutableSignature.signatureSet)(data, null, true, true);
// Parse launcher.
const exe = _resedit.NtExecutable.from(exeData);
// Remove reloc so rsrc can safely be resized.
const relocRestore = exeRemoveReloc(exe);
// Apply resources to launcher.
rsrc.outputResource(exe, false, true);
// Add reloc back.
relocRestore();
// Update sizes.
exeUpdateSizes(exe);
// Generated the updated launcher.
exeData = exe.generate();
// Add back signature if one present.
if (signedData) {
exeData = (0, _portableExecutableSignature.signatureSet)(exeData, signedData, true, true);
}
return new Uint8Array(exeData);
}
/**
* Patcher patch.
*/
/**
* Converts a hex string into a series of byte values, with unknowns being null.
*
* @param str Hex string.
* @returns Bytes and null values.
*/
function patchHexToBytes(str) {
return (str.replace(/\s/g, '').match(/.{1,2}/g) || []).map(s => {
if (s.length !== 2) {
throw new Error('Internal error');
}
// eslint-disable-next-line unicorn/prefer-number-properties
return /[\da-f]{2}/i.test(s) ? parseInt(s, 16) : null;
});
}
// A list of patch candidates, made to be partially position independant.
// Basically these patches just increase the temporary buffer sizes.
// Enough to provide amply room for anything that should be in the registry.
// Sizes 0x10000 for ASCII, and 0x20000 for WCHAR.
// Not enough room to calculate the correct size, and use it directly.
const patch3dDisplayDriversSizePatches = [
// director-8.5.0 - director-11.0.0-hotfix-1:
{
find: patchHexToBytes([
// call DWORD PTR ds:-- -- -- --
'FF 15 -- -- -- --',
// Change:
// mov esi, 0x104
'BE 04 01 00 00',
// push esi
'56',
// call -- -- -- --
'E8 -- -- -- --'].join(' ')),
replace: patchHexToBytes([
// call DWORD PTR ds:-- -- -- --
'FF 15 -- -- -- --',
// Changed:
// mov esi, 0x10000
'BE 00 00 01 00',
// push esi
'56',
// call -- -- -- --
'E8 -- -- -- --'].join(' '))
},
// director-11.0.0-hotfix-3 - director-11.5.0:
{
find: patchHexToBytes([
// call DWORD PTR ds:-- -- -- --
'FF 15 -- -- -- --',
// Change:
// mov edi, 0x104
'BF 04 01 00 00',
// push edi
'57',
// call -- -- -- --
'E8 -- -- -- --'].join(' ')),
replace: patchHexToBytes([
// call DWORD PTR ds:-- -- -- --
'FF 15 -- -- -- --',
// Changed:
// mov edi, 0x10000
'BF 00 00 01 00',
// push edi
'57',
// call -- -- -- --
'E8 -- -- -- --'].join(' '))
},
// director-11.5.8 - director-11.5.9:
{
find: patchHexToBytes([
// push -- -- -- --
'68 -- -- -- --',
// push edi
'57',
// call esi
'FF D6',
// Change:
// push 0x208
'68 08 02 00 00',
// call -- -- -- --
'E8 -- -- -- --'].join(' ')),
replace: patchHexToBytes([
// push -- -- -- --
'68 -- -- -- --',
// push edi
'57',
// call esi
'FF D6',
// Changed:
// push 0x20000
'68 00 00 02 00',
// call -- -- -- --
'E8 -- -- -- --'].join(' '))
},
// director-12.0.0 - director-12.0.2:
{
find: patchHexToBytes([
// push -- -- -- --
'68 -- -- -- --',
// push ebx
'53',
// call edi
'FF D7',
// Change:
// push 0x208
'68 08 02 00 00',
// call -- -- -- --
'E8 -- -- -- --'].join(' ')),
replace: patchHexToBytes([
// push -- -- -- --
'68 -- -- -- --',
// push ebx
'53',
// call edi
'FF D7',
// Changed:
// push 0x20000
'68 00 00 02 00',
// call -- -- -- --
'E8 -- -- -- --'].join(' '))
}];
/**
* Patch data buffer once.
*
* @param data Data buffer.
* @param candidates Patch candidates.
* @param name Patch name.
*/
function patchDataOnce(data, candidates, name) {
// Search the buffer for patch candidates.
let foundOffset = -1;
let foundPatch = [];
for (const patch of candidates) {
const {
find,
replace
} = patch;
if (replace.length !== find.length) {
throw new Error('Internal error');
}
const end = data.length - find.length;
for (let i = 0; i < end; i++) {
let found = true;
const {
length
} = find;
for (let j = 0; j < length; j++) {
const b = find[j];
if (b !== null && data[i + j] !== b) {
found = false;
break;
}
}
if (!found) {
continue;
}
if (foundOffset !== -1) {
throw new Error(`Multiple patch candidates found for: ${name}`);
}
// Remember patch to apply.
foundOffset = i;
foundPatch = replace;
}
}
if (foundOffset === -1) {
throw new Error(`No patch candidates found for: ${name}`);
}
// Apply the patch to the buffer, and write to file.
const {
length
} = foundPatch;
for (let i = 0; i < length; i++) {
const b = foundPatch[i];
if (b !== null) {
data[foundOffset + i] = b;
}
}
}
/**
* Patch Windows Shockwave 3D InstalledDisplayDrivers size.
*
* @param data File data.
*/
function windowsPatch3dDisplayDriversSize(data) {
patchDataOnce(data, patch3dDisplayDriversSizePatches, 'Windows Shockwave 3D InstalledDisplayDrivers Size');
}
//# sourceMappingURL=windows.js.map