Executive Summary
In late 2024, we discovered a malware variant related to the SLOW#TEMPEST campaign. In this research article, we explore the obfuscation techniques employed by the malware authors. We deep dive into these malware samples and highlight methods and code that can be used to detect and defeat the obfuscation techniques.
Understanding these evolving tactics is essential for security practitioners to develop robust detection rules and strengthen defenses against increasingly sophisticated threats.
We focus on the following techniques used by the threat actors for the SLOW#TEMPEST campaign:
- Control flow graph (CFG) obfuscation using dynamic jumps
- Obfuscated function calls
Palo Alto Networks customers are better protected from the threats discussed in this article through the following products and services:
If you think you might have been compromised or have an urgent matter, contact the Unit 42 Incident Response team.
Background
In this article, we analyze a more recent variant of the malware sample (SHA256 hash: a05882750f7caac48a5b5ddf4a1392aa704e6e584699fe915c6766306dae72cc) from the SLOW#TEMPEST campaign. The attackers distribute the malware as an ISO file, which is a common technique used to bundle multiple files to potentially evade initial detection. This ISO file contains 11 files; two are malicious, and the remainder are benign.
The first malicious file is zlibwapi.dll, which we’ll refer to as loader DLL, decrypts and executes the embedded payload. The payload is not integrated into the loader DLL in a typical manner. Instead, it is appended to the end of another DLL named ipc_core.dll.
The loader DLL is executed via DLL side-loading by a legitimate signed binary named DingTalk.exe. DLL side-loading is a technique where attackers use a legitimate program to load a malicious DLL file, causing the legitimate program to execute the attacker’s code. Separating the payload from the loader DLL complicates detection, as the malicious code will only execute if both the loader and payload binaries are present.
In the following sections, we dive deeper into the anti-analysis techniques used by the malware authors to obfuscate the code in the loader DLL.
Control Flow Graph (CFG) Obfuscation Using Dynamic Jumps
CFG obfuscation alters the execution order of program instructions, making static and dynamic analysis more difficult. This makes it harder to understand the program’s logic, identify malicious functionality and create effective signature-based detection.
For static analysis, traditional tools relying on linear sequences or predictable control flow become ineffective. Dynamic analysis also becomes more challenging because misleading execution paths obscure actual malicious operations. CFG obfuscation breaks the mapping between the original source or compiled code and runtime execution, making it significantly more difficult to create reliable detection rules.
To demonstrate this technique, we analyze the application of CFG obfuscation, specifically using dynamic jumps to the loader DLL’s main function. Figure 1 illustrates the CFGs of this function, both with and without obfuscation. Once we remove the obfuscation, the continuous code flow becomes apparent, marked by two colored lines:
- Green for True branches
- Red for False branches
This function is extensive, comprising over 17,000 lines of assembly instructions. Given the size of the main function for the loader DLL, we turned to the Hex-Rays decompiler to speed up analysis. However, the Hex-Rays decompiler was only able to generate 10 lines of pseudocode for the same main function, as shown in Figure 2.

The reason for the incomplete Hex-Rays decompiler output is because the sample used dynamic jump instructions. Dynamic jumps are where target addresses in code are computed at runtime.
Unlike direct jumps to fixed addresses, dynamic jumps make it impossible for the decompiler to determine the execution flow without actually running the program. This lack of a clear, predetermined path severely hinders the decompiler’s ability to reconstruct the original high-level code, often leading to incomplete or inaccurate decompilation results.
Figure 3 shows one of the dynamic jumps using the JMP RAX instruction near the entry point of the main function of the loader DLL. The JMP instruction will cause the execution flow to be diverted to a different target address.
The target address depends on factors like memory contents, register values and the results of conditional checks performed during execution. In this case, it is computed at runtime by a sequence of preceding instructions and stored in the CPU register RAX.

We countered CFG obfuscation employing dynamic jumps by first identifying all instances of these jumps using the IDAPython script shown in Figure 4.

