Обзор Scrambler-защиты. Написание UnScrambler.
==============================================
intro
-----
UPX. Хороший компрессор исполняемых файлов, позволяющий сократить размер файла после упаковки более чем на 50%, да
и к тому же он распространяется по типу opensource, другими словами - каждый может использовать его в своих целях
совершенно бесплатно. Вдобавок, исходные коды UPX открыты. Респект такому автору!
Отличается этот упаковщик от остальных и тем , что он перестраивает секции файла при упаковке, благодаря чему,
упаковка становится более эффективной. Но это не всё, ведь упакованный файл может быть распакован самим же UPX! Для
этого достаточно набрать в командной строке "upx.exe FileName -d". Не секрет, что именно для UPX написано много
дополнительных программ. Их можно разделить по типам:
- GUI - графические оболочки.
- Scramblers - программы, обрабатывающие сжатые UPX'ом файлы. Делается это для того, чтобы не допустить
распаковку средствами UPX.Вдруг кому-то захочется распаковать чужую программу и покопаться в её коде ;-) Часто
программы этого вида умеют шифровать код XOR'ом или другими простыми методами.
- Unpackers/UnScramblers - Программы, препятствующие Scramblers-программам. Обычно это расшифровщики.
В этой статье я познакомлю читателя с принципами работы последних двух видов программ. Т.е. Scramblers &
Unpackers/UnScramblers. Как пользоваться новым материалом - решать Вам,а моя задача - рассказать о защите такого вида.
Let's roll!
-----------
Предлагаю разобрать всё на примере. Нам потребуется программа, сжатая UPX и дополнительно обработанная
Scrambler-утилитой UPXShit 0.06. Где взять такую программу? Можно откомпилировать простой проект в Delphi (или создать
любым другим компилятором) и упаковать UPX, а затем поверх повесить UPXShit 0.06. Если этого нет, то можно взять
утилиту PEiD v0.92 и разобрать работу скрэмблера на примере этой программы, тем более, что она распространяется по
принципу FreeWare. Я не призываю лазить в чужом коде, ведь речь пойдет только об упаковщике и скрэмблере. Говоря
другими словами, нам понадобится только результат работы программы-скрэмблера.
Откроем подопытную программу в любимом отладчике. Я буду использовать OllyDbg v1.10 step2, поэтому на предложение
отладчика проанализировать файл - ответить нет. Перед глазами код вида:
004611E1 > B8 CB114600 MOV EAX,Project.004611CB
004611E6 B9 15000000 MOV ECX,15
004611EB 803408 7F XOR BYTE PTR DS:[EAX%],7F
004611EF ^E2 FA LOOPD SHORT Project.004611EB
004611F1 ^E9 D6FFFFFF JMP Project.004611CC
004611F6 0000 ADD BYTE PTR DS:[EAX],AL
004611F8 0000 ADD BYTE PTR DS:[EAX],AL
Вот что это значит: В регистр-аккумулятор (EAX) загружается число (адрес 004611CB). В регистр-счетчик (ECX)
загружается число 15. Записать по адресу EAX+ECX результат выполнения операции XOR над байтом по адресу EAX+ECX и
числом 7F. Уменьшать регистр-счетчик (ECX) на единицу, и если ECX не равен нулю, то перейти на адрес 004611EB.
Безусловный переход на адрес 004611CC.
Посмотрим как это работает, потрассировав программу до безусловного перехода (JMP). Последовав за "прыжком", откроется
часть кода:
//ЭТА ЧАСТЬ ТОЛЬКО ЧТО РАСШИФРОВАЛАСЬ
004611CC B8 B6114600 MOV EAX,Project.004611B6
004611D1 B9 15000000 MOV ECX,15
004611D6 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
004611DA ^E2 FA LOOPD SHORT Project.004611D6
004611DC ^E9 D6FFFFFF JMP Project.004611B7
//А НИЖЕ СТАРЫЙ КОД
004611E1 > B8 CB114600 MOV EAX,Project.004611CB
004611E6 B9 15000000 MOV ECX,15
004611EB 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
004611EF ^E2 FA LOOPD SHORT Project.004611EB
004611F1 ^E9 D6FFFFFF JMP Project.004611CC
004611F6 0000 ADD BYTE PTR DS:[EAX],AL
004611F8 0000 ADD BYTE PTR DS:[EAX],AL
Если и здесь трассировать код вручную, то откроется новый участок расшифрованного кода. Схема понятна: каждый раз
открывается часть кода, затем и расшифровывает другую часть. Вот и весь алгоритм защиты. А если на таком нехитром
механизме построен весь смысл скрэмблера и таких участков очень много? Что делать?.. AntiScrambler свой делать!
Coding
------
Предлагаю написать свой AntiScrambler на Delphi, используя API. Создать форму,нарисовать кнопок покрасивее, картинок
и всего-всего, что сделает интерфейс привлекательнее - дело несложное. Поэтому я приведу здесь только функцию
устранения скрэмблера:
function ScramblerEliminate(FileName: pchar): integer;
Но ещё нужна будет функция: RVAToOffset. Она необходимы для пересчета "Реального-Виртуального адреса" в файловое
смещение. Эту функцию мы тоже напишем. А в помощь нам будет библиотека от Microsoft - IMAGEHLP.DLL и юнит Delphi -
imagehlp. Допишите его в uses.
Будут встречаться, возможно, новые для читателя структуры и функции. Их содержание можно посмотреть в imagehlp. А
сейчас, код:
;---------------------------------------------------------------------------------------------------------------------
function ScramblerEliminate(FileName: pchar):integer;
var
hFile: dword; //Handle файла
hFileMap: dword; //Handle отображаемого файла
FileMap: pointer; //Указатель на спроецированный файл
//В этой структуре хранится PE заголовок файла.
//Ее поля можно посмотреть в юните imagehlp.
Header: PIMAGENTHEADERS;
//Попробуем эмулировать действия оригинальной программы
EIP: DWORD;
EAX: DWORD;
ECX: DWORD;
//Промежуточные переменные
p: dword;
RVA: dword;
begin
//Нужно вызвать функцию с такими атрибутами
hFile:=CreateFile(
FileName,GENERIC_WRITE or GENERIC_READ,
FILE_SHARE_WRITE,nil,OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,0
);
if (hFile=INVALID_HANDLE_VALUE) then begin
//Не получается открыть файл - до свидания.
MessageBox(0,'Cannot open file', 'UnScrambler',0);
Result:=0;
end
else begin
//Создаем файл-объект, и получаем к нему доступ.
hFileMap:=CreateFileMapping(hFile,nil,PAGE_READWRITE,0,0,nil);
FileMap:=MapViewOfFile(hFileMap,FILE_MAP_WRITE,0,0,0);
//С помощью ImageNtHeader получим доступ к любому полю заголовка
//без лишних усилий.
Header:=ImageNtHeader(FileMap);
//Программа начинается со своей первой инструкции. Адрес
//первой инструкции записан в заголовке файла в соответствующем
//поле - AddressOfEntryPoint. Пересчитаем RVA EntryPoint в простое
//внутрифайловое смещение.
EIP:=RVAToOffset(Header, FileMap,
Header.OptionalHeader.AddressOfEntryPoint
);
//Прибавив к смещению адрес начала проекции файла,
//и EIP будет указывать на первую инструкцию. Аналогично
//тому, как если бы программа была запущена.
EIP:=EIP+dword(FileMap);
//Повторять цикл, пока байт по адресу EIP не будет равен $60.
//$60 - это опкод PUSHAD. Так можно узнать, не начался ли код UPX.
while ( byte(ptr(EIP)^) <> $60 ) do begin
p:=EIP;
//Первый байт - B8, где старшие 4 бита (B) - команда MOV.
//Сразу за этим байтом записан виртуальный адрес, поэтому
//увеличив EIP на единицу - получим по адресу EIP - DWORD с
//виртуальным адресом, который надо превратить в реальный.
//Просто вычитаем из него ImageBase.
inc(EIP);
RVA:=dword(ptr(EIP)^) - Header.OptionalHeader.ImageBase;
//Загрузим в EAX файловое смещение от реального виртуального адреса
//и прибавим адрес начала спроецированного файла ->
//-> получим в EAX указатель на ту часть файла, которая подлежит
//расшифровке.
EAX:=RVAToOffset(Header, FileMap, RVA);
EAX:кX+dword(FileMap);
//Мы выполнили строку (см. код выше):
//004611E1 > B8 CB114600 MOV EAX,Project.004611CB
//поэтому прибавив к EIP четыре, остановимся на следующей
//команде(приступим к ее выполнению):
//004611E6 B9 15000000 MOV ECX,15
EIP:=EIP+4;
inc(EIP);
ECX:=dword(ptr(EIP)^);
//Ок! Пропустив ещё байт (B9), загрузили ECX DWORD'ом!
//А теперь внимательно. Если в ECX число, отличное от 15,
//то, пропустив четыре байта (того самого числа), пропускаем
//ещё три байта и EIP указывает на число для по'XOR'ивания (7F):
//004611EB 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
//Вообще, советую сейчас прочитать код, который выполнился,
//если бы в ECX было 15 ;) Дело в том, что после расшифровки
//маленьких кусочков кода, идет расшифровка кода побольше
//и разности адресов там немного другие. Короче, посмотрим, что
//написано после слова "else" =)
if (ECX<>$15) then begin
EIP:=EIP+4;
EIP:=EIP+3;
//Цикл - имитация строк:
//004611EB 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
//004611EF ^E2 FA LOOPD SHORT Project.004611EB
while (ECX <> 0) do begin
byte(ptr(EAX+ECX)^):=
( (byte(ptr(EAX+ECX)^)) xor (byte(ptr(EIP)^)) );
dec(ECX);
end;
//В EAX находится адрес начала по'XOR'ивания, а EAX+1 -
//это начало UPX. Итак, всё закончилось...
EIP:кX + 1;
end
else begin
//Пропустив 7 байт, EIP укажет на число для XOR'а:
EIP:=EIP+4;
EIP:=EIP+3;
//Это знакомо. Цикл:
//004611EB 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
//004611EF ^E2 FA LOOPD SHORT Project.004611EB
while (ECX <> 0) do begin
byte(ptr(EAX+ECX)^):=
( (byte(ptr(EAX+ECX)^)) xor (byte(ptr(EIP)^)) );
dec(ECX);
end;
//В переменной "p" записан псевдо-EIP - тот самый EIP,
//с которого начинает выполняться блок:
//004611E1 > B8 CB114600 MOV EAX,Project.004611CB
//004611E6 B9 15000000 MOV ECX,15
//004611EB 803408 7F XOR BYTE PTR DS:[EAX+ECX],7F
//004611EF ^E2 FA LOOPD SHORT Project.004611EB
//004611F1 ^E9 D6FFFFFF JMP Project.004611CC
//Т.е. EIP указывает на опкоды по адресу 004611E1.
//Безусловный переход ведёт на 004611CC, что на 15 меньше,
//чем текущий псевдо-EIP. Отняв от начала блока (004611E1)
//пятнадцатьh, EIP будет указывать на начало нового участка:
//004611CC B8 B6114600 MOV EAX,Project.004611B6
EIP:=p-$15;
end;
//А теперь предлагаю вернуться назад и посмотреть
//что произошло бы, если ECX <> 15.
end;
//Итак, всё закончилось. В заголовок файла пропишем настоящее
//EntryPoint, равное RVA участка, с которого начинается расшифровка
//последнего участка кода (того что побольше :) плюс один.
Header.OptionalHeader.AddressOfEntryPoint:=RVA+1;
//Освободим память и уходим. Здесь можно показать мессаЖБоХ ;)
MessageBox(0,'All done!', 'UnScrambler',0);
UnMapViewOfFile(FileMap);
CloseHandle(hFileMap);
Closehandle(hFile);
end;
Result:=1;
end;
;---------------------------------------------------------------------------------------------------------------------
Вот и вся функция. Вызывать её можно так:
if ScramblerEliminate(pChar(Edit1.Text))<>0 then
Form1.Caption='Ok!';
А лучше так
try
ScramblerEliminate(ИмяФайла);
except
MessageBox(0,'Some problems...','UnScrambler',0);
end;
Теперь допишем RVAToOffset как я и обещал.
Итак, RVAToOffset(FileMap: pointer; RVA: dword;):dword;
Offset от RVA рассчитывается по формуле:
offset = RVA - IMAGE_SECTION_HEADER.VirtualAddress + IMAGE_SECTION_HEADER.PointerToRawData
Здесь нет ничего страшного, IMAGE_SECTION_HEADER - это структура. Исследовать подобные структуры можно по юниту
imagehlp.
;---------------------------------------------------------------------------------------------------------------------
function RVAToOffset(FileMap: pointer; RVA: dword):dword;
var
Sec: PIMAGESECTIONHEADER;
begin
//ImageRvaToSection укажет на секцию, которой принадлежит RVA.
Sec:=ImageRvaToSection(Header, FileMap, RVA);
Result:=RVA - Sec.VirtualAddress + Sec.PointerToRawData;
end;
;---------------------------------------------------------------------------------------------------------------------
PS
--
Как Вы уже наверное догадались, после работы программы слой скрэмблера устранен. В заключение скажу, что
Scramblser-программы не только модифицируют исполняемый код (вдруг там внутри троян), но и портят сигнатуры. Например,
в UPX-пакованой программе хранится информация для распаковки (с ключом "-d"), которую скрэмблер не сохранил. На
работоспособности программы это никак не сказывается, но сам UPX теперь откажется распаковать ее автоматически.
Теперь, разобрав все на примере, возможно кто-то напишет аналогичную или лучшую защиту, обратив внимание на слабые
места подобного вида защит. Как пользоваться знаниями - решать Вам. А сейчас, читатель, можно гордиться первым
прообразом распаковщика, ведь по сути, похож он на VirtualMachine...
AlexZ