In this blog I will talk about the Signed kernel driver that is used in a recent PlugX attack, the signed kernel drivers that were found on Virus Total are signed through Windows Hardware compatibility program (WHCP) and Sharp Brilliance Communication Technology Co., Ltd..

In summary the kernel driver act as user-mode loader which decrypt a 32-bit user-mode PE file and inject it inside Svchost.exe as child process for services.exe.

in this blog i focused my analysis on the sample “ab7ebc82930e69621d9bccb6698928f4a3719d29”

Driver Analysis:

the driver first registers a mini-filter callback functions and create a communication port with the name “\DtSfProtect{A71A0369-D7CA-4d4f-9EEE-01F8FE53C0D3}” to be able to communicate with the user-mode agent, the driver allows only for one user-agent to connect and accept connection from any process, and the port communication was not used by the user-client.

Register Mini-Filter Driver and create Port Communication

Figure 1: Register Mini-Filter Driver and create Port Communication.

Port Connection CallBack function

Figure 2: Port Connection CallBack function.

Also, the registered filesystem callback pre-operation and post-operation does not do any monitor/protection and just return.

FileSystem Pre-Operation

Figure 3: FileSystem Pre-Operation.

Then it creates Process Object Notifications for protection it monitors any attempt to open the user-mode process and forbids any attempt to access it from kernel drivers and user mode process, so the user-mode component can not be terminated either from user-mode and from kernel mode.

Register Process object Callback

Figure 4: Register Process object Callback.

Pre-Process Callback Function

Figure 5: Pre-Process Callback Function.

After those initializations it creates a thread that will be responsible for resolving all the needed functions address and starting the main user-mode component.

It first tries to check if services.exe process started or not, it do that by using the NtQuerySystemInformation API to get information about the running process, and if services.exe still not running it will go in infinite loop until it starts before continue its operation.

Check if Services.exe process running

Figure 6: Check if Services.exe process running.

Loop untill Services.exe start

Figure 7: Loop untill Services.exe start.

Then it reads configuration from the registry key “\Registry\Machine\SOFTWARE\DtSft\d1” and subkeys “M1” and the data is compared to the current system time, and based on the data in that registry “76 da 34 01” if the current time is after “Wednesday, March 9, 2033 8:07:29 PM” the driver will not continue operation and return and will not start the user-mode component

Read Attack time from registry

Figure 8: Read Attack time from registry.

check if current time before configured time

Figure 9: check if current time before configured time.

Then the driver will read the decryption key from the registry subkeys “M3” under the key “\Registry\Machine\SOFTWARE\DtSft\d1*, the decryption key will be used to decrypt the PE module from the registry

Decryption_Key= “ec,a4,00,c4”

Read Decryption Key from Registry

Figure 10: Read Decryption Key from Registry.

After that the driver will resolve the needed API functions from the windows kernel and from ntdll.dll and kernel32.dll the driver keeps the API information in the structure API_Info and it will do the following steps to fill in the structure fields:

  • For ntdll.dll and kernel APIs
    1. Locate the KeServiceDescriptorTable (SSDT Table)
    2. Read ntdll.dll from hard disk.
    3. Manually Map ntdll.dll DLL to kernel memory.
    4. Search the export address table for the API it needs using the field “API_Info.API_Name” from the API struct.
    5. Extract the value that will be moved inside the EAX register before the sysenter instruction. It will be used as index in the SSDT table to resolve the Kernel API.
    6. Fill in the rest of the fields in the struct (kernel address, user-mode address, EAX value)
  • For kernel32.dll APIs
    1. Read kernel32.dll from hard disk.
    2. Manually Map kernel32.dll DLL to kernel memory.
    3. Search the export address table for the API it needs using the field “API_Info.API_Name” from the API struct.
    4. Fill in the user-mode address field in the struct (the rest of the fields will be null values)
typedef Struct API_Info{
DWORD64 Kernel_API_Address; 	// will be null when used in resolving address in kernel32.dll
DWORD64 User_API_Address;
DWORD64 EAX_Value;		        //index of the function in the SSDT table, will be null in case kernel32.dll
char    API_Name[80h];
}

Resolving API Address

Figure 11: Resolving API Address.

Locate the SSDT table:

To resolve the kernel API address the driver first locate the SSDT table, it does so by scanning the nt!ZwClose Function for the byte “0xE9” which is a JUMP instruction to “nt!KiServiceInternal”.

Locating the nt!KiServiceInternal function

Figure 12: Locating the nt!KiServiceInternal function.

Locating the nt!KiServiceInternal function

Figure 13: Locating the nt!KiServiceInternal function.

After locating “nt!KiServiceInternal” code the driver will search in it for the pattern “0x8D4C” which is “lea r11,[nt!KiSystemServiceStart]” to locate the address of the function “nt!KiSystemServiceStart

Locating the nt!KiSystemServiceStart function

Figure 14: Locating the nt!KiSystemServiceStart function.

Then search for the pattern “0x4c8d15” to locate the address of “lea r10,[nt!KeServiceDescriptorTable]” and from there it will have the address of KeServiceDescriptorTable to continue the operation to resolve Kernel API address.

