Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

src: add WDAC integration (Windows) #54364

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

rdw-msft
Copy link

Add calls to Windows Defender Application Control to enforce integrity of .js, .json, .node files.

Motivation

In the past I've spoken to @RafaelGSS and the Node security work group about code integrity. We feel like it's an important defense in depth feature and the removal of the --experimental-policy feature gives us room to use OS-level code integrity features. This is a first pass at a CI implementation that cooperates with the OS.

These additions add an additional layer of defense against malicious code execution on a system that is enforcing code integrity. Code Integrity enforcement mitigates the risk of malicious code modifications after signing-time. For example, they can prevent an arbitrary file-write vulnerability from turning into an RCE. These additional checks are only made if the OS in code integrity enforcement mode and has explicitly set a configuration value in their code integrity policy to have Node enforce CI.

Windows Defender Application Control (WDAC)

Official documentation: https://learn.microsoft.com/en-us/windows/security/application-security/application-control/windows-defender-application-control/wdac-and-applocker-overview

WDAC is the Windows code integrity subsystem. It enforces code integrity using digital signatures. This is straightforward for DLLs and EXEs, since they're loaded using well known entry points in the OS. However, dynamic runtimes (interpreters like Node.js or Python) make it difficult to determine whether a file being read will be executed. In order to assist dynamic runtimes, WDAC provides an interface for runtimes to call with the code they intent to execute, and WDAC will determine if the runtime is allowed to execute the code based off signature information. WDAC (wldp.dll) exposes various methods for runtimes to ask the OS questions about the code integrity policy on the system. These changes leverage three interfaces WldpCanExecuteFile, WldpGetApplicationSettingBoolean, and WldpQuerySecurityPolicy.

WldpCanExecuteFile - given a file handle, determines if the file is allowed to be executed from WDAC policy.

WldpGetApplicationSettingBoolean - Queries WDAC policy for an application-defined setting with a boolean value. This can be thought of as a more secure setting store.

WldpQuerySecurityPolicy - WdlpGetApplicationSettingBoolean is not available on all versions of Windows, so this method provides fallback behavior to query WDAC policy settings.

High-level implementation

At startup, Node will query WDAC policy to see if it should enter integrity enforcement mode. If the policy provider Node.js has set the policy setting EnforceCodeIntegrity to TRUE, then Node will call WldpCanExecuteFile when a .js, .json, or .node file is loaded using require. If this setting is not enabled, the call to WldpCanExecuteFile will not be made and execution will continue as normal.

Additionally, if EnforceCodeIntegrity is set to TRUE, we disable features that allow arbitrary code from being executed by Node (e.g. the "-e" command line option and the REPL) when the system is enforcing code integrity.

Signing files

Signatures are stored in catalog file, .cat. This catalog file can be thought of as a collection of filenames and their associated hashes. A Node application author can generate a catalog using the Powershell command New-FileCatalog documentation.

PS> New-FileCatalog -CatalogFilePath ./MyApplicationCatalog.cat -Path ./MyApplicationRelease/

The catalog then can be signed with a certificate trusted by WDAC policy using Powershell with Set-AuthenticodeSignature documentation or signtool.exe

The signed catalog can then be installed on the system MyApplication is being deployed to.

Other Questions

What about Linux?

At the moment, there is no unified code integrity subsystem that provides similar cooperative interfaces for interpreters on Linux. There are proposals in-flight and we're tracking this work and hope to keep the implementation as similar as possible across OSs.

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/gyp
  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Aug 14, 2024
@RafaelGSS RafaelGSS added semver-minor PRs that contain new features and should be released in the next minor version. security-wg-agenda labels Aug 14, 2024
@anonrig
Copy link
Member

anonrig commented Aug 14, 2024

Can you run the formatter?

@rdw-msft
Copy link
Author

I also want to add - this is meant to facilitate discussion regarding the general implementation. I'm not very familiar with the various module loaders and ways which code can be read off disk and executed. I assume there are gaps in my implementation and I would appreciate any expertise in the area to close them. Thanks!

@targos
Copy link
Member

targos commented Aug 14, 2024

@nodejs/platform-windows

@@ -198,6 +199,8 @@ const onRequire = getLazy(() => tracingChannel('module.require'));

const relativeResolveCache = { __proto__: null };

const ci = require('code_integrity');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would put this into an internal module right now.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure - this just involves moving it into the lib/internal/ folder? Let me know if I need to do anything else!

Copy link

codecov bot commented Aug 19, 2024

Codecov Report

Attention: Patch coverage is 90.24390% with 8 lines in your changes missing coverage. Please review.

Project coverage is 87.31%. Comparing base (880c446) to head (a7fef80).
Report is 67 commits behind head on main.

