ТРАССИРОВКА ПРОЦЕССОВ ПОД WIN32 (x) 2000 Z0MBiE http://z0mbie.host.sk Трассировка здесь рассматривается как частный случай отладочных фич предоставляемых win32 api. Описанными здесь хернями, по идее, должен пользоваться труподебагер. Примечание. Без WIN32.HLP не обойтись. Теперь, для чего мы это делаем. Вот есть у нас заражение PE файлов. 1. Можно директом изменять адрес точки входа (EntryPointRVA). Это отлавливается эвристикой даже без эмуляции. (типа, точка входа показывает в конец файла? ну и пиздец ему) 2. Можно в точку входа внутри файла впатчивать JMP на себя. Эмуляция проебет такие JMPы как нехуй ссать. Если же вместо JMPа будет нечто сложное, аверы напишут подпрограмму. 3. Можно впатчивать JMP на себя в случайное место программы. Это кардинально лучше первых двух способов. Но тут нет гарантий, что вы попадете именно в код, а не в данные, да еще и непосредственно на начало инструкции. А если используете в качестве начала PUSH EBP/MOV EBP,ESP и им подобную хрень, то такую точку входа будет относительно просто найти. Да и вообще, сканирование файла и проверка всего, куда показывают JMPы/CALLы -- штука несложная. Но самый главный минус в том, что неизвестно, когда выполнятся ваши впатченные инструкции, и выполнятся ли вообще. 4. Анализ кода без его выполнения. Лучше всего такую фишку проводит IDA в комбинации с мозгами. Похожие, но сильно упрощенные алгоритмы я использовал в: ZCME/AZCME(dos) и RPME/CODEPERVERTOR(win32). Эти вещи работают относительно быстро, и неплохо выделяют код среди данных, хоть и не весь. Но и здесь есть качественный недостаток. В случае, если, например, какой-нибудь участок кода не выполняется никогда, а например условный переход (Jxx) на него существует, этот код будет проанализирован вместе с "нормальным". 5. Трассировка. А вот эта вещь позволяет не только пометить выполняемый (а не весь) код, но и узнать ПОРЯДОК выполнения инструкций и подпрограмм. А для чего нахрен нужен порядок выполнения инструкций? Вот здесь-то и кроется самая охуительная фишка, которая призвана вконец разорвать ламерско-антивирусное очко. Дошло до меня, что нечто подобное пытались и даже делали под dos-ом, но поскольку результата не видно, маза видно заглохла. Итак, сам вирус, как пиздец зашифрованный, может лежать где угодно в файле. А передавать ему управление по-идее мог бы JMP, впатченый в некоторое, заведомо исполняемое место файла. Но JMP-это говно. Поэтому мы развратим его в несколько разных полиморфных инструкций, и при этом, впатчим их не одну за другой, а в РАЗНЫЕ места файла, но так, что выполнятся они в нужной нам последовательности. Но по плану в этом тексте будет рассказываться только о трассировке и ни о чем больше. Итак, что у нас есть. Для еще не запущенных процессов отладка начинается с CreateProcessA, с флажками DEBUG_PROCESS+DEBUG_ONLY_THIS_PROCESS. Если же требуемый процесс открыт, то можно юзать DebugActiveProcess. После того, как вызванная функция вернула success, можно крутить цикл. А в цикле происходит такое действо: вызывается WaitForDebugEvent и мы получаем так называемый debug event, то бишь структурку заполненную всяким отстоем. ; DEBUG_EVENT de label byte de_code dd ? de_pid dd ? de_tid dd ? de_data db 1024 dup (?) ; кил - от фени После того, как структурка проанализирована, вызываем ContinueDebugEvent. После чего можно снова надеяться на получение от WaitForDebugEvent какой-нибудь херни. Теперь о структурке. Идентификаторы каждого евента лежат в de_code. de_pid и de_tid - это id текущих отлаживаемых процесса и нити, для которых и сгенерился евент. Ибо мы можем отлаживать несколько процессов, и у каждого процесса могут быть свои нити. Теперь о том, какие debug event'ы приходят в наш цикл. CREATE_PROCESS_DEBUG_EVENT -- самый первый event который мы получаем. В нем лежит такая хрень как хендлы файла, процесса и нити. Файл открыт на только-чтение или на чтение-запись. (наябывать тут нечего) Еще там начальный адрес, имя файла и прочий отстой. LOAD_DLL_DEBUG_EVENT -- эти события происходят когда загружается новая DLL-ка в контекст отлаживаемого процесса. Загрузчиком, либо LoadLibrary. EXIT_PROCESS_DEBUG_EVENT -- по этим событиям надо из цикла сваливать, RIP_EVENT -- ибо это конец отладки CREATE_THREAD_DEBUG_EVENT -- здесь надо выставлять TF EXIT_THREAD_DEBUG_EVENT -- прочие отстойные евенты, совершенно UNLOAD_DLL_DEBUG_EVENT нам не интересны OUTPUT_DEBUG_STRING_EVENT EXCEPTION_DEBUG_EVENT -- а вот это самый интересный event. Он говорит что случился exception, в частности INT1 или INT3. Фичи тут такие. 1. Перед тем как исполнять отлаживаемую программу, система вызывает кернеловскую функцию DebugBreak, в которой происходит INT3 (0xCC). 2. По умолчанию флаг TF (трассировка) не установлен, и мы (вроде бы) должны его устанавливать сами после КАЖДОЙ обработки событий типа INT1/INT3, ибо система так и норовит этот флажок сбросить. Работа с памятью отлаживаемого процесса (ибо память эта находится в другом контексте и по-другому не доступна) производится посредством функций ReadProcessMemory и WriteProcessMemory. Работа с регистрами отлаживаемой нити производится функциями GetThreadContext и SetThreadContext. Итак, структура трассировщика примерно такая: CreateProcessA() while(1) { WaitForDebugEvent() if (EXIT_PROCESS_DEBUG_EVENT or RIP_EVENT) break if (EXCEPTION_DEBUG_EVENT) { if (int1 or int3) set_trace_flag() } ContinueDebugEvent() } Подобный трассировщик будет обладать одним недостатком. Дело в том, что трассировать он будет не только текущий процесс, но и все загруженные в него DLL-ки. А у DLL-ек этих тоже есть свои точки входа, и вызываются они ДО точки входа основной программы. Кроме того, когда процесс сделает CALL в DLL-ку, трассировщик будет там долго и упорно охуевать, и вернется нескоро. И потом, здесь не поддерживаются нити. Обходится это дело таким образом. Чтобы поскипать DLLкин код в начале, надо 1. проебать кернеловский INT3 (который в DebugBreak), то есть попросту забить на него, и TF не устанавливать 2. написать простенький механизм впатчивания в прогу INT3 (0xCC) и восстановления оных (хранить оригинальные байты), аргумент -- адрес. 3. по получении евента CREATE_PROCESS_DEBUG_EVENT впатчить в начало программы (lpStartAddress) INT3. Чтобы не выполнять трассировку в DLL-ках, проверяете текущий адрес, и если он вне программы, то 1. берете дворд со стэка (это будет адрес возврата) и впатчиваете по нему INT3 2. убираете TF к херам Для поддержки разных нитей придется поебаться. 1. Надо будет впатчивать INT 3 в lpStartAddress при создании новой нити. 2. Хранить в себе список всех нитей, т.е. соответствие их ThreadId <--> ThreadHandle 3. При вызове екскепшенов int1/int3 конвертить TheadId (данный для текущего евента) в соответствующий ему ThreadHandle 4. Апдейтить эту инфу о нитях при создании процесса, создании нити и закрытии нити. В дополнение. Флажок TF по-видимому, охуячивается при глюках, то есть сгенерив глюк и отловив его SEHом можно наебать отладчик. Как разобраться с такой хуйней? Это дело непростое. 1. При получении екскепшена взять FS от отлаживаемой нити 2. Зная FS, и юзая GetThreadSelectorEntry получить VA от FS:[0] 3. Разобрать SEH'овую структуру и выявить адрес SEH'ового хандлера 4. Вхуячить в хандлер INT3 Вот собственно и все. Вышеописанное дело производит программа tracer32. Теперь, как это реально использовать. 1. выбрать программу 2. протрассировать 2-3 секунды 3. полученный адрес использовать для впатчивания JMPа на вирус