Стартовый код
В девяностых годах двадцатого века, когда вирусы создавались преимущество на ассемблере и писались преимущественно профессионалами, а коммерческие программисты в своем подавляющем большинстве полностью отказались от ассемблера и перешли на языки высокого уровня, для разработчиков антивирусов наступили золотые дни, ибо распознать зараженный файл зачастую удавалось с одного взгляда. Действительно, любая нормально откомпилированная программа начинается с так называемого стартового кода (Start-Up code), который легко отождествить визуально (как правило, он начинается с вызова функций GetVersion, GetModuleHandleA и т. д.), а дизассемблер IDA и вовсе идентифицирует стартовый код по обширной библиотеке сигнатур, выдавая тип и версию компилятора. Ассемблерные же программы стартового кода лишены и потому, когда ассемблерный вирус внедряется в программу, написанную на языке высокого уровня, стартовый код отодвигается как бы "вглубь" файла , демаскируя тем самым факт своего заражения.
Сегодня, когда ассемблерные вирусы становятся музейной редкостью, такой способ отождествления мало-помалу перестает работать, однако до полного списывания в утиль дело еще далеко.
Строго говоря, никаких формальных признаков "нормального" start-up'a не существует, поэтому всяк разработчик волен реализовывать его по своему. Однако редкий разработчик цепляет к программе свой собственный start-up и все чаще для этих целей используется библиотечный стартовый код, поставляемый вместе с компилятором. Несмотря на то что даже в рамках одного-единственного компилятора существует множество разновидностей стартового кода, все они легко узнаваемы и факт отсутствия стартового кода надежно обнаруживается даже самыми начинающими из исследователей!
Приблизительная структура типичного стартового кода такова: сначала идет пролог, затем настройка обработчика структурных исключений (для Си++ программ), обнаруживающая себя по обращению к сегментному регистру FS. Далее следует вызов функций GetVersion (GetVersionEx), GetModuleHandleA и GetStartupInfoA.
Подробнее об идентификации стартового кода можно прочитать в книге "Фундаментальные основы хакерства" Криса Касперски или в "Hacker Disassembling Uncovered" его же.
Здесь же мы не можем позволить себе подробно останавливаться на этом обширном вопросе и просто сравним start-up код нормальной программы с кодом вируса Win2K.Inta.1676
.text:00401670 start | proc near | |
.text:00401670 | push | ebp |
.text:00401671 | mov | ebp, esp |
.text:00401673 | push | 0FFFFFFFFh |
.text:00401675 | push | offset stru_420218 |
.text:0040167A | push | offset __except_handler3 |
.text:0040167F | mov | eax, large fs:0 |
.text:00401685 | push | eax |
.text:00401686 | mov | large fs:0, esp |
.text:00401696 | call | ds:GetVersion ; Get current version number of Windows |
.text:004016EC | push | 0 |
.text:004016EE | call | __heap_init |
.text:00401704 | mov | [ebp+var_4], 0 |
.text:0040170B | call | __ioinit |
.text:00401710 | call | ds:GetCommandLineA |
.text:00401716 | mov | dword_424F44, eax |
.text:0040171B | call | ___crtGetEnvironmentStringsA |
.text:00401720 | mov | dword_4235C0, eax |
.text:00401725 | call | __setargv |
.text:0040172A | call | __setenvp |
.text:0040172F | call | __cinit |
.text:00401754 | call | _main |
.text:00401763 | call | _exit |
.text:00011000 start | proc near | |
.text:00011000 | mov | eax, [esp+arg_0] |
.text:00011004 | lea | edx, loc_11129 |
.text:0001100A | mov | [eax+34h], edx |
.text:0001100D | lea | edx, dword_117A0 |
.text:00011013 | lea | eax, aHh ; "HH" |
.text:00011019 | mov | [edx+8], eax |
.text:0001101C | mov | [eax+4], aSystemrootSyst |
.text:0001101C | ; "\\SystemRoot\\system32\\drivers\\inf.sys" | |
.text:00011023 | push | 1200h |
.text:00011028 | push | 0 |
.text:0001102D | call | ExAllocatePool |
.text:00011032 | or | eax, eax |
.text:00011034 | jnz | short loc_1103E |
.text:00011036 | mov | eax, 0C0000001h |
.text:0001103B | retn | 8 |
Листинг 3 … а так выглядят окрестности точки входа вируса Win2K.Inta.1676
Смотрите! В то время как "хорошая" программа лениво опрашивает текущую версию операционной системы и иже с ней, зловредный вирус сломя голову несется в объятья драйвера inf.sys. Правильные программы так себя не ведут, и коварность вирусных планов разоблачается с первого взгляда!
Разумеется, отсутствие стартового кода еще не есть свидетельство вируса! Быть может, исследуемый файл был упакован или разработчик применил нестандартный компилятор или набор библиотек. Ну, с упаковкой мы уже разобрались, а с идентификацией компилятора поможет справиться IDA и наш личный опыт (замечание: текущие версии IDA PRO определяют версию компилятора непосредственно по стартовому коду: если же он отсутствует или был изменен, механизм распознавания дезактивируется, и поиском подходящей библиотеки сигнатур нам приходится заниматься вручную, через меню File -> Load file -> FLIRT signature file). И если обнаружиться, что нормальный start-up у файла все-таки есть, но выполнение программы начинается не с него, – шансы на присутствие вируса значительно возрастут!
Троянские программы, в большинстве своем написанные на языках высокого уровня, имеют вполне стандартный Start-Up и потому на такую наживку не обнаруживаются. Взять например того же Kilez'a, стартовый код которого выглядит так:
.text:00408458 start | proc near | |
.text:00408458 | push | ebp ; sub_408458 |
.text:00408459 | mov | ebp, esp |
.text:0040845B | push | 0FFFFFFFFh |
.text:0040845D | push | offset stru_40D240 |
.text:00408462 | push | offset __except_handler3 |
.text:00408467 | mov | eax, large fs:0 |
.text:0040846D | push | eax |
.text:0040846E | mov | large fs:0, esp |
.text:0040847B | mov | [ebp+var_18], esp |
.text:0040847E | call | ds:GetVersion ; Get current version number of Windows |
.text:004084AF | xor | esi, esi |
.text:004084B1 | push | esi |
.text:004084B2 | call | __heap_init |
.text:004084C4 | mov | [ebp+var_4], esi |
.text:004084C7 | call | __ioinit |
.text:004084CC | call | ds:GetCommandLineA |
.text:004084D2 | mov | dword_494E68, eax |
.text:004084D7 | call | ___crtGetEnvironmentStringsA |
.text:004084DC | mov | dword_493920, eax |
.text:004084E1 | call | __setargv |
.text:004084E6 | call | __setenvp |
.text:004084EB | call | __cinit |
.text:004084F0 | mov | [ebp+StartupInfo.dwFlags], esi |
.text:004084F3 | lea | eax, [ebp+StartupInfo] |
.text:004084F6 | push | eax ; lpStartupInfo |
.text:004084F7 | call | ds:GetStartupInfoA |
.text:004084FD | call | __wincmdln |
Даже "невооруженным" глазом видно, что стартовый код червя идентичен стартовому коду Microsoft Visual C++ 6.0, что и не удивительно, так именно на нем червь и написан.