Files Patch % Lines
lib/internal/main/eval_string.js 72.72% 3 Missing ⚠️
src/node_code_integrity.cc 86.36% 3 Missing ⚠️
lib/code_integrity.js 92.85% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #54364      +/-   ##
==========================================
+ Coverage   87.09%   87.31%   +0.22%     
==========================================
  Files         648      650       +2     
  Lines      182216   182441     +225     
  Branches    34965    34997      +32     
==========================================
+ Hits       158704   159305     +601     
+ Misses      16785    16398     -387     
- Partials     6727     6738      +11     
Files Coverage Δ
lib/internal/errors.js 94.45% <100.00%> (+0.01%) ⬆️
lib/internal/modules/cjs/loader.js 92.99% <100.00%> (+0.06%) ⬆️
src/node_binding.cc 83.66% <ø> (ø)
src/node_external_reference.h 100.00% <ø> (ø)
lib/code_integrity.js 92.85% <92.85%> (ø)
lib/internal/main/eval_string.js 70.42% <72.72%> (+0.42%) ⬆️
src/node_code_integrity.cc 86.36% <86.36%> (ø)

... and 71 files with indirect coverage changes

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've discussed on the security-wg and the next steps are:

  • Creating documentation
  • Improving the test coverage

Then we can go the a proper code review. Meanwhile, I'm tagging @nodejs/tsc for awareness.

This is a Windows specific feature that will enabled by a node key in Windows system dictionary. So, it shouldn't be considered a semver-major or a specific Node.js API.

Currently, Windows should be the only environment that makes those checks available via API. We are still evaluating the status of OSX and Linux support for that. However, it's fair to assume this PR will only implement such guarantees for Windows users.

I recommend watching our meeting as we've discussed the usage and how this is turned-on in Windows systems (https://www.youtube.com/watch?v=w4zzH-otKNI)

@l0kod
Copy link

l0kod commented Sep 11, 2024

FYI, we are working on a similar feature for Linux. The goal is the same but the implementation and API are different. Linux already provides access control systems and this new feature (previously open+O_MAYEXEC, now execveat+AT_CHECK) is the missing piece to control script execution the same way other kind of execution can already be controlled. This article gives a good overview: https://lwn.net/Articles/982085/

I plan to send a new patch series in a few weeks. Please let me know what you think.

@l0kod
Copy link

l0kod commented Sep 11, 2024

Cc @tiran, @zooba

@rdw-msft
Copy link
Author

Hey @RafaelGSS , I've added more documentation about code integrity mode. Please let me know if it looks adequate, or if you'd like me to explain more.

I also added doc/api/wdac-manifest.xml. This is the manifest we spoke about in the last meeting I attended. It declares the WDAC Application settings that NodeJS will query for. It should be hosted in an accessible place so when system admins author their Windows Code Integrity policy they know what settings are available to them, and it also adds some type-checking for the policy writer.

I'll plan on attending the next security WG meeting to discuss in more detail if needed. Thanks

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment in this document explaining what this is and why it is here would be helpful.

<AppManifest Id="NodeJS" xmlns="urn:schemas-microsoft-com:windows-defender-application-control">
<SettingDefinition Name="EnforceCodeIntegrity" Type="Boolean" IgnoreAuditPolicies="false"/>
<SettingDefinition Name="DisableInterpretiveMode" Type="Boolean" IgnoreAuditPolicies="false"/>
</AppManifest>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
</AppManifest>
</AppManifest>

@@ -0,0 +1,41 @@
'use strict';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here explaining a bit about what this is would be helpful. Also, if this feature is limited to windows, comments indicating so should be added as well as possible asserts to ensure it is only running on windows.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file should throw if required by a non-windows environment.

