Normal view

Red Teaming in the age of EDR: Evasion of Endpoint Detection Through Malware Virtualisation

25 September 2024 at 12:36

Authors: Boudewijn Meijer && Rick Veldhoven

Introduction

As defensive security products improve, attackers must refine their craft. Gone are the days of executing malicious binaries from disk, especially ones well known to antivirus and Endpoint Detection and Reponse (EDR) vendors. Now, attackers focus on in-memory payload execution for both native and managed applications to evade defensive products. Meanwhile, defensive technologies are becoming increasingly sophisticated, which is forcing attackers to further adapt. In times of such an arms race, how does an attacker stay ahead? And how can malware be future-proofed to evade the sophisticated EDR systems that currently exist and are actively being developed?

This blog post reviews the evolution of one of Fox-IT’s evasive tools, designed to aid in payload delivery during Red Teaming engagements. We will touch on the tool’s history and its future potential in the face of offensive and defensive progress.

Historical Perspective

The core of the arms race between malware and antimalware is as follows: antimalware must classify arbitrary programs, in memory or at-rest, as either benign or malicious while operating under a set of constraints. The products are constrained by the amount of performance a user or customer is prepared to surrender in terms of CPU time, memory or bandwidth while the classification takes place, and by how many false-positives the product generates. If the product is too resource intensive, a customer will complain it is slow. If it quarantines important documents, it potentially does more harm than good. These constraints shape and limit each step in the evolution of antimalware products. Not only AV vendors need to worry about performance when writing tools. Malware authors need to take execution speed, or other system changes, into account when deploying malware. Take for example the recently uncovered XZ1 backdoor that was spotted by a software engineer due to an increase in login time from 0.2 to 0.8 seconds. Had the authors of this piece of code not observably changed the behavior of the system, the backdoor would have likely been deployed successfully.

Since the early days of viruses circulating on floppy disks, writing undetected malware has been a cat-and-mouse game between attackers and defenders. Originally, antivirus software focused strictly on true-positive detection of viruses on the basis of signatures and patterns in a program’s instructions. Absent a mistake in the signature database, a unique signature match guarantees a true-positive match of a malicious sample after which the malicious file can be removed or quarantined. This method of detection strongly adheres to the constraints placed on antimalware products, because simple pattern matches are performant and true-positive detection is almost guaranteed.

For malware authors, the solution was simple: to evade detection, the virus must be made impossible to detect through a unique pattern. This may be achieved by changing the code, or by encrypting the code and decrypting it at runtime. If you automate this, you get what is called a packer: a tool that encrypts, compresses or otherwise changes a virus to evade detection. A packer changes the majority of the code in the virus and adds a stub to the code. This stub is often the first piece of code that is executed when the program is launched. Its job is to undo all changes previously made to the original code (e.g. compression or encryption). After all changes are reverted, execution will be passed to the original code. This stub can also make use of anti-reversing/anti-tampering code that attempts to protect the original code from prying eyes.

This reduces the amount of “attack surface” for signature creation for samples that are on the disk or otherwise stored at rest. This method is also used to compress binaries for distribution, allowing for smaller release packages. Therefore, not all compressed binaries can be marked as malicious.

However, even very small unpacker stubs may match a signature that can be uniquely tied to the packer itself. Combining this signature with some rules related to the amount of entropy in a file, a packer can still be detected with a high degree of accuracy. At this point, the antimalware solution has evolved to utilize metadata about a file, such as entropy, obtaining the ability to detect packed files but at the cost of a higher false-positive rate.

The next step in the arms race for malware authors is to eliminate the potential for a signature match in the unpacker stub. This means that the stub must consist of different instructions each time a new sample is created. An important insight is that “what the code does” and “how the code looks” are not 1:1 mappings. There are infinitely many ways to write down computer code to achieve a certain effect or result. There are therefore infinitely many ways in which a particular unpacking algorithm can be written. A packer that is designed to create the unpacking stub that looks different each time can be called polymorphic. The algorithm or code that performs the changes is called a polymorphic engine2.

Combining a packer with a polymorphic engine eliminates the “attack surface” for simple signature matches of malware at-rest. Fox-IT has written and maintained two polymorphic packers like this since 2015. Although they still produce good results against modern EDR, even these tools are getting more and more difficult to sneak past defenses. That’s because there’s a conceptual flaw in the polymorphic packer: the original malicious code is still decrypted at some point in order to execute. If antimalware products can time the moment to start scanning for malicious patterns when the packer has finished decoding the malicious code, then detecting malware becomes easy again.

Modern operating systems and processors try to ensure that not all data in a computer’s memory can be executed as code for safety reasons3. Particularly, systems are typically designed to prevent the execution of code from writable pages. Therefore, a virus or malware sample that wants to decrypt and/or decompress its own code must first make the changes in writable memory pages. After, the virus changes the page protection to readable and executable and transfers control to the newly modified executable memory. Antimalware products equipped to analyze the behavior of other programs at runtime make use of behavioral patterns like this to decide when to scan the memory of a process for malicious patterns. Because the memory, once decrypted, cannot be changed anymore due to the aforementioned limitations, scanning a process after making memory executable is the ideal time to spot malicious patterns.

Antimalware products that are equipped with rules that generate additional signals to determine if a program is malicious or not, are said to employ “heuristics”. Conceptually, antimalware products have achieved a comprehensive set of features to detect malware execution. The evolutions we’ve seen since the early days of these feature complete products can all be understood as attempts to loosen or lift the constraints set out above: “Cloud-based protection” runs resource intensive heuristics on someone else’s computer; adding human oversight, the “R” in “EDR” lowers the impact of a false-positive and brings humans into the detection and response loop.

How then, can a Red Team smuggle their malware past these new and advanced defenses? In the past, a virus writer might employ what is called a “metamorphic” engine4. This is an algorithm designed to re-write the entire virus each time it infects a new file, including the entire metamorphic engine itself. Using it ensures that there is never one ‘true’ virus sample that can be detected with a static signature; each copy of the virus is completely different. With a tool like this you would not need a packer, because there are no static patterns that can ever be uniquely tied to your virus. However, the explosion in modern software complexity and the requirement for malware to work on a variety of systems

Hiding From Analysis: Virtualisation