Locating the KeServiceDescriptorTable Address

Figure 15: Locating the KeServiceDescriptorTable Address.

After locating the SSDT table it will read the DLLs from disk and map it to memory to fill in the API_Info structure.

Reading and mapping ntdll.dll to kernel memory

Figure 16: Reading and mapping ntdll.dll to kernel memory.

Filling the API_Info structure

Figure 17: Filling the API_Info structure.

also the driver will resolve the functions address twice once to get the kernel API, and the second time to get the user-mode API from ntdll.dll and kernel32.dll, and the reason for that is because the services.exe process might not be fully initialized and the ntdll.dll and kernel32.dll DLLs might not be fully loaded yet.

Get Kernel API Address

Figure 18: Get Kernel API Address.

Get User API Address

Figure 19: Get User API Address.

Then the driver will read the User-Mode component from registry subkeys “M2” under registry key “\Registry\Machine\SOFTWARE\DtSft\d1” and then XOR decrypt it.

Read User-mode component and decrypt it

Figure 20: Read User-mode component and decrypt it.

Then it confirms that the user-mode component is a 32-bit file and if not it will not start it, after that it will allocate memory and copy a ShellCode function which will be injected in services.exe to start the main user-component after that it will do a sequence of NtWriteVirtualMemory calls to write the ShellCode, path to Svchost.exe file and the User-mode component to the services.exe process.

Allocate Memory for the shellcode

Figure 21: Allocate Memory for the shellcode.

write the shellcode and svchost path to services.exe process

Figure 22: write the shellcode and svchost path to services.exe process.

change permission of memory to be able to write to it

Figure 23: change permission of memory to be able to write to it.

And to make the ShellCode gets executed it will hook the Ntdll!NtClose to make it jump to the ShellCode after the ShellCode gets execute it will restore the Ntdll!NtClose Function to its original state and make the process continue operation and normal

Hook Ntdll!NtClose to make the shellcode execute

Figure 24: Hook Ntdll!NtClose to make the shellcode execute.

Hook Ntdll!NtClose to make the shellcode execute

Figure 25: Hook Ntdll!NtClose to make the shellcode execute.

User-Mode Component

User-mode component is a simple code that injects another 32-bit PE module in svchost.exe process and monitors it if it gets terminated it will start it again.

User-Mode Component

Figure 26: User-Mode Component.

Yare Rule:

rule PlugX{
    meta:
author = "Mahmoud Zohdy"
date_created = "2024-01-20"
description = "Kernel driver used in recent PlugX attack"

strings:
$string0 = "\\SystemRoot\\system32\\drivers\\DtSfProtect" wide ascii
$string1 = "\\DtSfProtect{A71A0369-D7CA-4d4f-9EEE-01F8FE53C0D3}" wide ascii

condition:
uint16(0) == 0x5A4D and uint32(uint32(0x3C)) == 0x00004550 and any of ( $string* )
}

IOC:

SHA-1 Hash Signer Signing Date Program Name
4307c1e76e66fb09e52c44b83f12374c320cea0d Microsoft Windows Hardware Compatibility Publisher 2023-03-23 淮南锋川网络科技有限责任公司 (Huainan Fengchuan Network Technology Co., Ltd.)
b421c7fb5a041b9225e96f9c82b418b5637dd763 Sharp Brilliance Communication Technology Co., Ltd. 2023-08-27
43e00adbbc09e4b65f09e81e5bd2b716579a6a61 Microsoft Windows Hardware Compatibility Publisher 2022-09-14 大连纵梦网络科技有限公司 (Dalian Zongmeng Network Technology Co., Ltd.)
ab7ebc82930e69621d9bccb6698928f4a3719d29 Microsoft Windows Hardware Compatibility Publisher 2022-09-14 大连纵梦网络科技有限公司 (Dalian Zongmeng Network Technology Co., Ltd.)
7e836dadc2e149a0b758c7e22c989cbfcce18684 Microsoft Windows Hardware Compatibility Publisher 2022-08-17 大连纵梦网络科技有限公司 (Dalian Zongmeng Network Technology Co., Ltd.)
0dd72b3b0b4e9f419d62a4cc7fa0a7d161468a5e Microsoft Windows Hardware Compatibility Publisher 2023-03-22 淮南锋川网络科技有限责任公司 (Huainan Fengchuan Network Technology Co., Ltd.)
097e32d2d6f27a643281bf98875d15974b1f6d85 N/A N/A
2084dd19a5403a4245f8bad30b55681d373ef638 N/A N/A
c4d4489ee16ee537661760879bd36e0d4ab35d61 N/A N/A
c98b3ce984b81086cea7b406eb3857fd6e724bc8 N/A N/A
7079c000d9d25c02d89f0bae5abfe54136daf912 N/A N/A
c4aa3e66331b96b81bd8758e5abcba121a398886 Sharp Brilliance Communication Technology Co., Ltd. 2023-08-23
9883593910917239fc8ff8399e133c8c73b214bc N/A N/A
501114B39A3A6FB40FB5067E3711DC9389F5A802 N/A N/A