MythicAgents / Apollo

A .NET Framework 4.0 Windows Agent
BSD 3-Clause "New" or "Revised" License
434 stars 90 forks source link

Added inline_assembly Task (Apollo v2) #60

Closed thiagomayllart closed 2 years ago

thiagomayllart commented 2 years ago

Hi!

This is similar to the previous pull request: https://github.com/MythicAgents/Apollo/pull/56

I've tried to follow the design patterns of Apollo v2. This pull request adds the inline_assembly task and also modifies the way the assemblies are stored and retrieved by assembly_inject, execute_assembly and execute_pe by using DPAPI keys to decrypt the in-memory assemblies.

djhohnstein commented 2 years ago

Publishing previous description from #56:

Hi! I've added a new file DarkMelkor.cs in the Utils folder, while also modifying the AssemblyManager.cs file to add the inline_assembly function.

Some months ago, @b33f (FuzzySecurity) developed Melkor: https://github.com/FuzzySecurity/Sharp-Suite/. This tool uses CryptProtectData and CryptUnprotectData functions to encrypt and decrypt .NET assemblies in memory, which is a good way to avoid memory scans flagging registered assemblies in the Apollo agent. Melkor also leverages the capability to run these loaded assemblies in disposable AppDomains, which is a good alternative to fork&run tasks in case process injection (at least through some techniques) is not an option.

The only issue with Melkor is the fact that it is not capable of running these Assemblies in case you are doing this in an injected process. Because of that, some days ago I've modified Melkor into this version: https://github.com/thiagomayllart/DarkMelkor.

This version still encrypts and decrypts the .NET assemblies through the crypt32 functions. However, the way it runs is based in this article: https://www.accenture.com/us-en/blogs/cyber-defense/clrvoyance-loading-managed-code-into-unmanaged-processes.

Bryan Alexander and Josh Stone found a way to "bypass" the way the AppDomains segregate the .NET assemblies. They found that it is possible to create two Cross App Domain Delegates: one of them pointing to a function that can be resolved by our disposable AppDomain (generally a function in the mscorlib) and another one pointing to the malicious function (which, in the context of the Apollo agent, will be the one actually invoking the loaded bytes of our registered assembly).

By patching the initial bytes of the function (the one that can be resolved) with the jmp instructions (mov rax, &delegate; jmp rax) to the malicious delegate, it is possible to callback this function in a way that, instead of running the non-malicious one, it will end up jumping to the address of the other one, thus, loading the malicious code.

I've also modified the other functions that handle .NET assemblies in AssemblyManager.cs in order to decrypt the registered assemblies. This might help to avoid some memory scans even when using the fork&run tasks.

Some things to mention:

Since Apollo is compiled for .NET 4.0, I have not added the correct position to patch in case of .NET 4.5 and above. In case that's necessary, i've added these in here: https://github.com/thiagomayllart/DarkMelkor/blob/main/DarkMelkor/DarkMelkor/DarkMelkor/hDarkMelkor.cs (line 126). It seems that in .NET 4.5, the patch position when you are doing this in an injected process is different from when you are doing it by running the compiled Apollo.exe directly. I have not verified these positions in >.NET 4.5.

Currently, the way DarkMelkor modifies the memory regions to RWX is by using the VirtualProtect function. It also leverages RtlZeroMemory by declaring an extern. These problems might be solved by adding ntprotectvirtualmemory and the capability to resolve syscalls dynamically (without dinvoke/without reading ntdll from disk), which solves the problem of having an EDR hooking these functions. I intend to add these in a further request.

Credit goes specially to @b33f (Fuzzy Security), Bryan Alexander and Josh Stone, who actually researched the mentioned techniques, I've just assembled them.

djhohnstein commented 2 years ago

First and foremost, great work. I'm going to write my thoughts about the structure of this PR in no particular order.

I believe that while the PR may be functional, the way they are implemented violates some of the design patterns put in place by the code base. From what I can understand, this PR wants to achieve the following:

  1. Implement a new task called inline_assembly
  2. Implement functionality to create new, disposable AppDomains (and modify their attributes, execution, runtime, etc)
  3. Implement a DPAPI store to store and retrieve files from

The DPAPI Problem =============

So let's start at DPAPI store. The DPAPI store, as it stands, is a more complex blob of information than I believe it currently needs to be. Fundamentally, what you're implementing is an encrypted file store, but in the PR its implemented as its entirely own management interface. Moreover, even though there's a management interface, the task is still the one responsible for freeing the memory associated with that DPAPI module.

So, here's where I stand. First, DPAPI encryption should be its own EncryptedFileStore. At the end of the day, the Task shouldn't care how that data is encrypted. It wants a file, identified by a key name, and what it wants in return is byte data.

Second, DPAPI "modules" themselves have the issue of freeing decrypted data. The task shouldn't have to worry about those kinds of details, so I propose these need to be refactored in a way that implements the IDisposable interface, such that the memory is freed when the object is disposed of.

Third, these DPAPI modules can either be implemented at the EncryptedFileStore level or, at the API level. I'll talk more about the API in a second.

Disposable App Domains ==================

Disposable App Domains have been implemented on a common library of Apollo. I believe this type of functionality should live on the API interface and those that implement it. The details surrounding how its spun up, its constructors, and whatever else I haven't really thought through, but I know the Interop library is not the place for it to live. Eventually I want to extract the API in general to its own project and away from the core, so that it can be unit tested itself.

As mentioned above, I also believe that DPAPI "modules" could potentially live here. How they're implemented in the "decryption/unwrapping" I'm not entirely sure. The flow I have in my head is:

  1. You store a file in the DPAPI Encrypted File Store
  2. You retrieve a DPAPI byte blob from the store.
  3. You forward that to the API to unwrap that into its own disposable object, which will free the memory associated with the decryption process.

I don't know if that's exactly right until I get my hands on working it a bit more, but that's just my initial thought at how something like that should be implemented.

Inline Assembly Task ===============

Lastly, I don't think there's anything wrong with the task itself. It's the changes in the surrounding agent that facilitated this task is the issue. To make this task work, it had to fundamentally change how other taskings work, which I think is an indicator there's something wrong with the implementation of its core components.

Overall, I think we're close, but there still needs to be work done to get this off the ground.

thiagomayllart commented 2 years ago

Thanks!! Yeah, the PR objective is exactly what you've mentioned. Your solution of using the native .NET DPAPI encryption/decryption library really solves the issue of having the DPAPI blob to store all the information for the decryption process.

The modification in FileManager.cs to use DPAPI encryption as the default encryption also removes the necessity to update the other assembly tasks: the GetFileManager().GetFileFromStore function call can stay the same in all assembly tasks (no need to create another FileManager class, instantiate in the agent, etc).

The AppDomain function calls/modules were removed from the ApolloInterop solution. One of the alternatives was to move it to its own solution, however, this would probably add extra workload when debugging. Also, since there are not many related functions to perform the AppDomain creation/disposing I've just moved it to the inline_assembly task.