PDA

Просмотр полной версии : Анализаторы кода в антивирусах



Andrey
05.01.2005, 09:13
Автор: beauty on the fire: beautyonthefire (at) mail.ru

Статья написана специально для UInC (http://www.uinc.ru).

Источник http://www.uinc.ru/articles/45/


Анализаторы кода в антивирусах
Содержание

1. Вступление
2. Технология анализа кода
2.1. Инициализация анализатора кода
2.2. Эвристический анализ
2.3. Поиск вирусных сигнатур
2.4. "Поиск" процедуры подсчета "delta value"
2.5. Полная имитация выполнения инструкций
2.6. Изменение параметров работы эмулятора
3. Заключение

1. Вступление

Данная статья посвящена технологии анализатора кода, которая используется в антивирусных программах. Можно сказать, что это продолжение моей предыдущей статьи, которая была посвящена основам разработки эмуляторов программного кода.

Я надеюсь, что Вы имеете понятие о принципе разработки антивирусных программ и о технологиях, которые используются в этом типе программного обеспечения. Потому, что без начальных (хотя бы) знаний понять что-то в этой статье будет очень и очень проблематично.

Все рассуждения и примеры будут приводиться на ассемблере, для исполняемых файлов формата Portable Executables.

2. Технология анализа кода

Анализатор кода - предназначен для учета особенностей исследуемого (эмулируемого) кода и передачи выявленных особенностей в эмулятор. Обычно кодо-анализатор представляет собой несколько процедур, которые используются:

перед вызовом эмулятора (сбор первоначальных сведений о участке кода, который будет "запущен на исполнение")
в процессе эмуляции участка кода (выявление особенностей разбираемого кода)
Начнем разбор технологии, с процедур, которые используются в эмуляторе. В связи с этим, рассмотрим порядок его работы:

Инициализация анализатора кода
Инициализация переменных, содержащих информацию об исследуемом коде (обнуление значений виртуальных регистров, настройка стека).
Установка параметров переданных антивирусом в эмулятор (размер исследуемого кода, его местоположение в памяти)
Начало цикла эмуляции участка кода, по одной инструкции:
Запуск эвристического анализатора кода. В том случае если в текущей позиции не было обнаружено эвристической маски, перейдем к пункту 3 (второй пропускаем).
Поиск вирусных сигнатур с текущего участка кода, в том случае если процесс прошел успешно, работа эмулятора завершается. Данные об обнаруженной вирусной записи передаются в антивирус.
"Поиск" процедуры подсчета "delta value".
Разбор инструкции дизассемблером. В том случае, если во время работы была допущена ошибка (например, встретилась неизвестная команда), эмулятор завершает работу.
Разбор эмулятором, параметров инструкции (размер инструкции, используемые регистры, тип имитации исполнения) полученной от дизассемблера. В зависимости от действий необходимых для имитации исполнения инструкции (параметр, который определяет дизассемблер) переходим к пункту 6 или 7.
Запуск инструкции в специальных условиях («карантине») и сохранение результатов работы (состояния регистров, EFLAGS). Переходим к пункту 8.
Полная имитация выполнения инструкции.
Перевод указателя на следующую инструкцию.
Если в ходе эмуляции инструкции не произошло ошибок, и требуемый участок кода не был исследован до конца, перейдем к началу цикла.
В данной статье мы рассматриваем антивирус, эмулятор которого предназначен для имитации выполнения небольших участков кода. Если тело вируса зашифровано, эмулятор расшифровывает его и анализатор кода производит поиск в расшифрованном коде всех известных вирусных сигнатур. Это является основной и единственной задачей эмулятора. В эмуляторе отсутствует вызов эвристического анализатора кода, по этому анализ кода и выявление в нем свойственных вирусам участков (поиск новых вирусов) не производится.

2.1. Инициализация анализатора кода

Инициализация, в данном случае подразумевает собой установку начальных значений всем переменным, которые используются анализатором для хранения в них информации различного рода и последующей их передачи в эмулятор.

.data
;
; тип значений хранящихся в регистрах, в соотвествии с их номерами, например:
; _regs+000 - eax
; _regs+001 - ecx
; ...
; _regs+007 - edi
;
; типы: 2 - смещение расположенное в виртуальном стеке
; 1 - delta value или заготовка его последующего расчета
; 0 - все остальные значения не подходящие типам 1 и 2
;
; подробнее о типах
;
_regs db 8 dup (?)
;
; переменные, необходимые анализатору для поиска процедур по расчету delta value
; (см. ниже)
;
; ca_ip_stage - номер этапа, на котором находится эта функция кодо-анализатора
; ca_ip_instr - количество инструкций проверенных после вызова "CALL"
; ca_ip_call_off - смещение полученной после вызова "CALL"
;
ca_ip_stage db ?
ca_ip_instr db ?
ca_ip_call_off dd ?
.code
;
ca_init proc
;
push eax ecx edi
mov word ptr [ca_ip_stage],0 ; присвоение нулевого значения
; переменным "ca_ip_stage" и "ca_ip_instr"
reg_analyze: sub eax,eax
sub ecx,ecx
mov cl,8
mov edi,offset _regs
ra_init_loop: stosb
loop ra_init_loop
mov byte ptr [_regs+4],2; регистр в любом случае esp будет
; смещение на данные внутри виртуального стека
pop edi ecx eax
ret
endp


2.2. Эвристический анализ

Эвристические анализаторы кода, предназначены для определения новых типов вирусов, т.е. тех, информации о которых нет в базе антивируса. Это необходимо для того, что бы пользователь мог обнаружить у себя на компьютере файлы, зараженные новыми типами вирусов и выслать их разработчикам программы (для включения их в последующие дополнения).

Участки кода, которые часто используются при создании различных типов вирусов, являются сигнатурами (масками), для эвристического анализатора, иногда их так же называют вирусными break point'ами.

Использование эвристического анализатора является не обязательным, но очень удобным, потому что позволяет не только обнаруживать новые вирусы, но и значительно ускорить процесс проверки файлов.
При наличии функций эвристического анализа, можно производить поиск известных вирусных сигнатур только в тех позициях, в которых "эвристик" встретит одну из масок. Потому что, проверка совпадения всех вирусных сигнатур, после каждой пройденной эмулятором инструкции займет достаточно много времени, особенно если:

база содержит большое количество записей
база небольшого размера, но сигнатуры проверяются полностью - для поиска модификаций известных вирусов
В этом случае сигнатура сравнивается полностью, и если совпало хотя бы 80% - возможно обнаружена модификация.
Более подробно описывать технологию эвристического анализа я не буду, так как она заслуживает отдельной статьи.

2.3. Поиск вирусных сигнатур

Анализ участков кода и их сравнение с вирусными сигнатурами, записанными в базе. Для детектирования известного программе вируса достаточно именно обычного сравнения участка кода с конкретной (вирусной) сигнатурой.

Приведем пример части анализатора, которая производит проверку всех вирусных сигнатур из базы. А так же пример формата вирусной базы.

;
entry_size equ e_type+e_name+e_slen+e_dlen+e_sdata+e_curem
; длина одной записи
;
e_type equ 1 ; тип записи для файлов:
etype_dos_com equ 0 ; - com (dos)
etype_dos_exe equ 1 ; - exe (dos)
etype_pe equ 2 ; - PE (win32)
;
e_name equ 25 ; длина названия записи (в ASCII символах)
e_slen equ 1 ; байт содержит "реальную" длину сигнатуры
e_dlen equ 2 ; обязательная длина исполняемого кода (размер
; декриптора)
e_sdata equ 30 ; полная длина сигнатуры
e_curem equ 4 ; метод лечения или смещение процедуры для лечения
cmethod_unk equ 0 ; - вылечить немогу
cmethod_del equ 1 ; - необходимо удалить
;
.data
; вирусная база
vir_no dd 2 ; количество записей в базе
@001_type db etype_pe
@001_name db 'Win32/HLLM Mydoom' ; штамм B (32.768 байт)
db 008 dup (0)
@001_sig_len db 020
@001_dec_len dw 000 ; декриптор отсуствует
@001_sig_data db 055h,08Bh,0ECh,081h, '?',005d,056h,057h,0E8h, '?'
db 004d,08Dh,085h, '?',004d,050h,06Ah,002h,0FFh,015h
db 010 dup (?)
@001_cure_loc dd cmethod_del
;-----------------------
@002_type db etype_pe
@002_name db 'Win32/Parite' ; штамм B
db 013 dup (0)
@002_sig_len db 020
@002_dec_len dw 049+1 ; размер декриптора постоянен - 49 байт
@002_sig_data db 055h,08Bh,0ECh,081h,0C4h,0C0h,0FEh,0FFh,0FFh,08Bh
db 0C5h,083h,0C0h,004h, '?',003h,056h,057h,033h,0DBh
db 010 dup (?)
@002_cure_loc dd offset parite_b_pe
;
.code
;
; detect - сравнение сигнатур из базы "bloc" с участком кода из "cloc"
;
; on start : bloc - расположение базы сигнатур
; cloc - расположение кода для сравнения
;
; on exit : eax == 0 - ни одна из сигнатур не совпала
; eax != 0 - содержит номер совпавшей сигнатуры в базе
;
detect proc cloc:dword, bloc:dword
;
push ecx edx esi
sub edx,edx ; edx - регистр под номер записи
inc edx ; edx=1 - рассмотрим 1 запись
mov esi,bloc
mov ecx,dword ptr [esi] ; ecx - кол-во записей в базе
add esi,4 ; esi - данные первой записи
det_main_loop: sub eax,eax
mov al,byte ptr [esi+e_type+e_name]
push eax
add esi,e_type+e_name+e_slen+e_dlen
push esi ; расположение сигнатуры в записи
push cloc ; расположение кода для сравнения
call detect_sig ; совпадает сигнатура ?
or eax,eax
jnz det_sig_found ; если сигнатура совпала
;
det_main_next: inc edx ; если сигнатура не совпала,
add esi,e_sdata+e_curem ; проверим следующую запись
loop det_main_loop
sub eax,eax ; если ни одна из сигнатур не
; совпала - eax=0
det_main_ret: pop esi edx ecx
ret
;
det_sig_found: xchg eax,edx ; eax=edx = номер совпавшей сигнатуры
jmp det_main_ret
endp
;
; проверка участка на совпадение с сигнатурой
;
; on start : slen - длина сигнатуры
; sloc - расположение сигнатуры
; cloc - расположение кода для сверки
; on exit : eax = 0 - не совпадают, иначе сигнатура совпала
;
detect_sig proc cloc:dword, sloc: dword, slen: dword
;
push ecx edx esi edi
; установим параметры сигнатуры:
mov ecx,slen ; ecx - размер
mov esi,sloc ; esi - расположение
mov edi,cloc ; edi - код для сравнения с сигнатурой
detect_loop: lodsb
cmp al,'?'
jnz det_compare
;
; если встретился символ "?", значит необходимо пропустить количество
; (указанное в следующем байте сигнатуры) байт в исследуемом коде
;
sub eax,eax
lodsb
dec ecx
add edi,eax
jmp detect_cont+1
det_compare: cmp byte ptr [edi],al ; сравним байты сигнатуры и кода
jz detect_cont
sub eax,eax
jmp detect_ret
detect_cont: inc edi
loop detect_loop ; если цикл окончен и сигнатура
mov al,1 ; совпала полностью EAX = 1
detect_ret: pop edi esi edx ecx
ret
endp


2.4. «Поиск» процедуры подсчета "delta value"

Delta value - это число, которое является разницей между начальным положением кода (тем, которое он имел после компиляции) в памяти и текущим положением. Прибавляя это значение к смещениям различных "ресурсов", мы всегда получим их текущее местоположение в памяти.

Различные варианты (внешне видоизмененные), таких процедур используются в вирусах, так как местоположение их кода изменяется в зависимости от файлов, которые они заражают. Так же, эти процедуры используются в декрипторах большинства само шифрующихся вирусов и достаточно часто в вирусах использующих простые полиморфные алгоритмы. Это, несмотря на то, что наличие таких процедур, плюс несколько обнаруженных в декрипторе постоянных участков, позволяют составлять достаточно устойчивую сигнатуру расшифровщика, для детектирования с помощью "плавающей сигнатуры".

Процедура подсчета delta value, может выглядеть примерно так:

delta: call get_ip или delta: call get_ip
get_ip: pop ebp get_ip: mov ebp,dword ptr [esp]
sub ebp,offset get_ip add esp,4
sub ebp,offset get_ip


Первый вариант является самым простым и распространенным, именно по этому он и несколько других, являются дополнительной эвристической сигнатурой у многих антивирусов. Естественно при написании вирусов, стараются использовать видоизмененные варианты этих процедур.

Как работает эмулятор? Кусок кода читается в буфер антивируса, разбирается на инструкции и имитируется их исполнение с помощью различных трюков. Многие инструкции, после разбора (дизассемблером) можно «запускать» в специальной среде, а после этого учитывать произошедшие изменения (новое состояние регистров, EFLAGS). Но есть и такие, работу которых нужно полностью имитировать.

Полностью имитируется работа инструкций, которые в своей работе обращаются к данным по смещениям. В связи с изменением местоположения исследуемого кода в памяти (который мы перенесли в антивирусный буфер), необходимо рассчитать разницу между его оригинальным положением и положением в антивирусном буфере. Эта разница добавляется ко всем смещениям, использующимся в инструкциях, и является эквивалентом значения, которое вычисляется с помощью процедур "delta". Следовательно, если подобные процедуры присутствуют в расшифровщике, то один из регистров (в примерах рассмотренных выше это ebp) будет содержать delta value, если он будет использоваться в указании смещений, то эмулятору просто нельзя никаким образом изменять смещение, потому что без вмешательства эмулятора и так получится "правильное" смещение. Значит анализатору кода необходимо выявлять наличие процедур подсчета delta value и определять номер регистра, в котором будет находиться это число.

Самый удобный метод определения процедур, это поиск по сигнатурам. Но здесь тот случай, когда явную сигнатуру выделить никак нельзя. В обеих процедурах постоянной остается одна инструкция – "CALL", точнее сказать только один ее байт (первый) - 0E8h. В этом случае попытаемся воспользоваться поиском сигнатур другого типа:

В процедурах подсчета delta value, первой всегда выполняется инструкция "CALL", в результате стек будет содержать смещение инструкции следующей за ней.
После исполнения следующей инструкции (или нескольких) это значение (заготовкa для подсчета "разницы смещений") будет содержать один из регистров, который в последующем содержать подсчитанный результат.
В большинстве случаев регистр, содержащий delta value, не изменяется в ходе выполнения расшифровки или работы вирусного кода.
Приведем пример простейшей реализации части анализатора кода, которая предназначена для выявления процедур подсчета delta value и регистра, содержащего это значение:

;
; ca_ip_stage - номер этапа, на котором находится эта функция кодо-анализатора
; ca_ip_instr - количество инструкций проверенных после вызова "CALL"
; ca_ip_call_off - смещение полученной после вызова "CALL"
;
.data
ca_ip_stage db ?
ca_ip_instr db ?
ca_ip_call_off dd ?
.code
;
; начальные параметры : esi - смещение разбираемой инструкции
;
ca_ip_srch proc
;
push eax ecx edx ebp edi esi
;
; Работа процедуры делится на два этапа.
; После успешного завершения второго этапа, функция прекращает свою работу для
; текущего файла.
;
cmp byte ptr [ca_ip_stage],2
jz ca_ip_srch_ret
cmp byte ptr [ca_ip_stage],1
jz ca_ip_stage_2
;
; Первый этап
;
; - Поиск инструкции CALL (0E8h)
; - Сохранение в "ca_ip_call_off" смещения, полученного в результате работы
; найденной инструкции "CALL".
; - ca_ip_stage = 1
;
ca_ip_stage_1: lodsb
cmp al,0e8h
jnz ca_ip_srch_ret
lodsd
inc byte ptr [ca_ip_stage]
mov dword ptr [ca_ip_call_off],esi
jmp ca_ip_srch_ret
;
; Второй этап: первая часть
;
; Проверка значений всех регистров (которые храняться в буфере "regs"),
; на наличие в них значения совпадающего с "ca_ip_call_off".
; Ведется счетчик инструкций. Если, в течении разбора эмулятором четырех
; инструкций, ни один из регистров не содержит значения - заготовки для подсчета
; delta value. То начинаем с первого этапа - обнуляем "ca_ip_percent".
;
ca_ip_stage_2: mov esi,offset [regs]
sub ecx,ecx
mov cl,7+1
ca_ip_srch_lp: lodsd
cmp dword ptr [ca_ip_call_off],eax
jz ca_ip_stage_3 ; перейдем ко второй части этапа
loop ca_ip_srch_lp
inc byte ptr [ca_ip_instr]
cmp byte ptr [ca_ip_instr],4
jnz ca_ip_srch_ret
mov word ptr [ca_ip_stage],0
mov dword ptr [ca_ip_call_off],0
jmp ca_ip_srch_ret
;
; Второй этап: вторая часть
;
; - Рассчитаем номер регистра, который содержит delta_value и укажем это в
; таблице информации о регистрах ("_regs").
; - Присвоим значение 2 переменной "ca_ip_percent", это означает, что работа этой
; части кодо-анализатора прекращена.
;
ca_ip_stage_3: inc byte ptr [ca_ip_percent]
sub eax,eax
mov al,7+1
sub al,cl
mov byte ptr [eax+_regs],1 ; в eax номер регистра
; содержащего "заготовку" для delta value
;
ca_ip_srch_ret:pop esi edi ebp edx ecx eax
ret
endp


Получается, что анализатор – "заведует" информацией о типе значений, которые хранятся в каждом из регистров. В примере, для этого использовался буфер "_regs". Нормальная работа эмулятора без информации такого рода просто невозможна. Ниже я расскажу о том, как эмулятор использует эту информацию.

В процессе работы, эмулятор «предоставляет» разбираемой программе "виртуальный стек". То есть программа будет использовать в своей работе стек расположенный в специальном буфере. Следовательно, если в ходе эмуляции инструкции будут обращаться по смещениям находящимся в стеке, каких либо изменений со стороны эмулятора не требуется. В данном случае при инициализации кодо-анализатора в буфере "_regs", для регистра esp ("regs+4") должна делаться специальная пометка типа регистра (см. выше).

2.5. Полная имитация выполнения инструкций

Допустим в цикле работы эмулятора, дизассемблер встретил инструкцию типа:

xor dword ptr [erega+eregb],eregc или xor dword ptr [erega+eregb],_dword_

Рассмотрим процесс разбора параметров инструкции:

Определяется размер инструкции.
Действия необходимые для имитации выполнения этой инструкции
Инструкция использует смещение, которое получается в результате сложения значений двух регистров – erega и eregb, следовательно необходима полная имитация выполнения инструкции.
Для полной имитации необходим разбор инструкции на дополнительные параметры: количество регистров, использующихся в указании смещения (в данном случае 2) и номера этих регистров.
Номера регистров:


AX или AL - 000 = 0
CX или CL - 001 = 1
DX или DL - 010 = 2
BX или BL - 011 = 3
SP или AH - 100 = 4
BP или CH - 101 = 5
SI или DH - 110 = 6
DI или BH - 111 = 7

Для хранения количества регистров использующихся в указании смещения и номеров этих регистров отведем специальный буфер "dregs". Процесс сохранения параметров необходимых для полной имитации выполнения будет выглядеть примерно так:

mov dword ptr [dregs+000],2 - количество регистров
mov dword ptr [dregs+004],erega - номер регистра erega
mov dword ptr [dregs+008],eregb - номер регистра eregb
Рассмотрим участок кода эмулятора, который производит вычисление смещения использующегося в инструкции и последующую иммитацию выполнения:

.data
;
; dregs+000 - кол-во регистров использующихся в инструкции для указания смещения
; dregs+1*4 - номер первого регистра использующегося для ...
; dregs+2*4 - номер второго регистра ...
; dregs+n*4 - немер n-ого регистра ...
;
dregs dd 010 dup (?)
regs dd 008 dup (?)
.code
@found_spec: push ebp
;
; настроим регистры:
;
; ecx = dregs+000 - количество регистров использующихся в указании смещения
; ebx = по окончанию цикла, будет содержать смещение, которое содержалось в
; регистрах
; ebp = 0 - если в инструкции не использовались регистры содержащие
; delta value или смещение в стеке
; esi = смещение в буфере dregs
;
sub ebp,ebp
sub ebx,ebx
mov esi,offset dregs
lodsd
xchg eax,ecx
count_sum_lp: lodsd
cmp byte ptr [eax+_regs],0 ; тип регистра не = 0, значит
; содержит вычисленное delta value
jz count_sum ; или смещение в области стека, ...
inc ebp ; ... ebp = ebp + 1
count_sum: push ecx edx
sub edx,edx
mov dl,4
mul edx
pop edx ecx
;
; буфер regs содержит значения регистров эмулируемой программы, добавим значение
; использующегося регистра в смещение.
;
add ebx,dword ptr [regs+eax]
loop count_sum_lp
;
; Далее, необходимо рассчитать разницу между старым местоположением в файле и
; тем по которому инструкция расположена в антивирусном буфере. Но если в
; указании смещения участвовал регистр содержащий delta value или смещение в
; области "виртуального стека", подобные изменения будут лишними (смотри пункт
; 2.4).
; Если значение регистра EBP > 0, то изменений не требуется.
;
or ebp,ebp
pop ebp
jnz @spec_emul_01
sub ebx,старое местоположение кода в файле
add ebx,местоположение кода в антивирусном буфере
@spec_emul_01: ...


Далее следует:

Проверка полученного смещения: смещение должно быть расположенно исключительно внутри антивирусного буфера или "виртуального стека", ко всем остальным "данным" исследуемая программа обращаться не может.
Разбор третьего параметра инструкции mov eax,номер регистра eregc или mov eax,_dword_
Имитация исполнения инструкции: xor dword ptr [ebx],eax
2.6. Изменение параметров работы эмулятора

При запуске эмулятора, антивирус передает ему некоторые параметры, например:

Расположение кода для эмуляции в антивирусном буфере.
Оригинальное расположение кода в файле.
Время на исполнение этого кода или размер участка, который необходимо выполнить.
Рассмотрим часть анализатора кода, которая выполняется перед запуском эмулятора и изменяет размер участка данных, который необходимо выполнить (параметр номер 3). Зачем это нужно? Как я уже говорил, эмулятор антивируса может предназначаться только для имитации работы расшифровщиков (декрипторов). В этом случае основная и единственная его задача - расшифровать вирусное тело и сравнить с сигнатурами известных вирусов (информация о которых присутствует в базе). Как правило длина таких расшифровщиков несколько сотен байт, однако существуют различные утилиты, которые позволяют упаковать или зашифровать код вирусных дропперов или программ зараженных вирусами. Такими утилитами, часто пользуются, когда распространяют вирусы (особенно те, в которых отсутствуют технологии полиморфизма или шифровки - черви или троянские кони), в надежде на то, что антивирусы не смогут обнаружить вирусы в таких файлах. Подобные утилиты прикрепляют к "обработанным" файлам достаточно большие и комплексные декрипторы. Если эмулятор антивируса, способен имитировать работу декрипторов создаваемых, той или иной утилитой, то при их «встрече», размер эмулируемого кода должен равняться максимальному размеру декрипторов создаваемых этой утилитой. Очень часто, декрипторы содержат множество сигнатур, которые и могут являться критерием встречи с "продуктом" какой либо утилиты.

Пример процедуры входящей в состав кодо-анализатора, назначением которой является определение в коде наличие декриптора, создаваемого утилитой UPX:

;
; структура записей полностью аналогична структуре вирусной базы, для поиска
; сигнатуры используется функция описанная (выше)
;
.data
pck_no dd 1 ; количество записей в базе
_001_type db etype_pe ; запись для PE-файлов
_001_name db 'UPX' ; название утилиты - UPX
db 022 dup (0)
_001_sig_len db 006 ; длина сигнатуры 6 байт
_001_dec_len dw 1000 ; максимальная длина декриптора ;)
_001_sig_data db 060h,0BEh, '?',004d,08Dh,0BEh ; сигнатура
db 024 dup (0)
_001_cure_loc dd 0 ; это поле не используется, возможно
; любое значение
.code
;
; det_pack - проверка на упаковщики и шифровщики
;
; on exit : ebx - длина кода, которую необходимо разобрать для "распаковки" файла
;
det_pack proc
;
push eax ecx edx ebp edi esi
;
push offset pck_no
push смещение точки входа в код
call detect
or eax,eax
jz det_pack_ret
;
dec eax
mov edx,( (_001_cure_loc+1) - _001_type )
mul edx
add eax,offset _001_name ; eax - смещение на имя

; выведем информацию о том, что файл упакован (зашифрован) утилитой ...

sub ebx,ebx
mov bx,word ptr [_001_type+e_type+e_name+e_slen]
det_pack_ret: pop esi edi ebp edx ecx eax
ret
endp


3. Заключение

В статье были использованы знания, полученные по время разработки Explosion Antivirus, а так же части его исходных текстов. Исходные тексты полностью доступны на странице Most Needful Things.

Автор: beauty on the fire: beautyonthefire (at) mail.ru

Статья написана специально для UInC (http://www.uinc.ru).

Источник http://www.uinc.ru/articles/45/