Connects to any signal across any vendor stack and powers adaptive AI Operatives that expose, detect, and neutralize cyberattacks.
Atlas Operations CenterSee what our SOC sees, review investigations, and see how we are protecting your business.
Technology IntegrationsAtlas connects to any signal across your current security tools. Whatever you're running, we're running with you.
Extend your team with immediate expertise, hands-on remediation, and the human accountability layer that boards, regulators, and cyber insurers require.
Threat Response UnitProactive threat intelligence, original threat research and a world-class team of seasoned industry veterans.
Response and RemediationPairs machine-speed containment with human judgment, delivering full threat response that's policy-bounded, reversible, and explainable.
MDR that moves first, multi-signal attack surface coverage, and 24/7 Elite threat hunters working as one continuous security program across any vendor stack.
Get unlimited Incident Response with threat suppression guarantee- anytime, anywhere.
Atlas Preempt deploys AI Operatives to continuously validate attack paths exposing attacker targets of opportunity before they take advantage.
Flexible MDR pricing and packages that fit your unique security requirements.
Entry level foundational MDR coverage
Comprehensive Next Level eSentire MDR
Next Level eSentire MDR with Cyber Risk Advisors to continuously advance your security program
Stop ransomware before it spreads.
Identity ResponseStop identity-based cyberattacks.
Zero Day AttacksDetect and respond to zero-day exploits.
Cybersecurity ComplianceMeet regulatory compliance mandates.
Third-Party RiskDefend third-party and supply chain risk.
Cloud MisconfigurationEnd misconfigurations and policy violations.
Cyber RiskAdopt a risk-based security approach.
Mid-Market SecurityMid-market security essentials to prioritize.
Sensitive Data SecurityProtect your most sensitive data.
Cyber InsuranceMeet insurability requirements with MDR.
Cyber Threat IntelligenceOperationalize cyber threat intelligence.
Security LeadershipBuild a proven security program.
In June 2026, an ongoing campaign targeting Fortinet firewalls and VPN appliances was discovered, dubbed FortiBleed. The campaign was discovered through the identification of an exposed…
On June 10th, 2026, Oracle disclosed a critical unauthenticated Remote Code Execution (RCE) vulnerability impacting its Oracle PeopleSoft PeopleTools application, tracked as CVE-2026-35273…
eSentire is a leader in Controlled Autonomy SecOps, protecting 2,000+ organizations across 35+ industries around the world. Founded in 2001, the company’s Controlled Autonomy SecOps operating model pairs agentic AI operatives with engineered human-judgment controls, delivering expert-depth security outcomes at machine speed without ceding accountability to opaque automation.
About Us Leadership Careers Event Calendar → Newsroom → Aston Villa Football Club →We provide sophisticated cybersecurity solutions for Managed Security Service Providers (MSSPs), Managed Service Providers (MSPs), and Value-Added Resellers (VARs). Find out why you should partner with eSentire, the Authority in Managed Detection and Response, today.
Search our site
Multi-Signal MDR with 300+ technology integrations to support your existing investments.
24/7 SOC-as-a-Service with unlimited threat hunting and incident handling.
We offer three flexible MDR pricing packages that can be customized to your unique needs.
The latest security advisories, blogs, reports, industry publications and webinars published by TRU.
Compare eSentire to other Managed Detection and Response vendors to see how we stack up against the competition.
See why 2000+ organizations globally have chosen eSentire for their MDR Solution.
In May 2026, eSentire's Threat Response Unit (TRU) detected EKZ Stealer within a customer environment in the Energy, Utilities & Waste industry. Further investigation revealed exploitation of CVE-2026-35616, an improper access control vulnerability affecting Fortinet EMS versions 7.4.5 through 7.4.6. This threat was previously reported in the blog, FortiClient EMS Exploited via CVE-2026-35616 to Deliver EKZ Infostealer Disguised as a Fortinet Patch and includes Fortinet EMS log artifacts observed post-exploitation.
The attack chain begins with threat actors compromising a Fortinet EMS server through CVE-2026-35616, then deploying EKZ Stealer and exfiltrating harvested credentials using PowerShell. In our investigation, we decoded the PowerShell command shown in the Attack Diagram and found that it downloads EKZ Infostealer, disguises it as a Fortinet update (FortiEndpoint_Patch.exe), and executes it.
It then sleeps for 90 seconds to allow the malware to write collected credentials to C:\ProgramData\log.txt, before exfiltrating that file through an HTTP POST request to the host at 83.138.53[.]110 (ASN 63473, HostHatch, LLC).