To hide from both static and dynamic analysis of payloads, the generated sample must be resilient to code inspection and code flow analysis. If the real instructions are not revealed to an observer, hardly any conclusions can be drawn from the outer shell. If this is achieved, defensive products would be met with the following limitations when inspecting the payload:

  • Difficult to observe instruction patterns;
  • Difficult to patch instructions;
  • Difficult to ignore instructions;
  • Difficult to predict behavior.

Hiding instructions is not something new. Products like VMProtect5 cloak parts of the code by embedding a virtual machine and generate unique instructions to be executed on this VM. Code that is to be virtualized must be identified either by a marker added to the source code or by the presence of a PDB file containing the symbols. This requirement is something that cannot always be met when using third-party tools. Additionally, this type of protection is aimed at protecting specific functions, like license key checking algorithms, limiting the use for an adversary. Lastly, using existing tools can have a negative impact on the detection ratio, as these products are heavily researched and can contain static signatures like hardcoded section names.

Considering the benefits of a virtualisation layer, however, it is clear that this technique is very powerful.

Creating a Custom Virtualisation Layer

It was decided that a virtualisation layer should be created. This layer consists of a virtual machine implementing opcodes6, and bytecode7 executing on the virtual machine. The virtualisation layer that was to be created must match the following requirements and limitations:

  1. Bytecode instructions are executed sequentially;
  2. Bytecode instructions are hidden before and after execution;
  3. The instruction set supports basic x86-64 instructions only;
  4. The virtual machine must provide an interface to the system API;
  5. The virtual machine implementation must be simple and position independent to support morphing;
  6. The virtualisation layer must work without access to source code or debug symbols.

Creating a virtualisation layer started with a design of the instructions to be executed, the virtual machine, and the supported instruction set. Additionally, the layout of the final payload was created where all data must be present in a position independent format and could be executed like shellcode. This allows the payload to be embedded in other executable formats (e.g. executables or DLLs), and allows for dynamic execution when staging malware.

For example, the following layout would allow for the above functionality. In this example, the virtual machine must start with a correcting stub that correctly sets the virtual machine argument registers to their respective values:

Example of a data structure containing all required building blocks within position independent code.

The Anatomy of an Instruction

To keep the virtual machine architecture simple, an instruction format was created to be consistent in length between instruction and operand types. This design decision allows the omission of a Length Disassembler Engine (LDE)8, and can simply use the instruction pointer as an index to the current instruction. All information present in normal, non-SSE9/AVX10 x86 instructions must be included.

At its core, an instruction identifies the operation that must be performed, and optionally what data is provided in the form of operands. An operand can be one of three types:

  1. Immediate value: a constant value embedded in the instruction;
  2. Memory location: a memory location pointed to by the instruction;
  3. Register: a register, or part of one, identified by the instruction.

In order to obtain data from an operand, a generic format must be created that encompasses the different operand types. It was decided that a single 64-bit field could be used to hold the different types of operands, as all of the necessary data of the aforementioned types can be embedded into 64 bits.

The structures below show the layout of each operand type:

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
struct ImmediateOperand {
Value value; // A constant value
}; // size: 8 bytes
struct MemoryOperand {
uint8_t size; // The effective size of the operand (8, 16, 32, 64 bits)
uint8_t base; // A regiser holding a pointer value to the base address
uint8_t index; // A register holding the index of the array
uint8_t scale; // A constant multiplier of 1, 2, 4, or 8
int32_t displacement; // A value to be added to the calculated address
}; // size: 8 bytes
struct RegisterOperand {
uint8_t reg; // A base register of the x86-64 register set
uint8_t chunk; // The specific register chunk: low, high, word, dword, qword
uint16_t size; // The effective size of the operand (8, 16, 32, 64 bits)
uint32_t pad; // Padding to meet the 64 bit size requirement
}; // size: 8 bytes
union Operand {
ImmediateOperand imm; // View the data as an immediate operand
MemoryOperand mem; // View the data as a memory operand
RegisterOperand reg; // View the data as a register operand
}; // size: 8 bytes
view raw operand.h hosted with ❤ by GitHub

Note: The Value type of the immediate operand is a simple union with (u)int8_t to (u)int64_t members. This makes it trivial to index the correct data during implementation of opcodes.

To indicate the instruction’s opcode, a single 1-byte value can be used. This provides 256 unique opcodes, which should be enough to implement basic behavior. Lastly, the type of each operand must be embedded within the instruction format, as opcode implementations must be able to interrogate these types.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
struct Instruction {
uint8_t opcode; // The opcode of the instruction
uint8_t lparam_type : 4; // The type of the first (left) operand
uint8_t rparam_type : 4; // The type of the second (right) operand
Operand lparam; // The first (left) operand
Operand rparam; // The second (right) operand
}; // size: 18 bytes
view raw instruction.h hosted with ❤ by GitHub

Protecting Instructions

To meet requirement two, “Instructions are hidden before and after execution”, instructions are protected using encryption. Many encryption algorithms can be used to hide instructions. However, it is required for the instruction size to remain the same, as the instruction will be decrypted and encrypted in-place and will not be moved to a temporary buffer. This removes the necessity for dynamic memory allocation from within the virtual machine. Additionally, the chosen encryption scheme must be trivial to implement, as the code will be located in the virtual machine and thus create an ‘attack surface’ for signature detection. Implementing complex algorithms is detrimental to the ability to effectively manipulate the code using a polymorphic engine.

The Anatomy of the Virtual Machine

The virtual machine resembles a virtual CPU, implementing all the available opcodes. Furthermore, the available registers, CPU flags, and stack are part of the virtual machine object. Lastly, the virtual machine holds a pointer to the bytecode buffer necessary for execution. An added benefit of implementing the virtual machine is that the real stack is also abstracted away. Heuristics that attempt to spot malicious behavior from the stack will not succeed.

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
struct Context {
uint32_t ip; // Instruction pointer
uint8_t flags; // CPU flags to be manipulated by opcodes
Register registers[17]; // General Purpose Registers (rax, … r15 and gs)
Instruction* instructions; // A pointer to the start of the bytecode buffer
uint8_t stack[STACK_SIZE]; // The virtual machine stack
};
view raw context.h hosted with ❤ by GitHub

Functions to initialize the virtual machine context, to obtain the current instruction, and to load and store values based on the instruction operands were created to aid in the implementation of opcodes within the virtual machine.

