16 minute read

Key Findings

C2Looper is a Rust backdoor packaged as a Windows proxy DLL. Loading the DLL is enough to start the implant: its DllMain loads the legitimate wtsapi32.dll from System32, resolves CreateThread, and starts a long-running worker. A forwarded WTS export does not need to be called first.

The sample uses the GitHub Contents API as a mailbox. It derives a bot ID as <computer_name>_<username> and maintains three repository objects beneath it:

  • beacon.json for liveness and timestamp updates;
  • cmd.json for a Base64-wrapped command object; and
  • result.json for Base64-wrapped command output.
  1. upload is named from the operator's perspective. On the host it is an inbound download: the sample fetches <bot>/upload/<arg> and writes the bytes beneath %TEMP%.
  2. inject is local-process execution, not remote-process injection. The preferred path overloads winspool.drv; the observed fallback allocated private memory, copied a 329-byte benign payload, changed the page from RW to RX, and started a local thread.
  3. The GitHub success parser contains a reproducible bounds bug. A short, valid-looking success response causes a Rust panic because the sample blindly requests a 400-byte slice after finding "content":{.
  4. HTTP 200 only confirms result publication. A handler can publish an Error: string and still receive HTTP 200 for the result PUT. After a successful run deleted the target, a later command against the same path returned Error: ShellExecute=2.
  5. The sample does not use API hashing in the analyzed paths. It reconstructs selected names with XOR and resolves plaintext names through ordinary LoadLibraryW and GetProcAddress calls. No malware-defined direct or indirect syscall path was found.
C2Looper execution and GitHub Contents protocol flow
Figure 1. C2Looper execution and GitHub Contents protocol flowOpen full size

Analyzed Sample

Zscaler ThreatLabz published C2Looper: A New Backdoor Likely Tied To Ransomware With GitHub C2 on August 17, 2026.

Field Value
SHA-256
File type PE32+ x86-64 DLL, 240,640 bytes, Rust
Proxy role wtsapi32.dll, forwarding 41 WTS exports
C2 transport GitHub Contents API over HTTPS

Proxy DLL Execution

The DLL presents itself as wtsapi32.dll. On DLL_PROCESS_ATTACH, DllMain:

  1. constructs the path to the legitimate C:\Windows\System32\wtsapi32.dll;
  2. loads it and resolves the forwarded WTS functions; and
  3. resolves CreateThread and starts the worker thread.

The worker starts during DLL load, so C2 activity can begin before the host calls any forwarded WTS export.

Configuration and API Resolution

Selected strings use a repeating eight-byte XOR key:

DB AA 78 17 C3 17 BC F7

The corresponding little-endian constant is 0xF7BC17C31778AADB. The decoder at 0x1800269E0 selects a key byte with input_index & 7 and XORs it with each input byte.

The decoder selects a key byte with input_index & 7 and XORs it with each input byte
Figure 2. The decoder selects a key byte with input_index & 7 and XORs it with each input byteOpen full size

At runtime, the same transform reconstructed VirtualAlloc; the result was passed in RDX to GetProcAddress.

x64dbg at 0x180006689: RDX points to the decoded, null-terminated VirtualAlloc string passed to GetProcAddress
Figure 3. At 0x180006689: RDX points to the decoded, null-terminated VirtualAlloc string passed to GetProcAddressOpen full size

Encrypted configuration contains a 93-byte GitHub PAT-formatted credential. For revocation and hunting, this report retains only its SHA-256:

a17c799967ad4dab2900bcdd63b7d537f34d345b29936d88ab6556d28e8e3bbc

The owner of this embedded credential should treat it as exposed and revoke it.

No custom API hashing

mw_resolve_kernel32_api at 0x180026920 and mw_resolve_winhttp_api at 0x180027120 rebuild their DLL names, then use imported LoadLibraryW and GetProcAddress. Most callers pass plaintext API names. The injection block XOR-deobfuscates a few names before the same ordinary resolution flow.

Calls through the resulting pointers are normal indirect user-mode API calls. No export-name hash loop, syscall-number extraction, clean-ntdll stub lookup, or custom direct/indirect syscall mechanism was identified.

Back to contents

GitHub Contents as a C2 Mailbox

Field Value
Host api.github.com:443
Owner/repository adioziaete/memio
Branch master
User-Agent OneDrive/24.170.0825.0001
Contents Accept application/vnd.github.v3+json
Bot ID <computer_name>_<username>
Object prefix /repos/adioziaete/memio/contents/<bot_id>/

Beacon format

mw_github_update_beacon at 0x1800251F0 constructs compact JSON:

{"id":"<computer_name>_<username>","ts":1787161278}

The timestamp calculation converts FILETIME to Unix seconds. The JSON is Base64-encoded once and written to <bot_id>/beacon.json. A missing object produces a create PUT; later updates include the cached content SHA.

Command format and replay gate

Commands are compact JSON wrapped once in the Contents response's Base64 content field:

{"seq":4,"cmd":"recon","arg":""}

The dispatcher extracts the three fields at these sites:

Field Address
seq 0x1800032FD
cmd 0x180003842
arg 0x180003B46

The gate at 0x180003CB3–0x180003CB8 requires new_seq > last_seq; commands execute only when their sequence exceeds the last accepted value.

Result format

Every handler returns a string. The worker serializes:

{"seq":4,"output":"<handler output>"}

The compact bytes are Base64-encoded at 0x180004D91 and passed to mw_github_put_content at 0x180004DE4 with commit message r.

Two dispatcher excerpts show the ping/recon branch group and the run branch with environment-string expansion
Figure 4. Two dispatcher excerpts show the ping/recon branch group and the run branch with environment-string expansionOpen full size

Content SHA and retry behavior

Create PUTs omit sha; updates include the last returned content.sha. On HTTP 409, the helper fetches the current object SHA and returns RETRY:<sha> to the caller, which caches it for a later loop. The SHA cache accepts 64 bytes and copies at most 63 characters plus the terminator.

The protocol tracks two independent state values:

  • last_seq prevents command replay; and
  • the content SHA coordinates GitHub object updates.

A reproducible 400-byte parser bug

The success-response parser contains a fixed-window bounds error.

After finding "content":{, the code adds 0x190 (400) to the match offset and asks mw_checked_utf8_slice_ptr to validate that full window. A minimal 62-byte success body therefore reaches Rust's bounds panic:

thread '<unnamed>' panicked at src\lib.rs:341:22:
end byte index 401 is out of bounds for string of length 62
After finding "content":{, the success parser validates a fixed 400-byte window
Figure 5. After finding "content":{, the success parser validates a fixed 400-byte windowOpen full size

The parser unconditionally validates match_offset..match_offset+400 before searching that window for sha; only then does it fall back to the full response. The 62-byte body panicked, whereas the 512-byte padded response satisfied the bounds check. This requirement comes from the parser bug, not TLS.

Back to contents

Decrypted HTTP Protocol

The captured TLS 1.2 connections used TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384. Each decrypted exchange pairs a result PUT with its HTTP/1.1 200 response. The figures retain the public repository path and protocol framing while masking sensitive identifiers; normalized output strips are clearly separated editorial annotations.

The request body is not double-Base64. The layering is:

handler output
  → compact {seq,output} JSON
  → Base64 once
  → GitHub Contents PUT JSON
  → TLS

Command-by-Command Analysis

All eight command checks are inlined in the worker.

Command Entry Behavior
ping 0x180004035 Returns a bot-specific pongv2 string
ls 0x180003ECE Enumerates one directory level
drives 0x180003F9D Enumerates logical drives
recon 0x18000407E Aggregates six host and domain discovery commands
upload 0x180004235 Downloads a repository object into %TEMP%
shell 0x1800042D5 Executes a command and captures stdout/stderr
run 0x1800040AA Opens a path, waits 2.5 seconds, then deletes it
inject 0x1800045F1 Executes decoded bytes locally through module overloading or private RW→RX memory

ping — bot-specific liveness

The inline dispatch branch at 0x180004035 does not require arg. It builds the return buffer directly from the constant prefix !!! v2 !!! pongv2 from and the bot ID derived earlier from GetComputerNameW and GetUserNameW; no secondary command helper is involved.

The ping branch matches the command discriminator and passes the cached bot ID into result formatting.
Figure 6. The ping branch matches the command discriminator and passes the cached bot ID into result formatting.Open full size

It returned:

!!! v2 !!! pongv2 from <computer_name>_<username>
ping result in Wireshark Follow HTTP Stream: PUT result.json, HTTP/1.1 200, and a separately labeled normalized output
Figure 7. ping result in the decrypted HTTP stream: PUT result.json, HTTP/1.1 200, and a separately labeled normalized outputOpen full size
x64dbg: the 66-byte plaintext ping result; the bot ID is masked in hex and ASCII
Figure 8. the 66-byte plaintext ping result; the bot ID is masked in hex and ASCIIOpen full size

The pongv2 prefix is a useful plaintext pivot in process memory or decrypted traffic.

ls — directory enumeration

The handler implementation at 0x180003ECE matches the two-byte, little-endian value ls and treats arg as a directory path. It trims trailing backslashes, appends \*, converts the search path to UTF-16, and enumerates one level with FindFirstFileW, FindNextFileW, and FindClose. The loop skips . and .., tests FILE_ATTRIBUTE_DIRECTORY, and combines nFileSizeHigh and nFileSizeLow for regular-file sizes.

If FindFirstFileW fails, the handler returns Error: cannot open <original_arg> plus LF. Once enumeration begins, any false FindNextFileW ends the listing without a GetLastError check. Output is formatted as:

[FILE] <name>  (<size> bytes)
[DIR]  <name>\

The implementation returns an empty result for an empty directory.

The first ls excerpt trims trailing backslashes; the second converts the search path to UTF-16 and resolves FindFirstFileW, FindNextFileW, and FindClose
Figure 9. The first ls excerpt trims trailing backslashes; the second converts the search path to UTF-16 and resolves FindFirstFileW, FindNextFileW, and FindCloseOpen full size

The branch maps to ATT&CK T1083 (File and Directory Discovery); its endpoint signature is the FindFirstFileW/FindNextFileW/FindClose sequence over <arg>\*.

drives — logical-drive enumeration

The dispatch entry at 0x180003F9D resolves GetLogicalDriveStringsW at 0x180003FDB and calls it at 0x18000400A with a fixed 0x200-WCHAR buffer. The handler does not consume arg. It walks the returned double-NUL-terminated drive list one root at a time, converts each UTF-16 root to UTF-8, and appends LF at 0x180004538.

drives resolves GetLogicalDriveStringsW, prepares a 0x400-byte buffer, and invokes the API with a 512-WCHAR capacity.
Figure 10. drives resolves GetLogicalDriveStringsW, prepares a 0x400-byte buffer, and invokes the API with a 512-WCHAR capacity.Open full size

It returned:

C:\
D:\
drives result in Wireshark Follow HTTP Stream: PUT result.json, HTTP/1.1 200, and normalized decoded drive output
Figure 11. drives result in the decrypted HTTP stream: PUT result.json, HTTP/1.1 200, and normalized decoded drive outputOpen full size
x64dbg: the 34-byte drives result, including C:\ and D:\; bytes beyond R8 are masked
Figure 12. the 34-byte drives result, including C:\ and D:\; bytes beyond R8 are maskedOpen full size

The endpoint signature is GetLogicalDriveStringsW in the side-loaded process followed by a GitHub Contents result PUT.

recon — six discovery commands

The branch at 0x18000407E calls mw_collect_recon at 0x180027D90, which runs six fixed command lines in order and ignores arg. It adds a banner before each command and passes it to mw_execute_shell_capture at 0x180026B00.

The shell helper prepends cmd.exe /c chcp 65001 >nul && , creates an anonymous pipe, clears inheritance on its read handle, assigns the shared inheritable write handle to stdout and stderr, launches the command with CreateProcessW, drains output with ReadFile, waits for completion, and closes the process, thread, and pipe handles. Command errors remain in the aggregate output:

ipconfig /all
whoami /all
nltest /dclist:
net group /domain "domain computers"
net group /domain "domain admins"
wmic product get name, version
The recon collector decrypts six fixed discovery commands into buffers before iterating over them.
Figure 13. The recon collector decrypts six fixed discovery commands into buffers before iterating over them.Open full size

Each block receives a banner in the aggregate output. After Base64 decoding, the captured result showed host network configuration; user, group, and privilege data; failed domain lookups on the workgroup host; and an installed-software inventory.

Sanitized excerpt:

========== ipconfig /all ==========
Windows IP Configuration
   Host Name . . . . . . . . . . . : <computer_name>
   IPv4 Address. . . . . . . . . . : <private_ip>

========== whoami /all ==========
<computer_name>\<username>  <user_sid>

========== nltest /dclist: ==========
Cannot find DC to get DC list from. Status = 1355 ERROR_NO_SUCH_DOMAIN

========== wmic product get name, version ==========
<installed-software inventory omitted>

The aggregate was approximately 20 KB and was byte-identical across two executions; only seq changed in the encoded result object.

recon result in Wireshark Follow HTTP Stream; a thin divider joins two positions from the same stream, and the discovery output remains redacted
Figure 14. recon result in the decrypted HTTP stream; a thin divider joins two positions from the same stream, and the discovery output remains redactedOpen full size
x64dbg: a 160-byte slice of the 20,331-byte recon result containing the nltest /dclist: banner and ERROR_NO_SUCH_DOMAIN; host-specific data is outside the crop
Figure 15. a 160-byte slice of the 20,331-byte recon result containing the nltest /dclist: banner and ERROR_NO_SUCH_DOMAIN; host-specific data is outside the cropOpen full size

Correlating all six commands is more specific than matching any one command.

upload — actually an inbound download

The branch treats arg as a repository object beneath <bot_id>/upload/; on the host, upload is a download. mw_github_download_content first looks for inline content, JSON-unescapes it, and Base64-decodes it once. If inline content is absent, it extracts download_url and performs a raw GET. The observed 115-byte transfer used this fallback.

The handler writes the response beneath %TEMP% with CreateFileW and WriteFile, then returns either OK: saved <size> bytes to <path> or an Error: string from the failing stage.

upload fetches the repository object, resolves CreateFileW, WriteFile, and CloseHandle, and records the number of bytes written.
Figure 16. upload fetches the repository object, resolves CreateFileW, WriteFile, and CloseHandle, and records the number of bytes written.Open full size

For the 115-byte lab_probe.txt fixture, it returned:

OK: saved 115 bytes to C:\Users\<username>\AppData\Local\Temp\lab_probe.txt
upload result in Wireshark Follow HTTP Stream with HTTP/1.1 200 and a normalized 115-byte save result; the absolute path is redacted
Figure 17. upload result in the decrypted HTTP stream with HTTP/1.1 200 and a normalized 115-byte save result; the absolute path is redactedOpen full size
x64dbg: the upload result reports 115 bytes saved; the path prefix is masked while lab_probe.txt remains visible
Figure 18. the upload result reports 115 bytes saved; the path prefix is masked while lab_probe.txt remains visibleOpen full size

The inert 115-byte source and destination both had SHA-256 399ef0ea04014a39810d18a97ea806bf8f66875dd1024e06d622097a6fff513e, confirming byte-for-byte inbound delivery.

The defining sequence is a Contents metadata GET, a raw-object GET, and a write beneath %TEMP%.

shell — captured command output

The handler at 0x1800042D5 passes the complete arg value to mw_execute_shell_capture at 0x180026B00. The helper prepends cmd.exe /c chcp 65001 >nul && , creates an anonymous pipe, prevents the read handle from being inherited, and assigns the shared inheritable write handle to stdout and stderr. It launches the child with CreateProcessW, collects output through ReadFile, waits, closes the process, thread, and pipe handles, and returns either the captured bytes or an explicit error.

The shell helper routes stdout and stderr to the same inherited write handle, starts a hidden child, reads 4096-byte chunks, waits, and closes the handles
Figure 19. The shell helper routes stdout and stderr to the same inherited write handle, starts a hidden child, reads 4096-byte chunks, waits, and closes the handlesOpen full size

The executed command was:

echo C2LOOPER_SHELL_PROFILE_OK

The returned marker includes one trailing space and CRLF:

C2LOOPER_SHELL_PROFILE_OK \r\n
shell result in Wireshark Follow HTTP Stream; a thin divider joins two positions from the same stream, followed by the normalized shell marker
Figure 20. shell result in the decrypted HTTP stream; a thin divider joins two positions from the same stream, followed by the normalized shell markerOpen full size
x64dbg: the 52-byte shell result containing the fixed marker; bytes beyond R8 are masked
Figure 21. the 52-byte shell result containing the fixed marker; bytes beyond R8 are maskedOpen full size

The branch accepts an arbitrary command line; echo exercised its pipe and process-capture path.

Correlate a side-loaded process spawning a command shell with redirected output through a shared anonymous pipe and a subsequent GitHub Contents result PUT.

run — launch, delay, and delete

The branch at 0x1800040AA treats arg as a launch path and expands environment variables with ExpandEnvironmentStringsW. It calls ShellExecuteW(NULL, L"open", expanded_path, NULL, NULL, SW_SHOWNORMAL) and treats values of 33 or greater as success.

On success, the continuation at 0x180005ABB sleeps for 2.5 seconds, calls DeleteFileW on the same path without checking its return value, and returns OK: launched <path>. Lower ShellExecuteW values become Error: ShellExecute=<code>.

run calls ShellExecuteW with the open verb, treats values of 33 or greater as success, sleeps for 2.5 seconds, and resolves DeleteFileW.
Figure 22. run calls ShellExecuteW with the open verb, treats values of 33 or greater as success, sleeps for 2.5 seconds, and resolves DeleteFileW.Open full size

For the staged text target, the handler returned:

OK: launched C:\Users\<username>\AppData\Local\Temp\\c2looper_run_probe.txt
run result in Wireshark Follow HTTP Stream with HTTP/1.1 200 and a normalized launch result; the absolute path is redacted
Figure 23. run result in the decrypted HTTP stream with HTTP/1.1 200 and a normalized launch result; the absolute path is redactedOpen full size
x64dbg: the run result reports OK: launched; the path prefix is masked while c2looper_run_probe.txt remains visible
Figure 24. the run result reports OK: launched; the path prefix is masked while c2looper_run_probe.txt remains visibleOpen full size

The doubled separator comes from concatenating a %TEMP% value ending in \ with an argument beginning with \.

A later command against the deleted path returned:

Error: ShellExecute=2

GitHub accepted that error result with HTTP 200.

The endpoint sequence is ShellExecuteW, a 2.5-second delay, and DeleteFileW against the same temporary path.

inject — module overloading with a private-memory fallback

The branch at 0x1800045F1 trims and Base64-decodes arg, then resolves LoadLibraryExW, VirtualAlloc, VirtualProtect, CreateThread, CloseHandle, and Sleep. It first tries to place the decoded buffer in the executable section of a locally loaded winspool.drv. The observed fallback instead allocates private PAGE_READWRITE memory, copies the buffer, changes it to PAGE_EXECUTE_READ, and starts a thread at that address.

Both paths remain inside the backdoor process. Their status strings identify the selected path; only the fallback also reports the decoded size.

The first inject excerpt trims the argument; the next two show the fallback's 0x3000/0x04 allocation, 0x20 protection change, and local thread start
Figure 25. The first inject excerpt trims the argument; the next two show the fallback's 0x3000/0x04 allocation, 0x20 protection change, and local thread startOpen full size
The inject handler's module-overloading and private RW-to-RX execution paths; the captured run selected the private-memory path
Figure 26. The inject handler contains module-overloading and private RW→RX execution paths; the captured run selected private memory.Open full size

winspool.drv module overloading

The module-overloading path:

  1. calls LoadLibraryExW("C:\\Windows\\System32\\winspool.drv", NULL, DONT_RESOLVE_DLL_REFERENCES) at 0x18000663D;
  2. locates the PE .text section;
  3. changes it to writable at 0x180006880;
  4. copies decoded bytes at 0x1800068AB;
  5. sets the section to execute/read at 0x1800068BE; and
  6. starts a thread inside the modified section at 0x18000691C.

Private RW → RX allocation

The fallback at 0x180006743 allocates MEM_COMMIT|MEM_RESERVE read/write memory, copies the bytes, changes the page to PAGE_EXECUTE_READ at 0x1800067C8, and calls CreateThread at 0x1800069D5.

The payload properties were:

Property Value
Decoded size 329 bytes (0x149)
SHA-256 16ca5af0dcf8fccac1875c26143e11f4e4d9df9e5ca21e70596dac215e55c2ba
Effect Load user32.dll, show a fixed MessageBox, return

At runtime, the executable-memory bytes matched the same hash. The thread reached LoadLibraryA("user32.dll"), displayed the fixed MessageBox test dialog, and returned. The handler reported:

[VirtualAlloc] Injected OK (size=329)
inject result in Wireshark Follow HTTP Stream with HTTP/1.1 200 and the normalized VirtualAlloc success result
Figure 27. inject result in the decrypted HTTP stream with HTTP/1.1 200 and the normalized VirtualAlloc success resultOpen full size
x64dbg: the 59-byte plaintext result reports fallback success and a 329-byte decoded size; bytes beyond R8 are masked
Figure 28. the 59-byte plaintext result reports fallback success and a 329-byte decoded size; bytes beyond R8 are maskedOpen full size

The result buffer records the published status; debugger state separately establishes the RW→RX transition.

At the return from VirtualAlloc (0x18000674B), RAX held a new committed 0x1000-byte PAGE_READWRITE region. Figure 29 shows that return state and the zeroed page.

x64dbg at VirtualAlloc return: RAX holds the new allocation and the Dump follows the returned address
Figure 29. At VirtualAlloc return: RAX holds the new allocation and the Dump follows the returned addressOpen full size

At the VirtualProtect call, RCX still pointed to the allocation, RDX was 0x1000, and R8 was 0x20 (PAGE_EXECUTE_READ). On return at 0x1800067CF, EAX=1, the saved prior protection was 0x04, and the region reported PAGE_EXECUTE_READ.

x64dbg at VirtualProtect return: flNewProtect is 0x20, EAX is 1, and copied payload bytes are visible in the Dump view
Figure 30. At VirtualProtect return: flNewProtect is 0x20, EAX is 1, and copied payload bytes are visible in the Dump viewOpen full size

CreateThread then received the allocation in R8 as lpStartAddress and returned a nonzero handle, completing the private RW→RX→thread sequence.

Back to contents

Detection and Hunting Opportunities

Network detections

A high-confidence decrypted-traffic signature combines:

Host: api.github.com
User-Agent: OneDrive/24.170.0825.0001
URI prefix: /repos/adioziaete/memio/contents/
Object suffix: /beacon.json, /cmd.json, or /result.json

Additional pivots:

  • frequent GET/PUT traffic beneath a path shaped as <computer_name>_<username>;
  • Accept: application/vnd.github.v3+json on Contents GET requests;
  • compact Base64 content decoding to {seq,cmd,arg} or {seq,output};
  • commit message r on results; and
  • raw-object retrieval immediately after a Contents response containing download_url.

Without TLS inspection, correlate api.github.com SNI or destination telemetry with the loading context and endpoint sequences below. GitHub alone is not a useful indicator.

Endpoint detections

Behavior High-value sequence
Proxy loading Non-system wtsapi32.dll → legitimate System32 copy → worker thread from DLL load
Recon ipconfig /allwhoami /allnltest /dclist: → domain group queries → wmic product
Shell capture CreatePipe → inheritable handles → CreateProcessW → repeated ReadFile
Run-and-delete ExpandEnvironmentStringsWShellExecuteW → ~2.5 s sleep → DeleteFileW same path
Module overloading LoadLibraryExW(winspool.drv, flag 1) → writable .text → thread in module section
Fallback execution private RW allocation → copy → RX protection → thread start at private page

Static pivots

The following strings are useful in combination:

adioziaete/memio
OneDrive/24.170.0825.0001
!!! v2 !!! pongv2 from
[ModuleOverloading] Injected into winspool.drv
[VirtualAlloc] Injected OK (size=

Indicators

Indicator Type Context
f96ff2f3abbff7f382ace509b90e54853b4b61c402ecde27d82f1c17b414867b SHA-256 Analyzed sample
api.github.com Domain C2 transport; low-confidence alone
adioziaete/memio Repository C2 namespace
/repos/adioziaete/memio/contents/ URI prefix Contents API traffic
OneDrive/24.170.0825.0001 User-Agent Masquerading client identifier
<computer_name>_<username>/{beacon,cmd,result}.json Path pattern Per-host mailbox
<computer_name>_<username>/upload/<arg> Path pattern Inbound file delivery
wtsapi32.dll DLL name Proxy DLL used by the analyzed sample
winspool.drv Module Preferred module-overloading target
a17c799967ad4dab2900bcdd63b7d537f34d345b29936d88ab6556d28e8e3bbc SHA-256 Embedded PAT-formatted credential hash

The normalized host identity, private addresses, private CA and leaf certificate, lab_probe.txt, c2looper_run_probe.txt, fixed echo marker, and benign MessageBox strings are analysis artifacts, not malware IOCs.

Conclusion

C2Looper starts its worker from DllMain, uses GitHub Contents objects as beacon, command, and result mailboxes, and tracks state with both seq and the current content SHA. Its eight handlers are inlined in the worker.

In the observed execution, inject selected the private RW→RX fallback and executed its benign 329-byte test payload inside the backdoor process.

Detection should correlate proxy-DLL loading, the repository/User-Agent pair, the six-command reconnaissance burst, and the private RW→RX→CreateThread sequence.