Эх,Петя, Петя, Петя…И как же раньше до этого никто не додумался! Часть I
Всем доброго времени суток! Недавно интернет облетела новость о том, что появился новый вид шифровальщика-вымогателя, который шифрует жёсткий диск целиком. Цель вредоносика ясна как божий день — вынудить пользователя заплатить деньги, чтобы восстановить свои файлы. Криптолоккер получил название — «Петя» (англ.»Petya»). И чем же отличается данный зловред от ему подобных? А отличается он принципиально новым подходом — шифровать не каждый файл по отдельности, а MFT (master file table) жёсткого диска. Кто не знает, что такое MFT, пусть не печалится, скоро это дело мы исправим, так как в дальнейшем цикле статей про Петю, мы подробно изучим устройство файловой системы NTFS. Пока скажу лишь, что в MFT хранятся все сведения о физическом расположении файлов на жёстком диске, имеющем файловую систему NTFS. Мне интересен следующий момент: будет ли работать «Петя» на компе, у которого рабочая ось установлена на диск, с файловой системой отличной от NTFS, например, с FAT32, скажем? Ответить на этот вопрос я не могу, так как, к сожалению, самого бинарника у меня нет. А теперь представьте следующую картину: у вас есть книга, содержащая миллиарды глав. Зачем шифровать сами главы, когда можно зашифровать только содержание?! Вот этим-то и знаменит наш герой дня. Подробнее про «Петю». Я против того, чтобы кого-то огорчать и, тем более, шантажировать, поэтому хоть и смекалка автора(ов) зловредика меня восхищает, я его(их) ни в коем случае не поддерживаю. Но давайте абстрагируемся на задаче и взглянем на проблему с точки зрения системного программирования и попробуем реализовать для личного использования очень полезную утилиту, которая будет шифровать нашу MFT, а перед загрузкой — расшифровывать. Для шифрования будем использовать какой-нибудь ассиметричный криптоалгоритм. Представьте, как это будет круто! Даже если кто сопрёт наш винт, он не сможет восстановить с него данные. В конце данного цикла статей мы накодим утилиту, реализующую нашу затею. Сразу хочу сказать, что полностью рабочий код выложен не будет по понятным причинам.
Первое, что меня удивило, так это то, что «Петя» получает доступ к MFT, а это, как-никак, — прямое обращение к жёсткому диску, которое давно запрещено мелкомягкими, после того, как одна чувиха продемонстрировала атаку, данным методом записав данные в файл подкачки, после чего они благополучно подгрузились в память. В 7-ке уж точно запрещён такой доступ к диску. Давайте подумаем, как всё же можно добиться результата. Первое, что приходит в голову — накодить драйвер, а из драйвера уже работать с MFT. Но, если почитать описание работы «Пети», то можно наткнуться на инфу, что загружается он, как утилита chkdsk. Тут сразу напрашивается идея рассмотреть второй способ — реализовать часть функционала в native режиме. Если кто не в курсах, то стоит прочесть замечательную статью, и сразу станет всё в порядке. Отмечу тот факт, что программирование в данном режиме очень похоже на программирование драйверов. Не буду вас томить, а сразу скажу, что прямой доступ к диску в данном режиме разрешён! Так давайте скорее кодить! Для начала попробуем написать прогу, которая сдампит нам наш MBR — первые 512 байт диска.
Прежде, чем писать код, реализуем некоторые процедурки, которые нам пригодятся. Первая — это вывод строчки на экран . Нам обязательно это будет нужно, чтобы можно было вывести код ошибки, если что-то не будет получаться.
void WriteLn(LPWSTR Message) { UNICODE_STRING string; RtlInitUnicodeString(&string, Message); NtDisplayString(&string); }
Далее, давайте напишем процедуру задержки — аналог Sleep();
void Sleep(DWORD mSec) { LARGE_INTEGER interval; interval.QuadPart = -1 * (int)(mSec * 10000.0f); NtDelayExecution(FALSE, &interval); }
Ну вот, теперь можно и закодить саму процедуру дампа MBR.
BOOL DumpMbr() { NTSTATUS Status; HANDLE fMbr, fileHandle; char mbr[MBR_SIZE]; OBJECT_ATTRIBUTES ObjectAttributes; IO_STATUS_BLOCK ioStatusBlock; UNICODE_STRING string; LARGE_INTEGER fileSize; fileSize.QuadPart = 0; memset(&mbr, '\0',MBR_SIZE); RtlInitUnicodeString(&string, L"\\??\\PhysicalDrive0"); InitializeObjectAttributes(&ObjectAttributes, &string, OBJ_CASE_INSENSITIVE, NULL, NULL); Status = NtCreateFile(&fMbr, GENERIC_READ | SYNCHRONIZE, &ObjectAttributes, &ioStatusBlock, &fileSize, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_SEQUENTIAL_ONLY | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); if (Status == STATUS_SUCCESS) { #ifdef DEBUG WriteLn(L"+ Open PhysicalDrive0 STATUS_SUCCESS\n"); #endif Status = NtReadFile(fMbr, NULL, NULL, NULL, &ioStatusBlock, mbr, MBR_SIZE, NULL, NULL); if (Status == STATUS_SUCCESS) { #ifdef DEBUG WriteLn(L"+ Read MBR OK!\n"); #endif RtlInitUnicodeString(&string, L"\\??\\C:\\mbr.bin"); InitializeObjectAttributes(&ObjectAttributes, &string, OBJ_CASE_INSENSITIVE, NULL, NULL); Status = NtCreateFile(&fileHandle, GENERIC_WRITE | SYNCHRONIZE, &ObjectAttributes, &ioStatusBlock, &fileSize, FILE_ATTRIBUTE_NORMAL, 0, FILE_SUPERSEDE, FILE_SEQUENTIAL_ONLY | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); if (Status == STATUS_SUCCESS) { #ifdef DEBUG WriteLn(L"+ Open C:\\mbr.bin STATUS_SUCCESS!\n"); #endif Status = NtWriteFile(fileHandle, NULL, NULL, NULL, &ioStatusBlock, mbr, MBR_SIZE, NULL, NULL); if (Status == STATUS_SUCCESS) { #ifdef DEBUG WriteLn(L"+ Dump MBR OK!\n"); #endif } else { #ifdef DEBUG WriteLn(L"- Dump MBR ERROR!\n"); #endif } } else { #ifdef DEBUG WriteLn(L"- Open C:\\mbr.bin Faled!\n"); #endif } NtClose(fileHandle); } else { #ifdef DEBUG WriteLn(L"- Read MBR ERROR!\n"); #endif } } else { #ifdef DEBUG WriteLn(L"- Open PhysicalDrive0 Faled\n"); #endif } NtClose(fMbr); return (Status == STATUS_SUCCESS); }
Точка входа в программу будет следующей:
void NtProcessStartup(void* StartupArgument) { HANDLE hKeyBoard, hEvent; UNICODE_STRING skull, keyboard; OBJECT_ATTRIBUTES ObjectAttributes; IO_STATUS_BLOCK Iosb; NTSTATUS Status; LARGE_INTEGER ByteOffset; KEYBOARD_INPUT_DATA kbData; RtlInitUnicodeString(&keyboard, L"\\Device\\KeyboardClass0"); InitializeObjectAttributes(&ObjectAttributes, &keyboard, OBJ_CASE_INSENSITIVE, NULL, NULL); NtCreateFile(&hKeyBoard, SYNCHRONIZE | GENERIC_READ | FILE_READ_ATTRIBUTES, &ObjectAttributes, &Iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, FILE_OPEN,FILE_DIRECTORY_FILE, NULL, 0); InitializeObjectAttributes(&ObjectAttributes, NULL, 0, NULL, NULL); NtCreateEvent(&hEvent, EVENT_ALL_ACCESS, &ObjectAttributes, 1, 0); if (WriteIntoMBR()) WriteLn(L"Write Into MBR Ok!\n"); else WriteLn(L"Write Into MBR Error!\n"); WriteLn(L"\n\n\n"); if (DumpMbr()) WriteLn(L"Dump Created!\n"); else WriteLn(L"Dump Error!\n"); while (TRUE) { Status = NtReadFile(hKeyBoard, hEvent, NULL, NULL, &Iosb, &kbData, sizeof(KEYBOARD_INPUT_DATA), &ByteOffset, NULL); if (Status == STATUS_PENDING) Status = NtWaitForSingleObject(hEvent, TRUE, NULL); if (kbData.MakeCode == 0x01) //ESC { //Sleep(5000); break; } } NtTerminateProcess(NtCurrentProcess(), 0); }
Пояснять код я не буду, тут всё предельно понятно, при загрузке программы, у нас считывается MBR и записывается в файл c:\mbr.bin. Функция WriteIntoMBR() записывает на место видоизменённый MBR. Единственное, что в нём изменено — так это слово Failed на 012alid. Далее DumpMbr, которая читает MBR (который уже видоизменённый) и пишет его в файл. Далее ожидается нажатие клавиши ESC, после чего загружается то, что не дозагрузилось). Результат работы программы следующий:
If you found an error, highlight it and press Shift + Enter or click here to inform us.