Last week I found out that I had some Windows ATOM issues before, but this beats them easily was still a draft in stead if in the blog queue.
I got reminded to it by someone asking on Telegram about
“Do I need to use GarbageCollectAtoms
in Delphi? I used it in delphi 7, but I dont know what is benefit. š”.
The short answer is: yes, if your Delphi application does terminate in a way that the Controls unit cannot cleanly unload (and cannot free the Windows atoms) or leaks Windows atoms in a different way. I have been in that situation and that’s why I wrote the above blog post that got published in 2016.
The longer answer is likely no, both the Windows atom and registered Windows message table share a heap and that registered VCL Windows message leaking bug got fixed some 10 years ago in Delphi XE2, see:
Note that there is still a ton of applications built in older versions without Andreas Hausladen’s fix for those older versions (the fix used to be at Wayback: andy.jgknet.de, but is now at Archive.is Downloads ā Andy’s Blog and Tools as [Wayback] ControlsAtomFix1.7z).
Let’s explain the long answer
Back in 2016, I already hinted I would like to give more information in Some notes and links: when a filled ATOM table is not caused by your Delphi app.
For non-Delphi applications, it was excessive registration of Windows messages through RegisterWindowMessage
or leaking atoms by not deleting them.
Both make use of the atom tables (atoms directly, registered Windows messages indirectly, just like clipboard formats also use them indirectly) as per [Wayback/Archive.is] About Atom Tables – Win32 apps | Microsoft Docs; look for “Atom table size“.
For Delphi applications, the most common problem was an excessive registration in the global Windows message table by the InitControls
method of the Controls
(for older Delphi versions), or Vcl.Controls
(for newer) unit
: each Delphi application instance would register a new message there.
Before Delphi XE2, the relevant portion of the code used to be this:
var
WindowAtom: TAtom;
ControlAtom: TAtom;
WindowAtomString: string;
ControlAtomString: string;
RM_GetObjectInstance: DWORD; // registered window message
...
procedure InitControls;
...
WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
WindowAtom := GlobalAddAtom(PChar(WindowAtomString));
ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
...
As of Delphi XE2, it is this:
var
WindowAtom: TAtom;
ControlAtom: TAtom;
WindowAtomString: string;
ControlAtomString: string;
RM_GetObjectInstance: DWORD; // registered window message
...
procedure InitControls;
...
WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
WindowAtom := GlobalAddAtom(PChar(WindowAtomString));
ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
RM_GetObjectInstance := RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance')); // Do not localize
...
The bold italic bits are different, and this is why:
- Though atoms and registered Windows messages share the same ID space, and
GlobalAddAtom
has an inverse GlobalĀDeleteĀAtom
, there is no inverse of RegisterWindowMessage
. The actual Windows API function names of these are:
What does the fix in ControlsAtomFix1.7z
do?
That 7z file contains unit ControlsAtomFix
which for Delphi versions before XE2 needs to be in the uses list before the Controls
unit and supports Delphi versions 6 through XE.
It hooks the RegisterWindowMessage
Windows API method (actually [Wayback/Archive.is] RegisterWindowMessageA
function (winuser.h) – Win32 apps | Microsoft Docs for non-unicode Delphi before Delphi 2009 and [Wayback/Archive.is] RegisterWindowMessageW
function (winuser.h) – Win32 apps | Microsoft Docs for Delphi 2009 and up).
Inside the hooked method, it checks if the calling parameters are consistent with RegisterWindowMessage(PChar(ControlAtomString))
. If so, it returns the result for RegisterWindowMessage(PChar('DelphiRM_GetObjectInstance'))
, otherwise it calls the original method.
That way, Delphi versions 6 through XE will behave like Delphi XE2 and up.
What does GarbageCollectAtoms
do?
Back to “GarbageCollectAtoms in Delphi? I used it in delphi 7, but I dont know what is benefit. š”. It is part of the answers to question [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow of which each has a distinctive edge:
- [Wayback/Archive.is] about the
GlobalDeleteAtom
method where [Wayback/Archive.is] Christian explains the workaround of manually modifying the Controls unit with
RM_GetObjectInstance := RegisterWindowMessage('RM_GetObjectInstance');
..and explaining the working of his GlobalDeleteAtom
method.
- [Wayback/Archive.is] https://stackoverflow.com/users/62123/steve-black where user [Wayback/Archive.is] Steve BlackĀ explains how to change the second number in the
SharedSection
parameter of csrss.exe
(the Windows Client/Server Runtime Subsystem) inside the Windows
value data of the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems
registry key. It is further documented in these Microsoft articles:
- [Wayback/Archive.is] Microsoft KB Archive/126962 – BetaArchive Wiki covering Windows NT 3.1, NT 3.5/3.51, NT 4.0, 2000, XP and 2003 Server:
The first SharedSection
value (1024
) defines the heap size common to all desktops. This includes the global handle table (Window handles are unique machine wide) and shared system settings (such as SystemMetrics). It is unlikely you would ever need to change this value.
The second SharedSection
value (3072
) controls the size of the desktop heap (used for Windows objects). This static value is used to prevent ill- behaved applications from consuming too many resources. Because the desktop heap is mapped into each process’ address space, this value should not be set to an arbitrarily high value (as it would decrease performance), but should only be increased sufficiently to allow all the desired applications to run.
- [Wayback/Archive.is] User32.dll or Kernel32.dll does not initialize – Application Developer | Microsoft Docs (formerly KB184802) covering Windows Vista through Windows Server 2012 R2:
The firstĀ SharedSection
Ā value (1024
) is the shared heap size common to all desktops. This includes the global handle table. This table holds handles to windows, menus, icons, cursors, and so on, and shared system settings. It is unlikely that you would ever have to change this value.
The secondĀ SharedSection
Ā value is the size of the desktop heap for each desktop that is associated with theĀ interactiveĀ window station WinSta0
. User objects such as hooks, menus, strings, and windows consume memory in this desktop heap. It is unlikely that you would ever have to change this value.
- [Wayback/Archive.is] Desktop heap limitation causes out of memory error – Windows Server | Microsoft Docs (formerly KB947246) covering Windows 7 and Windows Server 2012 R2:
- The second value of theĀ
SharedSection
Ā registry entry is the size of the desktop heap for each desktop that is associated with an interactive window station. The heap is required for each desktop that is created in the interactive window station (WinSta0
). The value is in kilobytes (KB).
- The thirdĀ
SharedSection
Ā value is the size of the desktop heap for each desktop that is associated with aĀ non-interactiveĀ window station. The value is in kilobytes (KB).
- We don’t recommend that you set a value that is over
20480
KB for the secondĀ SharedSection
Ā value.
- [Wayback/Archive.is] CreateDesktopExA function (winuser.h) – Win32 apps | Microsoft Docs
- The first
SharedSection
value is the size of the shared heap common to all desktops, in kilobytes.
- The second
SharedSection
value is the size of the desktop heap needed for each desktop that is created in the interactive window station, WinSta0
, in kilobytes.
- The third
SharedSection
value is the size of the desktop heap needed for each desktop that is created in a noninteractive window station, in kilobytes.
The second SharedSection
value has grown over time (with one step back in Windows 3.5):
- 512 (Windows NT 3.5)
- 3072 (Windows NT 3.1, NT 4.0, 2000, XP and Vista)
- 12288 (x86 editions of Windows Vista SP1, 7, 8, 8.1, and newer x86 Windows versions)
- 20480 (x64 editions of Windows Vista, 7, 8, 8.1, Server 2008, Server 2008 R2, Server 2012, and Server 2012 R2, and newer x64 Windows versions)
- [Wayback/Archive.is] delphi – System Error. Code: 8. Not enough storage is available to process this command – Stack Overflow where [Wayback/Archive.is] Jordi Corbilla explains the bugs filed in QC (which have now been fixed), the structure of the atom and message names registered by the
Controls
unit, and his [Wayback/Archive.is] Global Atom Monitor GUI application on GitHub that allows you to visually watch the global atom table. The GlobalDeleteAtom
method is based on this code. I amended that post with the fix for Delphi 6-XE by Andreas Hausladen.
I forked the Global Atom Monitor intoĀ [Wayback/Archive] jpluimers/atom-table-monitor: Automatically exported from code.google.com/p/atom-table-monitorĀ back in 2015 because the original repository did not show any activity for a long time.
There I added a console application which displays all the User Atoms including their names, see the trick in [Wayback/Archive] atom-table-monitor/ATOMScannerConsoleApplicationUnit.pas at master · jpluimers/atom-table-monitor of [Wayback/Archive] atom-table-monitor/ATOMScannerConsole at master · jpluimers/atom-table-monitor commit [Wayback/Archive] First try at console version of ATOM scanner; Delphi 2007 version of ⦠· jpluimers/atom-table-monitor@bd48b5b:
constructor TRegisteredWindowsMessageInformation.Create(const AIndex: Integer);
var
LLength: Integer;
LName: string;
LWindowsMessageNameChars: array [Byte] of Char;
begin
..
LLength := GetClipboardFormatName(AIndex, LWindowsMessageNameChars, High(Byte));
if LLength = 0 then
inherited Create(AIndex, '')
else
begin
LName := StrPas(LWindowsMessageNameChars);
SetLength(LName, LLength);
inherited Create(AIndex, LName);
end;
end;
In retrospect, I made a wordblindness error (WindowsMessage
in stead of WindowMessage
), so I will likely post a commit there some day: [Wayback/Archive] Fix `WindowsMessage` into `WindowMessage` in various identifiers. Ā· Issue #2 Ā· jpluimers/atom-table-monitor
This lists any User Atom nam be it registered with RegisterClass
, RegisterWindowMessage
, or RegisterClipboardFormat
(as they share the same User Atom table)
The trick is explained in the Microsoft documentation linked further below.
More about Windows messages
Windows messages can be a mess, so two years ago, I put a lot of related links in Some odd Windows Messages for my research list (Windows 10 with a very basic Delphi application).
Edit 20221021: more on Windows Atoms
Seems not all of you are dinosaurs like me, so below are some links on Windows Atoms (most viaĀ [Wayback/Archive] microsoft windows atoms – Google Search).
Windows Atoms are basically dictionaries having an index of 16-bit SHORT
integers and storing corresponding null terminated strings. This structure type helped preserve memory in the 16-bit Windows days. That way you can communicate using Windows messages (see below) where SendMessage
and PostMessage
had 16-bit WPARM
and LPARAM
typed parameters.
Each process could have their own atom tables (local atom tables), but there are also global atom tables shared by all running Windows processes, for instance the user atom table (API calls like RegisterClass
, RegisterWindowMessage
, or RegisterClipboardFormat
Ā add entries here and the last two calls are persistent over your Windows Station session until you logoff or reboot) and the global atom table (often used for DDE – Dynamic Data Exchange – and OLE – Object Linking and Embedding).
Being 16-bit in index size, there can be only 65536 entries in an atom table, of which only 16383 can be used for string entries, so they can fill up. In the 16-bit Windows era, that was usually not much of a problem because processors were mainly single-core, memory was limited and therefore few processes ran at a time. Fast forward today, with ubiquitous memory and CPU resources, especially global atom tables can fill up faster than you might expect so keep an eye on that when adding entries to them.
- [Wayback/Archive] About Atom Tables – Win32 apps | Microsoft Learn (dense, but very well coverage of both basics and side effects including the below debugging remark using Clipboard Format names; that’s what I did in the above Delphi ).
Another way to dump the contents of the user atom table is by callingĀ GetClipboardFormatNameĀ over the range of possible atoms from 0xC000
to 0xFFFF
. If the total atom count steadily goes up while the application is running or does not return to baseline when the app is closed, there is a problem.
- [Wayback/Archive] atom (WinDbg) – Windows drivers | Microsoft Learn (atom support in WinDbg can be really convenient to track down Windows Atom table related issues)
- [Wayback/Archive] Atom Functions – Win32 apps | Microsoft Learn
- [Wayback/Archive] Window Stations – Win32 apps | Microsoft Learn
- [Wayback/Archive] RegisterClassA function (winuser.h) – Win32 apps | Microsoft Learn and[Wayback/Archive] RegisterClassExA function (winuser.h) – Win32 apps | Microsoft Learn
- [Wayback/Archive] RegisterWindowMessageA function (winuser.h) – Win32 apps | Microsoft Learn
- this Windows Message name registration cannot be undone, which means any registered unique Window Message name will full up the atom table; registering the same Window Message name twice will return back the same 16-bit User Atom table ID
- [Wayback/Archive] RegisterClipboardFormatA function (winuser.h) – Win32 apps | Microsoft Learn
- this Clipboard Format Name registration cannot be undone, which means any registered unique Window Message name will full up the atom table; registering the same Window Message name twice will return back the same 16-bit User Atom table ID
- [Wayback/Archive] GetClipboardFormatNameA function (winuser.h) – Win32 apps | Microsoft Learn (which can obtain any User Atom table name, as this table is shared for
ClassName
, WindowMessage
and ClipboardFormat
names)
- [Wayback/Archive] security – So, just what are Windows Atom tables for? – Stack Overflow (thanks [Wayback/Archive] Stone True, [Wayback/Archive] Hans Passant and [Wayback/Archive] Anders):
C
They are a simpleĀ Dictionary<int, string>
. One of the many tricks Microsoft used to shoe-horn a GUI operating system and its apps into 640 KB of RAM. Carrying around a 16-bit int is a lot cheaper than having to use a string literal. It doesn’t care that it is actually a string at all, any blob of bytes will do. It is still just plain data. Exploiting it doesn’t just require already having control over the process, you’d still need to turn that data into code. A fallacy that Raymond Chen likes to [Wayback/Archive] make fun of.
A
An atom table lets you associate a string with a 16-bit number. You give Windows your string and it gives you back a number. You can then retrieve the string again just by knowing the assigned number.
Every normal process has its own local atom table but it is usually empty and is not a security issue.
There are multiple “global” atom tables that are shared by all processes in the sameĀ window station. 1 of them is documented and it is called the global atom table. MSDN is also nice enough toĀ tell usĀ thatĀ RegisterClipboardFormat
Ā andĀ RegisterClass
Ā also use their own atom tables internally in their current implementation. Other functions likeĀ SetProp
Ā also use atoms but we are only interested in the atom table used by the exploit and atoms are added to that table with theĀ GlobalAddAtom
Ā function.
The main purpose of this atom table is to act as a simple storage location so that different processes can communicate with each other in a protocol calledĀ DDE. When a process wants to send a message to a window in a different process you cannot send more than 8 bytes (2 parameters, 4 bytes each) and this is not enough space to transfer a filesystem path or a URL.
To work around this limitation the application stores the string/path/URL in the public global atom table by callingĀ GlobalAddAtom
.Ā GlobalAddAtom
Ā returns a number that the application can send to the other process. When the other process receives the DDE message it just passes the number to theĀ GlobalGetAtomName
function to retrieve the string.
- The “make fun of” link above is dead due to link rot, but is now at [Wayback/Archive] It rather involved being on the other side of this airtight hatchway | The Old New Thing.
- [Wayback/Archive] AtomBombing: Brand New Code Injection for Windows – Breaking Malware /[Wayback/Archive] BreakingMalwareResearch/atom-bombing: Brand New Code Injection for Windows
Windows Messaging:
Related blog posts:
–jeroen
Like this:
Like Loading...
Related
Leave a Reply