Once initialized, the virtual machine can enter its dispatch loop. This loop consists of obtaining the current instruction and executing the opcode identified by the opcode field in the instruction object. The instruction is decrypted before execution and is encrypted after. A dispatch function could be implemented as follows:

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
void dispatch_instruction(Context* vm) {
uint32_t ip = vm->ip;
decrypt_instruction(vm, ip);
switch (vm->instructions[ip].opcode) {
case Opcode::ADD: opcode_add(vm); next_instruction(vm); break;
case Opcode::AND: opcode_and(vm); next_instruction(vm); break;
case Opcode::BT: opcode_bt(vm); next_instruction(vm); break;
}
encrypt_instruction(vm, ip);
}
view raw dispatch.cpp hosted with ❤ by GitHub

An attentive reader may have noticed the construction of the temporary variable ip, which is used in further operations. This originates from the fact that any instructions modifying the instruction pointer, like jcc, call, and ret, will result in a modified instruction pointer when the opcode is finished. Therefore, the instruction pointer can no longer be used to re-encrypt the original instruction that was executed.

Implementation of a Basic Opcode

The following function implements the bit test (bt) opcode11:

Improving the Bytecode Process: Transpiling

Initially, all bytecode created to execute in the virtual environment was written in assembly by hand. This provided the control needed to make sure specific opcodes and operand types were used, and as a test a PE loader was implemented in bytecode. As this limitation came at a major cost in development time and flexibility, a new method of generating bytecode was used: compiling and transpiling of C/C++ programs. This was chosen over using output directly from the assembler, as parsing these text files proved to be cumbersome and error-prone. Instead, the resulting linked binary was fed to a disassembler.

The disassembling of a binary is performed using the iced-x86 library12. This library allows for the conversion of x86 instructions to the custom format -described in the earlier section: The Anatomy of an Instruction– by checking the opcode of the instruction, the types of operand(s) and its value(s). Eventually, once all the x86 instructions are converted, the now transpiled bytecode can be interpreted by the virtual machine.

The bytecode generation process from source code to eventual bytecode.

The implementation of the transpiler instantly enabled us to support a large amount of existing tools, and made writing new tools easier. Most Position-independent Code (PIC)13 tools that compile from C/C++, including some BOFs14, can also be ported to execute on the virtual machine with relative ease.

Limitations to Bytecode Implementation

One of the limitations of the virtual machine implementation is shared with that of the bytecode. PIC must be created in order to generate valid bytecode that executes on the VM. In practice, this means that everything is relative to the current instruction pointer, and no references to other libraries or parts of other sections can exist:

  • No static variables;
  • No global variables;
  • No strings;
  • No static dependencies on libraries.

Supporting Native API Calls

To allow interfacing with the OS layer, bytecode must be able to perform native API calls. A translation layer must exist between the bytecode and native environment. The call instruction is used by compilers to invoke APIs, requiring the virtual machine’s call implementation to support this translation. Unfortunately, once a call instruction is encountered, no information is known to the virtual machine related to the number of arguments that must be forwarded. To resolve this problem, the bytecode can prepend the number of arguments when calling an API, giving the virtual machine layer enough information to translate the call into native execution. To programmatically perform this task, variadic arguments in C++ templates can be used to automatically deduce the amount of arguments passed:

As specified in Microsoft’s x64 __stdcall15 calling convention, the first four integer or pointer arguments are passed using the registers rcx, rdx, r8 and r9, with the remaining arguments being passed on the stack. This means that at the time of executing the call instruction, rcx holds the number of arguments that must be passed to the API. The virtual machine can extract and inspect this value, and use it to correctly perform the call:

The real values of the arguments are stored in rdx, r8, r9 and on the stack. When extracting the arguments from the stack, one must remember to keep the shadow space16 in mind.

Visually, the process looks like this:

A virtualized call instruction invokes ntdll!NtAllocateVirtualMemory. This call is translated to a native call and the API is invoked. The resulting value is returned to the VM.

Supporting Bytecode Function Callbacks

Keeping in mind the porting of existing programs to the bytecode architecture, one cannot omit the support for function callbacks within code. Take for example a simple linked list implementation, with a list_search function taking a predicate callback:

However, a problem arises: how does the virtual machine differentiate between a normal bytecode function call, a native API call, and a function callback? The difference between the first two is clear: the bytecode function call is a call to an address within the bytecode and is known at compile time, where the API call is a dynamic call, meaning a call to a function pointer stored in a register or memory location. Given that a callback within bytecode is a dynamic call, too, the virtual machine must be provided with information about the type of call being made.

To load a function pointer as an argument, a lea17 instruction is generated with its right operand referencing a memory address. This referenced memory address uses the instruction pointer (rip) register as the base field of the memory operand. When transpiling, such cases can be identified. To store this information, a new type of operand can be added to the already existing three types -listed in “The Anatomy of an Instruction”– (e.g. Function). When the virtual machine executes the lea instruction, it can check for the type of operand. If this operand’s type is Function, a tag can be added to the high 32 bits of the value, for example 0xDEADBEEF.

Once the call instruction is invoked, the value of the operand can be interrogated. If this value contains the previously added tag, a callback is requested. To perform the call, the tag is stripped from the value and the instruction pointer can be set accordingly.

Supporting User-Defined Arguments

Depending on the type of program that is executing, user-defined arguments are required. Take for example a program that simply sleeps for a period of time. How long should this program sleep for? Hardcoding these values is not always an option. Early on in the development of the project, a simple data structure was defined which could be provided to the bytecode’s entry point:

Accompanying this, each bytecode project contained a script that packaged data in a way that could be understood by the bytecode. However, there was no consistency between these scripts and the method of extraction. For example, extracting two 4-byte integers is simpler than extracting two strings due to their variable size.

To standardize this process, and to include it into the building step itself instead of running a random script, a key-value solution was created in combination with an API that can interrogate the type and value of each argument. This is different from parameter packing that Cobalt Strike uses in its BOFs18, as default arguments, or arguments that are not strictly required are supported. Additionally, each argument is encrypted separately. This allows for a PE packer to extract domain-keying information before extracting the PE data.

The following API is defined:

The signature of the bytecode’s entry point is updated to incorporate this change:

Supporting DLLs

Executables and DLLs are very similar in the way they look and in the way they execute. Both have an entry point to which execution is passed, and both return a value. However, the execution flow of an executable starts at the entry point, and does not reach its function’s end until the program stops. DLLs often perform very limited initialization within their entry point, and return execution to the loader to not lock the loader threads. Additionally, the entry point of the DLL is called more than once: on process startup and shutdown, and on thread creation and destruction. The reason for calling the entry point is passed by the loader in the second, dwReason, argument. This allows the code inside of the DllMain function to differentiate between the reasons the entry point was invoked, and can act accordingly.

