АНАТОМИЯ DOC-ФАЙЛОВ by DrMAD ВВЕДЕНИЕ 1. О КАКИХ МАКРОВИРУСАХ ПОЙДЕТ РЕЧЬ 2. КАК УСТРОЕН DОС-ФАЙЛ 3. КАК УСТРОЕН WORD-ДОКУМЕНТ 4. ПРО АНТИВИРУС 5. ОПИСАНИЕ ПРИМЕРА ЛИТЕРАТУРА ПРИЛОЖЕНИЕ. ИСХОДНИК ДЕМОНСТРАЦИОННОЙ УТИЛИТЫ. ВВЕДЕНИЕ Эта небольшая статья адресована тем, кто хочет узнать, где конкретно живут макровирусы, как на них посмотреть глазками, и как их пощупать ручками. 1. О КАКИХ МАКРОВИРУСАХ ПОЙДЕТ РЕЧЬ Только о макровирусах для MS Word. Макровирусы для MS Access, MS Excel и т.п. здесь не рассматриваются. Кроме того, будем иметь в виду, что рассматриваются в-основном макровирусы, написанные на языке Visual Basic for Application (VBA) для MS Word версий 97, 2000 и XP. Макровирусы для Word 6.0/7.0 уже не актуальны, а как дело обстоит в Word 2003 - пока не слишком понятно. 2. КАК УСТРОЕН DОС-ФАЙЛ Подробности см. в /1,2/, а здесь вкратце. Это файл с расширением .DOC (или .DOT) со сложной внутренней структурой под названием "структурированное хранилище" (structured storage). MS Word также может создавать документы в "неродных" форматах, например, в .RTF, но мы эту ситуцию рассматривать не будем. Правила structured storage предусматривают, что: - файл разбит на множество секторов (размер сектора по умолчанию - 512 байтов); - сектора пронумерованы и связаны в цепочки; +-----------------------+ | V +---------+ +----+----+ +---------+ +---------+ +---------+ +---------+ |Заголовок| | 0 | | 1 | | 2 | | 3 |->| 4 | +---------+ +---------+ +----+----+ +---------+ +---------+ +---------+ | ^ +-----------------------+ - внутри файла есть заголовок, описывающий параметры хранилища; - внутри файла есть FAT-таблица, описывающая, какие сектора каким цепочкам принадлежат; - внутри файла есть каталоги, описывающие имя цепочки и адрес ее первого сектора. Цепочки образуют потоки (streams), в которых и содержится вся полезная информация. (На самом деле, есть еще lock bytes, в которых может храниться "безымянная" информация, о которой MS Word ничего не знает). 3. КАК УСТРОЕН WORD-ДОКУМЕНТ Подробности см. в /3,4/, а здесь вкратце. В общем случае документ занимает несколько потоков внутри файла (но не обязательно все!). Вот достаточно типичная конфигурация потоков для здорового документа: 1Table <1>CompObj ObjectPool WordDocument <- Здесь текст документа <5>SummaryInformation <5>DocumentSummaryInformation А вот для документа, содержащего макросы (вирус): 1Table Macros VBA dir __SRP_0 <-+ __SRP_1 | Здесь exe-code макросов __SRP_2 | __SRP_3 <-+ ThisDocument <- Здесь s-code и p-code макроса Tralivali <- И здесь Newmacros <- И здесь тоже _VBA_PROJECT <- Здесь описание различных частей проекта PROJECT PROJECTwm [1]CompObj ObjectPool WordDocument <- Здесь текст документа [5]SummaryInformation [5]DocumentSummaryInformation Один и тот же макрос распределен по разным потокам: 1) s-code - это его сжатый методом LZSS текстовый исходник; 2) p-code - это перекодировка исходника в промежуточный постфиксный язык, например ; a = 1234 00A4 Let 1234 1234 0027 -> 0220 номер в списке имен переменных 3) exe-code - это его код, скомпонованный для исполнения виртуальной VBA-машиной, с командами типа: Push Аргумент1 Push Аргумент2 [Стек] := [Стек] + [Стек+2] S-code и соответствующий ему p-code лежат вместе в одном потоке. Все такие потоки начинаются с сигнтуры 01 16 01, по этому признаку их легко отличить. 4. ПРО АНТИВИРУС Вообще-то, в 95% случаев исходного текста макроса вполне достаточно, чтобы распознать конкретный вирус или заподозрить в нем заразу. О наличии заразы, как правило, говорит присутствие методов: 1) OrganizerCopy; 2) .Import/.Export; 3) .InsertLine, .AddFile, и т.п. Остальные 5% приходятся на случаи: 1) 1% (на самом деле, еще реже) - когда запакованный исходник из файла удален (например, каким-нибудь кривым антивирусом); 2) 4% - когда вирус полиморфный. Можно попробовать так: не декомпилировать p-code и тем более ex-code из недокументированных Microsoft-овских кодов (хотя так поступают в "настоящих" антивирусах), а наоборот, загнать хорошо известную VBA-грамматику в какой-нибудь Yacc и получить транслятор s-code в свой собственный внутренний код. А уж по этому коду (очищенному от пробелов, комментариев, "скленных" строк и т. п.) распознавать вирей и даже попытаться проэмулировать некоторые их куски. 5. ОПИСАНИЕ ПРИМЕРА Устройство DOC-файла можно изучить при помощи плагина DocFile Browser к FAR Manager-у: увидеть дерево объектов, посмотреть внутренности потока в виде 16-ричного дампа... Но FAR не умеет распаковывать и показывать исходный текст скрывающихся внутри макросов, а мы умеем /3/. В качестве примера - консольная утилитка, которая обходит в doc-файле потоки, находит в них сигнатуру 01 16 01, находит в этих потоках запакованный исходник, распаковывает его, ищет в нем подстроки '.Import' и '.Export' и ругается, если вдруг нашла. Утилита использует библиотеку OLE2.DLL и ее методы для работы со structured storage /5/. Кроме того, под Windows 95/ 98 утилитка работать не будет и тихо завершится, потому что в этих операционках присутствует только огрызок библиотеки NTDLL.DLL. Решения проблемы: 1) выдрать из "хорошей" библиотеки код процедуры RtlDecompressBuffer; 2) самостоятельно распаковывать исходники макросов, это достаточно просто. ЛИТЕРАТУРА 1. Martin Schwartz. Laola file system. 2. DrMAD. Внутрений формат документов MS Word. 3. DrMAD. Прямой доступ к макросам в документах MS Word. 4. DrMAD. Как Пымать Выря За Хвост. Глава 6. Борьба с макровирусами. 5. Артем Каев. ActiveX по шагам. ПРИЛОЖЕНИЕ. ИСХОДНИК ДЕМОНСТРАЦИОННОЙ УТИЛИТЫ. //********************************************************************* // Демонстрация поиска неполиморфных вирей в документах // Компилятор Borland C/C++ v5.02 // (c) Климентьев К. aka DrMAD, Самара 2003 //********************************************************************* #include "windows.h" #include "ole2.h" #include "iostream.h" #define MAXBUF 0xFFFF #define NAMELEN 256 char Buf[MAXBUF]; // Буфер под поток char SBuf[MAXBUF]; // Буфер под распакованный текст /* Поиск чунка внутри потока */ ULONG search_chunk( char *Buf, ULONG streamlen) { ULONG ch_pos; // Позиция чунка внутри потока if ((Buf[0]==1)&&(Buf[1]==0x16)&&(Buf[2]==1)) { ch_pos = streamlen/2; while ((ch_pos<(streamlen-3)) && ((Buf[ch_pos]!=1)|| ((Buf[ch_pos+2]&0xF0)!=0xB0))) ch_pos++; if (ch_pos < (streamlen-3)) return ch_pos; } return 0; } /* Поиск в исходнике .Import/.Export */ LookAtVirus() { int i=0; while (SBuf[i]) { if ((SBuf[i]=='.') && (SBuf[i+3]=='p') && (SBuf[i+4]=='o') && (SBuf[i+5]=='r') && (SBuf[i+6]=='t')) cout << endl << "*** Maybe virus! ***" << endl; i++; } } typedef UINT (WINAPI* RTLD)(ULONG, PVOID, ULONG, PVOID, ULONG, PULONG); /* Распаковка исходника */ view_src( char *StreamName, char *Buf, ULONG ch_pos) { HMODULE h; // Хэндл библиотеки NTDLL.DLL RTLD RtlDecompressBuffer; // Указатель на функцию ULONG ulen; // Длина распакованного фрагмента int i=0; LoadLibrary("ntdll.dll"); h = GetModuleHandle("ntdll.dll"); RtlDecompressBuffer = (RTLD) GetProcAddress(h, "RtlDecompressBuffer"); if (RtlDecompressBuffer) { RtlDecompressBuffer(0x2, SBuf, MAXBUF, &(Buf[ch_pos+1]), MAXBUF-ch_pos, &ulen); cout << " *** " << StreamName << " *** " << endl; while (SBuf[i]) cout << SBuf[i++]; LookAtVirus(); } } /* Рекурсивный обход потоков */ walk(char *s, LPSTORAGE ls) { OLECHAR FileName[NAMELEN]; // Unicode-имя для structured storage char StreamName[NAMELEN]; // ASCII-имя потока LPENUMSTATSTG lpEnum=NULL; // Интерфейс перечислителя LPSTORAGE pIStorage=NULL; // Интерфейс структурированного хранилища LPSTORAGE pIStorage2=NULL; // Интерфейс хранилища нижнего уровня LPSTREAM pIStream=NULL; // Интерфейс потока STATSTG stat; // Очередная запись в каталоге ULONG uCount; // Счетчик перечисления ULONG streamlen; // Реальная длина потока ULONG ch_pos; // Позиция чунка внутри потока if (!ls) // Первый вызов { mbstowcs(FileName, s, 256); StgOpenStorage(FileName,NULL,STGM_READ|STGM_SHARE_EXCLUSIVE, NULL,0,&pIStorage); walk("", pIStorage); } else // Повторный рекурсивный вызов { ls->EnumElements(0,NULL,0,&lpEnum); if (lpEnum) while (lpEnum->Next(1,&stat,&uCount)==S_OK) { if (stat.type==STGTY_STORAGE) // Это хранилище { ls->OpenStorage(stat.pwcsName,NULL,STGM_READ|STGM_SHARE_EXCLUSIVE, NULL,0,&pIStorage2); walk("", pIStorage2); } else // Это поток { ls->OpenStream(stat.pwcsName,NULL,STGM_READ|STGM_SHARE_EXCLUSIVE, 0,&pIStream); pIStream->Read(Buf, MAXBUF, &streamlen); wcstombs(StreamName, stat.pwcsName, 256); ch_pos = search_chunk(Buf, streamlen); if (ch_pos) view_src(StreamName, Buf, ch_pos); } }; ls->Release(); } } int main(int argc, char* argv[]) { if (argc>1) walk(argv[1],NULL); else cout << "Usage: SVIR docfile" << endl; } |