BAM internals

This is a reply to the Sunday Funday 4/5/20 challenge. The goal of this post is to document the process, not just the results. You have been warned.

The Background Activity Moderator (BAM) is a Windows 10 thing that does… something! Because we don’t know much about it.

We know that this thing provides evidence of execution by listing executables under the following registry key:

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\bam\State\UserSettings\<User SID>

Each piece of evidence is stored as a registry value (REG_BINARY), its name is set to an executable path and its data is set to a binary structure with a FILETIME timestamp inside (this is believed to be the last execution timestamp).

Here is an example:

  • Value name: “\Device\HarddiskVolume2\Users\U\Desktop\Arsenal Image Mounter v3.0.64 Alpha\ArsenalImageMounter.exe“.
  • Value type: REG_BINARY.
  • Value data (as HEX bytes): D0 35 6D 50 B9 09 D6 01 00 00 00 00 00 00 00 00 00 00 00 00 02 00 00 00 (0x01D609B9506D35D0 is 2020-04-03T13:10:58+00:00).

The BAM service points to the following executable: \Windows\System32\drivers\bam.sys.

This is a driver and my starting point. Let’s load this file into the IDA Pro disassembler.

After initial reconnaissance, the following statements can be made:

  1. There are multiple calls to the ZwSetValueKey() routine, from the following list of functions:
    • BamUserSettingsAddOrUpdate,
    • BamUserSettingsGet,
    • BamUserSettingsTouch,
    • BampUserSettingsOpenUserKey (the “p” letter means “this is a private function”),
    • BampUserSettingsUpdateSequenceNumber,
    • PpRegStateUpdateStackCreationSettings,
    • CmRegUtilUcValueSetUcString,
    • CmRegUtilUcValueSetUcString.
  2. There is one call to the ZwDeleteValueKey() routine, from the BamUserSettingsRemove function.
  3. The “UserSettings” strings is referenced in one function only: BamUserSettingsInitialize.

Upon further investigation, I found that only three functions call the ZwSetValueKey() routine after a successful call to the BampUserSettingsReadEntry function:

  • BamUserSettingsAddOrUpdate,
  • BamUserSettingsGet,
  • BamUserSettingsTouch.

The BampUserSettingsReadEntry function is shown below:

Снимок экрана от 2020-04-06 01-35-46
Fig. 1. BampUserSettingsReadEntry

The function finds a registry value by its name, then checks its type and data size, they must be REG_BINARY and 24 respectively. So, I have a clear indicator that this function is used to read BAM registry values, because they have exactly the same type and data size. Other functions listed above don’t have any similar indicators showing that they deal with BAM registry values I’m interested in (but the BamUserSettingsRemove function looks pretty interesting though).

Thus, three functions mentioned above are suspects!

The BamUserSettingsInitialize function is used to create (or open) the “UserSettings” key under the registry key specified (“returned”) by the IoOpenDriverRegistryKey() routine (this routine “returns a handle to a driver-specific registry key for a particular driver”).

Now, let’s see possible call graphs for the BamUserSettingsAddOrUpdate, BamUserSettingsGet, BamUserSettingsTouch, BamUserSettingsRemove functions.

BamUserSettingsAddOrUpdate:

1. DriverEntry -> BampRegisterKernelExtension -> BampSetUserSettings -> BampThrottlingSetUserSettings -> BamUserSettingsAddOrUpdate

2. DriverEntry -> BampThrottlingStartModule -> BampThrottlingWorker -> BampCommitThrottledProcessStateChange -> BamUserSettingsAddOrUpdate

BamUserSettingsTouch:

DriverEntry -> BampRegisterKernelExtension -> BampCreateProcessCallback -> BampThrottlingOnProcessTerminated -> BamUserSettingsTouch

BamUserSettingsRemove:

DriverEntry -> BampThrottlingStartModule -> BampThrottlingWorker -> BampScavengeUserSettings -> BampScavengeUsersCallback -> BamUserSettingsEnumerateSettingsForUser -> BampScavengeUserSettingsCallback -> BamUserSettingsRemove

BamUserSettingsGet:

(It’s more complicated in the first case, thus not all levels are shown.)

1. (…) -> BampCreateProcessCallback or BampThrottlingSetProcessThrottleState -> BampThrottlingRegisterProcessEx -> BampStartThrottledProcessInformation -> BamUserSettingsGet

