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

Add 38 new registry-based persistence techniques #954

Merged

Conversation

jorik-utwente
Copy link
Contributor

Add 38 new registry-based persistence techniques.
All new rules have references to online resources describing the technique.

This PR requires #952 to be merged.

I am planning to add examples later. Would it be preferred to have POC implementations, or real-world malware that uses the technique?

@mr-tz
Copy link
Collaborator

mr-tz commented Nov 28, 2024

Would it be preferred to have POC implementations, or real-world malware that uses the technique?

We currently have both, most test files are actual malware samples though.

Copy link
Collaborator

@mr-tz mr-tz left a comment

Choose a reason for hiding this comment

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

Wow, great enumeration of persistence mechanisms here. I've not seen most of them used in malware so I'm very curious to their prevalence in the wild.

As before comments and suggestions inline.

I'd also like to get another reviewer on all of these because these are quite big updates.

Thank you!

nursery/persist-via-cor_profiler_path-registry-value.yml Outdated Show resolved Hide resolved
nursery/persist-via-disk-cleanup-handler-registry-key.yml Outdated Show resolved Hide resolved
nursery/persist-via-dotnet_startup_hooks-registry-key.yml Outdated Show resolved Hide resolved
nursery/persist-via-filter-handlers-registry-key.yml Outdated Show resolved Hide resolved
nursery/persist-via-path-registry-key.yml Outdated Show resolved Hide resolved
nursery/persist-via-task-scheduler.yml Outdated Show resolved Hide resolved
nursery/persist-via-universal-app-uri-registry-key.yml Outdated Show resolved Hide resolved
@mr-tz
Copy link
Collaborator

mr-tz commented Dec 4, 2024

Updates look great to me, thank you very much!

Copy link
Collaborator

@mike-hunhoff mike-hunhoff left a comment

Choose a reason for hiding this comment

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

Wow, awesome work @jorik-utwente ! These updates look good but I did notice that the dynamic scope should be thread instead of call for rules that match against both the registry subkey and value, as the subkey and value strings are likely going to be referenced in separate function calls.

That raises another issue with capa's existing rule set registry value. The dynamic scope for this rule should also be thread instead of call otherwise the optional match for create or open registry key can never match (it's optional so it's not show stopper). Thoughts @mr-tz ?

static: function
dynamic: call
att&ck:
- Persistence::Boot or Logon Autostart Execution::Port Monitors [T1547.010]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should the rule name use Port Monitors instead of Print Monitors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for your input, @mike-hunhoff !

To my understanding, the cape report always contains the registry path and value name within one registry write function call.

E.g.

         {
            ...
            "category": "registry",
            "api": "RegSetValueExW",
            "status": true,
            "return": "0x00000000",
            "arguments": [
              {
                "name": "Handle",
                "value": "0x00000228"
              },
              {
                "name": "ValueName",
                "value": "DefaultNotificationsSetting"
              },
              {
                "name": "Type",
                "value": "4",
                "pretty_value": "REG_DWORD"
              },
              {
                "name": "Buffer",
                "value": "1"
              },
              {
                "name": "BufferLength",
                "value": "4"
              },
              {
                "name": "FullName",
                "value": "HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Google\\Chrome\\DefaultNotificationsSetting"
              }
            ],
            ...
          },

In what case do the registry path and value name spread out over multiple Windows API calls?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not familiar with the underlying representation used by CAPE for API calls, but it appears that it may be adding some syntactic sugar e.g. FullName is a field specific to CAPE.

Other sandboxes may not do this, e.g. VMRay:

<fncall ts="126662" fncall_id="28904" process_id="1" thread_id="1" name="RegOpenKeyExW" addr="0x7ff8e7439670" from="0x7ff8a1570edf">
	<in>
		<param name="hKey" type="void_ptr" value="0xffffffff80000001"/>
		<param name="lpSubKey" type="ptr" value="0x2c7d58c">
			<deref type="str" value="Software\\Microsoft\\Windows\\CurrentVersion\\Run"/>
		</param>
		<param name="ulOptions" type="unsigned_32bit" value="0x0"/>
		<param name="samDesired" type="unknown" value="0x2001f"/>
		<param name="phkResult" type="void_ptr" value="0xbbdb78"/>
	</in>
	<out>
		<param name="phkResult" type="ptr" value="0xbbdb78">
			<deref type="unsigned_64bit" value="0x75c"/>
		</param>
		<param name="ret_val" type="unknown" value="0x0"/>
	</out>
