This is a joint blog written by William Burgess (@joehowwolf) and Henri Nurmi (@HenriNurmi).
In our ‘Cobalt Strike and YARA: Can I Have Your Signature?’ blog post, we highlighted that the sleep mask is a common target for in-memory YARA signatures. In that post we recommended using the evasive sleep mask option to scramble the sleep mask at run time and break any static signatures. However, this solves the problem at the cost of introducing further forensic artefacts onto a host and increasing our footprint. A much simpler solution is to mutate the sleep mask each time we compile it to make static signatures redundant.
This blog introduces the mutator kit, which uses an LLVM obfuscator to break in-memory YARA scanning of the sleep mask. In the following sections, we will give a quick background to the mutator kit and then show you how to apply it so that a uniquely mutated sleep mask can be applied every time a payload is exported.
The mutator kit is available in the Arsenal Kit now.
Mutator Kit
Typically, given the same source code, compilers will generate the same machine code (I.e. they can be considered, with some caveats, deterministic). As an example, we can build the sleep mask with MinGW and compare the .text sections between different builds. Closer analysis reveals the .text sections are the same:
// [1] Build the sleepmask.
$ ./build.sh 49 WaitForSingleObject true none /tmp/dist
[ ... ]
[Sleepmask kit] [*] Compile sleepmask.x64.o
[ ... ]
// [2] Use objdump to find the text section size.
$ objdump -h sleepmask.x64.o
sleepmask.x64.o: file format pe-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000200 0000000000000000 0000000000000000 00000104 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
// [3] Extract the .text section.
// NB skip is the offset('File off') of the .text section
// from objdmp and count is the size of the section
// e.g. python -c 'print(int("104", 16))' == 260.
$ dd if=sleepmask.x64.o of=sleepmask1.bin skip=260 count=512 bs=1
// [4] Calculate shasum.
$ shasum sleepmask1.bin
4f7813a6aae018a4cf6a78040d9c20024b5a83da sleepmask1.bin
// [5] Repeat the steps again to build another sleep
// mask and extract/hash the .text section - the hash is identical!
$ shasum sleepmask2.bin
4f7813a6aae018a4cf6a78040d9c20024b5a83da sleepmask2.bin
This is clearly a problem when attempting to hide from YARA signatures which look for specific op code patterns. An example of this can be found in the following YARA signature, which looks for the following op code pattern in the default sleep mask (0x4C 0x8B 0x53 0x08
etc.):
mov r10, [rbx+0x08]
mov r9d, [r10]
mov r11d, [r10+0x04]
lea r10, [r10+0x08]
test r9d, r9d
jnz 0x0000000000000007
test r11d, r11d
jz 0x0000000000000035
cmp r9d, r11d
jnb 0xFFFFFFFFFFFFFFE8
mov rdi, r9
mov r8, [rbx]
As the sleep mask is visible in memory when Beacon is sleeping, this can be a trivial detection opportunity, as demonstrated below:
Ideally, we would like to compile the sleep mask and get a unique build each time, in order to make it impossible to produce high fidelity YARA signatures at scale. A common technique for mutating code is using LLVM, of which there are numerous well documented open-source projects (for example, see 0xpat’s blog). Typically, these make use of LLVM Intermediate Representation (IR) code to apply a number of transformation passes to produce obfuscated / mutated machine code.
Our mutator kit adopts a similar approach and contains four obfuscation passes which are based on eShard’s obfuscator-llvm plugin. This in turn is based on mutations introduced in the research by Pascal J., et al. These passes include:
- Substitution- Replace binary operators with functionally equivalent ones
- Bogus- Insert fake control flow blocks
- Code Flattening- Aims to break higher level code/control flow structure
- Basic-block Splitting- Aims to break higher level code/control flow structure
More information on these can be found in the research paper referenced above. Note that we are not overly concerned with making the sleep mask hard to reverse engineer; we are primarily interested in breaking static signatures. Hence, we will not go into any more detail into creating obfuscation passes for LLVM. However, the mutator kit README.md
contains a number of references should you wish to fork our obfuscator-llvm repo and create your own passes.
Usage
We have provided two methods to install the mutator kit:
- Installing the requirements directly (referred to as ‘native’)
- Docker
As LLVM plugins require a specific LLVM version and environment, docker makes it easy to handle the required setup and obfuscation plugin compilation. However, if you do not wish to use docker, scripts are provided to bootstrap this process for you (I.e. method 1/native). Additionally, both docker and LLVM can be complicated to use on Windows, so the native method has the advantage of being simple to run on Windows via the Windows Subsystem for Linux (WSL). This blog will assume installation via the native method but see the README.md
in the mutator kit repo for more guidance on using the provided docker container.
After installing the requirements, the primary workflow is to load the sleepmask_mutator.cna
script into Cobalt Strike. This will automatically apply a mutated sleep mask to your exported Beacon payloads (see the Cobalt Strike Client
section below). This script abstracts away the low level details of the mutator kit and makes it very easy to get up and running. However, in order to demonstrate some of the functionality of the mutator kit we will use the command line in this section.
The mutator kit can be manually invoked from the command line via the mutator.sh
script. The script takes the following arguments:
mutator.sh <target architecture> <clang args>
To demonstrate the obfuscation passes in action we can take the following simple C program (example.c
):
void go() {
int a = 5;
int b = a + 6;
}
We can compile this with only the substitution pass enabled with the following command:
$ OBFUSCATIONS=substitution mutator.sh x64 –c example.c -o example.o
One helpful way of demonstrating the effect of specific obfuscation passes is by generating LLVM IR code and comparing it to the original (unmutated) code. This is demonstrated in the example below:
// Build example.c with only the substitution pass enabled
$ OBFUSCATIONS=substitution mutator.sh x64 -emit-llvm -S example.c -o example_with_substitutions.ll
// Compare the original LLVM IR code with the mutated version
$ diff --color example.ll example_with_substitutions.ll
12,13c12,16
// example.ll
< %4 = add nsw i32 %3, 6
< store i32 %4, i32* %2, align 4
// example_with_substitutions.ll
> %4 = sub i32 %3, 1041996456
> %5 = add i32 %4, 6
> %6 = add i32 %5, 1041996456
> %7 = add nsw i32 %3, 6
> store i32 %6, i32* %2, align 4
This trivial example demonstrates the impact of only including the substitution pass on the generated code.
More detailed documentation on LLVM IR is available, but with a basic understanding this can help debug any problems and to sanity check that specific obfuscation passes have been applied correctly. This makes for a quicker feedback loop, rather than opening the generated object file in IDA.
Having demonstrated the basic usage of the mutator kit, we can now apply it to the sleep mask with the following command:
$ mutator.sh x64 -c -DIMPL_CHKSTK_MS=1 -DMASK_TEXT_SECTION=1 -o sleepmask.x64.o src49/sleepmask.c
Obfuscation flattening enabled
Obfuscation substitution enabled
Obfuscation split-basic-blocks enabled
Note that the -D*
arguments are used to add an implicit #define
to the sleep mask which are consistent with the options provided in the build.sh
script within the sleep mask kit. For more information on clang command line arguments see the Clang documentation. Additionally, the –DIMPL_CHKSTK_MS=1
flag is needed to avoid any issues when loading the sleep mask into Cobalt Strike.
By default, only three passes are applied (flattening, substitution, and split-basic-blocks) as bogus can increase the code size. However, you can override the default behaviour by passing in the OBFUSCATIONS
environment variable (OBFUSCATIONS=flattening,substitution,split-basic-blocks,bogus mutator.sh x64..
etc.). See the README.md
included in the mutator kit for more guidance.
At this stage, we can compare the default sleep mask to an LLVM mutated sleep mask. The screenshot below shows the call graph for the same function in the default sleep mask (left) and a mutated one (right):
Cobalt Strike Client
The example above has demonstrated basic use of the mutator kit and the impact it has on compiled code. However, to make experimenting with the mutator kit and sleep mask as simple as possible we have included a cna script (sleepmask_mutator.cna
) which adds a menu item allowing you to configure the mutator kit through the Cobalt Strike client. This option is demonstrated in the screenshot below:
This menu allows you to:
- Select what obfuscation passes to apply
- Select whether you want to rebuild the sleep mask for every payload export
The script will then automatically apply a mutated sleep mask based on these options to your exported Beacon payloads. Therefore, if desired, it is possible to ensure that every time you export a payload, a different LLVM mutated sleep mask will automatically be applied. The script will also ensure that an error is thrown if any problems are encountered to guarantee a default sleep mask is not accidentally applied (and which could subsequently endanger OPSEC).
The output of the sleepmask_mutator.cna
script can be seen in the screenshot of the Script Console below when generating a raw HTTP Beacon DLL:
With our mutated sleep mask, we can re-run the YARA scan against a process hosting beacon and reveal that no hits are found:
We can also use the mutator kit to compile other BOFs. You may want to consider doing this for a higher level of OPSEC. As a note, most open source BOFs are intended to be compiled with MinGW. Hence, when compiling BOFs with the mutator kit it is highly likely you will encounter compiler issues which you will need to resolve on your own.
Conclusion
This blog has introduced the mutator kit which is available in the Arsenal Kit now. This kit is designed with the intention of making the creation of high fidelity YARA signatures targeting the sleep mask in-memory impracticable. With this release you can now generate mutated sleep masks on every payload export, which will fundamentally break pre-canned YARA signatures and provide enhanced OPSEC against in-memory signatures.