2. DriverEntry -> BampThrottlingStartModule -> BampThrottlingWorker -> BampScavengeUserSettings -> BampScavengeUsersCallback -> BamUserSettingsEnumerateSettingsForUser -> BampScavengeUserSettingsCallback -> BamUserSettingsGet

It’s interesting that callbacks like the BampCreateProcessCallback function are registered using the undocumented ExRegisterExtension() routine (some technical information can be found here). A structure passed to this routine as an argument starts with a magic constant – 0x0E0005. The same constant can be found in the kernel – it’s in the PspInitializeBackgroundActivityModeratorCallouts function. Interesting, yes?

But let’s get back to the ZwSetValueKey() routine. Each call to that routine from three functions mentioned above actually updates the timestamp field of value data (the first eight bytes). Here is how it looks like (this is the BamUserSettingsGet function):

Снимок экрана от 2020-04-06 02-29-20.png
Fig. 2. BamUserSettingsGet

(If you don’t know that memory address, see this. Note that the value type and data size specified are 3 = REG_BINARY and 0x18 = 24 respectively.)

The BamUserSettingsRemove function is shown below:

Снимок экрана от 2020-04-06 12-29-34
Fig. 3. BamUserSettingsRemove

This function is called from the BampScavengeUserSettingsCallback function:

Снимок экрана от 2020-04-06 12-30-55.png
Fig. 4. BampScavengeUserSettingsCallback

So, this function deletes a value if it contains invalid data (see the STATUS_BAD_DATA branch) or “app doesn’t exist”. The BampDoesAppExist function is below:

Снимок экрана от 2020-04-06 12-35-56.png
Fig. 5. BampDoesAppExist

Basically, this function checks whether a specified file is found or not. The result is stored as the first byte in the second argument (one if found, zero otherwise).


So, here is the first discovery: BAM entries can be removed if an executable is removed from its original location. Based on the call graph shown above, this can occur during the boot. And I can easily verify that in a virtual machine.

I copy an executable file to the root directory of the C: drive, boot the machine, check the BAM key for all users – the executable isn’t listed. Then, I launch that executable and immediately see a new BAM entry for that file. After this, I close the application window and see an updated value in the same BAM entry (the first eight bytes are updated). Finally, I delete the executable file, but its BAM entry is still there, so I reboot the machine and… the BAM entry is gone. So, my initial conclusion is now confirmed. (The test system is Windows 10, build: 19592.)

Also, I did a similar test, but additionally rebooted the machine before deleting the executable file. The results are the same.


During these tests, I identified that a FILETIME timestamp is updated when a process is created and when a process is terminated. This matches the call graphs shown above (“BampCreateProcessCallback -> BampThrottlingOnProcessTerminated” and “BampCreateProcessCallback -> BampThrottlingRegisterProcessEx”). However, it’s too early to make conclusions about possible cases when the FILETIME timestamp can be updated.


Note that there is another check (see Fig. 4): “if ( v10 > (__int64)v4 || v14 )”. Even if “app exists”, there is a condition that forces its BAM entry to be deleted: if “v14” is false (zero) and v10 <= v4, then the BAM entry is deleted. Go to Fig. 2 to see how “v14” is set, it’s “a4” in that function, you also need to see Fig. 1 to understand the meaning of the “Data” variable (it’s value data). So, “v14” is set to the third DWORD of value data (counting from zero).

In the example above, this DWORD is zero.

In a similar way, you can see that “v10” is the FILETIME timestamp from value data (the first QWORD in value data), while “v4” is a dereferenced argument (“a4” on Fig. 4). This argument is actually set in the BampScavengeUserSettings function (see “v5”):

Снимок экрана от 2020-04-06 19-39-38.png
Fig. 6. BampScavengeUserSettings

So, this value is calculated as: a current FILETIME timestamp minus (10000 * DWORD2(xmmword_1C00054F0)).

The “xmmword_1C00054F0” variable is set in the BampReadGlobalThrottlingSettings function, it actually contains DWORDs. The third DWORD is set to the value from the following registry location:

  • Key: “\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\BAM“.
  • Value: “UserSettingsLifetimeMs“.

On my system, this value doesn’t exist. But there is a default value in the code: 0x240C8400. And 10000*0x240C8400 in 100-nanosecond intervals is 604800 seconds (or exactly 7 days).


So, here is another discovery: BAM entries older than 7 days are removed during the boot. Again, this can be verified in a virtual machine.

