Added exactports description, #4104

This commit is contained in:
Ylian Saint-Hilaire 2022-06-09 21:25:11 -07:00
parent 3da0a16d74
commit cbc3ab49b6
2 changed files with 140 additions and 37 deletions

View File

@ -909,7 +909,7 @@ function createAuthenticodeHandler(path) {
const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
//console.log('RStringTableStruct', buf.slice(ptr, ptr + wLength).toString('hex'));
r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + 16)); // An 8-digit hexadecimal number stored as a Unicode string.
//console.log('readStringTableStruct', wLength, wValueLength, r.wType, r.szKey);
//console.log('readStringTableStruct', wLength, wValueLength, wType, r.szKey);
r.strings = readStringStructs(buf, ptr + 24 + wValueLength, wLength - 22);
return r;
}
@ -917,7 +917,7 @@ function createAuthenticodeHandler(path) {
// String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str
function readStringStructs(buf, ptr, len) {
var t = [], startPtr = ptr;
while (ptr < (startPtr + len)) {
while ((ptr + 6) < (startPtr + len)) {
const r = {};
const wLength = buf.readUInt16LE(ptr);
if (wLength == 0) return t;
@ -953,12 +953,30 @@ function createAuthenticodeHandler(path) {
return hash.digest();
}
// Hash of an open file using the selected hashing system
obj.getHashOfFile = function (fd, algo, filesize) {
var hash = crypto.createHash(algo);
runHashOnFile(fd, hash, 0, obj.header.peHeaderLocation + 88);
runHashOnFile(fd, hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
runHashOnFile(fd, hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : filesize);
return hash.digest();
}
// Hash the file from start to end loading 64k chunks
function runHash(hash, start, end) {
var ptr = start;
while (ptr < end) { const buf = readFileSlice(ptr, Math.min(65536, end - ptr)); hash.update(buf); ptr += buf.length; }
}
// Hash the open file loading 64k chunks
// TODO: Do chunks on this!!!
function runHashOnFile(fd, hash, start, end) {
var buf = Buffer.alloc(end - start);
var len = fs.readSync(fd, buf, 0, buf.length, start);
if (len != buf.length) { console.log('BAD runHashOnFile'); }
hash.update(buf);
}
// Checksum the file loading 64k chunks
function runChecksum() {
var ptr = 0, c = createChecksum(((obj.header.peOptionalHeaderLocation + 64) / 4));
@ -1158,14 +1176,9 @@ function createAuthenticodeHandler(path) {
}
// Save the executable
obj.writeExecutable = function (args) {
// Get version information from the resource
var versions = obj.getVersionInfo();
versions['FileDescription'] = 'This is a test';
obj.setVersionInfo(versions);
obj.writeExecutable = function (args, cert) {
// Open the file
var output = fs.openSync(args.out, 'w');
var output = fs.openSync(args.out, 'w+');
var tmp, written = 0;
// Compute the size of the complete executable header up to after the sections header
@ -1179,20 +1192,11 @@ function createAuthenticodeHandler(path) {
var newResSize = obj.header.sections['.rsrc'].rawSize; // Testing 102400
var resDeltaSize = newResSize - oldResSize;
/*
console.log('fileAlign', fileAlign);
console.log('resPtr', resPtr);
console.log('oldResSize', oldResSize);
console.log('newResSize', newResSize);
console.log('resDeltaSize', resDeltaSize);
*/
// Change PE optional header sizeOfInitializedData standard field
fullHeader.writeUInt32LE(obj.header.peStandard.sizeOfInitializedData + resDeltaSize, obj.header.peOptionalHeaderLocation + 8);
fullHeader.writeUInt32LE(obj.header.peWindows.sizeOfImage, obj.header.peOptionalHeaderLocation + 56); // TODO: resDeltaSize
// Update the checksum, set to zero since it's not used
// TODO: Take a look at computing this correctly in the future
// Update the checksum to zero
fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 64);
// Make change to the data directories header to fix resource segment size and add/remove signature
@ -1256,7 +1260,81 @@ function createAuthenticodeHandler(path) {
}
// Write the signature if needed
// TODO
if (cert != null) {
//if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
// Set the hash algorithm hash OID
var hashOid = null, fileHash = null;
if (args.hash == null) { args.hash = 'sha384'; }
if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHashOfFile(output, 'sha256', written); }
if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHashOfFile(output, 'sha384', written); }
if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHashOfFile(output, 'sha512', written); }
if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHashOfFile(output, 'sha224', written); }
if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHashOfFile(output, 'md5', written); }
if (hashOid == null) return false;
// Create the signature block
var p7 = forge.pkcs7.createSignedData();
var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
p7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
p7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
p7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
p7.addCertificate(cert.cert);
if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { p7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
// Build authenticated attributes
var authenticatedAttributes = [
{ type: forge.pki.oids.contentType, value: forge.pki.oids.data },
{ type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge
]
if ((typeof args.desc == 'string') || (typeof args.url == 'string')) {
var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] };
if (args.desc != null) { // Encode description as big-endian unicode.
var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString()
for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); }
codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] });
}
if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); }
authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes });
}
// Add the signer and sign
p7.addSigner({
key: cert.key,
certificate: cert.cert,
digestAlgorithm: forge.pki.oids.sha384,
authenticatedAttributes: authenticatedAttributes
});
p7.sign();
var p7signature = Buffer.from(forge.pkcs7.messageToPem(p7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
//console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
// Quad Align the results, adding padding if necessary
var len = written + p7signature.length;
var padding = (8 - ((len) % 8)) % 8;
// 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)); }
// Write the signature header
var addresstable = Buffer.alloc(8);
addresstable.writeUInt32LE(written);
addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
written += (p7signature.length + padding + 8); // Add the signature block to written counter
// 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);
@ -1301,6 +1379,16 @@ function start() {
console.log("");
console.log("Note that certificate PEM files must first have the signing certificate,");
console.log("followed by all certificates that form the trust chain.");
console.log("");
console.log("When doing sign/unsign, you can also change resource properties of the generated file.");
console.log("");
console.log(" --filedescription [value]");
console.log(" --fileversion [value]");
console.log(" --internalname [value]");
console.log(" --legalcopyright [value]");
console.log(" --originalfilename [value]");
console.log(" --productname [value]");
console.log(" --productversion [value]");
return;
}
@ -1321,6 +1409,15 @@ function start() {
if (exe == null) { console.log("Unable to parse executable file: " + args.exe); return; }
}
// Parse the resources and make any required changes
var resChanges = false, versionStrings = exe.getVersionInfo();
var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
for (var i in versionProperties) {
const prop = versionProperties[i], propl = prop.toLowerCase();
if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; }
}
if (resChanges == true) { exe.setVersionInfo(versionStrings); }
// Execute the command
var command = process.argv[2].toLowerCase();
if (command == 'info') { // Get signature information about an executable
@ -1362,14 +1459,32 @@ function start() {
if (typeof args.hash == 'string') { args.hash = args.hash.toLowerCase(); if (['md5', 'sha224', 'sha256', 'sha384', 'sha512'].indexOf(args.hash) == -1) { console.log("Invalid hash method, must be SHA256 or SHA384"); return; } }
if (args.hash == null) { args.hash = 'sha384'; }
createOutFile(args, args.exe);
const cert = loadCertificates(args.pem);
if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); }
console.log("Signing to " + args.out); exe.sign(cert, args); console.log("Done.");
var cert = loadCertificates(args.pem);
if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); }
if (resChanges == false) {
console.log("Signing to " + args.out);
exe.sign(cert, args); // Simple signing, copy most of the original file.
} else {
console.log("Changing resources and signing to " + args.out);
exe.writeExecutable(args, cert); // Signing with resources decoded and re-encoded.
}
console.log("Done.");
}
if (command == 'unsign') { // Unsign an executable
if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; }
createOutFile(args, args.exe);
if (exe.header.signed) { console.log("Unsigning to " + args.out); exe.unsign(args); console.log("Done."); } else { console.log("Executable is not signed."); }
if (resChanges == false) {
if (exe.header.signed) {
console.log("Unsigning to " + args.out);
exe.unsign(args); // Simple unsign, copy most of the original file.
console.log("Done.");
} else {
console.log("Executable is not signed.");
}
} else {
console.log("Changing resources and unsigning to " + args.out);
exe.writeExecutable(args, null); // Unsigning with resources decoded and re-encoded.
}
}
if (command == 'createcert') { // Create a code signing certificate and private key
if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
@ -1419,18 +1534,6 @@ function start() {
fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon]));
console.log("Done.");
}
if (command == 'test') { // Grow the resource segment by 100k
if (exe == null) { console.log("Missing --exe [filename]"); return; }
createOutFile(args, args.exe);
console.log("Writting to " + args.out);
exe.resourcesChanged = true; // Indicate the resources have changed
exe.writeExecutable(args);
// Parse the output file
var exe2 = createAuthenticodeHandler(args.out);
if (exe2 == null) { console.log("Unable to parse output executable file: " + args.out); return; }
console.log('Output executable parsed correctly.');
}
// Close the file
if (exe != null) { exe.close(); }

