meshcentral
Version:
Web based remote computer management server
874 lines (778 loc) • 145 kB
JavaScript
/**
* @description Authenticode parsing
* @author Ylian Saint-Hilaire & Bryan Roe
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/
/*jslint node: true */
/*jshint node: true */
/*jshint strict:false */
/*jshint -W097 */
/*jshint esversion: 6 */
"use strict";
const fs = require('fs');
const crypto = require('crypto');
const forge = require('node-forge');
const pki = forge.pki;
const p7 = require('./pkcs7-modified');
// Generate a test self-signed certificate with code signing extension
function createSelfSignedCert(args) {
var keys = pki.rsa.generateKeyPair(2048);
var cert = pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = (typeof args.serial == 'string')?args.serial:'012345'; // Serial number must always have a single leading '0', otherwise toPEM/fromPEM will not work right.
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
var attrs = [];
if (typeof args.cn == 'string') { attrs.push({ name: 'commonName', value: args.cn }); }
if (typeof args.country == 'string') { attrs.push({ name: 'countryName', value: args.country }); }
if (typeof args.state == 'string') { attrs.push({ name: 'ST', value: args.state }); }
if (typeof args.locality == 'string') { attrs.push({ name: 'localityName', value: args.locality }); }
if (typeof args.org == 'string') { attrs.push({ name: 'organizationName', value: args.org }); }
if (typeof args.orgunit == 'string') { attrs.push({ name: 'OU', value: args.orgunit }); }
cert.setSubject(attrs);
cert.setIssuer(attrs);
cert.setExtensions([{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]);
cert.sign(keys.privateKey, forge.md.sha384.create());
return { cert: cert, key: keys.privateKey, extraCerts: [] };
}
// Create the output filename if not already specified
function createOutFile(args, filename) {
if (typeof args.out == 'string') return;
var outputFileName = filename.split('.');
outputFileName[outputFileName.length - 2] += '-out';
args.out = outputFileName.join('.');
}
// Hash an object
function hashObject(obj) {
if (obj == null) { return null; }
const hash = crypto.createHash('sha384');
if (Buffer.isBuffer(obj)) { hash.update(obj); } else { hash.update(JSON.stringify(obj)); }
return hash.digest().toString('hex');
}
// Load a .bmp file.
function loadBitmap(bitmapFile) {
var bitmapData = null;
try { bitmapData = fs.readFileSync(bitmapFile); } catch (ex) { }
if ((bitmapData == null) || (bitmapData.length < 14) || (bitmapData[0] != 0x42) || (bitmapData[1] != 0x4D)) return null;
return bitmapData.slice(14);
}
// Load a .ico file. This will load all icons in the file into a icon group object
function loadIcon(iconFile) {
var iconData = null;
try { iconData = fs.readFileSync(iconFile); } catch (ex) { }
if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
if (r.resType != 1) return null;
var ptr = 6;
for (var i = 1; i <= r.resCount; i++) {
var icon = {};
icon.width = iconData[ptr + 0];
icon.height = iconData[ptr + 1];
icon.colorCount = iconData[ptr + 2];
icon.planes = iconData.readUInt16LE(ptr + 4);
icon.bitCount = iconData.readUInt16LE(ptr + 6);
icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
icon.iconCursorId = i;
const offset = iconData.readUInt32LE(ptr + 12);
icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
r.icons[i] = icon;
ptr += 16;
}
return r;
}
// Load certificates and private key from PEM files
function loadCertificates(pemFileNames) {
var certs = [], keys = [];
if (pemFileNames == null) return;
if (typeof pemFileNames == 'string') { pemFileNames = [pemFileNames]; }
for (var i in pemFileNames) {
try {
// Read certificate
var pem = fs.readFileSync(pemFileNames[i]).toString();
var pemCerts = pem.split('-----BEGIN CERTIFICATE-----');
for (var j in pemCerts) {
var k = pemCerts[j].indexOf('-----END CERTIFICATE-----');
if (k >= 0) { certs.push(pki.certificateFromPem('-----BEGIN CERTIFICATE-----' + pemCerts[j].substring(0, k) + '-----END CERTIFICATE-----')); }
}
var PemKeys = pem.split('-----BEGIN RSA PRIVATE KEY-----');
for (var j in PemKeys) {
var k = PemKeys[j].indexOf('-----END RSA PRIVATE KEY-----');
if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN RSA PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END RSA PRIVATE KEY-----')); }
}
PemKeys = pem.split('-----BEGIN PRIVATE KEY-----');
for (var j in PemKeys) {
var k = PemKeys[j].indexOf('-----END PRIVATE KEY-----');
if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END PRIVATE KEY-----')); }
}
} catch (ex) { }
}
if ((certs.length == 0) || (keys.length != 1)) return; // No certificates or private keys
var r = { cert: certs[0], key: keys[0], extraCerts: [] }
if (certs.length > 1) { for (var i = 1; i < certs.length; i++) { r.extraCerts.push(certs[i]); } }
return r;
}
function createAuthenticodeHandler(path) {
const obj = {};
obj.header = { path: path }
// Read a file slice
function readFileSlice(start, length) {
var buffer = Buffer.alloc(length);
var len = fs.readSync(obj.fd, buffer, 0, buffer.length, start);
if (len < buffer.length) { buffer = buffer.slice(0, len); }
return buffer;
}
// Close the file
obj.close = function () {
if (obj.fd == null) return;
fs.closeSync(obj.fd);
delete obj.fd;
}
// Private OIDS
obj.Oids = {
SPC_INDIRECT_DATA_OBJID: '1.3.6.1.4.1.311.2.1.4',
SPC_STATEMENT_TYPE_OBJID: '1.3.6.1.4.1.311.2.1.11',
SPC_SP_OPUS_INFO_OBJID: '1.3.6.1.4.1.311.2.1.12',
SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.21',
SPC_COMMERCIAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.22',
SPC_MS_JAVA_SOMETHING: '1.3.6.1.4.1.311.15.1',
SPC_PE_IMAGE_DATA_OBJID: '1.3.6.1.4.1.311.2.1.15',
SPC_CAB_DATA_OBJID: '1.3.6.1.4.1.311.2.1.25',
SPC_TIME_STAMP_REQUEST_OBJID: '1.3.6.1.4.1.311.3.2.1',
SPC_SIPINFO_OBJID: '1.3.6.1.4.1.311.2.1.30',
SPC_PE_IMAGE_PAGE_HASHES_V1: '1.3.6.1.4.1.311.2.3.1',
SPC_PE_IMAGE_PAGE_HASHES_V2: '1.3.6.1.4.1.311.2.3.2',
SPC_NESTED_SIGNATURE_OBJID: '1.3.6.1.4.1.311.2.4.1',
SPC_RFC3161_OBJID: '1.3.6.1.4.1.311.3.3.1'
}
// Open the file and read header information
function openFile() {
if (obj.fd != null) return true;
// Open the file descriptor
obj.path = path;
try { obj.fd = fs.openSync(path, 'r'); } catch (ex) { return false; } // Unable to open file
obj.stats = fs.fstatSync(obj.fd);
obj.filesize = obj.stats.size;
if (obj.filesize < 64) { obj.close(); return false; } // File too short.
// Read the DOS header (64 bytes)
var buf = readFileSlice(60, 4);
obj.header.peHeaderLocation = buf.readUInt32LE(0); // The DOS header is 64 bytes long, the last 4 bytes are a pointer to the PE header.
obj.header.peOptionalHeaderLocation = obj.header.peHeaderLocation + 24; // The PE optional header is located just after the PE header which is 24 bytes long.
// Check file size and signature
if (obj.filesize < (160 + obj.header.peHeaderLocation)) { obj.close(); return false; } // Invalid SizeOfHeaders.
if (readFileSlice(obj.header.peHeaderLocation, 4).toString('hex') != '50450000') { obj.close(); return false; } // Invalid PE header, must start with "PE" (HEX: 50 45 00 00).
// Read the COFF header
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image
var coffHeader = readFileSlice(obj.header.peHeaderLocation + 4, 20)
obj.header.coff = {};
obj.header.coff.machine = coffHeader.readUInt16LE(0);
obj.header.coff.numberOfSections = coffHeader.readUInt16LE(2);
obj.header.coff.timeDateStamp = coffHeader.readUInt32LE(4);
obj.header.coff.pointerToSymbolTable = coffHeader.readUInt32LE(8);
obj.header.coff.numberOfSymbols = coffHeader.readUInt32LE(12);
obj.header.coff.sizeOfOptionalHeader = coffHeader.readUInt16LE(16);
obj.header.coff.characteristics = coffHeader.readUInt16LE(18);
// Read the entire PE optional header
var optinalHeader = readFileSlice(obj.header.peOptionalHeaderLocation, obj.header.coff.sizeOfOptionalHeader);
// Decode the PE optional header standard fields
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only
obj.header.peStandard = {};
obj.header.peStandard.magic = optinalHeader.readUInt16LE(0);
switch (obj.header.peStandard.magic) { // Check magic value
case 0x020B: obj.header.pe32plus = 1; break;
case 0x010B: obj.header.pe32plus = 0; break;
default: { obj.close(); return false; } // Invalid Magic in PE
}
obj.header.peStandard.majorLinkerVersion = optinalHeader[2];
obj.header.peStandard.minorLinkerVersion = optinalHeader[3];
obj.header.peStandard.sizeOfCode = optinalHeader.readUInt32LE(4);
obj.header.peStandard.sizeOfInitializedData = optinalHeader.readUInt32LE(8);
obj.header.peStandard.sizeOfUninitializedData = optinalHeader.readUInt32LE(12);
obj.header.peStandard.addressOfEntryPoint = optinalHeader.readUInt32LE(16);
obj.header.peStandard.baseOfCode = optinalHeader.readUInt32LE(20);
if (obj.header.pe32plus == 0) { obj.header.peStandard.baseOfData = optinalHeader.readUInt32LE(24); }
// Decode the PE optional header windows fields
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-windows-specific-fields-image-only
obj.header.peWindows = {}
if (obj.header.pe32plus == 0) {
// 32bit header
//obj.header.peWindows.imageBase = optinalHeader.readUInt32LE(28);
obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32);
obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36);
obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40);
obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42);
obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44);
obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46);
obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48);
obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50);
obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52);
obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56);
obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60);
obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64);
obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68);
obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70);
//obj.header.peWindows.sizeOfStackReserve = optinalHeader.readUInt32LE(72);
//obj.header.peWindows.sizeOfStackCommit = optinalHeader.readUInt32LE(76);
//obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readUInt32LE(80);
//obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readUInt32LE(84);
obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(88);
obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(92);
} else {
// 64bit header
//obj.header.peWindows.imageBase = optinalHeader.readBigUInt64LE(24); // TODO: readBigUInt64LE is not supported in older NodeJS versions
obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32);
obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36);
obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40);
obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42);
obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44);
obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46);
obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48);
obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50);
obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52);
obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56);
obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60);
obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64);
obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68);
obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70);
//obj.header.peWindows.sizeOfStackReserve = optinalHeader.readBigUInt64LE(72);
//obj.header.peWindows.sizeOfStackCommit = optinalHeader.readBigUInt64LE(80);
//obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readBigUInt64LE(88);
//obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readBigUInt64LE(96);
obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(104);
obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(108);
}
// Decode the PE optional header data directories
// https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
obj.header.dataDirectories = {}
const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes.
obj.header.dataDirectories.exportTable = { addr: optinalHeader.readUInt32LE(96 + pePlusOffset), size: optinalHeader.readUInt32LE(100 + pePlusOffset) };
obj.header.dataDirectories.importTable = { addr: optinalHeader.readUInt32LE(104 + pePlusOffset), size: optinalHeader.readUInt32LE(108 + pePlusOffset) };
obj.header.dataDirectories.resourceTable = { addr: optinalHeader.readUInt32LE(112 + pePlusOffset), size: optinalHeader.readUInt32LE(116 + pePlusOffset) }; // Same as .rsrc virtual address & size
obj.header.dataDirectories.exceptionTableAddr = { addr: optinalHeader.readUInt32LE(120 + pePlusOffset), size: optinalHeader.readUInt32LE(124 + pePlusOffset) }; // Same as .pdata virtual address & size
obj.header.dataDirectories.certificateTable = { addr: optinalHeader.readUInt32LE(128 + pePlusOffset), size: optinalHeader.readUInt32LE(132 + pePlusOffset) };
obj.header.dataDirectories.baseRelocationTable = { addr: optinalHeader.readUInt32LE(136 + pePlusOffset), size: optinalHeader.readUInt32LE(140 + pePlusOffset) }; // Same as .reloc virtual address & size
obj.header.dataDirectories.debug = { addr: optinalHeader.readUInt32LE(144 + pePlusOffset), size: optinalHeader.readUInt32LE(148 + pePlusOffset) };
// obj.header.dataDirectories.architecture = optinalHeader.readBigUInt64LE(152 + pePlusOffset); // Must be zero
obj.header.dataDirectories.globalPtr = { addr: optinalHeader.readUInt32LE(160 + pePlusOffset), size: optinalHeader.readUInt32LE(164 + pePlusOffset) };
obj.header.dataDirectories.tLSTable = { addr: optinalHeader.readUInt32LE(168 + pePlusOffset), size: optinalHeader.readUInt32LE(172 + pePlusOffset) };
obj.header.dataDirectories.loadConfigTable = { addr: optinalHeader.readUInt32LE(176 + pePlusOffset), size: optinalHeader.readUInt32LE(180 + pePlusOffset) };
obj.header.dataDirectories.boundImport = { addr: optinalHeader.readUInt32LE(184 + pePlusOffset), size: optinalHeader.readUInt32LE(188 + pePlusOffset) };
obj.header.dataDirectories.iAT = { addr: optinalHeader.readUInt32LE(192 + pePlusOffset), size: optinalHeader.readUInt32LE(196 + pePlusOffset) };
obj.header.dataDirectories.delayImportDescriptor = { addr: optinalHeader.readUInt32LE(200 + pePlusOffset), size: optinalHeader.readUInt32LE(204 + pePlusOffset) };
obj.header.dataDirectories.clrRuntimeHeader = { addr: optinalHeader.readUInt32LE(208 + pePlusOffset), size: optinalHeader.readUInt32LE(212 + pePlusOffset) };
// obj.header.dataDirectories.reserved = optinalHeader.readBigUInt64LE(216 + pePlusOffset); // Must be zero
// Get the certificate table location and size
obj.header.sigpos = obj.header.dataDirectories.certificateTable.addr;
obj.header.siglen = obj.header.dataDirectories.certificateTable.size
obj.header.signed = ((obj.header.sigpos != 0) && (obj.header.siglen != 0));
// The section headers are located after the optional PE header
obj.header.SectionHeadersPtr = obj.header.peOptionalHeaderLocation + obj.header.coff.sizeOfOptionalHeader;
// Read the sections
obj.header.sections = {};
for (var i = 0; i < obj.header.coff.numberOfSections; i++) {
var section = {};
buf = readFileSlice(obj.header.SectionHeadersPtr + (i * 40), 40);
if ((buf[0] != 46) && (buf[0] != 95)) { obj.close(); return false; }; // Name of the section must start with a dot or underscore. If not, something is wrong.
var sectionName = buf.slice(0, 8).toString().trim('\0');
var j = sectionName.indexOf('\0');
if (j >= 0) { sectionName = sectionName.substring(0, j); } // Trim any trailing zeroes
section.ptr = obj.header.SectionHeadersPtr + (i * 40);
section.virtualSize = buf.readUInt32LE(8);
section.virtualAddr = buf.readUInt32LE(12);
section.rawSize = buf.readUInt32LE(16);
section.rawAddr = buf.readUInt32LE(20);
section.relocAddr = buf.readUInt32LE(24);
section.lineNumbers = buf.readUInt32LE(28);
section.relocNumber = buf.readUInt16LE(32);
section.lineNumbersNumber = buf.readUInt16LE(34);
section.characteristics = buf.readUInt32LE(36);
obj.header.sections[sectionName] = section;
}
// Compute the checkSum value for this file
obj.header.peWindows.checkSumActual = runChecksum();
// If there is a .rsrc section, read the resource information and locations
if (obj.header.sections['.rsrc'] != null) {
obj.resources = readResourceTable(obj.header.sections['.rsrc'].rawAddr, 0); // Read all resources recursively
}
if (obj.header.signed) {
// Read signature block
// Check if the file size allows for the signature block
if (obj.filesize < (obj.header.sigpos + obj.header.siglen)) { obj.close(); return false; } // Executable file too short to contain the signature block.
// Remove the padding if needed
var i, pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8);
var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
// Decode the signature block and check that it's valid
var pkcs7der = null, valid = false;
try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw)); } catch (ex) { }
try { valid = ((pkcs7der != null) && (forge.asn1.derToOid(pkcs7der.value[1].value[0].value[2].value[0].value) == "1.3.6.1.4.1.311.2.1.4")); } catch (ex) { }
if (pkcs7der == null) {
// Can't decode the signature
obj.header.sigpos = 0;
obj.header.siglen = 0;
obj.header.signed = false;
} else {
// To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
// Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
// Decode the PKCS7 message
var pkcs7 = null, pkcs7content = null;
try {
pkcs7 = p7.messageFromAsn1(pkcs7der);
pkcs7content = pkcs7.rawCapture.content.value[0];
} catch (ex) { }
if ((pkcs7 == null) || (pkcs7content == null)) {
// Can't decode the signature
obj.header.sigpos = 0;
obj.header.siglen = 0;
obj.header.signed = false;
} else {
// Verify a PKCS#7 signature
// Verify is not currently supported in node-forge, but if implemented in the future, this code could work.
//var caStore = forge.pki.createCaStore();
//for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); }
// Return is true if all signatures are valid and chain up to a provided CA
//if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); }
// Get the signing attributes
obj.signingAttribs = [];
try {
for (var i in pkcs7.rawCapture.authenticatedAttributes) {
if (
(pkcs7.rawCapture.authenticatedAttributes[i].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) &&
(forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) {
for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) {
if (
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) &&
(pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null)
) {
var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value;
if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else {
var r = ''; // This string value is in UCS2 format, convert it to a normal string.
for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); }
obj.signingAttribs.push(r);
}
}
}
}
}
} catch (ex) { }
// Set the certificate chain
obj.certificates = pkcs7.certificates;
// Set the signature
obj.signature = Buffer.from(pkcs7.rawCapture.signature, 'binary');
// Get the file hashing algorithm
var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value);
switch (hashAlgoOid) {
case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; }
case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; }
case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; }
case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; }
case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; }
}
// Get the signed file hash
obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary')
// Compute the actual file hash
if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); }
}
}
}
return true;
}
// Make a timestamp signature request
obj.timeStampRequest = function (args, func) {
// Create the timestamp request in DER format
const asn1 = forge.asn1;
const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
const asn1obj =
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature.toString('binary')) // Signature here
])
])
]);
// Serialize an ASN.1 object to DER format in Base64
const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
// Make an HTTP request
const options = { url: args.time, proxy: args.proxy };
// Make a request to the time server
httpRequest(options, requestBody, function (err, data) {
if (err != null) { func(err); return; }
// Decode the timestamp signature block
var timepkcs7der = null;
try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
// Decode the executable signature block
var pkcs7der = null;
try {
var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary')));
// Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
// TODO: We could look to see if the certificate is already present in the executable
const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
// Remove any existing time stamp signatures
var newValues = [];
for (var i in pkcs7der.value[1].value[0].value[4].value[0].value) {
const j = pkcs7der.value[1].value[0].value[4].value[0].value[i];
if ((j.tagClass != 128) || (j.type != 1)) { newValues.push(j); } // If this is not a time stamp, add it to out new list.
}
pkcs7der.value[1].value[0].value[4].value[0].value = newValues; // Set the new list
// Get the time signature and add it to the executables PKCS7
const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
const asn1obj2 =
asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
timeasn1Signature
])
]);
pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
// Re-encode the executable signature block
const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
// Open the output file
var output = null;
try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
if (output == null) return false;
var tmp, written = 0;
var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
// Compute pre-header length and copy that to the new file
var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
var tmp = readFileSlice(written, preHeaderLen);
fs.writeSync(output, tmp);
written += tmp.length;
// Quad Align the results, adding padding if necessary
var len = executableSize + p7signature.length;
var padding = (8 - ((len) % 8)) % 8;
// Write the signature header
var addresstable = Buffer.alloc(8);
addresstable.writeUInt32LE(executableSize);
addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
fs.writeSync(output, addresstable);
written += addresstable.length;
// Copy the rest of the file until the start of the signature block
while ((executableSize - written) > 0) {
tmp = readFileSlice(written, Math.min(executableSize - written, 65536));
fs.writeSync(output, tmp);
written += tmp.length;
}
// Write the signature block header and signature
var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
win.writeUInt16LE(512, 4); // WORD revision
win.writeUInt16LE(2, 6); // WORD type
fs.writeSync(output, win);
fs.writeSync(output, p7signature);
if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
written += (p7signature.length + padding + 8);
// Compute the checksum and write it in the PE header checksum location
var tmp = Buffer.alloc(4);
tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
// Close the file
fs.closeSync(output);
// Indicate we are done
func(null);
} catch (ex) { func('' + ex); return; }
});
}
// Read a resource table.
// ptr: The pointer to the start of the resource section
// offset: The offset start of the resource table to read
function readResourceTable(ptr, offset) {
var buf = readFileSlice(ptr + offset, 16);
var r = {};
r.characteristics = buf.readUInt32LE(0);
r.timeDateStamp = buf.readUInt32LE(4);
r.majorVersion = buf.readUInt16LE(8);
r.minorVersion = buf.readUInt16LE(10);
var numberOfNamedEntries = buf.readUInt16LE(12);
var numberOfIdEntries = buf.readUInt16LE(14);
r.entries = [];
var totalResources = numberOfNamedEntries + numberOfIdEntries;
//console.log('readResourceTable', offset, 16 + (totalResources) * 8, offset + (16 + (totalResources) * 8));
for (var i = 0; i < totalResources; i++) {
buf = readFileSlice(ptr + offset + 16 + (i * 8), 8);
var resource = {};
resource.name = buf.readUInt32LE(0);
var offsetToData = buf.readUInt32LE(4);
if ((resource.name & 0x80000000) != 0) {
var oname = resource.name;
resource.name = readLenPrefixUnicodeString(ptr + (resource.name - 0x80000000));
//console.log('readResourceName', offset + (oname - 0x80000000), 2 + (resource.name.length * 2), offset + (oname - 0x80000000) + (2 + resource.name.length * 2), resource.name);
}
if ((offsetToData & 0x80000000) != 0) { resource.table = readResourceTable(ptr, offsetToData - 0x80000000); } else { resource.item = readResourceItem(ptr, offsetToData); }
r.entries.push(resource);
}
return r;
}
// Read a resource item
// ptr: The pointer to the start of the resource section
// offset: The offset start of the resource item to read
function readResourceItem(ptr, offset) {
//console.log('readResourceItem', offset, 16, offset + 16);
var buf = readFileSlice(ptr + offset, 16), r = {};
r.offsetToData = buf.readUInt32LE(0);
r.size = buf.readUInt32LE(4);
//console.log('readResourceData', r.offsetToData - obj.header.sections['.rsrc'].virtualAddr, r.size, r.offsetToData + r.size - obj.header.sections['.rsrc'].virtualAddr);
r.codePage = buf.readUInt32LE(8);
//r.reserved = buf.readUInt32LE(12);
return r;
}
// Read a unicode stting that starts with the string length as the first byte.
function readLenPrefixUnicodeString(ptr) {
var nameLen = readFileSlice(ptr, 2).readUInt16LE(0);
var buf = readFileSlice(ptr + 2, nameLen * 2), name = '';
for (var i = 0; i < nameLen; i++) { name += String.fromCharCode(buf.readUInt16LE(i * 2)); }
return name;
}
// Generate a complete resource section and pad the section
function generateResourceSection(resources) {
// Call a resursive method the compute the size needed for each element
const resSizes = { tables: 0, items: 0, names: 0, data: 0 };
getResourceSectionSize(resources, resSizes);
// Pad the resource section & allocate the buffer
const fileAlign = obj.header.peWindows.fileAlignment
var resSizeTotal = resSizes.tables + resSizes.items + resSizes.names + resSizes.data;
var resNoPadding = resSizeTotal + 4; // TODO: Not sure why this is off by 4
if ((resSizeTotal % fileAlign) != 0) { resSizeTotal += (fileAlign - (resSizeTotal % fileAlign)); }
const resSectionBuffer = Buffer.alloc(resSizeTotal);
// Write the resource section, calling a recursive method
const resPointers = { tables: 0, items: resSizes.tables, names: resSizes.tables + resSizes.items, data: resSizes.tables + resSizes.items + resSizes.names };
createResourceSection(resources, resSectionBuffer, resPointers);
//console.log('generateResourceSection', resPointers);
// Done, return the result
return { size: resNoPadding, data: resSectionBuffer };
}
// Return the total size of a resource header, this is a recursive method
function getResourceSectionSize(resources, sizes) {
sizes.tables += (16 + (resources.entries.length * 8));
for (var i in resources.entries) {
if (typeof resources.entries[i].name == 'string') {
var dataSize = (2 + (resources.entries[i].name.length * 2));
if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
sizes.names += dataSize;
}
if (resources.entries[i].table) { getResourceSectionSize(resources.entries[i].table, sizes); }
else if (resources.entries[i].item) {
sizes.items += 16;
if (resources.entries[i].item.buffer) {
sizes.data += resources.entries[i].item.buffer.length;
} else {
var dataSize = resources.entries[i].item.size;
if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
sizes.data += dataSize;
}
}
}
}
// Write the resource section in the buffer, this is a recursive method
function createResourceSection(resources, buf, resPointers) {
var numberOfNamedEntries = 0, numberOfIdEntries = 0, ptr = resPointers.tables;
//console.log('createResourceSection', resPointers, ptr);
// Figure out how many items we have to save
for (var i in resources.entries) {
if (typeof resources.entries[i].name == 'string') { numberOfNamedEntries++; } else { numberOfIdEntries++; }
}
// Move the table pointer forward
resPointers.tables += (16 + (8 * numberOfNamedEntries) + (8 * numberOfIdEntries));
// Write the table header
buf.writeUInt32LE(resources.characteristics, ptr);
buf.writeUInt32LE(resources.timeDateStamp, ptr + 4);
buf.writeUInt16LE(resources.majorVersion, ptr + 8);
buf.writeUInt16LE(resources.minorVersion, ptr + 10);
buf.writeUInt16LE(numberOfNamedEntries, ptr + 12);
buf.writeUInt16LE(numberOfIdEntries, ptr + 14);
// For each table entry, write the entry for it
for (var i in resources.entries) {
// Write the name
var name = resources.entries[i].name;
if (typeof resources.entries[i].name == 'string') {
// Set the pointer to the name
name = resPointers.names + 0x80000000;
// Write the name length, followed by the name string in unicode
buf.writeUInt16LE(resources.entries[i].name.length, resPointers.names);
for (var j = 0; j < resources.entries[i].name.length; j++) {
buf.writeUInt16LE(resources.entries[i].name.charCodeAt(j), 2 + resPointers.names + (j * 2));
}
// Move the names pointer forward, 8 byte align
var dataSize = (2 + (resources.entries[i].name.length * 2));
if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
resPointers.names += dataSize;
}
buf.writeUInt32LE(name, ptr + 16 + (i * 8));
// Write the data
var data;
if (resources.entries[i].table) {
// This is a pointer to a table entry
data = resPointers.tables + 0x80000000;
createResourceSection(resources.entries[i].table, buf, resPointers);
} else if (resources.entries[i].item) {
// This is a pointer to a data entry
data = resPointers.items;
// Write the data
var entrySize = 0;
if (resources.entries[i].item.buffer) {
// Write the data from given buffer
resources.entries[i].item.buffer.copy(buf, resPointers.data, 0, resources.entries[i].item.buffer.length);
entrySize = resources.entries[i].item.buffer.length;
} else {
// Write the data from original file
const actualPtr = (resources.entries[i].item.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + obj.header.sections['.rsrc'].rawAddr;
const tmp = readFileSlice(actualPtr, resources.entries[i].item.size);
tmp.copy(buf, resPointers.data, 0, tmp.length);
entrySize = resources.entries[i].item.size;;
}
// Write the item entry
buf.writeUInt32LE(resPointers.data + obj.header.sections['.rsrc'].virtualAddr, resPointers.items); // Write the pointer relative to the virtual address
buf.writeUInt32LE(entrySize, resPointers.items + 4);
buf.writeUInt32LE(resources.entries[i].item.codePage, resPointers.items + 8);
buf.writeUInt32LE(resources.entries[i].item.reserved, resPointers.items + 12);
// Move items pointers forward
resPointers.items += 16;
var dataSize = entrySize;
if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
resPointers.data += dataSize;
}
buf.writeUInt32LE(data, ptr + 20 + (i * 8));
}
}
// Convert a unicode buffer to a string
function unicodeToString(buf) {
var r = '', c;
for (var i = 0; i < (buf.length / 2) ; i++) {
c = buf.readUInt16LE(i * 2);
if (c != 0) { r += String.fromCharCode(c); } else { return r; }
}
return r;
}
// Convert a string to a unicode buffer
// Input is a string, a buffer to write to and the offset in the buffer (0 is default).
function stringToUnicode(str, buf, offset) {
if (offset == null) { offset = 0; }
for (var i = 0; i < str.length; i++) { buf.writeInt16LE(str.charCodeAt(i), offset + (i * 2)); }
}
var resourceDefaultNames = {
'bitmaps': 2,
'icon': 3,
'dialogs': 5,
'iconGroups': 14,
'versionInfo': 16,
'configurationFiles': 24
}
// Return the raw signature block buffer with padding removed
obj.getRawSignatureBlock = function () {
if ((obj.header.sigpos == 0) || (obj.header.siglen == 0)) return null;
var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8);
var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
return pkcs7raw;
}
// Get bitmaps information from resource
obj.getBitmapInfo = function () {
const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
// Find and parse each icon
const bitmaps = {}
for (var i = 0; i < obj.resources.entries.length; i++) {
if (obj.resources.entries[i].name == resourceDefaultNames.bitmaps) {
for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
const bitmapName = obj.resources.entries[i].table.entries[j].name;
const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
bitmaps[bitmapName] = readFileSlice(actualPtr, size);
}
}
}
return bitmaps;
}
// Get icon information from resource
obj.getIconInfo = function () {
const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
// Find and parse each icon
const icons = {}
for (var i = 0; i < obj.resources.entries.length; i++) {
if (obj.resources.entries[i].name == resourceDefaultNames.icon) {
for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
const iconName = obj.resources.entries[i].table.entries[j].name;
const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
icons[iconName] = readFileSlice(actualPtr, size);
}
}
}
// Find and parse each icon group
for (var i = 0; i < obj.resources.entries.length; i++) {
if (obj.resources.entries[i].name == resourceDefaultNames.iconGroups) {
for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
const groupName = obj.resources.entries[i].table.entries[j].name;
const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
const group = {};
const groupData = readFileSlice(actualPtr, size);
// Parse NEWHEADER structure: https://docs.microsoft.com/en-us/windows/win32/menurc/newheader
group.resType = groupData.readUInt16LE(2);
group.resCount = groupData.readUInt16LE(4);
// Parse many RESDIR structure: https://docs.microsoft.com/en-us/windows/win32/menurc/resdir
group.icons = {};
for (var p = 6; p < size; p += 14) {
var icon = {}
icon.width = groupData[p];
icon.height = groupData[p + 1];
icon.colorCount = groupData[p + 2];
icon.planes = groupData.readUInt16LE(p + 4);
icon.bitCount = groupData.readUInt16LE(p + 6);
icon.bytesInRes = groupData.readUInt32LE(p + 8);
icon.iconCursorId = groupData.readUInt16LE(p + 12);
icon.icon = icons[icon.iconCursorId];
group.icons[icon.iconCursorId] = icon;
}
// Add an icon group
r[groupName] = group;
}
}
}
return r;
}
// Set bitmap information
obj.setBitmapInfo = function (bitmapInfo) {
// Delete all bitmaps resources
var resourcesEntries = [];
for (var i = 0; i < obj.resources.entries.length; i++) {
if (obj.resources.entries[i].name != resourceDefaultNames.bitmaps) {
resourcesEntries.push(obj.resources.entries[i]);
}
}
obj.resources.entries = resourcesEntries;
// Add all bitmap entries
const bitmapEntry = { name: resourceDefaultNames.bitmaps, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
for (var i in bitmapInfo) {
var name = i;
if (parseInt(i) == name) { name = parseInt(i); }
const bitmapItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: bitmapInfo[i], codePage: 0 } }] } }
bitmapEntry.table.entries.push(bitmapItemEntry);
}
obj.resources.entries.push(bitmapEntry);
// Sort the resources by name. This is required.
function resSort(a, b) {
if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; }
if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; }
if ((typeof a == 'string') && (typeof b == 'number')) { return -1; }
return 1;
}
const names = [];
for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); }