I decided to use the “ArsenalImageMounter.exe” executable, which was shown above as an example. It was executed on 2020-04-03. Let’s move the clock to 2020-04-11, boot, and… the BAM entry is gone!

I also did similar tests by launching an executable, moving the system clock forward and rebooting. The results are the same in multiple cases.

But keep in mind that there could be BAM entries with the third DWORD set to something different from zero. Such a BAM entry isn’t going to be deleted (according to the condition discussed above).

It’s still possible for the deletion to be initiated after the boot (because it’s unclear how exactly the BampThrottlingWorker function is operating). Don’t exclude that possibility. Also, it’s unclear how many expired BAM entries can be removed at once.


Given that there are no other ways to reach the ZwDeleteValueKey() routine, we have answered the third question in the challenge (“3. What can cause a program to no longer be listed in the BAM key?“).

Finally, when the third DWORD in a BAM entry is non-zero?

Let’s get back to these three functions:

  • BamUserSettingsAddOrUpdate,
  • BamUserSettingsGet,
  • BamUserSettingsTouch.

The third one doesn’t update anything except the first eight bytes (the FILETIME timestamp). The second one doesn’t update the third DWORD too (see Fig. 2). And the first one actually sets it to a supplied argument:

Снимок экрана от 2020-04-06 22-09-57.png
Fig. 7. BamUserSettingsAddOrUpdate

The third DWORD is set to “v6”, which equals to “a3”. Note that this argument can’t be greater than 2.

When called from the BampCommitThrottledProcessStateChange function, this argument (“a3”) is zero. When called from the BampThrottlingSetUserSettings function, the argument is equal to the third argument to the caller. The previous caller, the BampSetUserSettings function (see the graph above), sets this argument to the second argument to itself. And the BampSetUserSettings function seems to be called from the kernel (the driver refers to this function only to fill a kernel callout table).

I don’t want to get inside the static analysis hell, so I will use a kernel debugger to catch the call site of the BampSetUserSettings function.

Sounds easy, but I found that the BampSetUserSettings function isn’t called when running executables (whether they were previously seen in BAM entries or not). And it’s not called during the boot. (This could be due to virtual hardware, see below.)

But there is another way. Before calling BAM extension functions, the kernel calls the “ExGetExtensionTable(PspBamExtensionHost)” routine and uses the returned value as an array of extension functions. A solution is to set a memory access breakpoint on PspBamExtensionHost, wait until the call to the ExGetExtensionTable() routine, then parse memory at the returned address as a list of pointers. So, here is the BAM extension table:

  • offset = 0: “bam!BampCreateProcessCallback“;
  • offset = 8: “bam!BampSetThrottleStateCallback“;
  • offset = 16: “bam!BampGetThrottleStateCallback“;
  • offset = 24: “bam!BampSetUserSettings“;
  • offset = 32: “bam!BampGetUserSettingsHandle“.

Then, I listed all functions making calls based on that table and found that only the PsSetExeModerationState function is calling the BampSetUserSettings function. Here it is:

Снимок экрана от 2020-04-08 02-56-36
Fig. 8. PsSetExeModerationState

Again, this function just takes a supplied argument and uses it.

The PsSetExeModerationState function is called from the NtSetSystemInformation function. The system information class used is 0xBB. According to this source, this value stands for SystemActivityModerationExeState. A related structure and an enumeration can be found here:

// private - REDSTONE2
typedef struct _SYSTEM_ACTIVITY_MODERATION_EXE_STATE // REDSTONE3: Renamed SYSTEM_ACTIVITY_MODERATION_INFO
{
    UNICODE_STRING ExePathNt;
    SYSTEM_ACTIVITY_MODERATION_STATE ModerationState;
} SYSTEM_ACTIVITY_MODERATION_EXE_STATE, *PSYSTEM_ACTIVITY_MODERATION_EXE_STATE;

// private
typedef enum _SYSTEM_ACTIVITY_MODERATION_STATE
{
    SystemActivityModerationStateSystemManaged,
    SystemActivityModerationStateUserManagedAllowThrottling,
    SystemActivityModerationStateUserManagedDisableThrottling,
    MaxSystemActivityModerationState
} SYSTEM_ACTIVITY_MODERATION_STATE;

So, the highest value in the enumeration is 3 (MaxSystemActivityModerationState), such values are typically not used for anything except sanity checks. This matches the highest value allowed as seen on Fig. 7.