To allow our shellcode to be embedded within DLLs, both the virtual machine and its bytecode must be made aware of the reason for invocation. This requires the entry point of the virtual machine and bytecode to match that of a DLL, automatically receiving the reason by the OS loader. This does not interfere with the entry point used by a normal executable, as the default entry point of any executable does not take any arguments directly, but instead the arguments argc and argv are resolved by the C runtime, which is not linked against.

On initialization, the virtual machine sets the bytecode’s rdx register to the value of its reason argument, passing the value to the entry point as the second argument. The programmer must decide if this value is to be inspected within the bytecode and should not use the value when writing bytecode to be embedded in an executable.

Deceiving Behavioral Analysis: Multi-VM Execution

Earlier, the method of detection based on behavior was discussed. This dynamic form of inspecting an application’s execution flow regardless of static patens is difficult for attackers to rid their malware of. Opening Lsass.exe and reading its memory could be marked as malicious, even if the process looks like calc.exe. Often, defensive products receive events by kernel callbacks, such as PsSetCreateProcessNotifyRoutine19 or PsSetLoadImageNotifyRoutine20, API/syscall hooks in the local process or by using Event Tracing for Windows (ETW)21 consumers.

Patching hooks in the local process along with local ETW functions that provide events is trivial. This rids the process of intrusive monitoring by antivirus or EDR solutions, and stops the process from creating events. However, some events are still generated, mostly by the ETW providers present in the kernel, along with the kernel callbacks. Additionally, events created during patching could still be monitored. Lastly, blinding defensive products could have a negative effect, as failure to receiving check-ins could be considered an error and a signal of malicious behavior by itself.

As an attacker, generating arbitrary events along with ones that might cause detection could be a method of thwarting dynamic detection rules based on behavior. Adding code to generate events in between regular instructions would require manipulation of source code, and is not preferred. Creating a new thread that generates random events could be in vain, as events are registered per unique thread in the process.

The virtual machine was extended to support vmcalls. These types of call instructions made by the bytecode notify the virtual machine layer that a task needs to be performed. Among multiple different supported calls, most noteworthy are the following:

  • vminit: Initialize the virtual machine object with bytecode and arguments
  • vmexec: Execute N cycles on the virtual machine

The combination of these two calls allows bytecode to create a new virtual machine, and execute a predetermined number of instructions:

Because both sets of bytecode execute within the same virtual machine, and therefore on the same thread, no distinction can be made between the origin of each event. The OS, and any event consumers will observe a single thread generating multiple events, both benign and possibly malicious. Most importantly for an attacker, this could break patterns of behavior being monitored for.

As an additional benefit of these added instructions, bytecode can now be obtained and executed at runtime. This proved to be an extremely useful feature during payload development, as instead of staging shellcode during Command and Control, bytecode can be provided. This removes the necessity for allocating executable memory regions (or changing memory protection at a later stage) to execute shellcode in, in turn removing the opportunity for defensive products to inspect buffers used for dynamic code execution often leveraged by attackers.

For example, the following behavior could be implemented to create a simple polling implant, requesting bytecode every 10 seconds:

Protecting the Virtual Machine

At this point, we have defeated most detection measures that we are aware of, and set out to defeat. However, the VM shares a fundamental weakness with the original packers: static patterns in the native-code VM. Throughout its development, the VM was kept as simple as possible, adhering to constraints set out to enable support for a polymorphic engine to be executed on the VM’s binary code. This made the development significantly more cumbersome, but, given a sufficiently strong polymorphic engine, does close the detection loop fully. The polymorphic engine we developed has been battle tested over several years of use against modern EDR, and antimalware. Despite the fact that the code of the engine was designed years ago, and has not significantly changed since, it still manages to mutate malicious code to the extent that it becomes undetected at runtime and scan time.

Due to the way the universe works, the engine cannot support arbitrary programs. The largest constraint is that dynamic control flow is not supported. This means that indirect function calls, indirect jumps and the ret instruction could all potentially break the mutated code. Our engine assumes you know what you’re doing, and won’t complain when such instructions are encountered, but the resulting code will likely not work as intended.

The polymorphic engine supports several different mutation techniques, including:

  • Instruction substitution: replacing instructions with semantically equivalent ones. For example: mov eax, 0 can be replaced with xor eax, eax;
  • Basic block reordering: changing the order of basic blocks in the code;
  • Basic block creation: inserting new basic blocks into the code, through jumps and push rets;
  • NOP instruction insertion: inserting NOP instructions to change the code’s layout.

The most important feature is that the output of the engine can be fed back into the engine again. This allows for multiple iterations of mutation, which leads to virtually incomprehensible disassembly. This is especially useful when the input is a small piece of code, like a shellcode loader. Sufficient numbers of mutation will double, or quadruple the size of the output, further muddying the waters for defenders.

Conclusion

Due to the ever-changing security landscape, both attackers and defenders must stay on their toes. Defensive security products continue to improve over time, making it more difficult for attackers to remain undetected, or even execute malicious code at all. Detection of payloads has shifted from static analysis to a combination of heuristics and signatures, rendering some tools obsolete.

In this blog post, we have described a tool that was written to tackle both static and dynamic analysis by way of virtualisation. This technique, along with employing a custom polymorphic engine attempts to evade these types of analysis by layers of obfuscation. To bypass heuristic analysis, support for multiple virtual machines to run concurrently was added, disrupting patterns in created events. As an added bonus, reverse engineering a sample without prior knowledge could be a daunting task. Analysts would have to reverse not only the morphed virtual machine itself, but extract morphed bytecode for further analysis. This does not remediate the issue of reverse engineering payloads for an attacker, but does significantly slow down the process, providing the attacker with more time.

In practice, this project has allowed attacks to remain undetected during Red Teaming and TIBER exercises in some of the most heavily monitored environments, making use of state of the art EDR solutions. Moreover, due to the addition of a transpiler converting compiled binaries into custom bytecode, both the speed and ease of development of custom payloads was greatly improved.

The following is a non-exhaustive list of payloads that were created during a recent Red Teaming exercise, successfully evading detection:

  • Multiple persistence modules;
  • Multiple lateral movement modules;
  • Shellcode and bytecode executor;
  • Antivirus and EDR patchers;
  • HTTP(s) and DNS beacons;
  • Tools querying Active Directory information.

