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

[NTUSER] Implement BroadcastSystemMessage with 'Environment' parameter #6884

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Doug-Lyons
Copy link
Contributor

@Doug-Lyons Doug-Lyons commented May 14, 2024

Purpose

Implement BroadcastSystemMessage with Environment parameter.

JIRA issues: CORE-19372 and CORE-19583

Proposed changes

BroadcastSystemMessage needs to be implemented.
This allows programs to update Environment variables like PATH.

@github-actions github-actions bot added the Win32SS For Win32 subsystem (Win32k, GDI/USER DLLs, etc.) related components PRs. label May 14, 2024
@whindsaks
Copy link
Contributor

The WM_SETTINGCHANGE string in lparam can be any string but this should fix the most common broadcasts.

Copy link
Contributor

@oleg-dubinskiy oleg-dubinskiy left a comment

Choose a reason for hiding this comment

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

In other LGTM. ✅

win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved
win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved
win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved
win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved
win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved

/* Strings that are OK to pass between user and kernel mode
* There may be other strings needed that can easily be added here. */
WCHAR StrUserKernel[3][20] = {{L"intl"}, {L"Environment"}, {L"Policy"}};
Copy link
Contributor

Choose a reason for hiding this comment

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

Why putting these strings inside "sublists"? Why not just doing something like:

Suggested change
WCHAR StrUserKernel[3][20] = {{L"intl"}, {L"Environment"}, {L"Policy"}};
PCWSTR StrUserKernel[] = {L"intl", L"Environment", L"Policy"};

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@HBelusca, I know that what I have here will allow me to use understandable derived values as follows:
ARRAYSIZE(StrUserKernel) = 3
sizeof(StrUserKernel[0]) = 40
_countof(StrUserKernel[0]) = 20
To me what you are suggesting seems to be the very definition of "ill-defined".
What would be the values above when the array is defined without integer constant numbers?
With what you have proposed, I cannot tell what the above values would return by simple inspection.
but with the values [3][20] it seems obvious to me. Admittedly, this is because of my poor understanding.
Nevertheless, I do appreciate your comments and your trying to help me with this. Thanks.