</fncall>
[...]
<fncall ts="126664" fncall_id="28906" process_id="1" thread_id="1" name="RegSetValueExW" addr="0x7ff8e743b080" from="0x7ff8a1574d2c">
	<in>
		<param name="hKey" type="void_ptr" value="0x75c"/>
		<param name="lpValueName" type="ptr" value="0x2c7b5b4">
			<deref type="str" value="Windows Defender Updater"/>
		</param>
		<param name="Reserved" type="unsigned_32bit" value="0x0"/>
		<param name="dwType" type="unsigned_32bit" value="0x1"/>
		<param name="lpData" type="ptr" value="0x2c7d33c">
[...]
		</param>
		<param name="cbData" type="unsigned_32bit" value="0x7a"/>
	</in>
[...]
</fncall>

You can see the subkey Software\Microsoft\Windows\CurrentVersion\Run and value Windows Defender Updater are referenced in separate API calls RegOpenKeyExW and RegSetValueExW, respectively (0x75c is the handle to the opened registry subkey).

Although this does not appear to be the case for CAPE, we assume most sandboxes will log API calls, including parameter and return names and values, according to their documentation, e.g. https://learn.microsoft.com/en-us/windows/win32/api/winreg/nf-winreg-regsetvalueexw.

@mr-tz do you know if the CAPE extractor is extracting the FullName field?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think CAPE is doing some enhancements to the API trace by enhancing values. We cannot assume this for all sandbox backends so to be flexible rules should account for that.

Copy link
Contributor Author

@jorik-utwente jorik-utwente Dec 6, 2024

Choose a reason for hiding this comment

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

Yes, you are right, CapeVM resolves the FullName, which is not a function argument.

To support other sandbox backends that only log the raw arguments, there will be an impact on the performance of the rules:

  • Extra false positives: Reading a registry key in a rule and writing to a completely different key will result in a false positive. Some registry paths are frequently read from, so rules mentioning these paths will result in many false positives.
  • For other sandbox backends, detection can still easily be evaded. E.g. if you want to write to HKCU\a\b\c, you can do:
a = RegOpenKey(HKCU, "a")
b = RegOpenKey(a, "b")
c = RegOpenKey(b, "c")
RegSetValue(c, key, val)

In this example HKCU\a\b\c will never be passed as an argument to a Windows API call, so we cannot detect it.

I expect that for some rules, changing the scope from call to threat will result in many false positives. I think that as a result we will have to remove these rules, which is a pitty.

When writing rules for the threat scope you have to be very careful, e.g., look at the current run registry key rule: https://github.com/mandiant/capa-rules/blob/master/persistence/registry/run/persist-via-run-registry-key.yml
It has many false positives because it matches the following:

  • HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE mentioned anywhere (e.g., to read a key, which happens a lot), and
  • "Software\Microsoft\Windows\CurrentVersion" mentioned anywhere (common registry path), and
  • "run" mentioned anywhere. (Common word, occurs a lot)
    As a result, there are a bunch of false positives for this rule.

My point is: I understand you want to support different sandbox backends, but it makes writing accurate registry write-rules more challenging and less accurate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good points. For much better handling moving forward we need a different approach (like a new scope within a certain range, #951 (comment)).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good discussion here!

For much better handling moving forward we need a different approach (like a new scope within a certain range, #951 (comment)).

Agree.

I expect that for some rules, changing the scope from call to threat will result in many false positives. I think that as a result we will have to remove these rules, which is a pitty.

In spirit, I agree with you. Though, I'm curious about this in practice. It seems like there might be FPs, but could we test this a bit? Maybe the behavior isn't quite as bad as we worry about.

I only suggest this (which doesn't feel very good) because we think we have a plan for a better implementation (#951) so many a little inaccuracy in the meantime is acceptable.

Copy link
Collaborator

Choose a reason for hiding this comment

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

That raises another issue with capa's existing rule set registry value. The dynamic scope for this rule should also be thread instead of call otherwise the optional match for create or open registry key can never match (it's optional so it's not show stopper). Thoughts @mr-tz ?

@mike-hunhoff mandiant/capa#2124 raises the issue that capa doesn't validate scope dependencies correctly, so there are probably more of these hidden mistakes lurking around.

In the meantime, I agree this isn't a showstopper, since it's only an optional term that can't match. We have a bit of work to do to get all these scopes straightened out...

Copy link
Collaborator

@mr-tz mr-tz left a comment

Choose a reason for hiding this comment

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

Willi and I discussed this offline:

  • even if there are some minor issues, we can address those when we have better thread/region/call scope
  • if we run into many FPs (or any FNs) we can adjust on a case by case basis
  • let's see how common these are in practice

@mr-tz mr-tz merged commit e033410 into mandiant:master Dec 9, 2024
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants