Note: This blog is quite technical and I wanted it to be a quick, concise reference for TTPs. If you find the text explanations to be a bit dense, I also went over these TTPs in our stream last week and a recording is embedded at the end. If you [...]
Note: This blog is quite technical and I wanted it to be a quick, concise reference for TTPs. If you find the text explanations to be a bit dense, I also went over these TTPs in our stream last week and a recording is embedded at the end.
If you have been paying attention to some of our recent updates to Empire, you may have noticed that we have been focusing a lot on IronPython. We have written about this a few times before, but now seems like a good time to go into a little more detail about what we have been doing. If you have never read anything about Bring Your Own Interpreter (BYOI), I highly recommend reading Byt3Bl33d3r’s intro article to the concepts, as it was one of the core inspirations for the dev work we have been doing. The second source of inspiration that we have previously written about was the leak of IronNetInjector.
With the update of IronPython from 2.7 to 3.4 and some creative problem-solving from Coin, you can now execute IronPython without issue entirely from memory. This has led to being able to do all kinds of cool things with it and we have already published a couple of things like the IronPython agent for Empire and IronSharpPack, but we haven’t given many details on how the tradecraft works. In this blog, we will examine some of the basics of the tradecraft. From a practical implementation standpoint, some of this has been adapted from our Taming Offensive IronPython workshop.
Before we get into the practical implementations, let’s talk a little bit about what advantages IronPython provides and why they exist. As I mentioned above, By3t3bl33d3r published several research talks and GitHub repos about using DLR languages for offensive operations. These languages run on top of the Common Language Runtime (CLR) environment, the core component of .NET within Windows. He liked that these languages returned to the simplicity of scripting languages after security improvements had forced a move from PowerShell to C#.
That’s a lot of technical jargon, so let’s give a quick overview. .NET is not a language but an ecosystem that allows for platform-agnostic software development by leveraging a runtime environment similar to Java. In fact, it was essentially built by Microsoft as a competitor to Java. However, it is more expansive than Java, with multiple programming languages built on top of the managed runtime. Here, you can see how the various CLR languages are built on top of common classes and specifications.
This framework is what makes C# such a powerful language for offensive operations. As long as the .NET framework is available on the target machine, we can largely reuse our code without knowing the underlying specifics of the computer we are attacking. However, C# requires us to precompile our code and know specifics about the version of the .NET runtime installed on the target. While there are multiple ways to overcome these limitations, there is another reason that we may want to move away from directly using traditional languages and that is the introduction of security features to .NET. Increased scrutiny of the use of .NET processes has also made it necessary to further migrate TTPs in modern environments. For many testers, this has meant moving to unmanaged C/C++ code. This has quite a few advantages but comes with the price of making development much harder.
Alternatively, we can move to the DLR. The DLR runs on top of the CLR and, as mentioned in the BYOI concept, has the advantage of bringing back the ease of scripting. More importantly, the DLR uses a different base compiler class than the CLR. Without diving too deep into the technical, when a DLR language is interpreted, it is parsed into Abstract Syntax Trees (ASTs) and then compiled via lambdas, which differ from the compiler leveraged by languages like the CLR. This means that we are not having .NET assemblies scanned by AMSI and that our IL and bytecode look different when it hits the Just-In-Time (JIT) compiler of the managed runtime. Below shows the interpretations between common languages and their execution/compilation paths. So, with that, let’s look at a couple of key pieces of tradecraft.
Win32 API Access and the Importance of Data Types
One of the most important pieces of tradecraft that we need access to is the Win32 API. This allows us to make calls to functions like VirtualAlloc or GetModuleHandle and is key to doing things like dump memory, AMSI Bypasses, API unhooking, and more.
In IronPython, we have two options for accessing the Win32 API. The first and easiest is to leverage Python’s ctypes. This code has been fully ported for IronPython and allows us to use IronPython to load DLLs into our process and access the exported functions. Below is an example of an AMSI bypass leveraging this:
There are a couple of things to note here. First, ctypes make accessing these functions super easy. Second, managing data types is one of the most sensitive parts of using IronPython. Where possible, you need to define the function signature. <function>.argtypes defines the data types for the arguments when we call the function and <functoin>.restypes defines the return type for the function. Without this, IronPython will automatically convert the types to Python types that the functions may or may not be able to interpret. This is true in regular Python as well. In general, unmanaged functions accessed via Win32 types will always take ctypes data types. These are typically designated with the c_ prefix. Below the first image shows GetModuleHandleW failing with an invalid handle value when the arguments aren’t defined and the second shows proper execution with a valid handle value.
In addition to defining the data types in the function signatures, we also need to ensure that we use consistent data types across our code. It is best to choose either ctypes data types or .NET data types. Above, we imported the .NET types and used them for everything outside of the function signature designations. Generally, I default to always using the ctypes data types, but I wanted to show an example using the .NET types.
If we want to avoid ctypes, for whatever reason, it is also possible to access .NET’s P/Invoke functionality. This is more complicated but has the advantage of not needing the ctypes module from the standard library. If you ever want to try and run an op entirely without the standard library brought along, this is the path you will have to take. Here is the same AMSI bypass implemented with P/Invoke:
As is immediately obvious, this method requires significantly more code to implement than using ctypes. It also requires a couple of extra Python classes beyond the base IronPython Interpreter DLLs. Specifically, it requires the clrtype.py file. This can be embedded as a resource, as the standard library is in Empire, or the code can be executed as part of the script. Other than that, no additional files need to be brought along, unlike the previous example. While this code may look intimidating, it follows the same pattern as the one above. The decorators (the stuff with the @) above each function allow us to define the function signature, just like in the previous example. Since we don’t have the standard library available to us, we instead define the signatures using .NET types.
Another thing to take note of is that we must Marshal a string to an ANSI string because the Win32 API requires us to pass a pointer to a string rather than the string itself. When using ctypes this is automatically done for us in the background, but we have to manage that ourselves if we go the P/Invoke route. As a result of the extra code and additional overhead of managing additional types, we tend to use ctypes for most of our code, but as the DLR becomes more monitored, additional options are always nice to have. Below is the code with the function signature and the marshal string to ptr labeled.
Importing Additional .NET assemblies
Standard .NET assemblies like System and its sub-classes can be easily imported using standard Python syntax as we did in the previous examples to access the Marshal functions. However, many assemblies are not directly accessible without first adding them to our CLR object. It’s a very easy process but an important one to know about. Here is a quick example of loading in the PowerShell assembly, System.Management.Automation.
import clr
clr.AddReference("System.Management.Automation")
from System.Management.Automation import Runspaces, RunspaceInvoke
runspace = Runspaces.RunspaceFactory.CreateRunspace()
runspace.Open()
scriptInvoker = RunspaceInvoke(runspace)
pipeline = runspace.CreatePipeline()
pipeline.Commands.AddScript(r"'hello from PowerShell'")
pipeline.Commands.Add("Out-String")
output = pipeline.Invoke()
for o in output:
print(o)
runspace.Close()
The key piece here is that clr.AddReference("System.Management.Automation") once you have run this, you are free to use normal Python syntax for importing classes. Extremely easy and extremely useful.
Building Structs/Enums and Marshalling to Pointers
Structs are an important part of Windows memory interactions, and many Win32 APIs take them as input. As done in PowerShell, it is possible to build Enums and other objects by leveraging TypeBuilder and the dynamic emitters. However, I find this tedious, with minimal advantage in terms of detection at the moment. Here is a quick example of building a dynamic enum, but we will not spend time going over how this works:
import clr
import System
clr.AddReference("mscorlib")
from System import AppDomain, Int32
from System.Reflection import AssemblyName, TypeAttributes
from System.Reflection.Emit import AssemblyBuilderAccess
domain = AppDomain.CurrentDomain
assembly_name = AssemblyName("TestEnumAssembly")
assembly_builder = domain.DefineDynamicAssembly(assembly_name, AssemblyBuilderAccess.Run)
module_builder = assembly_builder.DefineDynamicModule("TestEnumModule")
# Define the enum name, Public or Private and the data type of the fields
type_builder = module_builder.DefineEnum(
"TestEnum",
TypeAttributes.Public,
Int32
)
type_builder.DefineLiteral("Attribute1", Int32(1))
type_builder.DefineLiteral("Attribute2", Int32(2))
type_builder.DefineLiteral("Attribute3", Int32(3))
dynamic_enum = type_builder.CreateType()
print(f"Enum type created: {dynamic_enum.FullName}")
for name in dynamic_enum.GetEnumNames():
value = System.Convert.ToInt32(dynamic_enum.GetField(name).GetValue(None))
print(f"{name}: {value}")
Instead, we will focus on using the Python Enum and ctypes.Structure classes for this kind of thing. They are very straightforward. You create a class that inherits from these two types. The only catch is in defining the underlying values when working with structs. An Enum will look like this:
For a struct, we follow the solution outlined in this stack overflow post. The main thing is that we need to define the special ctypes.Structure attribute fields. Each field is a tuple, with the first element being the field name and the second element being the data type. Here is an example:
One of the drawbacks of IronPython is that we can’t directly create blittable structures that can be used with Marshal.StructureToPtr. For many unmanaged functions, we must pass the pointer to a struct rather than the struct itself. So, instead of using the .NET function, we can leverage ctypes again to cast our structure to a pointer. If we want to provide a pointer to the structure we defined in the previous section, it would look like this:
pKRB_TICKET = ctypes.POINTER(KRB_TICKET)()
From this, we can then proceed to leverage the pointer for use with any number of APIs. If we need to go back in the other direction (pointer to struct) we can use a function like this:
With these two tricks, we can access most APIs that involve structures.
Delegates
With the tips above, we have solved most of the big problems in standard offensive tradecraft, but we need one more, the ability to define delegates. If you aren’t familiar with delegates, this article on system calls has an excellent primer on them and how they relate to callbacks under the Understanding Delegates and Native Code Callbacks section of the article. The TLDR about them is that they are a reference to a method, similar to how a function pointer works. They are required for making many system calls and required for advanced tradecraft. They are fairly easy once we know which library definition to use. For this example, we will use the EnumWindows function in user32.dll. For this, we will return to our P/Invoke method to provide another example of how it works. Here is what the first part of a P/invoke method will look like:
import clr
clr.AddReference("System")
clr.AddReference("System.Core") # Add this reference for LINQ support
import System
from System import IntPtr, Func, UInt64
from System.Runtime.InteropServices import DllImportAttribute, PreserveSigAttribute
import clrtype
from enum import Enum
#delegate signature
EnumWindowsProc = Func[IntPtr, IntPtr, bool]
class NativeMethods(object, metaclass=clrtype.ClrClass):
__metaclass__ = clrtype.ClrClass
DllImport = clrtype.attribute(DllImportAttribute)
PreserveSig = clrtype.attribute(PreserveSigAttribute)
#Function signature. Same as in our AMSI Bypass example
@staticmethod
@DllImport("user32.dll", SetLastError=True)
@PreserveSig()
@clrtype.accepts(EnumWindowsProc, IntPtr)
@clrtype.returns(UInt64)
def EnumWindows(lpEnumFunc, lParam: IntPtr):raise NotImplementedError("No EnumWindow")
# Callback function. Called for each window handle enumerated
def outputwindow(hwnd, lParam):
print("callback called: ", hwnd)
return True
delegate_instance = EnumWindowsProc(outputwindow)
NativeMethods.EnumWindows(delegate_instance, IntPtr.Zero)
print("testing complete")
The code looks code but unfortunately, when we try to execute our script, we get the following result:
This is because although EnumWindowsProc = Func[IntPtr, IntPtr, bool] is a valid method of building a delegate, we once again run into issues with Marshal. Marshal does not know how to work with a generic delegate. This method does not assign a name to the delegate and so is held in a different memory structure.
Instead, we have to use the Microsoft.Dynamic library and import Microsoft.Scripting.Generation.Snippets. This will allow us to define a delegate with an arbitrary name, which is all that Marshall requires for it to no longer be considered generic. The final code looks like this:
import clr
clr.AddReference("System")
clr.AddReference("System.Core") # Add this reference for LINQ support
import System
from System import IntPtr, Func, UInt64
from System.Runtime.InteropServices import DllImportAttribute, PreserveSigAttribute
import clrtype
clr.AddReference("Microsoft.Dynamic")
from Microsoft.Scripting.Generation import Snippets
#this defines a none generic delegate as the marshaller does not accept generic delegates such as Func
EnumWindowsProc = Snippets.Shared.DefineDelegate("MyDelegate", bool, IntPtr, IntPtr)
class NativeMethods(object, metaclass=clrtype.ClrClass):
__metaclass__ = clrtype.ClrClass # https://github.com/IronLanguages/ironpython3/issues/836
DllImport = clrtype.attribute(DllImportAttribute)
PreserveSig = clrtype.attribute(PreserveSigAttribute)
@staticmethod
@DllImport("user32.dll", SetLastError=True)
@PreserveSig()
@clrtype.accepts(EnumWindowsProc, IntPtr)
@clrtype.returns(UInt64)
def EnumWindows(lpEnumFunc, lParam):raise NotImplementedError("No EnumWindow")
def outputwindow(hwnd, lParam):
print("callback called", hwnd)
return True
delegate_instance = EnumWindowsProc(outputwindow)
NativeMethods.EnumWindows(delegate_instance, IntPtr.Zero)
When we try our code again. We successfully get execution with a large number of windows listed.
Conclusion
With the above information, you should be able to write about any technique you need in IronPython. We have had a lot of success using IronPython recently, but here are a few more parting tips to keep in mind as you get ready to leverage this new tradecraft. First, the biggest bottleneck is loading the IronPython runtime DLLs. You can compile the DLLs from the git project and leverage some obfuscation tools to address or create a loading cradle, as most security tools are just flagging on known loaders like the default Empire loader rather than the IronPython DLLs themselves.
The second is to always remember your type management. If, while developing code, you run into functions inexplicably failing, then it is almost certain that the variable you are passing is the wrong data type. Unfortunately, many APIs have little to no documentation, and figuring out the exact data type can be difficult.
I hope this guide has been helpful. As I mentioned before, I also went over this in our stream last week and embedded the recording below. Happy hacking.
Not Your Grandfather’s Empire I’ve wanted to put this blog together since returning home from DEFCON. Anytime we ran into someone who recognized our swag, they mentioned how much they ...