{
	"id": "79d09c17-c907-4735-94b7-44a016f32daa",
	"created_at": "2026-04-06T00:21:37.218057Z",
	"updated_at": "2026-04-10T03:21:00.394754Z",
	"deleted_at": null,
	"sha1_hash": "f8725e5cd41491e0702eb6ca422948af7dbe2204",
	"title": "SysWhispers2 analysis ??????",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 204106,
	"plain_text": "SysWhispers2 analysis 🙊\r\nBy pbo\r\nPublished: 2024-03-09 · Archived: 2026-04-05 13:37:44 UTC\r\nThis helper comes in handy when reversing samples that use SysWhispers2 to recover ntdll call from\r\nSysWhispers2 hashes.\r\nReadme.md #\r\nSysWhispers github.com/jthuraisamy/SysWhispers2 helps with evasion by generating header/ASM files implants\r\ncan use to make direct system calls.\r\nVarious security products place hooks in user-mode API functions which allow them to redirect execution flow to\r\ntheir engines and detect for suspicious behaviour. The functions in ntdll.dll that make the syscalls consist of\r\njust a few assembly instructions, so re-implementing them in your own implant can bypass the triggering of those\r\nsecurity product hooks. This technique was popularized by @Cn33liz and his blog post has more technical details\r\nworth reading.\r\nAnalysis #\r\nVMray recently tweeted that Pikabot incorporates SysWhispers2 This note offers a step-by-step guide to identify\r\nthe syscalls made by malware that utilizes SysWhispers2, a technique that can be applied in any situation where\r\nSysWhispers2 is present. NB: Tools: IDA decompiler and xdbg The analysis began with the sample\r\nPERFERENDISF.jar shared in VMRay tweet, which is available on Malware Bazaar, with the SHA-256:\r\nd26ab01b293b2d439a20d1dffc02a5c9f2523446d811192836e26d370a34d1b4\r\nWe skipped to the stage 2 of the Pikabot loader, which employs SysWhispers2 to load the malware’s core. The\r\nmalware executes the following steps to perform a direct syscall:\r\n1. Saves the return address;\r\n2. Resolves the syscall ID from a hash (a behavior related to SysWhispers2);\r\n3. Retrieves a stub to invoke the syscall based on the host architecture;\r\n4. Executes the syscall and resumes program execution.\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 1 of 7\n\nFigure 1: Function used to made the direct syscall\r\nHere are examples of direct syscalls made by the malware.\r\nFigure 2: Example of SW2Syscall stubs\r\nTo operate SysWhispers2, it is necessary to populate the _SW2_SYSCALL_LIST structure, which is an array\r\ncontaining correspondences between hashes and ntdll.dll addresses. According to the file base.h\r\njthuraisamy/SysWhispers2/blob/main/data/base.h the two structures are:\r\nstruct _SW2_SYSCALL_ENTRY\r\n{\r\n DWORD Hash;\r\n DWORD Address;\r\n}\r\nCode Snippet 1: SysWhispers2 syscall entry\r\nThe Hash field contains a hash value corresponding to a particular syscall, and the Address field contains the\r\naddress of the corresponding function in ntdll.dll .\r\nstruct _SW2_SYSCALL_LIST\r\n{\r\n DWORD Count;\r\n SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES];\r\n}\r\nCode Snippet 2: SysWhispers2 syscall list\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 2 of 7\n\nThe malware stores a pointer to the syscall list as a global variable, which is convenient when we later retrieve the\r\npopulated data with the debugger.\r\nFigure 3: Reference of the _SW2_SYSCALL_LIST structure\r\nAccording to the source code See function SW2_GetSyscallNumber line 131. the function used to get the address\r\nin ntdll from hash ensure that _SW2_SYSCALL_LIST structure is populated.\r\nThe most “challenging” task is now to identify a call to SW2_GetSyscallNumber and set a breakpoint after the\r\nSW2_PopulateSyscallList function, at which point a dump of the list can be made.\r\nFigure 4: Hex memory view of the _SW2_SYSCALL_LIST structure populated\r\nHere is a clearest visualization of the memory using ImHex.\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 3 of 7\n\nFigure 5: Visualization of the _SW2_SYSCALL_LIST structure populated\r\nMapping Hashes to Syscalls #\r\nFirst, the hashes (SW2) must be listed, and then the hash must be resolved to obtain the syscall number.\r\nThe following IDA script lists the hashes by retrieving the first (single one) function argument:\r\ns2w_direct_call_addr = 0x04111000\r\nfor x in XrefsTo(s2w_direct_call_addr):\r\n syscall_hash = get_wide_dword(x.frm - 0x4) # First args of the function\r\n print(f\"call to SW2 at:0x{x.frm:x} hash:0x{syscall_hash:x}\")\r\nWhich gives the following hashes: 0x312294161 , 0x228075779 , 0x2553518241 , 0x3309424832 ,\r\n0x1605204094 , 0x2236128452 , 0x1881308343 , 0x3327455464 , 0x3319017158 , 0x2249560824 ,\r\n0x397169428 , 0x4066245879 , 0x2629212700 .\r\nSubsequently, the _SW2_SYSCALL_LIST structure was parsed to obtain the address corresponding to each of the\r\naforementioned hashes.\r\nimport struct\r\nwith open(\"syscall_entries.dmp\", \"rb\") as f:\r\n # offset 0x8 is used to remove the DWORD Count of the struct _SW2_SYSCALL_LIST\r\n SW2_syscallList_raw = f.read()[0x8:]\r\nNTDLL_BASE_ADDRESS = 0x77DA0000 # specifics for each sample\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 4 of 7\n\nSW2_Entrie = namedtuple(\"SW2_Entrie\", [\"hash\", \"address\"])\r\nSW2_syscallList: List = []\r\nfor hash, addr_offset in struct.iter_unpack(\"\u003cLi\", SW2_syscallList_raw):\r\n print(f\"0x{hash:x} 0x{addr_offset + NTDLL_BASE_ADDRESS:x}\")\r\n SW2_syscallList.append(SW2_Entrie(hash, addr_offset + NTDLL_BASE_ADDRESS))\r\nNext, take a snapshot of ntdll (to avoid rebasing the DLL base address) to list the export functions of ntdll.dll\r\nand their corresponding addresses.\r\nThe subsequent step involves taking a snapshot of ntdll.dll to obtain a list of its export functions along with\r\ntheir corresponding address. This approach eliminates the need to rebase the DLL base address.\r\nimport pefile\r\ndef get_section(pe: pefile.PE, section_name: str) -\u003e pefile.SectionStructure:\r\n \"\"\"return section by name, if not found raise KeyError exception.\"\"\"\r\n for section in filter(\r\nlambda x: x.Name.startswith(section_name.encode()), pe.sections\r\n ):\r\nreturn section\r\n raise KeyError(f\"{section_name} not found\")\r\nPE_FILE = \"ntdll.dll\"\r\npe = pefile.PE(PE_FILE)\r\ntext = get_section(pe, \".text\")\r\nimage_base = pe.OPTIONAL_HEADER.ImageBase\r\nsection_rva = text.VirtualAddress\r\nmapping_syscall_id_fn = []\r\n# Build a corresponding address and ntdll function name\r\nfor exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:\r\n mapping_syscall_id_fn.append((pe.OPTIONAL_HEADER.ImageBase + exp.address, exp.name))\r\nFinally, map the addresses populated in the _SW2_SYSCALL_ENTRIES structure with the corresponding addresses\r\nexported from ntdll.dll to obtain their export names.\r\n# hashes obtained in IDA\r\nhashes = [\r\n 0x129D3B11,\r\n 0xD982903,\r\n 0x983398A1,\r\n 0xC541D0C0,\r\n 0x5FAD787E,\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 5 of 7\n\n0x85489CC4,\r\n 0x70227CB7,\r\n 0xC654F0E8,\r\n 0xC5D42EC6,\r\n 0x861592F8,\r\n 0x17AC5314,\r\n 0xF25DFCF7,\r\n 0x9CB69A1C,\r\n]\r\ndef find_syscall_by_hash(hash) -\u003e Optional[SW2_Entrie]:\r\n for syscall in SW2_syscallList:\r\nif syscall.hash == hash:\r\n return syscall\r\nfor addr, name in mapping_syscall_id_fn:\r\n for syscall in map(find_syscall_by_hash, hashes):\r\nif addr == syscall.address:\r\n print(f\"0x{syscall.hash:x} \u003c-\u003e {name.decode()}\")\r\n break\r\nOutput for this sample of Pikabot is:\r\n0xc5d42ec6 \u003c-\u003e NtAllocateVirtualMemory\r\n0x129d3b11 \u003c-\u003e NtClose\r\n0x85489cc4 \u003c-\u003e NtCreateUserProcess\r\n0x70227cb7 \u003c-\u003e NtFreeVirtualMemory\r\n0x17ac5314 \u003c-\u003e NtGetContextThread\r\n0x5fad787e \u003c-\u003e NtOpenProcess\r\n0xc541d0c0 \u003c-\u003e NtQueryInformationProcess\r\n0x983398a1 \u003c-\u003e NtQuerySystemInformation\r\n0xc654f0e8 \u003c-\u003e NtReadVirtualMemory\r\n0x9cb69a1c \u003c-\u003e NtResumeThread\r\n0xf25dfcf7 \u003c-\u003e NtSetContextThread\r\n0xd982903 \u003c-\u003e NtSystemDebugControl\r\n0x861592f8 \u003c-\u003e NtWriteVirtualMemory\r\n0xc5d42ec6 \u003c-\u003e ZwAllocateVirtualMemory\r\n0x129d3b11 \u003c-\u003e ZwClose\r\n0x85489cc4 \u003c-\u003e ZwCreateUserProcess\r\n0x70227cb7 \u003c-\u003e ZwFreeVirtualMemory\r\n0x17ac5314 \u003c-\u003e ZwGetContextThread\r\n0x5fad787e \u003c-\u003e ZwOpenProcess\r\n0xc541d0c0 \u003c-\u003e ZwQueryInformationProcess\r\n0x983398a1 \u003c-\u003e ZwQuerySystemInformation\r\n0xc654f0e8 \u003c-\u003e ZwReadVirtualMemory\r\n0x9cb69a1c \u003c-\u003e ZwResumeThread\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 6 of 7\n\n0xf25dfcf7 \u003c-\u003e ZwSetContextThread\r\n0xd982903 \u003c-\u003e ZwSystemDebugControl\r\n0x861592f8 \u003c-\u003e ZwWriteVirtualMemory\r\nThe full script is available on this gist, along with the S2W_SyscallList.dmp file in hexadecimal format. To use the\r\ndump, replace lines 32 to 34 with the following:\r\nimport binascii\r\nwith open(\"SW2_SyscallList_hex.dmp\", \"r\") as f:\r\n # offset 0x8 is used to remove the DWORD Count of the struct _SW2_SYSCALL_LIST\r\n SW2_syscallList_raw = binascii.unhexlify(f.read())[0x8:]\r\nResources #\r\nhttps://github.com/jthuraisamy/SysWhispers2\r\nhttps://twitter.com/vmray/status/1760647508038947080\r\nhttps://gist.github.com/lbpierre/c9c39de0c32bb96a5e12556f75744d42\r\nhttps://joshfinley.github.io/posts/2020-04-17-sycall-dumping/\r\nSource: https://blog.krakz.fr/notes/syswhispers2/\r\nhttps://blog.krakz.fr/notes/syswhispers2/\r\nPage 7 of 7",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"references": [
		"https://blog.krakz.fr/notes/syswhispers2/"
	],
	"report_names": [
		"syswhispers2"
	],
	"threat_actors": [],
	"ts_created_at": 1775434897,
	"ts_updated_at": 1775791260,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/f8725e5cd41491e0702eb6ca422948af7dbe2204.pdf",
		"text": "https://archive.orkl.eu/f8725e5cd41491e0702eb6ca422948af7dbe2204.txt",
		"img": "https://archive.orkl.eu/f8725e5cd41491e0702eb6ca422948af7dbe2204.jpg"
	}
}