The decoded PowerShell can be seen in the code snippet below.
Within a Wireshark capture, the stolen credentials are sent in the body via HTTP POST request (base64 encoded).

SHA-256: 0da123adf9251957a4b850a3f6bd6a753dd4892be176a84a18450e899534cc5e
The file was compiled with MinGW/GCC and statically links open‑source libraries used to extract credentials from Chromium‑based browsers and Firefox. While its credential-harvesting methods are publicly documented, the sample is notable for being compiled with an obfuscating compiler that distorts control flow to hinder analysis, as discussed in depth in the following sections.
The stealer's usage instructions can be displayed by passing the "help" argument:

When executed without any command-line arguments, the stealer writes stolen credentials to a file named log.txt in the current working directory and prints the stolen credentials to standard output. It also outputs the targeted browsers and their corresponding versions.

The first obfuscation technique observed in the malware is the use of indirect jumps: when a disassembler encounters an indirect branch whose target cannot be resolved (for example, because it is obscured by arithmetic), it can no longer reliably continue linear code analysis, which may mislead the analyst and is a well-known, effective technique to hinder the malware analysis process.
The figure below illustrates an example function extracted from EKZ Stealer that exhibits this behavior (pseudo-code view).

In the disassembly view of the same function, Binary Ninja shows only a single basic block: the function prologue which ends in the indirect jump, jmp rax. Each jump gadget computes the final jump address by dereferencing a global QWORD and adding a jump table offset to it, which in this case is 0x60 (the true branch of both opaque predicates).
It then decodes the result using a per-function key, in this case r15, yeilding the encoded jump table entry. The entry is then decoded using a different key, in this case r13, yeilding the final jump address. Both decode steps add two QWORDs using 64-bit modular arithmetic, wrapping around modulo 2^64.

The figure below illustrates the encoded jump table and includes comments identifying the function's jump-table base "slot 1" and the entry referenced by the jump gadget at offset 0x60 (slot 12 since each entry is a QWORD (8 bytes) and 12 * 8 = 0x60).

The first class of opaque predicate is trivially always-true or always-false (depending on the comparison operator, e.g. < versus >), because the dereferenced 32-bit integer is 0: 0 < 0xa is always true, and 0xa > 0 is always true. The second class of opaque predicate is also trivial: the left side of the AND expression always evaluates to 0, and 0 & 1 is always equal to 0.
This highlights a weakness in the obfuscator: if it wrote a non-zero value and a corresponding predicate expression for each original branch, the opaque predicates would no longer be trivially reducible and would instead require per-predicate evaluation, e.g. via an SMT (Satisfiability Modulo Theories) solver.
The process of patching indirect jumps via Binary Ninja Workflows first involves iterating over Low-Level Intermediate Language (LLIL) instructions for LLIL_JUMP instructions without a LLIL_CONST_PTR destination.
Each jump-gadget's LLIL follows a consistent pattern, in which opaque predicates hide the true jump-table offset carried through a phi φ variable. Our plugin handles these predicates by identifying LLIL_IF instructions that match the expected opaque-predicate forms and evaluating them to determine the constant truth value.
That truth value selects the real successor edge; the plugin then follows that edge and walks back to the corresponding predecessor block. The live offset is the phi operand that dominates the chosen predecessor block. The other operand (the decoy offset) reaches the phi only via the unreachable edge, so it fails the dominance check and is discarded. The remaining value is the real jump-table offset.

The code that handles evaluation of the truth value for opaque predicate expressions is included in the code snippet below.
Once we have all of the necessary constants extracted from the LLIL jump gadget, including the encoded table base's address (slot), table base key, table entry key, and offset (offset into the jump table), we are able to resolve each indirect jump's target address using the following code, effectively emulating the behavior of the jump gadget.
With each resolved address, we can then make use of Binary Ninja's replace_expr API method to replace the LLIL_JUMP expression's destination with a LLIL_CONST_PTR.
After resolving all indirect jump targets within the function, it becomes clear that another layer of obfuscation is present: Control-Flow Flattening (CFF). Control flow is flattened by a compare tree keyed on a 32-bit state variable.
The instructions highlighted in red correspond to jump gadgets responsible for returning control flow to the central dispatcher (compare node at the top of the tree).
Instructions highlighted in yellow belong to the dispatcher backbone, which route state to blocks containing original instructions (and interleaved jump gadget instructions).
The green highlights mark the original instructions, typically followed by a state write - a mov instruction of a constant into the state variable for unconditional transitions, and cmovcc instructions for conditional transitions (exactly 2 state constants).

