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

Ylian Saint-Hilaire committed Aug 10, 2022 at 13:12 UTC 48c6b42a0b9f5c1bc78fa2dd5e8e389e8a8499bb
5 files changed +109 -49
authenticode.js
+43 -38
@@ -50,6 +50,38 @@ function createOutFile(args, filename) {
50 args.out = outputFileName.join('.');
51 }
52
53 +// Hash an object
54 +function hashObject(obj) {
55 + const hash = crypto.createHash('sha384');
56 + hash.update(JSON.stringify(obj));
57 + return hash.digest().toString('hex');
58 +}
59 +
60 +// Load a .ico file. This will load all icons in the file into a icon group object
61 +function loadIcon(iconFile) {
62 + var iconData = null;
63 + try { iconData = fs.readFileSync(iconFile); } catch (ex) { }
64 + if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
65 + const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
66 + if (r.resType != 1) return null;
67 + var ptr = 6;
68 + for (var i = 1; i <= r.resCount; i++) {
69 + var icon = {};
70 + icon.width = iconData[ptr + 0];
71 + icon.height = iconData[ptr + 1];
72 + icon.colorCount = iconData[ptr + 2];
73 + icon.planes = iconData.readUInt16LE(ptr + 4);
74 + icon.bitCount = iconData.readUInt16LE(ptr + 6);
75 + icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
76 + icon.iconCursorId = i;
77 + const offset = iconData.readUInt32LE(ptr + 12);
78 + icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
79 + r.icons[i] = icon;
80 + ptr += 16;
81 + }
82 + return r;
83 +}
84 +
85 // Load certificates and private key from PEM files
86 function loadCertificates(pemFileNames) {
87 var certs = [], keys = [];
@@ -720,38 +752,6 @@ function createAuthenticodeHandler(path) {
752 return pkcs7raw;
753 }
754
723 - // Hash an object
724 - obj.hashObject = function (obj) {
725 - const hash = crypto.createHash('sha384');
726 - hash.update(JSON.stringify(obj));
727 - return hash.digest();
728 - }
729 -
730 - // Load a .ico file. This will load all icons in the file into a icon group object
731 - obj.loadIcon = function (iconFile) {
732 - var iconData = null;
733 - try { iconData = fs.readFileSync(iconFile); } catch (ex) {}
734 - if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
735 - const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
736 - if (r.resType != 1) return null;
737 - var ptr = 6;
738 - for (var i = 1; i <= r.resCount; i++) {
739 - var icon = {};
740 - icon.width = iconData[ptr + 0];
741 - icon.height = iconData[ptr + 1];
742 - icon.colorCount = iconData[ptr + 2];
743 - icon.planes = iconData.readUInt16LE(ptr + 4);
744 - icon.bitCount = iconData.readUInt16LE(ptr + 6);
745 - icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
746 - icon.iconCursorId = i;
747 - const offset = iconData.readUInt32LE(ptr + 12);
748 - icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
749 - r.icons[i] = icon;
750 - ptr += 16;
751 - }
752 - return r;
753 - }
754 -
755 // Get icon information from resource
756 obj.getIconInfo = function () {
757 const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
@@ -1661,11 +1661,15 @@ function createAuthenticodeHandler(path) {
1661 var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40);
1662 var fullHeader = readFileSlice(written, fullHeaderLen);
1663
1664 + // Compute the size of the resource segment
1665 + //const resSizes = { tables: 0, items: 0, names: 0, data: 0 };
1666 + //getResourceSectionSize(obj.resources, resSizes);
1667 +
1668 // Calculate the location and original and new size of the resource segment
1669 var fileAlign = obj.header.peWindows.fileAlignment
1670 var resPtr = obj.header.sections['.rsrc'].rawAddr;
1671 var oldResSize = obj.header.sections['.rsrc'].rawSize;
1668 - var newResSize = obj.header.sections['.rsrc'].rawSize; // Testing 102400
1672 + var newResSize = obj.header.sections['.rsrc'].rawSize; // TODO: resSizes.data;
1673 var resDeltaSize = newResSize - oldResSize;
1674
1675 // Change PE optional header sizeOfInitializedData standard field
@@ -2041,12 +2045,12 @@ function start() {
2045 if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; }
2046 const iconName = parseInt(iconToAddSplit[0]);
2047 const iconFile = iconToAddSplit[1];
2044 - const icon = exe.loadIcon(iconFile);
2048 + const icon = loadIcon(iconFile);
2049 if (icon == null) { console.log("Unable to load icon: " + iconFile); return; }
2050 if (icons[iconName] != null) {
2047 - const iconHash = exe.hashObject(icon); // Compute the new icon group hash
2048 - const iconHash2 = exe.hashObject(icons[iconName]); // Computer the old icon group hash
2049 - if (iconHash.toString('hex') != iconHash2.toString('hex')) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
2051 + const iconHash = hashObject(icon); // Compute the new icon group hash
2052 + const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash
2053 + if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
2054 } else {
2055 icons[iconName] = icon; // We are adding an icon group
2056 resChanges = true;
@@ -2261,4 +2265,5 @@ if (require.main === module) { start(); }
2265 // Exports
2266 module.exports.createAuthenticodeHandler = createAuthenticodeHandler;
2267 module.exports.loadCertificates = loadCertificates;
2264 -
2268 +module.exports.loadIcon = loadIcon;
2269 +module.exports.hashObject = hashObject;
\ No newline at end of file
meshcentral-config-schema.json
+1
@@ -574,6 +574,7 @@
574 "additionalProperties": false,
575 "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.",
576 "properties": {
577 + "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." },
578 "fileDescription": { "type": "string", "description": "Executable file description." },
579 "fileVersion": { "type": "string", "description": "Executable file version, in the form of 'n.n.n.n', for example: '1.2.3.4'." },
580 "internalName": { "type": "string", "description": "Executable internal name." },
meshcentral.js
+62 -10
@@ -1368,6 +1368,25 @@ function CreateMeshCentralServer(config, args) {
1368 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; }
1369 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; }
1370 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; }
1371 + if (typeof obj.config.domains[i].agentfileinfo.icon == 'string') {
1372 + // Load the agent .ico file
1373 + var icon = null;
1374 + try { icon = require('./authenticode.js').loadIcon(obj.path.join(obj.datapath, obj.config.domains[i].agentfileinfo.icon)); } catch (ex) { }
1375 + if (icon != null) {
1376 + // The icon file was correctly loaded
1377 + obj.config.domains[i].agentfileinfo.icon = icon;
1378 + obj.config.domains[i].agentfileinfo.iconhash = require('./authenticode.js').hashObject(icon);
1379 + } else {
1380 + // Failed to load the icon file, display a server warning
1381 + addServerWarning("Unable to load agent icon file: " + obj.config.domains[i].agentfileinfo.icon + ".", 23, [obj.config.domains[i].agentfileinfo.icon]);
1382 + delete obj.config.domains[i].agentfileinfo.icon;
1383 + delete obj.config.domains[i].agentfileinfo.iconhash;
1384 + }
1385 + } else {
1386 + // Invalid icon file path
1387 + delete obj.config.domains[i].agentfileinfo.icon;
1388 + delete obj.config.domains[i].agentfileinfo.iconhash;
1389 + }
1390 }
1391 }
1392
@@ -2951,24 +2970,41 @@ function CreateMeshCentralServer(config, args) {
2970 for (var i in versionProperties) {
2971 const prop = versionProperties[i], propl = prop.toLowerCase();
2972 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo[propl] == 'string')) {
2954 - 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.
2973 + 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.
2974 } else {
2956 - 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.
2975 + 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.
2976 }
2977 }
2978
2979 // Check file version number
2961 - if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
2962 - 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.
2963 - } else {
2964 - 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.
2980 + if (destinationAgentOk == true) {
2981 + if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['fileversionnumber'] == 'string')) {
2982 + 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.
2983 + } else {
2984 + 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.
2985 + }
2986 }
2987
2988 // Check product version number
2968 - if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
2969 - 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.
2970 - } else {
2971 - if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2989 + if (destinationAgentOk == true) {
2990 + if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object') && (typeof domain.agentfileinfo['productversionnumber'] == 'string')) {
2991 + 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.
2992 + } else {
2993 + if (orgVersionStrings['~ProductVersion'] != versionStrings['~ProductVersion']) { destinationAgentOk = false; } // if the resource of the orginal agent not the same as the destination executable, we need to re-sign the agent.
2994 + }
2995 + }
2996 +
2997 + // Check the agent icon
2998 + if ((destinationAgentOk == true) && (domain.agentfileinfo != null) && (domain.agentfileinfo.iconhash != null)) {
2999 + const agentIconGroups = destinationAgent.getIconInfo();
3000 + if (agentIconGroups != null) {
3001 + const agentIconGroupNames = Object.keys(agentIconGroups);
3002 + if (agentIconGroupNames.length > 0) {
3003 + const agentMainIconGroupName = agentIconGroupNames[0];
3004 + const agentMainIconGroupHash = require('./authenticode.js').hashObject(agentIconGroups[agentMainIconGroupName]);
3005 + 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.
3006 + }
3007 + }
3008 }
3009 }
3010
@@ -3003,17 +3039,33 @@ function CreateMeshCentralServer(config, args) {
3039 if ((domain.agentfileinfo != null) && (typeof domain.agentfileinfo == 'object')) {
3040 versionStrings = originalAgent.getVersionInfo();
3041 var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
3042 + // Change the agent string properties
3043 for (var i in versionProperties) {
3044 const prop = versionProperties[i], propl = prop.toLowerCase();
3045 if (domain.agentfileinfo[propl] && (domain.agentfileinfo[propl] != versionStrings[prop])) { versionStrings[prop] = domain.agentfileinfo[propl]; resChanges = true; }
3046 }
3047 + // Change the agent file version
3048 if (domain.agentfileinfo['fileversionnumber'] && (domain.agentfileinfo['fileversionnumber'] != versionStrings['~FileVersion'])) {
3049 versionStrings['~FileVersion'] = domain.agentfileinfo['fileversionnumber']; resChanges = true;
3050 }
3051 + // Change the agent product version
3052 if (domain.agentfileinfo['productversionnumber'] && (domain.agentfileinfo['productversionnumber'] != versionStrings['~ProductVersion'])) {
3053 versionStrings['~ProductVersion'] = domain.agentfileinfo['productversionnumber']; resChanges = true;
3054 }
3055 if (resChanges == true) { originalAgent.setVersionInfo(versionStrings); }
3056 +
3057 + // Change the agent icon
3058 + if (domain.agentfileinfo.icon != null) {
3059 + const agentIconGroups = originalAgent.getIconInfo();
3060 + if (agentIconGroups != null) {
3061 + const agentIconGroupNames = Object.keys(agentIconGroups);
3062 + if (agentIconGroupNames.length > 0) {
3063 + const agentMainIconGroupName = agentIconGroupNames[0];
3064 + agentIconGroups[agentIconGroupNames[0]] = domain.agentfileinfo.icon;
3065 + originalAgent.setIconInfo(agentIconGroups);
3066 + }
3067 + }
3068 + }
3069 }
3070
3071 const signingArguments = { out: signeedagentpath, desc: signDesc, url: signUrl, time: timeStampUrl, proxy: timeStampProxy }; // Shallow clone
sample-config-advanced.json
+1
@@ -308,6 +308,7 @@
308 "fileName": "compagnyagent"
309 },
310 "_agentFileInfo": {
311 + "icon": "agent.ico",
312 "filedescription": "sample_filedescription",
313 "fileversion": "0.1.2.3",
314 "internalname": "sample_internalname",
views/default.handlebars
+2 -1
@@ -2328,7 +2328,8 @@
2328 19: "SMS gateway has limited use in LAN mode.",
2329 20: "Invalid \"LoginCookieEncryptionKey\" in config.json.",
2330 21: "Backup path can't be set within meshcentral-data folder, backup settings ignored.",
2331 - 22: "Failed to sign agent {0}: {1}"
2331 + 22: "Failed to sign agent {0}: {1}",
2332 + 23: "Unable to load agent icon file: {0}."
2333 };
2334 var x = '';
2335 for (var i in message.warnings) {