Porting of additional tools is taking place, and we expect to have virtualized versions of most tools used in a Red Team exercise in the near future.

Looking Forward

The motivations for this blog post are two-fold. Firstly, we wanted to share what we think is exciting research with the community. We learned what we did from openly shared blog post and articles, and want to give back to the community. We use all the knowledge we gained to improve the security of our customers through offensive security testing, and we hope that this blog post will help and inspire others to do the same.

Secondly, although security products have advanced tremendously, we want to show that there is still room for improvement. We have noticed a tendency to “slap an EDR on it and call it a day” in certain niches of the security industry. Although that might work for some time, because a modern EDR truly adds a strong layer of security, the door is still open for attackers to bypass these products. As the landscape evolves, and general cyber security knowledge increases, the skill and sophistication of cyber criminal elements will rise. Consider this blog post, and the technique explained within, as a warning and a call to action. We hope security vendors will think about how they can detect these types of payloads, and how they can improve their products to stay ahead of the curve, as they are right now.

References

Why Policy in Amazon Bedrock AgentCore chose Cedar for securing agentic workflows

20 May 2026 at 22:56

Agents have agency: they adapt and find multiple ways to solve problems. This autonomy creates a fundamental security challenge: the large language model (LLM) at the heart of the agent is non-deterministic, and its decisions can’t be predicted or guaranteed in advance. It can hallucinate harmful actions with complete confidence. It’s vulnerable to prompt injection attacks, where adversaries inject malicious commands through tool responses or user inputs. LLMs don’t robustly differentiate between commands and data, everything is only tokens. For these reasons, if you want defense in depth, you must treat the LLM as an untrusted actor from a security point of view.

The insight is that the LLM can’t affect the external world directly: it has to go through an orchestrator that invokes tools based on the LLM’s output. This is precisely where the controls must be applied. What you need at this boundary is authorization: a decision about whether each tool invocation should be allowed and under what conditions. Consider a customer service agent for an online retailer. Without proper controls, it could process refunds that exceed authorized limits, apply discounts to product categories that should be excluded, or look up one customer’s data while handling another customer’s session.

If you control agents’ access to tools, you can establish a safety envelope within which the agent can operate freely. This differs from two common but unsatisfactory approaches:

  • Creating hard-coded workflows eliminates uncertainty, but by itself defeats the purpose of using an LLM as the brain of the agent, because you’ve built a traditional application with an LLM interface. And even with this restriction, using LLM outputs at any step can open up the same risks. While it’s a useful technique for well-understood workflows, it’s not sufficient for agents that need to adapt.
  • Human-in-the-loop provides a safety net for critical operations, and it will always have a role. But relying on it as the main control mechanism sacrifices autonomy and can lead to approval fatigue.

You need agents that are safe and autonomous. This requires an auditable, deterministic enforcement layer that sits outside the agent and tools. Why outside? Because the LLM’s plan is the thing you can’t trust—it can’t be responsible for enforcing its own constraints. Controls at the LLM layer—such as system prompts and training-time alignment—can be bypassed by prompt injection or hallucination. Hard-coded checks in agent or tool code are more robust, but become difficult to audit and manage at scale, especially when security logic is scattered across many tools and services. Centralizing authorization outside both gives you a single checkpoint the LLM can’t circumvent; one that’s auditable and can be verified independently of the application code.

This is where AgentCore Policies come in. Amazon Bedrock AgentCore Gateway sits between the agent and the remote tools it calls. When you associate a Policy with a Gateway, it blocks everything by default. Policies selectively open this boundary by specifying which tool invocations are allowed and under what conditions. This enforcement applies to all tool traffic routed through the Gateway. For this approach to scale, it must be more straightforward to reason about the policies than about the agent’s behavior.

AgentCore policies are expressed in Cedar. Cedar is an open source authorization policy language developed by AWS that has recently joined the Cloud Native Computing Foundation (CNCF). Cedar was designed with exactly these properties: it’s purpose-built for authorization, readable by humans, and analyzable by machines using automated reasoning. This gives enterprises the ability to scale policy definition and enforcement to their AI agents.

How Cedar is used by Amazon Bedrock AgentCore

Amazon Bedrock AgentCore provides the infrastructure to deploy and manage agents at scale. It includes AgentCore Runtime for hosting agents, AgentCore Gateway for managing how agents connect to tools using Model Context Protocol (MCP), and Policy in AgentCore. Policy intercepts all agent traffic through AgentCore gateways and evaluates each request against defined policies in the policy engine before allowing tool access. Cedar powers the policy layer.

AgentCore Policy uses Cedar and its mathematical analysis capabilities at several points in the AgentCore Gateway workflow: the Cedar authorization engine is used at policy evaluation and Cedar Analysis is used during policy authoring, and in the control plane.

Policy authoring: Developers can write Cedar policies directly or use natural language that gets translated to Cedar through a neuro-symbolic AI feedback loop. Neuro-symbolic AI combines machine learning’s flexibility with automated reasoning’s provable correctness. An LLM generates policies from natural language, while Cedar Analysis validates them using symbolic, mathematical reasoning. The following diagram illustrates this workflow:

Figure 1: Cedar policy generation workflow

Figure 1: Cedar policy generation workflow

An administrator specifies—in natural language—which MCP tools the agent can call and under what conditions. The neuro-symbolic feedback loop then formalizes this description into Cedar policies. Here’s how it works: first, the LLM translates the natural language into Cedar policies. These policies are then run through two stages of verification. In the first stage, AgentCore Policy uses a Cedar schema generator that takes the MCP tool descriptions and produces a Cedar schema. Cedar validates the policies against this schema, helping to ensure that they reference valid tools and parameters and ruling out whole classes of runtime errors. If validation passes, the second stage runs Cedar Analysis, which encodes each policy as a mathematical formula and detects issues like policies that grant or deny everything, or that contain impossible conditions. These mathematical proofs identify errors in the process of translating from the natural language description to Cedar policies, and guide corrections.

The neuro-symbolic feedback loop significantly improves the accuracy of the generated policies. This demonstrates the power of combining neural and symbolic approaches—the LLM provides creative translation from natural language, while automated reasoning provides rigorous validation.

Control plane: When attaching policies to an AgentCore Gateway, Cedar Analysis performs holistic analysis of the entire policy set. Instead of analyzing policies in isolation, it examines how they interact and their combined effect. This analysis identifies potential logical errors—such as conflicting or redundant policies—and detects whether the policy set produces unintended authorization outcomes. When Cedar Analysis detects these errors, the operation fails and returns a description of the issue, so the policy author can fix and retry. See the Formal analysis for policy verification section for examples of the checks.

