Pavel Yosifovich, co-author of the Microsoft Press Windows Internals books, walks through a small Windows feature that most people run into without ever noticing: App Execution Aliases.
If you have ever typed notepad and gotten the new Store version instead of the classic one in System32, you have already met this feature. In this post I want to show what these aliases actually are, where Windows stores them, how they work at the file system level, and how you can read them yourself from code. This is aimed at Windows developers, security researchers, and anyone who likes to understand what the system is really doing instead of taking it at face value.
Why Does the Wrong Notepad Launch?
Let me start with something simple. I type notepad in the Run dialog, and I get the new Notepad. I go to the System32 directory, where Notepad has lived forever, double click it, and I still get the new Notepad. That is a little strange. The executable I clicked on is clearly a different file. If you open Task Manager, find the running Notepad, and choose Open file location, you land in a long path under Program Files\WindowsApps\Microsoft..., not System32. The sizes do not match either: the Store Notepad is around 3 MB, the System32 one is a few hundred kilobytes. So obviously these are not the same binary.
So how does typing a plain name like notepad end up running something from a completely different directory? This is where App Execution Aliases come in.
What Is an App Execution Alias?
If you open Settings and search for “app execution aliases”, you get a page titled Manage app execution aliases. It is a per user list, and depending on what you have installed it can be quite long. The basic idea is that an executable name, for example notepad.exe, can point to some real application wherever it happens to live, and that name is resolvable from anywhere: a command window, the Run dialog, an API call, (almost) anything really.
The Settings page is a bit limited. It shows you the aliases and lets you toggle them on and off, but it does not tell you what a given alias actually points to. It is still useful for one thing though: if I disable the notepad.exe alias and try again, I finally get the classic Notepad from System32, along with a gentle suggestion that a newer Notepad exists. So the alias was intercepting the name the whole time. You can see the same behavior with mspaint. Paint also has a second alias called pbrush, which is a nice bit of history. And on a modern Windows box the classic Paint is gone from System32 entirely, so once you disable the alias, mspaint simply fails, because there is nothing left for the name to resolve to.
Where Does Windows Store These Aliases?
The aliases live in a per user directory: %LOCALAPPDATA%\Microsoft\WindowsApps. Open that folder and you will find a file for each alias, including notepad.exe. Now here is the part that looks wrong at first. Every one of these files is zero bytes. Not small, zero. The size on disk is zero as well. A zero byte file should be completely useless, and yet if I double click notepad.exe in that folder, Notepad launches normally. Every one of these empty files runs whatever it is supposed to point to, behind the scenes.
So how can an empty file launch a real program? The answer is reparse points.
How Do Reparse Points Make an Empty File Run a Program?
A reparse point is an NTFS feature. You can attach a block of data to a file or directory, stored in the NTFS metadata rather than in the file contents. Each reparse point carries a tag, and when the file is accessed, a file system mini filter that recognizes that tag gets a chance to step in and do something interesting instead of the normal open. Reparse points are used for all kinds of things (symbolic links and mount points are the classic examples), and App Execution Aliases are just another consumer of the same mechanism. Windows ships a mini filter that understands the alias tag, and that is what turns a zero byte file into a launch of the real executable.
If you want to understand reparse points in a broader context, they are the same family of NTFS machinery behind more advanced tricks. I covered a related file system feature in NTFS transactions in Windows, which is worth a read if you like going below the API surface of the file system.
How Do You Inspect an Alias with fsutil?
You do not need to write any code to see this. The fsutil tool has been around for years, and it knows how to query reparse data. Copy the path to one of the alias files and run:
fsutil reparsepoint query "%LOCALAPPDATA%\Microsoft\WindowsApps\notepad.exe"If there is no reparse point on the file you get an error, which is fine. For an alias, you get real output: the reparse tag value, an indication that Microsoft is the provider of the tag, and a hex dump of the data. The tag for App Execution Aliases is always the same value, 0x8000001B, and it has a define in the Windows Driver Kit (reparse point tags are partitioned into ranges so different providers do not collide). In the hex dump you can squint and make out a few UTF-16 strings separated by null terminators: something like Microsoft.WindowsNotepad..., a package identity, and the actual executable path. The first few bytes are less obvious, probably a version or a reserved field. We will pin that down in code.
How Do You Enumerate App Execution Aliases in C++?
Reading this with a tool is fine, but I always like to do it programmatically as well. It is not just about using existing tools, it is about understanding them well enough to reproduce what they do. So let me build a small console application that enumerates every alias and prints where each one points.
Finding the alias files
First I need the directory. I could hardcode my own user path, but that is fragile. A better way is to ask for the LOCALAPPDATA location at runtime with GetEnvironmentVariable, then append the rest of the path. There are other ways to get the local app data folder (asking the shell for the known folder, for example), but the environment variable is good enough here.
WCHAR path[MAX_PATH];
GetEnvironmentVariable(L"LOCALAPPDATA", path, MAX_PATH);
std::wstring dir(path);
dir += L"\\Microsoft\\WindowsApps\\";Now I search that directory for *.exe using FindFirstFile and FindNextFile. Each call fills a WIN32_FIND_DATA with the next entry.
WIN32_FIND_DATA fd;
HANDLE hFind = FindFirstFile((dir + L"*.exe").c_str(), &fd);
if (hFind == INVALID_HANDLE_VALUE)
return 1; // directory missing, no permission, whatever it may be
do {
// fd.cFileName is one alias, for example notepad.exe
ProcessAlias(dir, fd.cFileName);
} while (FindNextFile(hFind, &fd));
FindClose(hFind);Opening the reparse point
For each alias I have to get at the reparse data, and that takes a bit of care. I open the file with CreateFile, but with one important flag: FILE_FLAG_OPEN_REPARSE_POINT. Without it, the open follows the reparse point in the normal way and you never see the underlying data. GENERIC_READ and FILE_SHARE_READ are fine, since we are just reading something that already exists.
std::wstring full = dir + fileName;
HANDLE hFile = CreateFile(full.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
if (hFile == INVALID_HANDLE_VALUE)
return; // no reparse point hereNow, how do I actually pull the data out? If you reach for ReadFile, it will not work, because that reads the file contents, and the contents are empty. The reparse data lives in the metadata, so I have to ask the file system for it directly with DeviceIoControl and the FSCTL_GET_REPARSE_POINT control code. There is no input buffer to give (we are getting, not setting), so I pass null and zero for the input, and a plain byte buffer for the output.
BYTE buffer[2048] = {}; // zero it, or you get leftovers from the previous run
DWORD returned;
if (!DeviceIoControl(hFile, FSCTL_GET_REPARSE_POINT,
nullptr, 0, buffer, sizeof(buffer), &returned, nullptr)) {
CloseHandle(hFile);
return;
}One small but real detail: zero the buffer. When I first ran this without doing that, I got residual characters left over from the previous alias, which made the output look messier than it was.
Interpreting the data
Instead of poking at raw bytes, it is nicer to lay a structure over the buffer. The fundamental shape of a reparse buffer is defined in the Windows Driver Kit headers. For our purposes the layout is: the tag, a length, a reserved or version field, and then the data itself, which in this case is a run of null terminated UTF-16 strings.
struct ReparseData {
ULONG Tag; // 0x8000001B for app execution aliases
USHORT Length; // size of the data that follows
USHORT Reserved;
ULONG Version; // undocumented: version or reserved, not sure
WCHAR Text[1]; // null separated strings start here
};
auto data = reinterpret_cast<ReparseData*>(buffer);
printf("\tTag: 0x%08X\n", data->Tag);
printf("\tBytes: %u\n", data->Length);
// Walk the null separated strings
const WCHAR* p = data->Text;
while (*p) {
printf("\t%ws\n", p);
p += wcslen(p) + 1; // skip the string and its null terminator
}
CloseHandle(hFile);When you run this, each alias prints its executable name, the same tag for every entry (it has to be the same tag), the byte count (which varies with the length of the strings), and then the strings it points to. Typically you get several strings: the package name, an Application User Model ID that uniquely identifies the app entry (see Application User Model IDs), and the actual executable path, which is the one you were really after. There is also a trailing zero string on the last entry whose exact meaning I have not fully chased down. It is clearly there on purpose, but the documentation does not say much. That is one of those things to investigate on your own, and honestly one of the more fun parts of this kind of work.
What This Means Practically
- Toggle an alias off in Settings, or delete the corresponding zero byte file in
%LOCALAPPDATA%\Microsoft\WindowsApps, when a plain name likenotepadormspaintkeeps resolving to a Store app you did not want. - Run
fsutil reparsepoint queryon any alias file to see its tag and the executable it points to, with no code required. - Open a reparse point without following it by passing
FILE_FLAG_OPEN_REPARSE_POINTtoCreateFile, then read the data withDeviceIoControlandFSCTL_GET_REPARSE_POINT.ReadFilewill not do it. - Enumerate every alias on a machine to audit exactly what those short names resolve to, which is useful when you care about what actually runs on a system.
- Treat the reparse tag as the extension point: if you ever write a file system mini filter, the same tag mechanism lets you attach and interpret your own metadata on files and directories.
Keep Learning
If you want to understand the file system internals and the Win32 APIs behind features like this, these TrainSec courses go deeper:
- Windows Internals Bundle: covers the file system, the I/O system, and reparse points as part of the full internals picture
- Windows System Programming: covers the Win32 file APIs,
DeviceIoControl, and working with the file system from user mode - Windows Internals Bundle for security researchers: the learning path if you are approaching Windows internals from a security angle
Related reading in the knowledge library: DLL Injection with Windows Application Verifier, another look at how Windows decides what actually gets loaded and run.