Our AI strategy for preemptive security
Red teamers often attempt to deploy malware as part of their engagements, but as they are simulating real adversaries, they need to be able to deal with the defensive software our clients will use. Modern EDR can be incredibly effective at detecting tools, leading to a never-ending cycle of cat and mouse development as red teamers (and real adversaries!) bypass detections, and blue teamers introduce new detections.
As the Principal Red Team Manager at Bugcrowd, I believe there are three primary components to consider when designing a malware loader:
I should note here that there is much more to think about when you consider what comes after loading, but we’ll save that for another blog. This blog post explores an example of the second point above—how we hide the payload—which will often be shellcode.
To understand what we are trying to achieve, we must first understand what shellcode is and why we want to hide it.
Shellcode consists of executable code, most often raw assembly code in byte form, that contains the payload we wish to execute. As red teamers, this will typically take the form of a Command and Control implant, such as a Cobalt Strike Beacon, Sliver Implant, or a custom C2 agent. In some cases we may deploy something more specific than a general C2 payload.
During testing (especially against Windows targets), we will often use a payload which will directly execute “calc.exe”, causing the Windows built-in calculator to pop up. You may see this referred to as “popping calc”.
The reason we have to hide shellcode is simple: Defensive tools, such as EDR and anti-virus, will be looking for it, especially shellcode that carries out actions typically associated with a cyber attack (e.g C2). We could have the best pretext in the world, persuading users to execute our malware, along with the best method of running the shellcode, but get caught before the user can even click if the EDR spots the payload sitting there waiting to run.
This blog post won’t go into the full details of shellcode and all the weird and wonderful things you can do with it. Many resources already exist on this topic, so if you think you’d find it interesting, I would recommend you dive down that rabbit hole (but don’t complain to me when you realize how much time you’ve spent on it!).
Now, this problem has existed for a long time, and both red teamers, and real adversaries have spent a long time working on it. Many methods to hide shellcode have been devised. Some are very simple, such as simply storing it directly in our compiled payload as encrypted text; others are more complicated, such as classic steganography, where the payload is encoded into the colours of pixels in an image.
But as with all things, it’s good to have many options and lots of backups.
I spend a lot of time thinking about malware and ways to bypass protections designed to stop us using it. The best methods often involve encoding the payload into something that is used for other, benign purposes, especially those that are commonly used in truly legitimate software.
In this case, I was completing a course to brush up on the basics of Machine Learning, and was re-learning how classic search algorithms work. The typical example for these is mazes. Generally in a search algorithm, being applied to a maze, you have three things:
The goal is generally to find a path from the starting point, to the goal point, and for this problem, many algorithms are widely known. Taking it a step further, you have algorithms known as “Shortest Path Algorithms.” These are designed to not only find a path from the starting point to the goal point, but to make sure that it is the shortest possible path between the two points.
The idea behind this technique is actually quite simple: We devise a maze, giving each cell of the maze a value, and encode our shellcode to be the values along the shortest path between a start point and goal point. We can then retrieve the shellcode for execution later by solving the maze and finding the shortest path between those points.
The first step to achieve this involves generating the maze and ensuring two points exist, for which the shortest path between them equals the length of our shellcode (technically, it could be longer, and we could pad the shellcode). Many maze generation algorithms exist which can achieve this. The simplest would involve a “perfect maze” in which there is exactly one path between any two points, and no dead ends. However, I decided to aim to allow loops and dead ends to enhance the complexity and make analysis slightly more awkward.
To achieve this, we can first consider that a connected maze breaks down into two components:
There are a whole host of methods we could use to achieve this. In testing, we used Kruskal’s Algorithm with some tweaks. First, we generated a list of every possible doorway in the maze, and randomized the order. Then we walked the list, tracking the connectivity between each cell. For each doorway, we check if it joins two cells which are not yet connected. If so, we open the doorway. This makes sure all cells will be reachable. If the doorway joins two cells that are already connected, then opening it would create a loop. Therefore, we open this on a random chance to ensure some loops are created. In our tooling, we had a variable named loop_probability which controlled this chance. Once the whole list of possible doorways had had this algorithm applied, we had our base maze.
Below is a visual representation of our maze generation:
The next step is to identify a start and end point and ensure the shortest path between them will be able to store our payload. To achieve this, we decide how much data we will pack into each cell. We then know our target shortest path length is the number of cells required to hold the data. In these demos, each cell will represent either one character or one byte of shellcode.
As our maze has ensured that every cell is reachable from any other cell, to decide how we encode the data is simple: We pick any cell as a starting point and execute a breadth-first search from that cell. We can then loop that search until we have explored the size of path required to fit the data. At this point, we will have located all cells that are exactly the target length away via their shortest path, providing us with a set of all possible goal cells. We then pick one of these at random to be our goal cell. Should we find our starting cell has no paths that are the correct length, we can try again with a different starting cell. As long as the overall maze is large enough, such paths will exist.
The issue with this technique is that our maze may contain multiple paths that are the same length (and all the shortest) between the selected start and goal cells. If we use a different algorithm to decode later than the one we used to encode, we might end up selecting another, perfectly legitimate path and fail to retrieve our data. Therefore, at this point, we prune the maze to ensure that the path we want is the only shortest path between the start and goal cells. To achieve this, we work back the other way, starting from the identified goal, and returning to the starting cell. As we have now explored in both directions, we can measure which cells exist on each shortest path and identify branches based on cells existing in multiple paths, then intentionally wall off branching paths, leaving us with a single pruned shortest path between the start and goal cells.
Once we have ensured there is a single shortest path between our start and goal, we can encode our data into the path and fill the remainder of the maze cells with dummy noise data. This in itself is an interesting challenge, as we need to ensure the payload data does not stand out against the noise. In our testing, we have implemented a variety of generation methods for this noise, such as random, weighted random, sampled data, and others (though exploring those techniques is left for a future blog post!).
We can then store the maze in a format of our choosing, ensuring we know the start and goal cells for later decoding.
Now we have generated a maze which contains our shellcode (or other hidden data), retrieving it again is fairly trivial, as we will have the shape of the maze, cell values, and a known start and goal cell. By ensuring only one shortest path exists between the cells, we can use any shortest path algorithm, as we do not have to worry about how the algorithm will decide between multiple shortest paths. Once executed, the encoded data will exist along the path found. This can then be passed to whatever method we are using to execute the shellcode as part of our malware.
Here are a few examples of algorithms we could use:
For the remainder of this post, we have chose to use the A* algorithm for decoding.
Below is an example of this technique being used to hide an ASCII message within a maze. The first image shows the completed maze, without the start or goal squares highlighted. This demonstrates how it is difficult to identify the hidden message within the maze with no further information or context regarding the data’s location.
Next, we have the same maze with the start and goal cells highlighted, and an animation showing the A* algorithm being used to identify the shortest path between these cells. Once the path has been found, the encoded message can be trivially retrieved from the cell values.
So now we have demonstrated the technique with text, how does it look when we try the same thing but with some shellcode? The principles remain the same: generate a maze, prune the shortest paths, encode the shellcode as byte values in the cells, and then fill in the rest of the maze cells with noise. We can then run the A* algorithm across the resulting maze using the start and end points to recover the shellcode.
The example below shows a maze in which a standard “calc” shellcode has been stored.
This decoding can be conducted within our malware code, with the retrieved shellcode passed to an execution function after it has been decoded. Allowing the establishment of Command and Control, or in this case, “popping calc.”
Although we have presented the concept of how to use this technique for hiding shellcode, there are some considerations that must be taken into account before creating a truly weaponized version. These include the size of mazes required for a large shellcode, the entropy of the files, and how you actually execute the shellcode once it has been retrieved. Additionally, there is room for significant improvements to the tradecraft when using this in the real-world.
However, we can’t spill all our secrets… can we?!
This is just one of thousands of examples of expert tradecraft myself and the red teams at Bugcrowd deploy daily to simulate real-world attacks so organizations can fix what matters faster. Talk to an expert today to learn more about how Red Team as a Service can help you address vulnerabilities before adversaries can exploit them.