MCP tool invocation enforcement: Each agent tool request made to the AgentCore gateway is evaluated against Cedar policies which determine whether the MCP tool invocation with the given arguments should be allowed. This creates the safety envelope while allowing the necessary bridges to enable the agent to perform its job.

MCP tool filtering: Cedar enables an additional layer of protection that operates before any tool invocation occurs. When an agent issues a list tools command, AgentCore Gateway uses Cedar’s partial evaluation capability to determine which actions would always be denied under the current policy set. Those actions are omitted from the list tool response. The agent and the underlying LLM never see those tool actions, eliminating an entire class of risk: the agent and LLM can’t attempt to invoke a tool it doesn’t know exists. This is a direct benefit of Cedar’s partial evaluation: the system can determine that certain tool actions are unreachable without needing to wait for an actual tool invocation attempt.

Why Cedar: Analyzability enables safety at scale

Natural language is too ambiguous for security-critical infrastructure, and general-purpose programming languages, like Python, are very expressive but too difficult to analyze. They can have unintended side effects, termination issues, and can be difficult to understand.

Cedar avoids these issues by excluding loops and stateful operations, so policy evaluation terminates in O(n) time in common cases. This bounded execution time means agents can make authorization decisions without disrupting user experience or workflow efficiency.

Cedar is straightforward to read. Regulatory compliance and security audits require policies that humans can understand and verify. Cedar policies read like structured natural language, making them accessible to security teams, compliance officers, and business stakeholders:

// Only allow bulk discounts for premium customers with sufficient quantity
permit (
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::"ApplyBulkDiscount",
  resource
)
when
{
  principal.hasTag("customer_tier") &&
  principal.getTag("customer_tier") == "Platinum" &&
  context.input.orderQuantity >= 50
}
unless
{
  context.input
    .productTypes
    .containsAny
    (
      ["limited_edition", "seasonal_specials"]
    )
};

Auditors without a technical background can understand this policy: “Allow bulk discounts for platinum customers who order at least 50 items, except for limited edition or seasonal special products.” The unless clause makes the exception clear, which is how business rules are typically expressed in natural language. Notice that this single policy constrains two different sources of data. The customer tier comes from a JSON Web Token (JWT) claim—it can’t be hallucinated or manipulated by the LLM. The tool inputs like order quantity and product types, however, originate from the LLM’s tool call. Cedar policies constrain these inputs to only allowed values, ensuring that even if the LLM produces unexpected arguments, the policy enforcement layer rejects them deterministically.

Cedar is the right choice because it’s fast, straightforward to read, and analyzable through automated reasoning. This analyzability is why you can reason about the safety envelope around agents that’s expressed as Cedar policies. As agentic systems grow the number of tools grows. Without proper tooling, policy management becomes intractable; policies can conflict, create security gaps, or produce unintended authorization outcomes.

In the rest of this section, we examine how Cedar’s analyzability directly addresses this challenge through its deterministic, mathematically sound analysis. Because Cedar analysis can reliably detect conflicts and logical errors across large policy sets it enables scalable policy management through neuro-symbolic AI.

Formal analysis for policy verification

Cedar policies can be encoded as mathematical formulas and analyzed using automated reasoning techniques through a symbolic encoder. This enables AgentCore Policy to provide sophisticated policy verification capabilities during policy authoring and beyond. AgentCore Policy uses this analysis when authoring or attaching policies to detect possible logical errors, such as conflicting or redundant policies. Policy analysis, including policy comparison is available as an open source CLI tool. Next, we will take a look at some concrete examples of these checks.

Detecting logical errors in policies: Cedar Analysis can detect when policies contain logical errors. For example, the following policy has contradictory constraints that mean it can’t allow any request: the customer tier can’t be both gold and platinum at the same time. The intention was to use an || instead of &&, a mistake that can be made by both humans and AI systems that author policies.

// This policy cannot allow any requests due to logical errors
permit (
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::"ProcessRefund",
  resource
)
when
{
  principal.hasTag("customer_tier") &&
  principal.getTag("customer_tier") == "Gold" &&
  principal.getTag("customer_tier") == "Platinum"
}
unless { context.input.refundAmount > 1000 };

Similarly, Cedar Analysis can detect policies that always allow a given action, usually an indication of an overly permissive policy. For example, the following policy will allow all ApplyBulkDiscount requests because any order quantity will either be greater than or equal to 100 or less than 100.

// This policy allows all ApplyBulkDiscount requests
permit (
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::"ApplyBulkDiscount",
  resource
)
when
{
  context.input.orderQuantity >= 100 ||
  context.input.orderQuantity < 100 ||
  (principal.hasTag("customer_tier") &&
   principal.getTag("customer_tier") == "Platinum")
};

Detecting such logical errors isn’t easy for humans, and can’t be done by pattern matching: you need the formal rigor of mathematical analysis, which is exactly what Cedar Analysis does.

Detecting policy conflicts: Cedar Analysis can also analyze the entire policy set to detect inconsistencies between different individual policies:

// These policies conflict - Analysis will detect the subtle issue
permit (
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::"ProcessRefund",
  resource
)
when
{
  principal.hasTag("customer_tier") &&
  principal.getTag("customer_tier") == "Gold" &&
  context.input.refundAmount < 100
};

forbid (
  principal is AgentCore::OAuthUser,
  action == AgentCore::Action::"ProcessRefund",
  resource
)
when
{
  principal.hasTag("customer_tier") &&
  ["Gold", "Platinum"].contains(principal.getTag("customer_tier")) &&
  context.input.refundAmount < 500
};

The permit policy allows gold customers to process refunds less than $100, while the forbid policy blocks gold customers (and platinum customers) from processing refunds less than $500. Because forbid overrides permit in Cedar, the forbid policy would block all gold customer refunds despite the permit policy.

