Started work on integrating Windows agent icon customization prior to agent signing.

This commit is contained in:
Ylian Saint-Hilaire 2022-08-10 13:12:59 -07:00
parent c5315ba0fc
commit 48c6b42a0b
5 changed files with 109 additions and 49 deletions

View File

@ -50,6 +50,38 @@ function createOutFile(args, filename) {
args.out = outputFileName.join('.');
}
// Hash an object
function hashObject(obj) {
const hash = crypto.createHash('sha384');
hash.update(JSON.stringify(obj));
return hash.digest().toString('hex');
}
// 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 = [];
@ -720,38 +752,6 @@ function createAuthenticodeHandler(path) {
return pkcs7raw;
}
// Hash an object
obj.hashObject = function (obj) {
const hash = crypto.createHash('sha384');
hash.update(JSON.stringify(obj));
return hash.digest();
}
// Load a .ico file. This will load all icons in the file into a icon group object
obj.loadIcon = function (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;
}
// Get icon information from resource
obj.getIconInfo = function () {
const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
@ -1661,11 +1661,15 @@ function createAuthenticodeHandler(path) {
var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40);
var fullHeader = readFileSlice(written, fullHeaderLen);
// Compute the size of the resource segment
//const resSizes = { tables: 0, items: 0, names: 0, data: 0 };
//getResourceSectionSize(obj.resources, resSizes);
// Calculate the location and original and new size of the resource segment
var fileAlign = obj.header.peWindows.fileAlignment
var resPtr = obj.header.sections['.rsrc'].rawAddr;
var oldResSize = obj.header.sections['.rsrc'].rawSize;
var newResSize = obj.header.sections['.rsrc'].rawSize; // Testing 102400
var newResSize = obj.header.sections['.rsrc'].rawSize; // TODO: resSizes.data;
var resDeltaSize = newResSize - oldResSize;
// Change PE optional header sizeOfInitializedData standard field
@ -2041,12 +2045,12 @@ function start() {
if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; }
const iconName = parseInt(iconToAddSplit[0]);
const iconFile = iconToAddSplit[1];
const icon = exe.loadIcon(iconFile);
const icon = loadIcon(iconFile);
if (icon == null) { console.log("Unable to load icon: " + iconFile); return; }
if (icons[iconName] != null) {
const iconHash = exe.hashObject(icon); // Compute the new icon group hash
const iconHash2 = exe.hashObject(icons[iconName]); // Computer the old icon group hash
if (iconHash.toString('hex') != iconHash2.toString('hex')) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
const iconHash = hashObject(icon); // Compute the new icon group hash
const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash
if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
} else {
icons[iconName] = icon; // We are adding an icon group
resChanges = true;
@ -2261,4 +2265,5 @@ if (require.main === module) { start(); }
// Exports
module.exports.createAuthenticodeHandler = createAuthenticodeHandler;
module.exports.loadCertificates = loadCertificates;
module.exports.loadIcon = loadIcon;
module.exports.hashObject = hashObject;

View File

@ -574,6 +574,7 @@
"additionalProperties": false,
"description": "Use this section to set resource metadata of the Windows agents prior to signing. In Windows, you can right-click and select properties to view these values.",
"properties": {
"icon": { "type": "string", "default": null, "description": "DO NOT USE. THIS FEATURE DOES NOT WORK YET. Sets the agent icon, this is the name of a .ico file with the file placed in the meshcentral-data folder." },
"fileDescription": { "type": "string", "description": "Executable file description." },
"fileVersion": { "type": "string", "description": "Executable file version, in the form of 'n.n.n.n', for example: '1.2.3.4'." },
"internalName": { "type": "string", "description": "Executable internal name." },

View File

@ -1368,6 +1368,25 @@ function CreateMeshCentralServer(config, args) {
if ((obj.config.domains[i].agentfileinfo.fileversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.fileversionnumber; }
if ((obj.config.domains[i].agentfileinfo.productversionnumber != null) && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.productversionnumber) == null)) { delete obj.config.domains[i].agentfileinfo.productversionnumber; }
if ((obj.config.domains[i].agentfileinfo.fileversionnumber == null) && (typeof obj.config.domains[i].agentfileinfo.fileversion == 'string') && (obj.common.parseVersion(obj.config.domains[i].agentfileinfo.fileversion) != null)) { obj.config.domains[i].agentfileinfo.fileversionnumber = obj.config.domains[i].agentfileinfo.fileversion; }
if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
// Load the agent .ico file
var icon = null;
try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
if (icon != null) {
// The icon file was correctly loaded
obj.config.domains[i].agentfileinfo.icon = icon;
obj.config.domains[i].agentfileinfo.iconhash = require('./authenticode.js').hashObject(icon);
} else {
// Failed to load the icon file, display a server warning
addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
delete obj.config.domains[i].agentfileinfo.icon;
delete obj.config.domains[i].agentfileinfo.iconhash;
}
} else {
// Invalid icon file path
delete obj.config.domains[i].agentfileinfo.icon;
delete obj.config.domains[i].agentfileinfo.iconhash;
}
}
}
@ -2951,20 +2970,23 @@ function CreateMeshCentralServer(config, args) {
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
if (domain.agentfileinfo[propl] != versionStrings[prop]) { destinationAgentOk = false; break; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
if (orgVersionStrings[prop] != versionStrings[prop]) { destinationAgentOk = false; break; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
// Check file version number
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
if (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
if (orgVersionStrings['~FileVersion'] != versionStrings['~FileVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
}
}
// Check product version number
if (destinationAgentOk == true) {
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
if (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // If the resource we want is not the same as the destination executable, we need to re-sign the agent.
} else {
@ -2972,6 +2994,20 @@ function CreateMeshCentralServer(config, args) {
}
}
// Check the agent icon
if ((destinationAgentOk == true) && (domain.agentfileinfo != null) && (domain.agentfileinfo.iconhash != null)) {
const agentIconGroups = destinationAgent.getIconInfo();
if (agentIconGroups != null) {
const agentIconGroupNames = Object.keys(agentIconGroups);
if (agentIconGroupNames.length > 0) {
const agentMainIconGroupName = agentIconGroupNames[0];
const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentIconGroups[agentMainIconGroupName]);
if (agentMainIconGroupHash != domain.agentfileinfo.iconhash) { destinationAgentOk = false; } // If the existing agent icon does not match the desired icon, we need to re-sign the agent.
}
}
}
}
// If everything looks ok, runs a hash of the original and destination agent skipping the CRC, resource and signature blocks. If different, sign the agent again.
if ((destinationAgentOk == true) && (originalAgent.getHashNoResources('sha384').compare(destinationAgent.getHashNoResources('sha384')) != 0)) { destinationAgentOk = false; }
@ -3003,17 +3039,33 @@ function CreateMeshCentralServer(config, args) {
if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
versionStrings = originalAgent.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
// Change the agent string properties
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
}
// Change the agent file version
if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
}
// Change the agent product version
if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
}
if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
// Change the agent icon
if (domain.agentfileinfo.icon != null) {
const agentIconGroups = originalAgent.getIconInfo();
if (agentIconGroups != null) {
const agentIconGroupNames = Object.keys(agentIconGroups);
if (agentIconGroupNames.length > 0) {
const agentMainIconGroupName = agentIconGroupNames[0];
agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
originalAgent.setIconInfo(agentIconGroups);
}
}
}
}
const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone

View File

@ -308,6 +308,7 @@
"fileName": "compagnyagent"
},
"_agentFileInfo": {
"icon": "agent.ico",
"filedescription": "sample_filedescription",
"fileversion": "0.1.2.3",
"internalname": "sample_internalname",

View File

@ -2328,7 +2328,8 @@
19: "SMS gateway has limited use in LAN mode.",
20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
22: "Failed to sign agent {0}: {1}"
22: "Failed to sign agent {0}: {1}",
23: "Unable to load agent icon file: {0}."
};
var x = '';
for (var i in message.warnings) {