Using the above script, we identified 10 dynamic jumps in the main function of the loader DLL.
The dynamic jump target is determined by a preceding sequence of nine instructions, termed a “dispatcher,” before each JMP RAX instruction. These 10 dispatchers, found within the loader DLL’s main function, share a similar structure. However, each dispatcher uses a distinct set of instructions to compute the jump’s destination address, effectively hiding the program’s control flow.
Imagine the program is a complex dance routine. Normally, the dancers move predictably from one step to the next. However, in this case, the program has hidden “jump points.” Before each jump, there’s a mini-routine, like a secret handshake, that decides exactly where the next jump will land. These secret handshakes are all a bit different, making it very hard to predict the dance’s true path, almost like the dancers are improvising where they’ll go next, even though it’s all pre-programmed.
Each dispatcher implements a two-way branching mechanism. The code path taken depends on the state of the Zero Flag (ZF) or the carry flag (CF) when the dispatcher is entered. These flags, which are set by previous instructions to indicate the result of an operation (e.g., zero or overflow), determine which branch is taken. Each dispatcher has a pair of conditional move (CMOVNZ) or set (SETNL) instructions and an indirect jump (JMP RAX). This creates a dynamic control flow that depends on runtime conditions and memory contents, making static analysis difficult.
ZF and CF are CPU status flags that reflect arithmetic and logical operation outcomes. These flags act as internal switches, enabling dynamic program execution based on prior computation results. Each conditional move or set instruction has two possible target addresses: one for a true condition and the other for a false condition. For example, the conditional move if not zero (CMOVNZ) instruction will only move data if the ZF is 0, indicating that the previous operation did not result in 0. If the ZF is 1 (meaning the previous result was 0), the CMOVNZ instruction will not move the data, and execution will continue to a different target address.
Figure 5 shows one of the dispatchers. We annotated the instructions to explain how the destination addresses are computed.

Using Unicorn — a multi-platform, multi-architecture CPU emulator framework — automates the identification of destination jump addresses. We achieve this by executing the nine instructions preceding each JMP RAX in a controlled manner, rather than running the entire binary. This allows us to determine the jump addresses for each dispatcher.
To extract the bytecodes of these instructions, we use the code shown in Figure 6.

Next, we emulate each dispatcher. Since each dispatcher uses a two-way branching mechanism with two target addresses, the emulation process must be repeated twice for each dispatcher to determine both destination addresses. Figure 7 shows the code used to emulate the dispatchers.

After computing the two destination addresses, we replaced the dispatcher instructions with direct jump instructions to those addresses, effectively removing the CFG obfuscation. This allowed us to see the original code flow easily in IDA Pro. Figure 8 shows the code to patch the instructions in the IDA database.

Finally, we forced IDA Pro to re-analyze the entire function that was patched using the code shown in Figure 9. This was to trigger IDA Pro to update its CFG based on the de-obfuscated instructions.

After executing this script, the Hex-Rays decompiler successfully decompiled the main function within the loader DLL. Figure 10 shows part of the decompilation output.

However, we observed that further obfuscation remained in the code. Specifically, most functions were called dynamically, and we did not observe any direct Windows API calls. This made it challenging to immediately discern the code’s purpose, as the actual functionality was obscured by indirect function resolution.
Obfuscated Function Calls
Obfuscated function calls use indirect calls, where the function’s address is calculated dynamically at runtime and then called through a pointer, instead of directly invoking the function by its name. Attackers use this technique to hinder static analysis, as the actual target function is not immediately apparent in the code. This makes it more difficult to understand the program’s behavior and identify malicious actions.
Analysis of the main function’s assembly code reveals the presence of multiple obfuscated function calls. The Call RAX instruction is a key indicator, as it signifies that the function address is being dynamically determined at runtime rather than being directly specified in the code. Similar to dynamic jumps, the target addresses of these function calls were calculated at runtime. We were not able to determine the target addresses without executing the binary. Figure 11 shows some of the obfuscated function calls.