Comparing policy changes: When updating policies, Cedar Analysis can also determine the exact impact of a change. Consider the following update to the unless clause (the policy lines with + have been added and those with - have been removed): we now block ApplyBulkDiscount only when the product type is limited_edition and the quantity exceeds 200.

 permit (
   principal is AgentCore::OAuthUser,
   action == AgentCore::Action::"ProcessRefund",
   resource
 )
 when
 {
   context.input.refundAmount < 500
 };
 
 permit (
   principal is AgentCore::OAuthUser,
   action == AgentCore::Action::"ApplyBulkDiscount",
   resource
 )
 when
 {
   context.input.orderQuantity >= 50
 }
 unless
 {
-  context.input.productTypes.containsAny(["limited_edition"])
+  context.input.productTypes.containsAny(["limited_edition"]) &&
+  context.input.orderQuantity > 200
 };

At first glance, adding a condition to the unless clause might seem more restrictive. In fact, it’s the opposite: narrowing when the unless applies means the permit now covers more requests. For example, an order of 73 units of a limited_edition product would have been blocked before but is now allowed. Cedar Analysis can automatically detect this and generates the following table showing the difference in permissiveness between the original policy set and the updated one:

Principal type

Action

Resource type

Status

OAuthUser

ProcessRefund

Gateway

Equivalent

OAuthUser

ApplyBulkDiscount

Gateway

More permissive

In the preceding example, the analysis tells us that the updated policy allows allows exactly the same ProcessRefund requests, but allows more ApplyBulkDiscount requests.

This formal verification capability is essential when agents operate autonomously and can affect the real world. Organizations need mathematical certainty that their policies will behave as intended.

Deterministic behavior for reliable governance

Unlike probabilistic AI models, enterprise security requires deterministic guarantees. Cedar policies always produce the same authorization decision for identical requests, regardless of evaluation order or system state. Cedar’s default deny, forbid wins, no ordering semantics help ensure predictable behavior.

// Policy evaluation order does not affect the authorization decision
permit(
    principal,
    action == AgentCore::Action::"ProcessRefund",
    resource
) when {
    context.input.refundAmount < 500
};

forbid(
    principal,
    action == AgentCore::Action::"ProcessRefund", 
    resource
) when {
    context.input.orderDate.offset(duration("90d")) < context.system.now
};

Whether the permit or forbid policy is evaluated first, a refund request over $500 will always be denied, and any refund issued more than 90 days after the order date will also be denied. This predictability gives enterprises confidence in their agent governance.

From policies to production

By choosing AgentCore Policy and Cedar, organizations can deploy autonomous agents with policies they can reason about mathematically, not only hope the agents work correctly. Cedar’s combination of expressiveness, readability, and formal verification means that you can design agents with the flexibility needed to function and the certainty security teams demand.

Automated reasoning has already proven its value across AWS, from AWS IAM Access Analyzer verifying access policies to provable security for network configurations. Applying these same techniques to agentic AI is a natural extension: as agents take on more responsibility, the need for mathematically grounded guarantees only grows. The neuro-symbolic approach we’ve described in this post—combining LLM flexibility with the rigor of automated reasoning—points toward a future where agents can be both more autonomous and more trustworthy, because the verification keeps pace with the autonomy.

Learn more

Policy is now available as part of Amazon Bedrock AgentCore Gateway. To learn more about Cedar and its capabilities, visit the Cedar website, try the Cedar playground, or join the Cedar community on Slack.

For more information about Policy in Amazon Bedrock AgentCore Gateway, visit the AWS documentation or explore the AgentCore Gateway console.

If you have feedback about this post, submit comments in the Comments section below.

Liana Hadarean

Liana Hadarean

Liana is a Principal Applied Scientist at AWS. She has worked on the code analysis tools that power Amazon Q Java security detectors, and is now a contributor to the Cedar policy language.

John Tristan

Jean-Baptiste Tristan

Jean-Baptiste is a Senior Principal Applied Scientist at AWS Agentic AI where he works on neurosymbolic AI and agentic safety.

AWS Security Hub Extended: Why enterprise security products should sell themselves

20 May 2026 at 19:32

Our largest security services customers started the same way every customer does – with a click. They enabled Amazon GuardDuty, Amazon Inspector, AWS WAF, and AWS Security Hub, experienced the benefits in real time, and evaluated with transparent pay-as-you-go pricing. No RFP. No six-month evaluation. No multi-year commitment up front. Our field teams played a critical role in that growth, not by selling the first click, but by building the trusted relationships that turned early adoption into deep, long-term commitment. We believe customers should have this same frictionless adoption experience and flexibility for all best-in-class security products and that’s why we developed Security Hub Extended.

In our first post, we introduced Security Hub Extended, a significant expansion of Security Hub that brings together curated partner solutions in a single, unified experience. In our second post, we walked through how it works technically, including the onboarding flow, the pricing model, the unified operations layer built on the Open Cybersecurity Schema Framework (OCSF). In this post, I want to step back and talk about why we built it the way we did and why I believe the way enterprises discover, evaluate, and adopt security solutions is ready for a fundamental shift.

The shift

If you’ve ever tried to evaluate a new enterprise security product, you know the drill. Request a demo. Wait. Take the demo. Request a PoC. Wait for professional services (or your team to stop building) to set it up. Negotiate pricing, which isn’t published, so you’re starting blind. Loop in procurement. Sign a multi-year commitment. Then, months later, find out whether the product actually solves your problem in your unique environment.

Meanwhile, an ambitious security engineer on your team has already spun up an open-source tool, connected real data, and knows in two hours whether it’s going to work for your use cases. They didn’t need a slide deck. They needed a solution they could put their hands on.

A Fortune 500 CISO recently told me: “I spent 9 months procuring a security solution and it still doesn’t work the way the demo showed.” That frustration isn’t unique. It’s the norm.

This isn’t a criticism of the sales motion. Sales-led has evolved for good reason. Enterprise procurement is complex, products need customization, customers need support. I respect the craft and have poured a significant portion of my career into trying to perfect it. Even the most product-driven companies still need great sales, marketing, field enablement, and support.

It doesn’t change the fact that threats are evolving constantly, and defenders need the flexibility to discover and deploy new solutions as fast as the landscape shifts. Having the best solutions discoverable and deployable in that moment of need isn’t just a convenience, it’s a competitive advantage that customers are demanding. A new threat emerges, security teams have access to industry-leading solutions, and in a few clicks they’ve found their answer and are already seeing value. That’s the model every security company should be building toward.

What we’ve learned at AWS

At AWS, we’ve spent two decades learning what it takes to let customers adopt complex enterprise technology on their own terms, at massive scale. We haven’t always gotten it right, but we learn fast and adjust. The result is one of the largest cloud businesses in the world. I bring up that scale for one reason. It’s proof that complex, enterprise-grade technology can be adopted without requiring a traditional procurement gauntlet. Compute, storage, databases, AI/ML, networking, and yes, security — adopted all through a console, on each customer’s own timeline, and scaled when they were ready.