While the previous example illustrates the flattened graph of a smaller function where control-flow tracing is relatively straightforward, the complexity increases significantly when a larger function is flattened.
The figure below shows the Control Flow Graph for a substantially larger function (chromium_extract), where static analysis becomes considerably more difficult and time-consuming.

Through trial and error at attempts to unflatten functions, nuances were discovered, largely centered around patchability and how the original instructions were interleaved with jump gadget instructions. As a result, the most viable approach was to rewrite the LLIL and MLIL (Medium Level Intermediate Language) expressions directly:
The dispatcher "backbone" however is easily recognizable: most blocks followed the typical cmp <reg>, <state> pattern, then a conditional branch into the original block (or a fallthrough back to blocks that lead to the dispatcher). The state variable (or an alias) is set in the blocks, either to a single constant (unconditional) or a set of two constants (conditional).
The following code consistently identifies the state variable through the MLIL view:
Reattaching the "head" (prologue) to the first original basic block was another challenge. In many cases, blocks that were part of the original prologue showed up as successors of the first block, and the state write frequently appeared in a successor block rather than in the prologue itself. The figure below shows one such case, where the second block contains the state write.
The most reliable patch point was the block immediately preceding the dispatcher. There, we replace the expression in that tail with a MLIL_CONST_PTR that targets the head of the first original basic block.

To identify the block immediately preceding the dispatcher, first we traverse forward through the MLIL basic blocks from the prologue block through its successors, collecting each block's start index into a list, and stop once the dispatcher is identified.
In the process of mapping state to original block index, any state encountered within this collected list is treated as the initial state i.e. the first original basic block the prologue should transition to. The code responsible for detecting the dispatcher block can be seen in the snippet below.
While iterating through MLIL instructions to find instructions that modify state, a Binary Ninja outlining caveat was discovered: some state-writing memory operations were being converted into builtin calls (for example, __builtin_strncpy).
Disabling builtin outlining (analysis.outlining.builtins) resolved the issue. The snippet below demonstrates how to disable builtin outlining in Python, though this setting can also be toggled from the UI.
The figure below shows the before and after results after disabling builtins outlining:

All original call instructions are obfuscated through indirection, like the indirect jump obfuscation previously described. In some cases, however, the process to resolving an indirect call simply involves adding a global constant to a per-function key to resolve the original call address.
The resolved address is either stored on the stack as a local variable, or it is resolved and called immediately. The example shown below is the latter. Indirect call targets may be computed and saved to a local variable, then later invoked from a different basic block after execution returns through the dispatcher.

With the following code, resolving each call target and printing its corresponding symbol name in Binary Ninja is straightforward:
During the MLIL pass, call instructions are enumerated and each operation is evaluated for MLIL_CALL. If the call destination does not contain an MLIL_CONST, the call is treated as indirect.
After identifying the target address, re-writing the call expression is straight forward:
The obfuscator uses XOR-based string encryption and distributes numerous corresponding decryption routines throughout the binary. These routines hard-code the ciphertext and key lengths and take two arguments: the address of the output buffer, and the blob containing the XOR key and ciphertext.
The blob's address is recovered via modular addition: a QWORD holding an encoded value (the blob base plus an offset) is decoded by adding a key. The figure below illustrates an example decryption routine call-site.

The Binary Ninja workflow plugin "DispatchThis" referenced in this blog is available here. The figures below illustrate the results of running the plugin with the IndirectJump/Call, Deflattener, and NOP pass for the function, "reg_read_str", and the large function, "chromium_extract" discussed in this blog. A separate IDA Pro plugin is also available here, built as a rapidly prototyped alternative for those that prefer that workflow.






To learn how eSentire can help you find exposures and defend your organization, connect with an eSentire Security Specialist now.
GET STARTEDThe eSentire Threat Response Unit (TRU) is an industry-leading threat research team committed to helping your organization become more resilient. TRU is an elite team of threat hunters and researchers that supports our 24/7 Security Operations Centers (SOCs), builds threat detection models across the eSentire XDR Cloud Platform, and works as an extension of your security team to continuously improve our Managed Detection and Response service. By providing complete visibility across your attack surface and performing global threat sweeps and proactive hypothesis-driven threat hunts augmented by original threat research, we are laser-focused on defending your organization against known and unknown threats.