@@ -2721,6 +2767,12 @@ NtUserMessageCall( HWND hWnd,
}
ExFreePoolWithTag(List, USERTAG_WINDOWLIST);
}
if (lParam && !wParam && wcscmp((WCHAR*)lParam, L"Environment") == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this comparison be done case-sensitively, or not? (i.e. if the string "enVIroNmEnT" is passed instead, should the broadcast still be done successfully or not?)

Copy link
Contributor

Choose a reason for hiding this comment

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

The string is not case sensitive. Not that it matters, all strings are supposed to be supported. Currently it's missing "SystemDockMode" and "ConvertibleSlateMode" (and all possible registry key names).

Copy link
Contributor

Choose a reason for hiding this comment

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

Where does this lParam parameter come from?

Copy link
Contributor

@whindsaks whindsaks May 14, 2024

Choose a reason for hiding this comment

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

Where does this lParam parameter come from?

When the message is WM_SETTINGCHANGE, lParam can be NULL or a string that says which setting you changed. There are maybe about 7-8 strings that are "standard" but it can also be the name of a registry key.

I believe it is supposed to marshal all strings in this case and checking for "Environment" seems like a hack to me.

Copy link
Contributor

Choose a reason for hiding this comment

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

checking for "Environment" seems like a hack to me.

I agree.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In accordance with the title of this PR, it was intended to only handle Environment strings. It can be expanded later in another PR. Anything else is "out of scope" for this PR. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@whindsaks, I see that you have given a "thumbs down" comment here. I just do not have the background in this area of the code to do much more with it for now. Are you willing to pick this up? If so, you are free to make any changes here that you want. Or if you want to create a completely new PR, I can close this one. I do appreciate your willingness to help here and want to encourage you in any way that I can. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know much about the kernel part of the window manager either, I just feel that we should try to make it work with all strings from the start, not a hack like this for just one string.

_SEH2_TRY
{
if (UserModeMsg->lParam)
RtlCopyMemory( lParamMsg, (WCHAR*)UserModeMsg->lParam, sizeof(lParamMsg));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
RtlCopyMemory( lParamMsg, (WCHAR*)UserModeMsg->lParam, sizeof(lParamMsg));
RtlCopyMemory(lParamMsg, (WCHAR*)UserModeMsg->lParam, sizeof(lParamMsg));

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not fully sure this can work reliably.
If someone passes an arbitrary pointer to a non-null-terminated string via the lParam with a size completely unrelated to the "hardcoded" sizeof(...) .

Later down you call PosInArray(lParamMsg) and wcscpy outside a SEH2 block, assuming the copied string is properly NUL-terminated, but you haven't verified that this assumption is correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If someone passes a bad value string as you suggest, when this goes to PosInArray then it will not match any of the 3 strings there and will return a -1 which will disallow it getting to wcscpy function. Maybe it can be extended later for more general strings. Thanks.

Copy link
Contributor

Choose a reason for hiding this comment

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

1- The local lParamMsg buffer is somewhere in memory. Let's suppose it can contain 12 WCHARs (so 24 bytes), but what's after it is undefined (let's say they can be non-zero bytes, or the beginning of an unaccessible new memory page).

2- Now, the caller (the user) passes in the lParam (which gets placed in UserModeMsg->lParam) a user-mode pointer to a non-NUL-terminated "string", i.e. pointer to some crafted random garbage, let's say to this string:
L"EnvironmentXXXXXXXXX" (it has more than 12 WCHARs ie. more than 24 bytes).

3- Your RtlCopyMemory will copy only the first 12 WCHARs into lParamMsg (without any NUL-terminator), so that lParamMsg will contain:
"Environment<GARBAGE_FOLLOWS>" (as described in point 1).

4- The PosInArray(lParamMsg) does a if (wcsncmp(String, StrUserKernel[i], sizeof(StrUserKernel[0])) == 0), scanning i = 0 to 2 or whatever (to try all the hardcoded strings in StrUserKernel array).
When it tries the second string (index i == 1), i.e. "Environment", it'll try to match the "N-th" first characters; but: first, your sizeof(StrUserKernel[0]) is ill-defined, and also the wcsncmp may try to compare whether the terminating NUL-character of the trial string "Environment" exists also in the caller-given String (where, I recall, there is garbage following). In that case it'll read into forbidden garbage and can cause a BSOD.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@HBelusca, is your statement that says your sizeof(StrUserKernel[0] is ill-defined still correct when I have used this define at line 19?
WCHAR StrUserKernel[3][20]...
I have done prints of the values of sizeof and _countof as you can see in my response to Mark and they seem to know that this is a WCHAR of length 20 wchar's or 40 bytes.
Thanks for your comments, explanation, and consideration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Extra space removed. I have added code to make sure that there is a UNICODE_NULL in lParamMsg now. See if this is OK with you. Thanks.

win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved
@@ -459,8 +483,30 @@ CopyMsgToKernelMem(MSG *KernelModeMsg, MSG *UserModeMsg, PMSGMEMORY MsgMemoryEnt
/* Copy data if required */
if (0 != (MsgMemoryEntry->Flags & MMS_FLAG_READ))
{
WCHAR lParamMsg[sizeof(StrUserKernel[0])/2 + 1] = { 0 };
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
WCHAR lParamMsg[sizeof(StrUserKernel[0])/2 + 1] = { 0 };
WCHAR lParamMsg[_countof(StrUserKernel[0]) + 1] = { 0 };

or using RTL_NUMBER_OF or ARRAYSIZE .

@HBelusca HBelusca self-requested a review May 14, 2024 15:32
@HBelusca HBelusca added the review pending For PRs undergoing review. label May 14, 2024
win32ss/user/ntuser/message.c Outdated Show resolved Hide resolved

for (i = 0; i < End; ++i)
{
if (wcsncmp(String, StrUserKernel[i], sizeof(StrUserKernel[0])) == 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why sizeof(StrUserKernel[0]) ? since the wcsncmp takes a number of characters (and not a number of bytes)

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (wcsncmp(String, StrUserKernel[i], sizeof(StrUserKernel[0])) == 0)
if (wcsncmp(String, StrUserKernel[i], _countof(StrUserKernel[0])) == 0)

Copy link
Member

Choose a reason for hiding this comment

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

This only checks the first 4 chars of each string indeed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@learn-more, Why do you say "This only checks the first 4 chars of each string indeed."?
Maybe I am not understanding something here?
I added this statement just above this "if" statement now at Line 36.
ERR("sizeof is %d, countof is %d\n", sizeof(StrUserKernel[0]), _countof(StrUserKernel[0]));
When the program is run, its debug output shows the following:
(win32ss/user/ntuser/message.c:36) err: sizeof is 40, countof is 20
What am I missing here? Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why sizeof(StrUserKernel[0]) ? since the wcsncmp takes a number of characters (and not a number of bytes)

@HBelusca, I will correct this, I missed the fact that wcsncmp takes a number of characters and not bytes. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed sizeof to _countof. Thanks.

Copy link
Member

Choose a reason for hiding this comment

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

Right, it tries to use 20 for every string, even strings that are smaller.
Why is that used as max length?

_SEH2_TRY
{
if (UserModeMsg->lParam)
RtlCopyMemory( lParamMsg, (WCHAR*)UserModeMsg->lParam, sizeof(lParamMsg));
Copy link
Contributor

Choose a reason for hiding this comment

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

1- The local lParamMsg buffer is somewhere in memory. Let's suppose it can contain 12 WCHARs (so 24 bytes), but what's after it is undefined (let's say they can be non-zero bytes, or the beginning of an unaccessible new memory page).

2- Now, the caller (the user) passes in the lParam (which gets placed in UserModeMsg->lParam) a user-mode pointer to a non-NUL-terminated "string", i.e. pointer to some crafted random garbage, let's say to this string:
L"EnvironmentXXXXXXXXX" (it has more than 12 WCHARs ie. more than 24 bytes).

3- Your RtlCopyMemory will copy only the first 12 WCHARs into lParamMsg (without any NUL-terminator), so that lParamMsg will contain:
"Environment<GARBAGE_FOLLOWS>" (as described in point 1).

4- The PosInArray(lParamMsg) does a if (wcsncmp(String, StrUserKernel[i], sizeof(StrUserKernel[0])) == 0), scanning i = 0 to 2 or whatever (to try all the hardcoded strings in StrUserKernel array).
When it tries the second string (index i == 1), i.e. "Environment", it'll try to match the "N-th" first characters; but: first, your sizeof(StrUserKernel[0]) is ill-defined, and also the wcsncmp may try to compare whether the terminating NUL-character of the trial string "Environment" exists also in the caller-given String (where, I recall, there is garbage following). In that case it'll read into forbidden garbage and can cause a BSOD.

@yagoulas
Copy link
Member

yagoulas commented May 16, 2024

Are you sure that in windows the wparam is 0 in these cases? If I remember correctly most of the times when the win32k marshals an lparam pointer the wParam has inforumation about the size (when it can be variable like in a string). My suspicion is that windows would pass the lenght of the string as the wParam.

@whindsaks
Copy link
Contributor

Are you sure that in windows the wparam is 0 in these cases?

For WM_SETTINGCHANGE, wParam is one of the SPI constants. I don't know if it is legal to combine a SPI with a string but I see no reason to block it.

@yagoulas
Copy link
Member

yagoulas commented May 17, 2024

Ok, after thinking about this a bit more, here are my thoughts. Whenever a pointer to a buffer of variable size enters the kernel from user32 to win32k, user32 does some preparation in order for win32k to be able to consume it. There has to be some work done in user32 to facilitate delivering of these strings. To make matters worse, some applications are ANSI only and some are unicode, some send messages with ansi strings and some send messages with unicode strings. You need to take all these details into consideration.

There is one fundamental question about how this feature works: if it supports only predefined strings or if it supports any kind of arbitrary string.

If it is the first I wouldn't be surprised if windows' user32 replaced the predefined string with an enumeration and leave win32k oblivious to the details, and likewise replace the enumeration with a real string pointer right before calling a message proc. (solution a)

If it has to carry an arbitrary string then it is most likely that user32 replaces the string pointer with a pointer to a UNICODE_STRING on the stack that will also carry the length of the string. Then the kernel in this case would marshal the UNICODE_STRING from one process to the other and right before the message proc gets called, user32 would replace the pointer to the UNICODE_STRING, with a pointer to the raw string. (solution b)

So please first of all write some tests to see the following:

  1. Test if this message supports arbitrary strings in windows
  2. Test how an ANSI window proc receives such a string sent with BroadcastSystemMessageExW
  3. Test how a unicode window proc receives such a string sent with BroadcastSystemMessageExA (there are test cases like that in user32 tests)

Depending on the test from 1 we should proceed with either solution a or solution b. Unfortunately there is no other way around getting this string pointer into the kernel , it can't enter as is without its length. User32 needs to be involved before the kernel call. This is can be a bit daunting but one of the reasons such a feature hasn't been implemented yet is because it is not trivial. In your solution for example you copy the string without probing and without knowing the real size of the string which is absolutely wrong.

PS: It would help if someone could check with a debugger to see what parameters user32 passes to NtUserMessageCall win windows.

PS2: Please also check what windows does when you send such a message with an lparam string with the BSF_POSTMESSAGE parameter. I guess it will return an error,

@whindsaks
Copy link
Contributor

whindsaks commented May 17, 2024

  1. Test if this message supports arbitrary strings in windows

I can confirm that you can send any string you want (in the < WM_USER range the kernel knows how to marshal broadcast string WPARAM and LPARAMs for).

screenshot

WNDPROC g;
void Append(HWND hWnd, LPCSTR s)
{
	SendMessage(hWnd, EM_SETSEL, 0, -1);
	SendMessage(hWnd, EM_SETSEL, -1, -1);
	SendMessageA(hWnd, EM_REPLACESEL, FALSE, (LPARAM)s);
}
LRESULT CALLBACK Sub(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
	CHAR b[MAX_PATH];
	if (Msg == WM_APP)
	{
		wsprintfA(b, ("\r\nI'm=%u %hs\r\n"), GetCurrentProcessId(), IsWindowUnicode(hWnd) ? "Unicode" : "Ansi");
		Append(hWnd, b);
	}
	if (Msg == WM_SETTINGCHANGE && lParam) Append(hWnd, (LPSTR)lParam), Append(hWnd, ("\r\n"));
	return CallWindowProcA(g, hWnd, Msg, wParam, lParam);
}

void __cdecl main()
{
	HANDLE mutex = CreateMutexA(NULL, TRUE, "ROS.PR#6884");
	BOOL another = WaitForSingleObject(mutex, 0) != WAIT_OBJECT_0;
	UINT cw = 300, ch = 200, x = 0, y = 0;
	UINT style = WS_VISIBLE|WS_OVERLAPPEDWINDOW|ES_MULTILINE|ES_AUTOVSCROLL|ES_AUTOHSCROLL;
	HWND h = CreateWindowExA(0, ("EDIT"), ("ROS.PR#6884"), style, x, another ? y + ch : y, cw, ch, 0, 0, 0, 0);
	g = (WNDPROC)SetWindowLongPtrA(h, GWLP_WNDPROC, (LONG_PTR)Sub);
	SendMessage(h, WM_APP, 0, 0);
	if (!another)
	{
		WCHAR p[MAX_PATH];
		GetModuleFileNameW(NULL, p, MAX_PATH);
		ShellExecuteW(NULL, NULL, p, NULL, NULL, SW_SHOW);
		Sleep(2222); // Wait for the other guy to start
		BroadcastSystemMessageW(BSF_SENDNOTIFYMESSAGE, NULL, WM_SETTINGCHANGE, 0, (LPARAM)L"BSend WM_SETTINGCHANGE"); // W func -> A window
		BroadcastSystemMessageW(BSF_POSTMESSAGE      , NULL, WM_SETTINGCHANGE, 0, (LPARAM)L"BPost WM_SETTINGCHANGE"); // W func -> A window
		PostMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)L"Post BROADCAST WM_SETTINGCHANGE"); // Does not work
		SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)L"Send BROADCAST WM_SETTINGCHANGE"); // W func -> A window
	}
	MessageBoxA(0, "cheap messageloop", another ? "2nd" : "first", 0);
}

Edit: And for WM_SETTINGCHANGE , it does not care if WPARAM is 0 or not, it will marshal the LPARAM string either way.

@yagoulas
Copy link
Member

So here is my assumption what needs to be done:
user32!IntBroadcastSystemMessage: When BSF_POSTMESSAGE is missing, should do MsgiUMToKMMessage and MsgiUMToKMCleanup just like SendMessageW. When Ansi is true it should also do MsgiAnsiToUnicodeMessage and MsgiAnsiToUnicodeCleanup just like SendMessageCallbackA.

user32!MsgiUMToKMMessage: for WM_SETTINGCHANGE that has lparam, allocate a UNICODE_STRING, initialize it from the user mode lparam and use its pointer as the lparam for the KMMsg.

user32!MsgiUMToKMCleanup: deallocate the UNICODE_STRING mentioend above

user32!MsgiKMToUMMessage: use the buffer of the UNICODE_STRING as the lparam for the user mode message

user32!MsgiAnsiToUnicodeMessage: handle WM_SETTINGCHANGE like WM_SETTEXT

user32!MsgiAnsiToUnicodeCleanup: handle WM_SETTINGCHANGE like WM_SETTEXT

The above are needed to let win32k always handle proper UNICODE_STRINGs instead of a mishmash of unicode or ansi string pointers.

Now for win32k I found this totally retarded thing. Right now, without the changes of this MR the first thing that win32k does when someone sends a WM_SETTEXT or a WM_SETTINGCHANGEm win32k goes one and does a wcslen on the lparam coming from user mode, without any probing and assuming that the string will be null terminated. (Only SEH prevents the kernel from crashing). So for win32k the correct way to go would be to assume that we get a UNICODE_STRING when a message has the MMS_SIZE_LPARAMSZ flag.

This got me into a never ending rabbit hole (typical for win32k stuff) and realized that our implementation of WM_SETTEXT is also a total mess and probably needs something like the UNICODE_STRING treatment I mentioned above.

So continuing with what needs to be done in win32k:
win32k!CopyMsgToKernelMem: should assume that messages that have the MMS_SIZE_LPARAMSZ flag, come with a UNICODE_STRING* in lparam. I guess after the string is copied into kernel memory we can keep passing the lparam as a WCHAR*

win32k!CopyMsgToUserMem: I'm not sure if this needs to be involved

Here is another important question however: win32k already has a (bad) implementation of marshaling the WM_SETTINGCHANGE lparam string (with the MMS_SIZE_LPARAMSZ flag). Why is this MR needed when some infrastructure for that is already in place?

@whindsaks
Copy link
Contributor

When BSF_POSTMESSAGE is missing

And when BSF_POSTMESSAGE is present? It still needs to marshal the string. How Windows does this, I don't know.

@yagoulas
Copy link
Member

yagoulas commented May 18, 2024

When BSF_POSTMESSAGE is missing

And when BSF_POSTMESSAGE is present? It still needs to marshal the string. How Windows does this, I don't know.

In this case it probably returns an error. This is just a guess but this is what I conclude judging how PostMessage works, which does not marshal pointers accross processes. If it did, win32k would need to keep track on its own the marshaled memory. What is a process never receives? The message stays forever in kernel memory? What windows does is that it marshals messages only when two threads synchronize through SendMessage (and similar funcitons). SendMessage blocks till the recipient message proc returns and during this time windows also do marshal pointers.

@whindsaks
Copy link
Contributor

This is just a guess

But I already tested this on Windows and BSF_POSTMESSAGE does work when LPARAM is a string!

@yagoulas
Copy link
Member

But I already tested this on Windows and BSF_POSTMESSAGE does work when LPARAM is a string!

even across processes?

@whindsaks
Copy link
Contributor

whindsaks commented May 18, 2024

even across processes?

Yes

XP

@learn-more
Copy link
Member

even across processes?

Yes

XP

Educated guess, could it be adding atoms for this?
There is an atom viewer in my WindowsHookEx application:
https://learn-more.github.io/WindowsHookEx/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
review pending For PRs undergoing review. Win32SS For Win32 subsystem (Win32k, GDI/USER DLLs, etc.) related components PRs.
Projects
None yet
8 participants