Thus, executables that don’t have the SystemActivityModerationStateSystemManaged state set aren’t deleted after 7 days since the latest BAM timestamp update. This is likely connected to the power throttling feature in Windows 10.


The next question is: “2. Are there any paths excluded from BAM?“.

The first thing to try is to search the driver for strings that look like a file system path. I did this and found nothing suspicious.

The next thing to try is to find all possible locations when a path of a process is queried and then check if there is anything similar to a blacklist. Again, no success. A path of a process is queried in the BampQueryProcessIdentifier function, but this path isn’t checked against any kind of lists.

A promising clue came from the BampSetUserSettings function:

Снимок экрана от 2020-04-07 00-55-23
Fig. 9. BampSetUserSettings

The BampDeviceNtFilePathPrefix string is “\Device\Harddisk“. It’s possible that executables from network shares don’t get a BAM entry! Also, note the “a2 >= 3” and “a3 >= 2” conditions. Since the BampSetUserSettings is called by the kernel, let’s switch to it… Oh, wait, that’s the same function as already seen, it’s not called when running an executable.

Next, the BampCreateProcessCallback function is called near the process-creation callbacks (which are registered using the PsSetCreateProcessNotifyRoutine() routine). So, it should have similar properties (i.e., no blacklists). At least on the kernel side of things.

Now, let’s try to run an executable from a network share. And… no, it doesn’t get a BAM entry. So strange!

Going back to the kernel debugging, setting up a breakpoint for the BampThrottlingRegisterProcessEx function. Trying to launch an executable from a local drive and the same executable from a network share… and both trigger the breakpoint. Still, no BAM entry for a network share.

While tracing the BampThrottlingRegisterProcessEx function and its callees (after launching an executable from a network share), I found that they work with a valid kernel path, one of which starts with the “\Device\Mup\” string (just as expected for a network share). However, this execution path doesn’t create a new BAM entry (it just updates an existing BAM entry, if it’s present).

The next candidate is the BamUserSettingsAddOrUpdate function. It’s called for both types of execution, it also calls the BamUserSettingsCheckOrigin function, which returns zero if launching from a local drive and non-zero (in particular, STATUS_INVALID_NETWORK_RESPONSE) if launching from a network share.

So, there is no blacklist, but the BAM driver actually checks the type of a device containing the executable being launched:

Снимок экрана от 2020-04-07 02-45-13
Fig. 10. BamUserSettingsCheckOrigin

And we have three points of “failure” here:

  1. the network device provides an invalid reply (this is what happened in my case: STATUS_INVALID_NETWORK_RESPONSE; my guess is that my Samba server is causing the error);
  2. the device is removable (“FsInformation.Characteristics & 1“, 0x00000001 is FILE_REMOVABLE_MEDIA);
  3. the device is remote (“(FsInformation.Characteristics >> 4) & 1“, this is the same as “FsInformation.Characteristics & 0x10“, 0x00000010 is FILE_REMOTE_DEVICE).

In the last two cases, the caller (BamUserSettingsAddOrUpdate) fails with the 0xC00000BB status (STATUS_NOT_SUPPORTED).

And the discovery is: BAM entries aren’t created for executables on removable media and/or on network shares.

This can be verified in the virtual machine too. I run two executables, one from a secondary fixed drive and another one from a USB Flash drive. And, finally, there is only one entry for the former executable. As expected, I also see a BAM entry for a program launched from a mounted shadow copy, this entry points to a shadow copy device (“\Device\HarddiskVolumeShadowCopy1\” in my case). (The test system is Windows 10, build: 18363.)

So, the second question in the challenge is answered. Or not?

During the final tests, which were described above, I found that console applications don’t get their BAM entries if they are launched from the command line session. This is unexpected, because no related conditions have been observed during the static analysis.

Again, I launch the kernel debugging, place a breakpoint on the BamUserSettingsAddOrUpdate function and try to launch a console application from the command line session. And there is no hit (but the application is running): this function isn’t called for this type of execution.

Let’s place a breakpoint on its caller (BampCommitThrottledProcessStateChange). No hit again.

After several hours of tracing, I found a suspicious check in the BampThrottlingWorker function, which gave different execution paths for these execution types. This behavior can be reproduced when running from the PowerShell session and even when starting a console application from the Python shell (using the subprocess.Popen() call). Given that the cmd executable makes calls to the CreateProcessW() routine when launching both types of applications, this is going to be a new challenge to solve!

Unfortunately, I ran out of spare time. Let’s do the post!

One thought on “BAM internals

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s