static void IsFileTrustedBySystemCodeIntegrityPolicy(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(true);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These could probably just be implemented in JavaScript so avoid the extraneous JS->C++ boundary call.


#include <Windows.h>

// {0xb5367df1,0xcbac,0x11cf,{0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear what this comment is.

#define WLDP_HOST_OTHER \
{0x626cbec3, 0xe1fa, 0x4227, \
{0x98, 0x0, 0xed, 0x21, 0x2, 0x74, 0xcf, 0x7c}};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment with a link to the relevant win32 API docs would be helpful for reviewers and future contributors.

#ifndef SRC_NODE_CODE_INTEGRITY_H_
#define SRC_NODE_CODE_INTEGRITY_H_

#ifdef _WIN32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is Windows only, I would generally prefer that this entire internal binding only be compiled on windows, with the non-functional stubs implemented solely in javascript on the other platforms.

'use strict';

// Flags: --expose-internals
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
'use strict';
// Flags: --expose-internals
// Flags: --expose-internals
'use strict';

Copy link
Member

@RafaelGSS RafaelGSS left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m concerned about synchronous calls blocking the event loop and ensuring compatibility with the Windows API across all Node.js versions supported on Windows

doc/api/code_integrity.md Show resolved Hide resolved
## Code Integrity on Linux

Code integrity on Linux is not yet implemented. Plans for implementation will
be made once the necessary APIs on Linux have been upstreamed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to link to the original issue

@@ -789,6 +789,18 @@ changes:
There was an attempt to use a `MessagePort` instance in a closed
state, usually after `.close()` has been called.

<a id="ERR_CODE_INTEGRITY_BLOCKED"></a>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same stability index applies. Stability: 1.1

@@ -1669,6 +1672,11 @@ Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const content = getMaybeCachedSource(module, filename);

const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be better placed inside Module._load (search for permission.isEnabled() on this file).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't even call this function for non-windows environments.


namespace codeintegrity {

#ifdef _WIN32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it work on all supported Windows environments?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WldpCanExecuteFile, WldpQueryApplicationSettingBoolean methods are implemented on Windows Server and Windows Client.

Is there a list of NodeJS supported environments I can check against?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the minimum version of Windows supported by Node?

Wldp.dll was added in Windows 8, however the APIs we're using to enforce file signatures were added in Windows 11 Build 22621

The APIs we're using:
WldpCanExecuteFile, checks if the file meets code integrity policy (e.g. hash is unchanged), is available in Windows 11 Build 22621+
WldpGetApplicationSettingBoolean, queries the security policy to see if a configuration setting is set, this is the flag we use to determine if we should be enforcing code integrity with calls to WldpCanExecuteFile, is also in Win11.
WldpQuerySecurityPolicy, is the fallback method we use if WldpGetApplicationSettingBoolean is not available. This allows backward compatibility to early Windows 10 builds.

We can assume that if we can't get a handle on Wldp.dll, the system is too old to support code integrity, so execution can just continue.

If we're on a system with WldpQuerySecurityPolicy, and the Node.js EnforceCodeIntegrity setting is enabled, but we can't load WldpCanExecuteFile, we should probably halt execution. I can add this behavior in the next iteration if it seems reasonable.

Copy link
Member

@RafaelGSS RafaelGSS Nov 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @nodejs/build @nodejs/platform-windows

}

HMODULE wldp_module = LoadLibraryExA(
"wldp.dll",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a default dll on Windows? Can't it be subject of hijack?

I remember that we faced an vulnerability in the past where Node.js was reading the openssl.conf from working_dir instead of absolute openssl path leading to some vulnerabilities. Could you double check if that's the case?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a default DLL, the OS grabs a handle on this DLL early in the boot process to prevent tampering.

We can also assume that if this DLL doesn't exist on a system, it is too old to support code integrity features.


const GUID wldp_host_other = WLDP_HOST_OTHER;
WLDP_EXECUTION_POLICY result;
HRESULT hr = WldpCanExecuteFile(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are those calls synchronous right? It seems to block the event loop on every check making the system useless from a nodejs perspective

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. These calls are synchronous, and there are no async versions. Is there a way to address this without the platform adding an async version?

This call will only be made on systems that have opted into code integrity enforcement. I can reach out to the Windows platform team and see if there's opportunity to add an async version if this is an issue.

At the moment, is it acceptable to say there will be performance hits when using this feature?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will make this feature hard to use considering Node.js constraints, we can measure but I suspect it will make things quite slow

@@ -0,0 +1,41 @@
'use strict';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file should throw if required by a non-windows environment.

@@ -1669,6 +1672,11 @@ Module._extensions['.js'] = function(module, filename) {
// If already analyzed the source, then it will be cached.
const content = getMaybeCachedSource(module, filename);

const isAllowedToExecute = ci.isAllowedToExecuteFile(filename);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't even call this function for non-windows environments.

namespace codeintegrity {

#ifdef _WIN32
static bool isWldpInitialized = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid using global variables, and use a snapshottable API.

using v8::Object;
using v8::Value;

namespace codeintegrity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
namespace codeintegrity {
namespace code_integrity {

} // namespace codeintegrity
} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(code_integrity,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we even create this binding context for non-windows environments?

Comment on lines +233 to +236
static void IsInterpretiveModeDisabled(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(false);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for this function, since it only returns a constant boolean value.

Suggested change
static void IsInterpretiveModeDisabled(
const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(false);
}

@@ -34,6 +34,7 @@
"buffer": ["./lib/buffer.js"],
"child_process": ["./lib/child_process.js"],
"cluster": ["./lib/cluster.js"],
"codeintegrity": ["./lib/codeintegrity.js"],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should also update the necessary files in typings folder for the internal APIs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. security-wg-agenda semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants