返回 TI 主页

Overview

In October 2023, QiAnXin Threat Intelligence Center published an article titled "Operation HideBear: Russian Threat Actors Targeting East Asia and North America" [1]. In the article, we mentioned that the attackers had dual objectives of economic and technological nature. Economically, they targeted investment institutions and Bitcoin companies (individuals), while technologically they showed strong interest in Chinese inductor component manufacturers and biopharmaceutical antibody research. With moderate confidence, we attributed these activities to Strom-0978 and captured a highly peculiar sample during continued tracking at the end of 2023. After extensive reverse analysis, we discovered that the attackers employed a previously undisclosed kernel injection technique, which we named "Step Bear." This injection technique utilizes a combination of "Heaven's Gate" and "Hell's Gate" invocation methods to initiate certain uncommon kernel functions, enabling it to bypass mainstream EDR detections. The injection process is as follows:

Step 1: The malicious process invokes the kernel function xxxSetClassLong to write carefully crafted shellcode into the extra class memory (tagCLS) within the kernel. The shellcode consists of a set of gadget utility functions and multiple pieces of smaller shellcode.

Step 2: The kernel automatically maps the extra class memory (tagCLS) into the read-only memory block of notepad process, achieving code injection.

Step 3: Using a kernel-level wordwrap technique, an RPC call chain is triggered to execute the shellcode within the read-only memory block of notepad. The RPC call chain first triggers the gadget functions within the shellcode, followed by a call to VirtualProtect to change the attributes of the smaller shellcode to RWX (read-write-execute) and execute it.

Step 4: The carefully crafted shellcode is written into the extra class memory (tagCLS) using the same method as in Step 1.

Step 5: The kernel maps the shellcode into the read-only memory block of the browser process, achieving code injection.

Step 6: Triggers the RPC call chain of the read-only shellcode in the browser process by sending a custom message to an undisclosed COM window in Windows.

Step 7: RWX memory is allocated to inject the CRX plugin into the browser.


Introduction to Injection Technique

Principle of Injection

During the injection process, the attacker utilizes the syscall of "Hell's Gate" to invoke NtUserSetClassLong, a function that ultimately calls xxxSetClassLong within win32kfull.sys at the kernel level.

NtUserSetClassLong takes the newly created window handle as the first parameter, an offset as the second parameter (initially set to 0), and the specified data as the third parameter. The structure of the xxxSetClassLong function is as follows. Its main functionality is to write the specified data into the extra class memory of the kernel object tagCLS:

In the kernel version we debugged, the size of the tagCLS structure is 0xA0.

This kernel memory area has an important attribute: the written data in this kernel space is memory-mapped in every user space process within the same user session. If we open some process at this moment, such as a calculator, the specified data written earlier can be found in a mapped memory block within the calculator process.

The size and content of the read-only memory block mapped into the user space process may change, but the base address remains the same. In other words, as long as the attacker manipulates malicious.exe to modify this shared kernel space data, they can achieve code injection into any process. However, the injected memory block has a read-only attribute, so the attacker needs to find a way to trigger the malicious code within the shared memory block.


Preparation before Triggering

The sample first performs some initialization operations, including loading system DLLs, obtaining its own module name, and checking its privileges.

Next, it creates a notepad process and retrieves the handle of the “edit” window within the notepad process.

It registers a window class with extra class memory, using the default NtdllDefWindowProc as the window procedure. The size of the extra class memory is 0x16010.

Then, it creates a message-only window using this window class and calls NtUserSetClassLong to populate the data in the shared memory block within the kernel.

The data filled during this step is only used to locate the specific address of the kernel shared data block mapped into malicious.exe. The sample determines the base address of the window memory by searching for AllocationBase under the TEB->Win32ClientInfo->phkCurrent structure. It then compares the memory in that region with the previously filled data one by one until it finds the address of the shared memory block. Once the position is found, it calls NtUserSetClassLong again to clear the filled data.

Searching for the specified API addresses and the address of the shared memory block in the notepad memory, which will be used for the subsequent injection of the shellcode.

The APIs being searched are as follows:

Once the API resolution is completed, a payload is decrypted in memory, referred to as ShellcodeA.

Then, using the invocation method known as "Heaven's Gate," the NtUserSetClassLongPtr is called to inject ShellcodeA into the kernel shared data.

The structure of ShellcodeA is complex and consists of gadget utility functions and another piece of shellcode, referred to as ShellcodeB. The specific structure is as follows:

At this point, ShellcodeA has been injected into the notepad process, and now we move on to the triggering process.


How is it triggered?

The attacker has implemented two injection operations (one for Notepad, and one for Chrome browser), both using the kernel shared memory block, but the triggering methods for the two injections are completely different. The triggering method for Notepad is relatively common, while the triggering method for Chrome is unprecedented.


Notepad Trigger

The attacker registers a text wrapper callback function by setting EM_SETWORDBREAKPROC and triggers the callback function by sending the WM_LBUTTONDBLCLK message. The address of the callback function's parameter is set earlier through the WM_SETTEXT message.

The final triggered function is I_RpcFreePipeBuffer in rpcrt4.dll.

After executing I_RpcFreePipeBuffer, the function at the position Message->Handle + 0x80 is directly called.

According to the structure of ShellcodeA, at this point, the address Message->Handle + 0x80 corresponds to NdrServerCallAll.

NdrServerCallAll inherits the Message parameter from RpcFreePipeBuffer. After some processing, it calls Ndr64StubWorker.

The relevant structure related to PRPC_MESSAGE is as follows (x86):

Ndr64StubWorker first parses the Message parameter using the Ndr64pServerUnMarshal function and passes the parsed data to the memory pointed to by the stack.

PRPC_MESSAGE_A is the parameter for NdrServerCallAll. It calls the Invoke function with the address from DispatchTable in PRPC_MESSAGE_A.

The address in PRPC_MESSAGE_A's DispatchTable is still NdrServerCallAll, forming a nested structure. It enters NdrServerCallAll for the second time, with PRPC_MESSAGE_B as the parameter. The DispatchTable function in PRPC_MESSAGE_B is VirtualProtect, which changes the memory attributes of ShellcodeB in the shared memory block to be readable, writable, and executable.

The call chain is as follows:

The attacker utilizes an undisclosed function within Ndr64StubWorker to execute ShellcodeB. After calling the Invoke function, the execution proceeds sequentially to Ndr64pFreeParams. This function allows the execution of attacker-controlled code.

Due to the unique structure of ShellcodeA, the current function calls are in a nested state, entering NdrServerCallAll twice. As a result, the attacker-controlled code will be executed at two instances of Invoke and two instances of Ndr64pFreeParams. The execution order is as follows: Invoke_A -> Invoke_B -> Ndr64pFreeParams_B -> Ndr64pFreeParams_A. Consequently, the attacker-controlled code within Ndr64pFreeParams_B will be executed. At this point, the function to be called is designed by the attacker as CallWindowProcW, with the first parameter lpPrevWndFunc filled with the starting address of ShellcodeB.

ShellcodeB, with a size of 0x3C0, is actually composed of three pieces of smaller shellcode, which we refer to as ShellcodeC, ShellcodeD, and ShellcodeE. The structure of ShellcodeB in the execution chain is as follows:

The code in ShellcodeC primarily consists of Jmp instructions, jumping to ShellcodeD.

ShellcodeD calls RtlAddVectoredExceptionHandler to register an exception handler. When the execution flow reaches after Ndr64pFreeParams_B and before Ndr64pFreeParams_A, an SEH (Structured Exception Handling) will inevitably be triggered.

The logic of the exception callback function is as follows:

It calls the SendMessageTimeoutW function to modify the “edit” window procedure of Notepad to RpcAsyncRegisterInfo. The second SendMessageTimeoutW communicates with a window created by the malicious.exe process. When the window receives a message, it writes the address of the starting structure of ShellcodeA (Message->Handle + 0x80) into the position of ShellcodeE. ShellcodeE represents the second phase of the shellcode after ShellcodeA.

Finally, it reaches Ndr64pFreeParams_A. The function designed by the attacker to be called is I_RpcFreePipeBuffer. In the SEH callback function, the address pointed to by (Message->Handle + 0x80) has already been modified to the starting address of ShellcodeE. Therefore, it proceeds to the second phase's logic. The execution of ShellcodeE is as follows:

ShellcodeE first allocates executable memory.

The first communication with malicious.exe is mainly for the relocation of addresses in ShellcodeF.

The second communication with malicious.exe notifies it to inject ShellcodeF into the shared memory block.

After the injection is completed, ShellcodeF is copied into the previously allocated executable memory block. Thus, the first round of injection is completed. The injection process is illustrated in the following flowchart:


Chrome Trigger

The main function of ShellcodeF is to inject malicious code from the Notepad process into the Chrome process, initiating the second round of injection process. It searches for the browser process through Chrome_MessageWindow.

Before injection, it verifies the current time and checks if it is less than 64AE6B71 (2023-07-12 08:59:29). If it is, it proceeds with the injection process.

This indicates that the specific attack activity occurred before 2023-07-12. Using the same injection method, the shellcode is written into a shared memory block. The attacker then searches for an undisclosed hidden window within the browser process.

By calling NtUserMessageCall, the specified message is sent to the undisclosed COM window class, OleMainThreadWndClass.

The first parameter is the handle of the undisclosed hidden window, the second parameter is the MSG message, which we speculate to be the custom message 0x405 of the unknown window. The third parameter represents unknown data, and the fourth parameter is a pointer to a structure that contains the address of the shared memory block in the target browser process. Through debugging, we discovered that the browser process utilizes MsgWaitForMultipleObjectsEx to wait for message event signals.

The function detects the event signal and returns, then enters the message dispatch process PeekMessageW.

PeekMessageW invokes the underlying message dispatch process in Windows: NtUserPeekMessage. After entering NtUserPeekMessage (R0), it proceeds to the user callback phase, KiUserCallbackDispatcher (R3), based on the retrieved message. The following call chain is involved: KiUserCallbackDispatcher -> __fnDWORD -> DispatchClientMessage -> UserCallWinProcCheckWow. At this point, the parameters for UserCallWinProcCheckWow are as follows:

The second parameter is set as ThreadWndProc, which is a function used to enter the window callback for receiving messages from the window handle. UserCallWinProcCheckWow calls this function, and upon entering ThreadWndProc, it dispatches based on the message value. Among them, when the value is 0x405, it enters the OleMainThreadWndProc function, and based on the function name, it is inferred to be the default callback function registered for the OleMainThreadWndClass window class.

In OleMainThreadWndProc, after a conditional check, it proceeds to GetSingleThreadedHost.

At this point, the lParam parameter of GetSingleThreadedHost is set to the address specified by the attacker. In the beginning of this function, it calls the address at [[lParam] + 0x18].

The address is set by the attacker as NdrServerCallAll, which leads to the same execution flow as the previous Notepad injection. The call stack is as follows:

The difference between the execution flow of Notepad and the browser process lies in the fact that Notepad enters NdrServerCallAll through the function I_RpcFreePipeBuffer, while the browser process enters NdrServerCallAll through GetSingleThreadedHost in the combase library. This triggers the RPC chain in the browser's shared memory block, creates a memory block with RWX attributes, and ultimately loads a CRX plugin into the browser's memory.

The main function of the CRX plugin is to steal data from the browser and there is a rule file used to replace the displayed information in the browser. For example, when performing operations related to cryptocurrencies, it replaces withdrawal requests with authorization requests.

Russian comments were found in the JS code.

Finally, the stolen data is sent back to the Command and Control server.


Impact

Plenty of current APT groups can be referred to as "Loader producers." They spend a significant amount of money to purchase low-quality loaders from outsourcing companies to load generic Trojan backdoors, and then initiate their attack activities. Even in the entire history of malware, it isn’t very common to see that in-the-wild attacks utilize such complex kernel injection techniques. While some of these techniques are popularly involved in exploitation for Windows kernel privilege escalation [2], the injection technique used in "Step Bear" incorporates some previously undisclosed methods, such as hijacking the execution flow through the Ndr64pFreeParams function and the trigger point of the custom message 0x405 for an unknown COM window. This indicates that some experienced Windows kernel researchers have leveraged their unique understanding of both the Windows kernel and the Chromium kernel to design such complex injection framework. As a result, attackers have dealt a blow to mainstream EDR products. Technicians behind this malware have a high level of expertise in designing malicious code. The logic of injection and triggering has already been modularized and can be reused in many attack scenarios.

Based on the validation timestamp in the shellcode and the packaging time of the CRX, we can infer that the Storm-0978 group carried out attacks between March and July 2023. During this timeline, the group was targeting Western countries using CVE-2023-36884. Although we haven't captured initial access of attackers, we speculate that they adopt methods similar to previous activities, involving phishing campaigns using highly deceptive download pages. QiAnXin Threat Intelligence Center will continue to monitor the "Operation HideBear" campaign.


Summary

Currently, all QiAnXin Threat Intelligence Center's products that rely on threat intelligence data, including QiAnXin Threat Intelligence Platform (TIP), Tianqing, Tianyan Advanced Threat Detection System, QiAnXin NGSOC, and QiAnXin Situational Awareness, are already capable of accurately detecting such attacks.


IOC

For detailed IOCs related to the threat actor, please contact QiAnXin Threat Intelligence Center (ti.qianxin.com).


Reference links

[1]. https://ti.qianxin.com/blog/articles/Operation-HideBear-Russian-Threat-Actors-Targeting-East-Asia-and-North-America-EN/
[2]. https://hackyboiz.github.io/2023/10/30/pwndorei/newjeans-hyper-v-pt5/

STORM-0978 STEP BEAR