{
	"id": "dfe490cb-8065-44f5-8f5c-dc4bffd389b5",
	"created_at": "2026-04-06T00:21:48.945853Z",
	"updated_at": "2026-04-10T03:24:24.510278Z",
	"deleted_at": null,
	"sha1_hash": "6bae4009e5e7baa14f9fc1ab11b8517eb21df2ef",
	"title": "Understanding Syscalls: Direct, Indirect, and Cobalt Strike Implementation",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 402914,
	"plain_text": "Understanding Syscalls: Direct, Indirect, and Cobalt Strike\r\nImplementation\r\nBy Mohamed Adel\r\nPublished: 2023-08-18 · Archived: 2026-04-05 17:01:27 UTC\r\nIn case images fail to load, it might be due to jsDelivr CDN ban in Egypt. To resolve this, consider using a VPN.\r\n:)\r\nTo Bypass user-mood hooks. why?\r\nFor Hiding a code inside a legitimate process (Process Injection)\r\nAvoiding EDR alerts!\r\nHooking user-mode functions by placing a jump to another code section. EDRs use hooks to check the function parameters.\r\nFor example, if you are trying to change the memory protections of some data to add executable protections. This is a very\r\nsuspicious activity so EDRs will be alert to that. Most Hooks are on the lowest level of the user-mode interface in ntdll.dll\r\nwhich are the system calls.\r\nWindows has a defined schema of how syscalls are used. Most of the documented windows APIs are just a wrapper of a\r\nlower-level Functions in ntdll.dll which are compiled to a syscall with the right SSN (System Service Number). To\r\nlook at how Nt* version of the higher-level API is implemented.\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n0:018\u003e uf NtOpenProcess\r\nntdll!NtOpenProcess:\r\n00007ffa`4874d4c0 4c8bd1 mov r10,rcx\r\n00007ffa`4874d4c3 b826000000 mov eax,26h\r\n00007ffa`4874d4c8 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1\r\n00007ffa`4874d4d0 7503 jne ntdll!NtOpenProcess+0x15 (00007ffa`4874d4d5) Branch\r\nntdll!NtOpenProcess+0x12:\r\n00007ffa`4874d4d2 0f05 syscall\r\n00007ffa`4874d4d4 c3 ret\r\nntdll!NtOpenProcess+0x15:\r\n00007ffa`4874d4d5 cd2e int 2Eh\r\n00007ffa`4874d4d7 c3 ret\r\nAt address 00007ffa~4874d4d2 there syscall instruction. This instruction transfers the execution to the system-handler at\r\nthe kernel. The handler is specified using pre-defined SSN number loaded into EAX Register (In this case EAX = 0x26 at\r\naddress 00007ffa~4874d4c3 ). So, to make a syscall The SSN associated. The code stub of the syscalls is simple.\r\n1\r\n2\r\n3\r\n4\r\nmov r10, rcx\r\nmov eax, \u003csyscall_number\u003e\r\nsyscall\r\nret\r\nNow, the missing thing is the syscall_number . These numbers are changing based on the Build version of windows. There\r\nare some techniques to get these numbers.\r\n1. SysWhispers\r\nSysWhispers That generate the table of these numbers in the form of a header file and assembly file that can be embedded in\r\nthe code. The generated code contains syscall number for multiple versions, The right windows build version is detected\r\nat runtime using PEB structure.\r\nhttps://d01a.github.io/syscalls/\r\nPage 1 of 11\n\n1\r\n2\r\n3\r\n4\r\n5\r\n...\r\n+0x118 OSMajorVersion : Uint4B\r\n+0x11c OSMinorVersion : Uint4B\r\n+0x120 OSBuildNumber : Uint2B\r\n...\r\nThe assembly code generated (Full document at example-output)\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n24\r\n25\r\n26\r\n27\r\n28\r\n29\r\n30\r\n31\r\n32\r\n33\r\n34\r\n35\r\n36\r\n37\r\n38\r\n39\r\n40\r\n41\r\n...\r\nNtOpenProcess PROC\r\nmov rax, gs:[60h] ; Load PEB into RAX.\r\nNtOpenProcess_Check_X_X_XXXX: ; Check major version.\r\ncmp dword ptr [rax+118h], 5\r\nje NtOpenProcess_SystemCall_5_X_XXXX\r\ncmp dword ptr [rax+118h], 6\r\nje NtOpenProcess_Check_6_X_XXXX\r\ncmp dword ptr [rax+118h], 10\r\nje NtOpenProcess_Check_10_0_XXXX\r\njmp NtOpenProcess_SystemCall_Unknown\r\nNtOpenProcess_Check_6_X_XXXX: ; Check minor version for Windows Vista/7/8.\r\ncmp dword ptr [rax+11ch], 0\r\nje NtOpenProcess_Check_6_0_XXXX\r\ncmp dword ptr [rax+11ch], 1\r\nje NtOpenProcess_Check_6_1_XXXX\r\ncmp dword ptr [rax+11ch], 2\r\nje NtOpenProcess_SystemCall_6_2_XXXX\r\ncmp dword ptr [rax+11ch], 2\r\nje NtOpenProcess_SystemCall_6_3_XXXX\r\njmp NtOpenProcess_SystemCall_Unknown\r\nNtOpenProcess_Check_6_0_XXXX: ; Check build number for Windows Vista.\r\ncmp dword ptr [rax+120h], 6000\r\nje NtOpenProcess_SystemCall_6_0_6000\r\ncmp dword ptr [rax+120h], 6001\r\nje NtOpenProcess_SystemCall_6_0_6001\r\ncmp dword ptr [rax+120h], 6002\r\nje NtOpenProcess_SystemCall_6_0_6002\r\njmp NtOpenProcess_SystemCall_Unknown\r\nNtOpenProcess_Check_6_1_XXXX: ; Check build number for Windows 7.\r\ncmp dword ptr [rax+120h], 7600\r\nje NtOpenProcess_SystemCall_6_1_7600\r\ncmp dword ptr [rax+120h], 7601\r\nje NtOpenProcess_SystemCall_6_1_7601\r\njmp NtOpenProcess_SystemCall_Unknown\r\nNtOpenProcess_Check_10_0_XXXX: ; Check build number for Windows 10.\r\ncmp dword ptr [rax+120h], 10240\r\nje NtOpenProcess_SystemCall_10_0_10240\r\ncmp dword ptr [rax+120h], 10586\r\nje NtOpenProcess_SystemCall_10_0_10586\r\n...\r\n1. SSN code stub This technique doesn’t Look for SSN number, instead it gets the code stub of the required API. This\r\ncan be done by opening the PE file and parsing the Export table of ntdll\r\n2. Extract SSN It Extract the SSN from ntdll by parsing the Export table. The difference between it and the previous\r\none is that it only extracts the syscall number. Both methods load ntdll.dll from the disk first using win32 API\r\nOpenFile which might be hooked. hell’s gate for more.\r\n3. Syscalls’ number sequence This method take advantage of the SSNs are in a sequence for example if a syscall\r\nnumber is 0x26 the following will be 0x27 and so on. This relies also on the fact that not all the system calls are\r\nhooked! So, to get the SSN of a function, you need to find the nearest unhooked syscall. this was presented by halos\r\ngate. But This is not valid in newer versions of Windows as the SSNs sequence is no longer valid.\r\nhttps://d01a.github.io/syscalls/\r\nPage 2 of 11\n\n4. Parallel loading This is an interesting technique explained in this blog. It uses windows feature introduced in\r\nwindows 10 to load DLLs through multiple threads instead of one in older versions of windows. It was found that the\r\nsyscall stub of native Functions\r\nNtOpenFile() ,  NtCreateSection() ,  ZwQueryAttributeFile() ,  ZwOpenSection()  and  ZwMapViewOfFile() -\r\nThere is a lot of things happens between the two actions, detailed explanation in the previously mentioned blog -are\r\ncopied into LdrpThunkSignature array. This is done to check the integrity of the functions’ code. These APIs’\r\nsyscall numbers can be used to load a new version of ntdll.dll from the disk and avoid any user-mood hooks.\r\n5. Sorting by system call address This technique uses the relation between the address of the system call stub and the\r\nSSN. It is known as FreshyCalls . In simple words, it walks the Export Address Table of ntdll and saves the Name\r\n-or a hash of the name- and Address of each entry in a table. Then, it sorts the entries by the addresses in ascending\r\norder. It was found that the first function NtAccessCheck (by address) has an SSN = 0\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n0:007\u003e uf NtAccessCheck\r\nntdll!NtAccessCheck:\r\n00007ffa`4874d000 4c8bd1 mov r10,rcx\r\n00007ffa`4874d003 b800000000 mov eax,0\r\n00007ffa`4874d008 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1\r\n00007ffa`4874d010 7503 jne ntdll!NtAccessCheck+0x15 (00007ffa`4874d015) Branch\r\nntdll!NtAccessCheck+0x12:\r\n00007ffa`4874d012 0f05 syscall\r\n00007ffa`4874d014 c3 ret\r\nntdll!NtAccessCheck+0x15:\r\n00007ffa`4874d015 cd2e int 2Eh\r\n00007ffa`4874d017 c3 ret\r\nand if we unassembled the next function by adding one to the last address (as ret opcode is one byte) we will get that the\r\nnext function’s SSN is 1!\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n0:007\u003e uf 00007ffa`4874d017+1\r\nntdll!NtAccessCheck+0x18:\r\n00007ffa`4874d018 0f1f840000000000 nop dword ptr [rax+rax]\r\n00007ffa`4874d020 4c8bd1 mov r10,rcx\r\n00007ffa`4874d023 b801000000 mov eax,1\r\n00007ffa`4874d028 f604250803fe7f01 test byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1\r\n00007ffa`4874d030 7503 jne ntdll!NtWorkerFactoryWorkerReady+0x15 (00007ffa`4874d035) Branch\r\nntdll!NtWorkerFactoryWorkerReady+0x12:\r\n00007ffa`4874d032 0f05 syscall\r\n00007ffa`4874d034 c3 ret\r\nntdll!NtWorkerFactoryWorkerReady+0x15:\r\n00007ffa`4874d035 cd2e int 2Eh\r\n00007ffa`4874d037 c3 ret\r\nSo, by sorting the functions by the addresses, we have the SSN. for the code, look at MDSec (8. Sorting by System Call\r\nAddress) blog or see FreshlyCalls implementation. The execution of the system call is not direct by calling syscall\r\ninstruction. Instead. It uses the method explained below. Briefly, it uses the syscall instructions from ntdll .\r\nAll the methods described are workarounds to get the system call number without getting caught. syscall instruction\r\nreveals that some suspicious activity is going on. This is done using KPROCESS!InstrumentationCallback in windows.\r\n1\r\n2\r\n3\r\n4\r\n5\r\n0:030\u003e dt _kprocess\r\nntdll!_KPROCESS\r\n +0x000 Header : _DISPATCHER_HEADER\r\n ...\r\n +0x3d8 InstrumentationCallback : Ptr64 Void\r\nhttps://d01a.github.io/syscalls/\r\nPage 3 of 11\n\n6\r\n7\r\n ...\r\n +0x3f8 EndPadding : [8] Uint8B\r\nAny time the windows is done with a syscall and returns to user-mode, it checks this member it is not NULL , the execution\r\nwill be transferred to that pointer. To check if the syscall is legit, the return address after finishing the syscall is checked to\r\nsee if it is not from a valid place. If the address is in the address space of the process running, it’s not a legitimate place to\r\nmake a syscall. This check was done by ScyllaHide to detect manual syscalls, the source code can be found here.\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n10\r\n11\r\n12\r\n13\r\n14\r\n15\r\n16\r\n17\r\n18\r\n19\r\n20\r\n21\r\n22\r\n23\r\n if (InterlockedOr(TlsGetInstrumentationCallbackDisabled(), 0x1) == 0x1)\r\n return ReturnVal; // Do not recurse\r\n const PVOID ImageBase = NtCurrentPeb()-\u003eImageBaseAddress;\r\n const PIMAGE_NT_HEADERS NtHeaders = RtlImageNtHeader(ImageBase);\r\n if (NtHeaders != nullptr \u0026\u0026 ReturnAddress \u003e= (ULONG_PTR)ImageBase \u0026\u0026\r\n ReturnAddress \u003c (ULONG_PTR)ImageBase + NtHeaders-\u003eOptionalHeader.SizeOfImage)\r\n {\r\n // Syscall return address within the exe file\r\n ReturnVal = (ULONG_PTR)(ULONG)STATUS_PORT_NOT_SET;\r\n // Uninstall ourselves after we have completed the sequence { NtQIP, NtQIP }. More NtSITs will follow but we can't do anyth\r\n NumManualSyscalls++;\r\n if (NumManualSyscalls \u003e= 2)\r\n {\r\n InstallInstrumentationCallbackHook(NtCurrentProcess, TRUE);\r\n }\r\n }\r\n InterlockedAnd(TlsGetInstrumentationCallbackDisabled(), 0);\r\n return ReturnVal;\r\n}\r\nIt checks the return address of the successful system call. If it resides on the address space of the binary we are running, it is\r\nan indication of manual system call.\r\nThe Solution The solution to this hooking method is done by Bouncy Gate and Recycled Gate method. The idea is quite\r\nsimple, it is an adjusted version of Hell’s Gate. Instead of directly executing syscall instruction and getting caught by\r\nstatic signatures and system call callbacks described above, the author replaces the syscall instruction with a trampoline\r\njump ( JMP ) to a syscall instruction address from ntdll.dll . now there is no direct syscall instruction and the\r\nsystem call originated from a legitimate place ntdll . This is also implemented in SysWhispers3. To get the address of the\r\nsyscall instruction in ntdll we can parse the export table and search for syscall, ret opcodes 0F 05 0C or the constant\r\npattern of syscalls in ntdll can be used to get the syscall address. If the function is not hooked, the syscall instruction is on\r\noffset 0x12 from the function’s address, we can verify that by comparing the opcodes.\r\nThe sample from Dodo’s blog Where he already analyzed how indirect syscalls implemented in Cobalt Strike. for easy\r\naccess, here is UnpacMe Results 020b20098f808301cad6025fe7e2f93fa9f3d0cc5d3d0190f27cf0cd374bcf04. The sample is\r\npacked. The unpacking process is easy. Just put a breakpoint on VirtualProtect and get the base address (First\r\nArgument). Function sub_18001B6B0 contains the important part, system call SSN retrieving and execution methods. You\r\ncan get to this function by following the call instruction to rax which contains a qword memory area or a call to the\r\nqword directly. These locations are populated with addresses of the required APIs in this function. We can see multiple\r\ncalls to sub_18001A73C with arguments: qword_* , a hash (such as 0B12B7A69h ), variable passed to the function\r\nsub_18001A7F4 and another allocated memory which is also passed to sub_18001A7F4 .\r\nhttps://d01a.github.io/syscalls/\r\nPage 4 of 11\n\nFunction sub_18001A73C is to resolve the function address ( syscall stub address) by the hash. And function\r\nsub_18001A7F4 used to populate the list with the system call SSN and system call stub. So, sub_18001A7F4 is our target.\r\nIn the following picture is the beginning of the function.\r\nThe function starts with getting a pointer to the first entry in InLoadOrderModuleList structure by going through reading\r\nthe Process Environment Block (PEB). here in the picture, r10 is holding the current entry of the structure and r9 is like a\r\nvariable to get each entry, this is the breaking condition of the loop as the _LIST_ENTRY structure wrap around itself (doubly\r\nlinked list).\r\nThe next step is to get the Export directory of ntdll.dll but first, get ntdll address in memory.\r\nhttps://d01a.github.io/syscalls/\r\nPage 5 of 11\n\nIt is looking for the right module in the InLoadOrderModuleList by going through each entry, the flink is a pointer to\r\nLDR_DATA_TABLE_ENTRY where we can get a pointer to the module. By parsing the module (going through PE file headers)\r\nto get the name of the DLL which resides in the Export directory (First member) which is the first member of\r\nIMAGE_DATA_DIRECTORY structure. It is then tested to see if it is the target module ( ntdll ). If the module is ntdll , it\r\nsaves a pointer to AddressOfFunctions , AddressOfNames and AddressOfNameOrdinals . A memory region of size 0x1f40\r\nis then zeroed as it will hold the structures of the system call information needed. The next part is checking the function\r\nprefix Ki and Zw . It looks for only one function prefixed by Ki with the hash 8DCD4499h , but I couldn’t find function\r\nwith this hash (using debugger). Then, a call to a hashing function is made. The hashing function is simple.\r\nIt uses 0x52964EE9 as an initial key value to start the process then:\r\nGet 2-bytes of the Function name (little endian).\r\nRotate the key by 8 (2 characters).\r\nAdd the key and the 2-bytes of the name.\r\nIncrement the counter by 1 (Resulting that all the chars in between the start and end taken two times in the\r\ncalculation for example ZwOpenProcess will take Wz in the first iteration and Ow in the second and so on).\r\nThe result of the addition is XORed with the key to produce the new key. The hash value returned is the last result of\r\nthe XOR operation.\r\nThe resulting value is stored in the following form, in the pre-allocated space.\r\nhttps://d01a.github.io/syscalls/\r\nPage 6 of 11\n\nThe first DWORD is the hash.\r\nThe second DWORD is the Relative Virtual Address (RVA) of the system call0.\r\nThe third QWORD is the Virtual Address (VA) of the system call stub (RVA + ntdll Base Address).\r\nSo, it can be written as:\r\n1\r\n2\r\n3\r\n4\r\n5\r\nstruct syscall_info {\r\nDWORD API_hash;\r\nDWORD syscall_stub_RVA;\r\nQWORD syscall_stub_address;\r\n};\r\nAfter populating the structure with the addresses. The structure elements are being sorted by the RVA of the system call stub\r\n(second entry in the structure).\r\nAfter the sorting algorithm is done, the memory structure look like the following:\r\nThe first address is the address to the Lowest address ZwMapUserPhysicalPagesScatter (Could be different at newer\r\nversions of windows) at address 00000000774E1340 If we see the system call SSN of it:\r\nsystem call number is zero. This is how it gets the SSN for any function, by iterating the structure to get the right hash, the\r\ncounter will be used to get the SSN (SSN = counter). So far, this is remarkably like MDSec (8. Sorting by System Call\r\nhttps://d01a.github.io/syscalls/\r\nPage 7 of 11\n\nAddress) implementation of the technique known as FreshlyCalls . We could rewrite the technique using MDSec\r\nimplementation as follows:\r\n 1\r\n 2\r\n 3\r\n 4\r\n 5\r\n 6\r\n 7\r\n 8\r\n 9\r\n 10\r\n 11\r\n 12\r\n 13\r\n 14\r\n 15\r\n 16\r\n 17\r\n 18\r\n 19\r\n 20\r\n 21\r\n 22\r\n 23\r\n 24\r\n 25\r\n 26\r\n 27\r\n 28\r\n 29\r\n 30\r\n 31\r\n 32\r\n 33\r\n 34\r\n 35\r\n 36\r\n 37\r\n 38\r\n 39\r\n 40\r\n 41\r\n 42\r\n 43\r\n 44\r\n 45\r\n 46\r\n 47\r\n 48\r\n 49\r\n 50\r\n 51\r\n 52\r\n 53\r\n 54\r\n 55\r\n 56\r\n 57\r\n 58\r\n 59\r\n 60\r\n 61\r\n#define RVA2VA(Type, DllBase, Rva) (Type)((ULONG_PTR) DllBase + Rva)\r\nstatic\r\nvoid\r\nGetSyscallList(PSYSCALL_LIST List) {\r\n PPEB_LDR_DATA Ldr;\r\n PLDR_DATA_TABLE_ENTRY LdrEntry;\r\n PIMAGE_DOS_HEADER DosHeader;\r\n PIMAGE_NT_HEADERS NtHeaders;\r\n DWORD i, j, NumberOfNames, VirtualAddress, Entries=0;\r\n PIMAGE_DATA_DIRECTORY DataDirectory;\r\n PIMAGE_EXPORT_DIRECTORY ExportDirectory;\r\n PDWORD Functions;\r\n PDWORD Names;\r\n PWORD Ordinals;\r\n PCHAR DllName, FunctionName;\r\n PVOID DllBase;\r\n PSYSCALL_ENTRY Table;\r\n SYSCALL_ENTRY Entry;\r\n //\r\n // Get the DllBase address of NTDLL.dll\r\n // NTDLL is not guaranteed to be the second in the list.\r\n // so it's safer to loop through the full list and find it.\r\n Ldr = (PPEB_LDR_DATA)NtCurrentTeb()-\u003eProcessEnvironmentBlock-\u003eLdr;\r\n // For each DLL loaded\r\n for (LdrEntry=(PLDR_DATA_TABLE_ENTRY)Ldr-\u003eReserved2[1];\r\n LdrEntry-\u003eDllBase != NULL;\r\n LdrEntry=(PLDR_DATA_TABLE_ENTRY)LdrEntry-\u003eReserved1[0])\r\n {\r\n DllBase = LdrEntry-\u003eDllBase;\r\n DosHeader = (PIMAGE_DOS_HEADER)DllBase;\r\n NtHeaders = RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader-\u003ee_lfanew);\r\n DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders-\u003eOptionalHeader.DataDirectory;\r\n VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;\r\n if(VirtualAddress == 0) continue;\r\n ExportDirectory = (PIMAGE_EXPORT_DIRECTORY) RVA2VA(ULONG_PTR, DllBase, VirtualAddress);\r\n DllName = RVA2VA(PCHAR,DllBase, ExportDirectory-\u003eName);\r\n if((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue;\r\n if((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break;\r\n }\r\n NumberOfNames = ExportDirectory-\u003eNumberOfNames;\r\n Functions = RVA2VA(PDWORD,DllBase, ExportDirectory-\u003eAddressOfFunctions);\r\n Names = RVA2VA(PDWORD,DllBase, ExportDirectory-\u003eAddressOfNames);\r\n Ordinals = RVA2VA(PWORD, DllBase, ExportDirectory-\u003eAddressOfNameOrdinals);\r\n Table = List-\u003eTable;\r\n do {\r\n FunctionName = RVA2VA(PCHAR, DllBase, Names[NumberOfNames-1]);\r\n if(*(USHORT*)FunctionName == 'iK' \u0026\u0026 HashSyscall(FunctionName) == 0x8DCD4499) {\r\n Table[Entries].API_Hash = HashSyscall(\u0026FunctionName);\r\n Table[Entries].syscall_stub_RVA = Functions[Ordinals[NumberOfNames-1]];\r\n Table[Entries].syscall_stub_address = RVA2VA(void, DllBase,Functions[Ordinals[NumberOfNames-1]]);\r\n Entries++;\r\nhttps://d01a.github.io/syscalls/\r\nPage 8 of 11\n\n62\r\n 63\r\n 64\r\n 65\r\n 66\r\n 67\r\n 68\r\n 69\r\n 70\r\n 71\r\n 72\r\n 73\r\n 74\r\n 75\r\n 76\r\n 77\r\n 78\r\n 79\r\n 80\r\n 81\r\n 82\r\n 83\r\n 84\r\n 85\r\n 86\r\n 87\r\n 88\r\n 89\r\n 90\r\n 91\r\n 92\r\n 93\r\n 94\r\n 95\r\n 96\r\n 97\r\n 98\r\n 99\r\n100\r\n101\r\n102\r\n103\r\n if(Entries == MAX_SYSCALLS) break;\r\n }\r\n if(*(USHORT*)FunctionName == 'wZ') {\r\n Table[Entries].API_Hash = HashSyscall(\u0026FunctionName);\r\n Table[Entries].syscall_stub_RVA = Functions[Ordinals[NumberOfNames-1]];\r\n Table[Entries].syscall_stub_address = RVA2VA(void, DllBase,Functions[Ordinals[NumberOfNames-1]]);\r\n Entries++;\r\n if(Entries == MAX_SYSCALLS) break;\r\n }\r\n } while (--NumberOfNames);\r\n //\r\n // Save total number of system calls found.\r\n //\r\n List-\u003eEntries = Entries;\r\n //\r\n // Sort the list by address in ascending order.\r\n //\r\n for(i=0; i\u003cEntries - 1; i++) {\r\n for(j=0; j\u003cEntries - i - 1; j++) {\r\n if(Table[j].syscall_stub_RVA \u003e Table[j+1].syscall_stub_RVA) {\r\n //\r\n // Swap entries.\r\n //\r\n Entry.Hash = Table[j].Hash;\r\n Entry.Address = Table[j].Address;\r\n Table[j].API_Hash = Table[j+1].API_Hash;\r\n Table[j].syscall_stub_RVA = Table[j+1].syscall_stub_RVA;\r\n Table[j].syscall_stub_address = Table[j+1].syscall_stub_address;\r\n Table[j+1].API_Hash = Entry.API_Hash;\r\n Table[j+1].syscall_stub_RVA = Entry.syscall_stub_RVA;\r\n Table[j+1].syscall_stub_address = Entry.syscall_stub_address;\r\n }\r\n }\r\n }\r\n}\r\nThe next thing is to use the structure to get the SSN. and syscall instruction to call. This is done by function\r\nsub_18001A73C .\r\nhttps://d01a.github.io/syscalls/\r\nPage 9 of 11\n\nThe function takes the following parameters:\r\nThe array of structures that has the system call info (called syscall_info above)\r\nconstant value 0x1F4 the maximum length of the structure members (structure size = 0x1F4 * 0x10).\r\nPre-Allocated memory\r\nThe function hash.\r\nGlobal variable to get the system call SSN and stub. The function is simple, it searches the populated structure to find\r\nthe given hash. If it’s found, the counter value is taken and to get the Address of the system call stub. To get the\r\naddress, the base address of the structure is added to the offset multiplied by 0x10 (struct size) and add 8 to get the\r\nlast QWORD.\r\n1 API_Address = *(STRUCT_BASE_ADDR + COUNTER * 0x10 + 8)\r\nThe address the passed to get_syscall_ret_address to get the syscall ret addresses to use it to execute the system call\r\nto bypass the callback mentioned before (call stack tracing is be used to detect this trick).\r\nThe global variable is used to store:\r\nQWORD to store System call address (function address at ntdll )\r\nQWORD to store syscall , ret instruction sequence address.\r\nDWORD to store system call number SSN. We can rewrite it as follows:\r\n1\r\n2\r\n3\r\n4\r\n5\r\nstruct syscall_required_addresses {\r\nQWORD syscall_stub_address;\r\nQWORD syscall_intruction_address;\r\nDWORD syscall_number;\r\n};\r\n(Creative names I know :) )\r\nThere are some choices to call the required function. This is done based on the value at a global variable (0x18004BC6C):\r\n1 : Direct call using the first member of the structure (Address of the function in ntdll )\r\n2 : Indirect system call using trampoline jump using the system call number and the syscall address stored before.\r\nhttps://d01a.github.io/syscalls/\r\nPage 10 of 11\n\nanything else: Direct call to Win32 API.\r\nSystem calls can be used to bypass user mood hooks but there are other methods to detect Direct and Indirect syscalls. To\r\ndetect Direct system calls, Windows provides a large set of callback functions, one of them is\r\nKPROCESS!InstrumentationCallback . This callback is triggered whenever the system returns from the kernel mode to user\r\nmode. This could be used to check the return address of the syscall which reveals the location of syscall instruction\r\nexecution. This location should be ntdll but in case of the direct system calls, it will be from the .text section of the PE\r\nfile. This was used by ScyllaHide. Indirect system calls solved this problem by getting the address of syscall instruction\r\nin ntdll and jump to it. To detect indirect syscalls the call stack tracing method can be used to check from where the\r\nsystem call originated -before jumping to ntdll -. This also can be bypassed by creating a new thread to get a new call\r\nstack using callback functions like TpAllocWork and RtlQueueWorkItem . If you want to know more about this, you can\r\nread Hiding In PlainSight 1\u00262\r\nNote: This was personal notes I wrote when I was learning about syscalls, if there’s anything not accurate, please let\r\nme know\r\nhttps://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams/\r\nhttps://www.youtube.com/watch?v=elA_eiqWefw\u0026t=3176s\r\nhttps://offensivedefence.co.uk/posts/dinvoke-syscalls/\r\nhttps://www.felixcloutier.com/x86/syscall.html\r\nhttps://www.mdsec.co.uk/2022/04/resolving-system-service-numbers-using-the-exception-directory/\r\nhttps://github.com/j00ru/windows-syscalls/\r\nhttps://cocomelonc.github.io/malware/2023/06/07/syscalls-1.html\r\nhttps://www.crummie5.club/freshycalls/\r\nhttps://github.com/x64dbg/ScyllaHide/blob/master/HookLibrary/HookedFunctions.cpp\r\nhttps://eversinc33.com/posts/avoiding-direct-syscall-instructions/\r\nhttps://redops.at/en/blog/direct-syscalls-a-journey-from-high-to-low\r\nhttps://github.com/dodo-sec/Malware-Analysis/blob/main/Cobalt Strike/Indirect Syscalls.md\r\nhttps://github.com/crummie5/FreshyCalls/blob/112bdf0db63a5f7104ba5243af6a672bc098a1ad/syscall.cpp#L65\r\nhttps://0xdarkvortex.dev/hiding-in-plainsight/\r\nhttps://0xdarkvortex.dev/proxying-dll-loads-for-hiding-etwti-stack-tracing/\r\nSource: https://d01a.github.io/syscalls/\r\nhttps://d01a.github.io/syscalls/\r\nPage 11 of 11",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://d01a.github.io/syscalls/"
	],
	"report_names": [
		"syscalls"
	],
	"threat_actors": [
		{
			"id": "610a7295-3139-4f34-8cec-b3da40add480",
			"created_at": "2023-01-06T13:46:38.608142Z",
			"updated_at": "2026-04-10T02:00:03.03764Z",
			"deleted_at": null,
			"main_name": "Cobalt",
			"aliases": [
				"Cobalt Group",
				"Cobalt Gang",
				"GOLD KINGSWOOD",
				"COBALT SPIDER",
				"G0080",
				"Mule Libra"
			],
			"source_name": "MISPGALAXY:Cobalt",
			"tools": [],
			"source_id": "MISPGALAXY",
			"reports": null
		}
	],
	"ts_created_at": 1775434908,
	"ts_updated_at": 1775791464,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/6bae4009e5e7baa14f9fc1ab11b8517eb21df2ef.pdf",
		"text": "https://archive.orkl.eu/6bae4009e5e7baa14f9fc1ab11b8517eb21df2ef.txt",
		"img": "https://archive.orkl.eu/6bae4009e5e7baa14f9fc1ab11b8517eb21df2ef.jpg"
	}
}