The proof is in the adoption

Amazon GuardDuty, Amazon Inspector, AWS Shield, AWS Security Hub are all available through the AWS Management Console. All pay-as-you-go. All activated with a click. Tens of thousands of customers rely on these security services today. When you make it easy to get started and deliver outcomes that earn confidence, expansion follows naturally.

These are sophisticated, enterprise-grade security solutions. And customers, from two-person startups to the world’s largest financial institutions, adopt them the same way. They try it, see the value, expand, and lean on the AWS team to go deeper.

We didn’t get here by accident, and we definitely didn’t get here without making mistakes. Building products that can be adopted and scaled on their own, without a sales engineer explaining away UX problems, without a solutions architect doing the first deployment, requires a different kind of product mindset. Time-to-value becomes your most important metric. Onboarding friction becomes your biggest enemy. Transparent pricing becomes non-negotiable. It’s hard. We’ve gotten a lot wrong along the way. And we’re still iterating.

But the results are clear. When customers adopt based on experience rather than commitment, they don’t just stay, they expand. They bring their teams. They become advocates. I’ve spent 15 years at AWS, the last 10 building security services like GuardDuty and Security Hub. When we launch a new security service or major feature, we consistently see rapid organic adoption at a pace that would be impossible through traditional sales cycles alone. These products are built to deliver value the moment customers turn them on and we make that as easy as we possibly can. That’s the scale a product-led motion unlocks.

Security Hub Extended

So, we asked ourselves: why can’t we build a similar approach that can expand to include industry leading partner solutions? Why can’t the CrowdStrikes, the Splunks, the Zscalers, and the fast-growing innovators solving tomorrow’s problems like Cyera, Noma, and 7AI also reach customers with the same frictionless motion that AWS services enjoy? Why can’t a security team that discovers a new threat on Monday have a proven solution deployed and delivering value by Tuesday? Our partners have built incredible products. What they haven’t always had is an avenue to put those products directly in the hands of the customers who need them most, at the moment they need them, at scale, in a way that feels as natural as turning on an AWS service. Not by replacing how our partners build or sell, but by giving them infrastructure that lets their products speak for themselves.

That’s what Security Hub Extended is. Security teams already using Security Hub can discover curated partner solutions right alongside their AWS security services. One click to evaluate, one click to deploy, pay-as-you-go pricing on your existing AWS bill with Enterprise Discount Program (EDP) discounts automatically applied. No separate procurement cycle. No long-term commitments required. Start fast, validate at scale, and commit for deeper discounts when you’re ready, versus making a three-year bet based on a few months of testing.

For customers, industry-leading enterprise security solutions become as easy to adopt as GuardDuty or WAF. For our partners, Security Hub Extended is a growth channel where the product leads and the customer experience mirrors what we’ve spent 20 years building at AWS. For the industry, it’s an invitation to reimagine what the relationship between a security product and a security practitioner can look like when you remove the friction standing between them.

But Security Hub Extended isn’t just a simpler way to buy security products. It’s a unified solution. When a customer enables a solution through Extended, we’re working toward an experience where AWS handles the rest. Sensors that deploy automatically across Amazon EC2, Amazon EKS, and AWS Fargate workloads using the same mechanism that powers GuardDuty Runtime Monitoring. IAM roles that provision across a customer’s Organization in one click. Resource inventory is automated from day one – S3 buckets, databases, AI workloads – without manual work.

Once enabled, solutions in Security Hub Extended emit findings in OCSF, automatically aggregated in Security Hub alongside findings from GuardDuty, Amazon Inspector, and every other AWS security service. Security Hub applies risk scoring and correlated risk analytics across all of them. AWS-native and third-party findings together, weighted and prioritized as a single view of your security posture. For example, an endpoint detection from CrowdStrike, correlated with a credential theft in GuardDuty, and a data access event from Cyera, produces an attack path that none of those solutions can produce alone. The correlation uses AWS context (IAM topology, VPC exposure, resource criticality) to improve the context of each attack path for security analysts. Deploying a solution through Security Hub Extended doesn’t add another pane of glass. It deepens the intelligence of the one you already have.

We’re also building toward automated response. Customers will be able to opt in to pre-built playbooks that take action through AWS-native services when a threat is detected, such as isolating compromised resources, revoking credentials, or containing active threats. The goal is detect-to-respond in seconds, not the hours it takes to context-switch across five consoles and two ticketing systems.

Where we are and where we’re headed

We’re still in the first inning — or Day 1, as we like to say at Amazon. We launched in February 2026 with 14 partners, now 21, spanning endpoint, identity, email, network, data, browser, cloud, AI, and security operations, and we’re continuously working backwards from customers as we operationalize for scale. We are building this because our customers asked for it. We’re learning alongside our partners and customers every week, identifying what works, what needs improvement, where the friction still lives, and iterating quickly.

We’re building and delivering at the speed of our customers. That means shipping fast, iterating faster, and not waiting for perfection. We’re not where we want to be just yet, and we need your feedback to get us there. What’s encouraging is that our partners aren’t waiting to be asked. They’re investing in this alongside us. Not because we’re demanding it, but because they see the same thing we do, that companies that make it effortless for customers to get started are the ones that will win at scale.

The early signals are encouraging. Customer response has exceeded our expectations, and the feedback we hear most often is that the procurement simplification and flexibility of pay-as-you-go with public pricing alone, even before the unified operations and data normalization benefits, is a meaningful differentiator.

If you’re a security leader: Security Hub Extended is live now. Log into Security Hub, look for the Security Hub Extended Plan (or visit the Security Hub Extended Pricing Page), and explore what’s available for your use cases. Start with what solves your most urgent problem. Pay-as-you-go, no commitment. Your team will tell you if it’s working in days, not months.

The vision is bigger than what’s live today, and we’re iterating fast. Share your feedback on AWS re:Post for Security Hub, reach out through contact AWS Support, or connect with me directly.


Michael Fuller

Michael Fuller

Michael has been with AWS for 16 years and led product for AWS Security Services for 11 years. Michael has 29 years in the industry and held several roles in product management, business development, and software development for IBM, Cisco, and Amazon. Michael has a Bachelor’s of Science in Computer Engineering from the University of Arizona and an MBA from the University of Washington.

 

❌