Однажды я писал какой-то модуль на ассемблере из нескольких процедур. Все было хорошо до тех пор,
пока я не решил перенести данные в секцию кода. Вот тут-то и начались проблемы, так как я писал под
win32. Основная особенность win32 программирования состоит в модели памяти. Как известно,
эта модель называется FLAT. Название говорит само за себя. Плоская модель памяти подразумевает то, что вы можете
обратиться в любое место отведенного вам пространства в 4 гигабайта. Теперь нет больше 64K сегментов. Вы можете использовать любой сегментный регистр для адресации к любой точке памяти. Это вовсе не означает,
что каждая программа имеет 4 гигабайта физической памяти, а только то, что программа может обращаться по любому адресу в этих пределах. Windows сделает все необходимое, чтобы сделать память, к которой
программа обращается, "существующей". Конечно, программа должна придерживаться правил, установленных
Windows, или это вызовет General рrotection Fault. Каждая программа одна в своем адресном пространстве,
в то время как в Win16 дело обстоит не так. Все Win16 программы могут "видеть" друг друга, что
невозможно в Win32. Этот особенность помогает снизить шанс того, что одна программа запишет что-нибудь
поверх данных или кода другой программы. Одно из правил, которым должна следовать программа - это не писать в секцию кода. В Win16 мы
написали бы так:
jmp next
x db ?
y db ?
next:
Это, в принципе, правильно и в Win32, но если мы только читаем из этих переменных, если попробовать
что-нибудь записать туда - мы получим большой облом. Как же это обойти??? Ведь плоская модель очень проста
и должна позволять как чтение, так и запись в любое место своего виртуального пространства. Один из самых
простых способов заключается в пропатченьи exe-шника. Так как это Win32, то и
формат исполняемых файлов у нас
PE. Не буду описывать все поля PE заголовка, и даже не буду говорить, что в нем такого особенного. Я лишь расскажу то, что нужно для того,
чтобы писаться в код. Полное описание PE формата можно найти на
wasm.ru.
Итак, как известно, по смещению 3Ch от начала файла находиться смещение PE заголовка. У него очень много всяких разных полей и общий его размер составляет F8h байт. Сразу за ним следуют таблицы секций файла (Object Table). Да да, именно следуЮт, так как в обычных exe-шниках секций не одна, а несколько (код, данные, оверлей и т.д.). Сколько таблиц секций можно узнать по смещению 06h от начала PE заголовка. Это поле имеет размер word. Узнав количество обжект тэйблов можно запросто обратиться к
любому из них. И первым полем там будет имя секции. Оно занимает целых 8 байт. Если имя меньше 8
символов, то остаток заполняется нулями, а если равен, то завершающего нуля вообще нет. Имя
- штука отфонарная и никого ни к чему не обязывает. Вот несколько примеров имен из жизни:
.text - сюда Микрософт бросает выполнимый код
CODE - а Борланд любит это делать здесь
.icode - переходники импорта старых версий TLINK32
.data - Микрософт швыряет данные сюда
DATA - а Борланд сюда
.bss - неинициализированные данные (равна 0 в файле)
.CRT - инициализированные данные C/C++ от Борланда
.rsrc - ресурсы
.idata - секция импорта
.edata - секция экспорта
.reloc - таблица настроек
.tls - данные на базе которых Windows запускает цепочки
.rdata - отладочная информация
_FREQASM - посмотрите в KERNEL32, я думаю и так понятно
PROTECTED - это взято из Хасповского сервера, вот так
Последним полем в таблице секции является флаг. Он может иметь следующие значения и находится по
смещению 24h от начала Object Table, длина его dword:
00000004h - используется для кода с 16 битными смещениями
00000020h - секция кода
00000040h - секция инициализированных данных
00000080h - секция неинициализированных данных
00000200h - комментарии или любой другой тип информации
00000400h - оверлейная секция
00000800h - не будет являться частью образа программы
00001000h - общие данные
00500000h - выравнивание по умолчанию, если не указано иное
02000000h - может быть выгружен из памяти
04000000h - не кэшируется
08000000h - не подвергается страничному преобразованию
10000000h - разделяемый
20000000h - выполнимый
40000000h - можно читать
80000000h - можно писать
Могут быть и комбинации значений не противоречащие друг другу. Итак, теперь я думаю ты уже догадался, что надо сделать для того, чтобы писаться в код. Правильно, надо поменять флаг соответствующей таблицы.
Как ее найти? Ну уж точно не по имени, а по флагу 00000020h. Но не обязательно там будет чистое значение,
tlink32, например, ставит его в 60000020h. Но в любом случае 20h в конце должно быть. Меняем этот флаг на 80000040h (хотя 40h можно
и не ставить, это я так, для большей уверенности). Вот и все. А теперь
реализуем это на ассемблере (MASM).
Сначала откроем файл:
invoke CreateFile,ADDR buf,\
GENERIC_READ or GENERIC_WRITE,\
FILE_SHARE_READ,NULL,OPEN_EXISTING,\
FILE_ATTRIBUTE_ARCHIVE,NULL
mov hFile,eax
Переменная buf содержит имя файла в формате ASCIIz, а в hFile мы сохраняем хендл на файл, который
вернула нам CreatrFile. Теперь считаем адрес PE заголовка:
invoke SetFilePointer,hFile,3Ch,NULL,FILE_BEGIN
invoke ReadFile,hFile,ADDR PE,4h,\
ADDR SizeReadWrite,NULL
SetFilePointer устанавливают указатель в открытом файле на смещение 3Ch от начала, а следующая API
функция читает dword в переменную PE, это и есть адрес PE заголовка. SizeReadWrite содержит количество
реально считанных байт. Должно использоваться для контроля, но я этого не делал.
Теперь мы узнаем сколько у нас таблиц секций. Для это к адресу PE заголовка добавляем 6 байт,
переходим на это место и считываем этот word в переменную NumOfObj. Вот код:
mov eax,PE
add eax,6h
invoke SetFilePointer,hFile,eax,NULL,FILE_BEGIN
invoke ReadFile,hFile,ADDR NumOfObj,2h,\
ADDR SizeReadWrite,NULL
Затем мы сразу прыгаем на флаг первого обжект тейбла. То есть это адрес PE заголовка плюс его длина
(F8h), плюс смещение на флаг (24h).
mov eax,PE
add eax,11Ch
Циклом переходим на флаг каждого тейбла, считываем его и если это та секция, что нам нужна, то мы ее
патчим. Вот код:
mov cx,NumOfObj
fnd:
invoke SetFilePointer,hFile,eax,NULL,FILE_BEGIN
push eax
invoke ReadFile,hFile,ADDR flag,4h,\
ADDR SizeReadWrite,NULL
cmp byte ptr flag,20h
jz patch
pop eax
add eax,28h
loop fnd
Мы пушим eax чтобы не испортить адрес флага секции API функцией. А сравниваем первый байт переменной
flag с 20h из-за архитектуры процессора (как ты знаешь, там все наоборот). Затем мы, если не попали на
таблицу кода, восстанавливаем eax, прибавляем к нему 28h, это длина всего тейбла и начинаем все сначала.
Сам патч выглядит так:
patch:
pop eax
invoke SetFilePointer,hFile,eax,NULL,FILE_BEGIN
invoke WriteFile,hFile,ADDR buffer,4h,\
ADDR SizeReadWrite,NULL
invoke CloseHandle,hFile
Здесь мы восстанавливаем eax, так как мы этого еще не сделали из-за джампа, устанавливаем указатель на
адрес в eax, так как при чтении он у нас сместился на количество считанных байт, пишем туда наш флаг,
который находиться в переменной buffer и потом сохраняем это все API
CloseHadle. Как видишь, все очень просто. Теперь, для проверки, напиши какую-нибудь прогу, которая захочет
написать что-нибудь в код и проверь как она работает без патча, а потом после того как ты применишь к ней
этот нехитрый код. Это можно использовать в вирусах, чтобы писать себя в секцию кода, или для полиморфных
программ, которые изменяют свой собственный код.