View File

@ -102,7 +102,7 @@
"agentCoreDumpUsers": { "type": "array", "description": "List of non-administrator users that have access to mesh agent crash dumps." },
"agentSignLock": { "type": "boolean", "default": false, "description": "When code signing an agent using authenticode, lock the agent to only allow connection to this server. (This is in testing, the default value will change to true in the future)." },
"ignoreAgentHashCheck": { "type": [ "boolean", "string" ], "default": false, "description": "When true, the agent no longer checked the TLS certificate of the server. This should be used for debugging only. You can also set this to a comma seperated list of IP addresses to ignore, for example: \"192.168.2.100,192.168.1.0/24\"." },
"exactPorts": { "type": "boolean", "default": false },
"exactPorts": { "type": "boolean", "default": false, "description": "When set to true, MeshCentral will only grab the required TCP listening ports or fail. It will not try to use the next available port of it's busy." },
"allowLoginToken": { "type": "boolean", "default": false },
"StrictTransportSecurity": { "type": ["boolean", "string"], "default": null, "description": "Controls the Strict-Transport-Security header, default is 1 year. Set to false to remove, true to force enable, or string to set a custom value. If set to null, MeshCentral will enable if a trusted certificate is set." },
"allowFraming": { "type": "boolean", "default": false, "description": "When enabled, the MeshCentral web site can be embedded within another website's iframe." },