To determine the target address of obfuscated function calls, we applied a similar approach to the one we used for dynamic jumps. This is because both techniques involve calculating target addresses at runtime.
Our script successfully calculated the destination addresses of these obfuscated function calls. However, we observed that IDA Pro failed to identify the arguments of standard Windows APIs even though the destination addresses were correctly resolved, as Figure 12 shows. This is because there was missing function signature information linking the addresses of the Windows APIs with the obfuscated function calls.

To enable IDA Pro to correctly identify the function arguments, rename local variables and perform proper analysis, we needed to explicitly set the “callee” address for each obfuscated function call using the code shown in Figure 13. This provides IDA Pro with the necessary information to recognize the function as a known Windows API.

After adding the code shown in Figure 13 to set the callee address, IDA Pro will automatically label function arguments and rename local variables for each obfuscated function call. This significantly improved our ability to read and analyze the code, allowing us to understand the function’s purpose more easily. Figure 14 shows some of the function arguments that IDA Pro labeled.

After executing this script, we successfully de-obfuscated both the control flow and the function calls within the loader DLL. With the code now significantly more readable and the Windows API calls properly identified, we could proceed with analyzing its core functionality. In the final section, we examine the main purpose of the loader DLL.
Loader DLL Analysis
After removing the obfuscation using the scripts emu_call_rax_idapython.py and emu_call_rax_idapython.py, we easily located the main functionality of the loader DLL.
First, we observed an anti-sandbox check that uses the Windows API GlobalMemoryStatusEx to determine the total physical memory available on the system. The loader DLL will only unpack its payload and execute it in memory if the target machine has at least 6 GB of RAM. Figure 15 shows the pseudocode of the core components of the loader DLL.

Conclusion
The SLOW#TEMPEST campaign’s evolution highlights malware obfuscation techniques, specifically dynamic jumps and obfuscated function calls. This illustrates the importance for security practitioners to adopt advanced dynamic analysis techniques (e.g., emulation) alongside static analysis to effectively dissect and understand modern malware.
The success of the SLOW#TEMPEST campaign using these techniques demonstrates the potential impact of advanced obfuscation on organizations, making detection and mitigation significantly more challenging. Understanding how threat actors leverage these methods is crucial for developing robust detection rules and strengthening defenses against increasingly complex threats.
Palo Alto Networks customers are better protected from the threats discussed above through the following products:
- Advanced WildFire can detect the malware samples discussed in this article.
- Cortex XDR and XSIAM are designed to prevent the execution of known malicious malware, and also prevent the execution of unknown malware using Behavioral Threat Protection and machine learning based on the Local Analysis module. The Cortex Shellcode AI module can help detect and prevent shellcode attacks.
If you think you may have been compromised or have an urgent matter, get in touch with the Unit 42 Incident Response team or call:
- North America: Toll Free: +1 (866) 486-4842 (866.4.UNIT42)
- UK: +44.20.3743.3660
- Europe and Middle East: +31.20.299.3130
- Asia: +65.6983.8730
- Japan: +81.50.1790.0200
- Australia: +61.2.4062.7950
- India: 00080005045107
Palo Alto Networks has shared these findings with our fellow Cyber Threat Alliance (CTA) members. CTA members use this intelligence to rapidly deploy protections to their customers and to systematically disrupt malicious cyber actors. Learn more about the Cyber Threat Alliance.
Indicators of Compromise
- SHA256 hash: a05882750f7caac48a5b5ddf4a1392aa704e6e584699fe915c6766306dae72cc
- File size: 7.42 MB
- File description: ISO file distributed in the SLOW#TEMPEST campaign
- SHA256 hash: 3d3837eb69c3b072fdfc915468cbc8a83bb0db7babd5f7863bdf81213045023c
- File size: 1.64 MB
- File description: DLL used to load and execute the payload
- SHA256 hash: 3583cc881cb077f97422b9729075c9465f0f8f94647b746ee7fa049c4970a978
- File size: 1.64 MB
- File description: DLL with encrypted payload in the overlay segment