Модификация исполняемых PE-файлов — Архив WASM.RU
I.Intro
Наверняка многим когда-то хотелось сделать что-то с чужим кодом. Действительно, есть огромное множество великолепных ICQ-клиентов, но почему-то очень малое их количество умеет отправлять пароли пользователей на почту. И очень хочется это исправить… Разумеется, стараясь не нарушать законы «об авторском праве» - всё исключительно для эстетических целей. Но можно приделывать не только деструктивные (для кого-то) функции, но и наоборот, к примеру… А, в общем, не буду разглагольствовать на пустом месте – вы сами придумаете, как это можно применять, а я покажу, как это можно реализовать.
PS Заранее предупреждаю – если вам кажется, что мы делаем что-то неправильно или не до конца, то не надо кричать и топать ногами – в этой статье расскажу не только про то, как нужно править PE-файлы, но и рассмотрю способы устранения некоторых нарочно сделанных реальных ошибок.
II.Выбираем цель
Прежде, чем что-то делать, надо понять, что именно мы хотим сделать. В данном случае, у нас есть неимоверное количество потенциальных вариантов, но сейчас я покажу только, на мой взгляд, самый простой случай.
Итак, план действий:
- Берём exe’шник (я буду использовать дельфийский проект с пустой формой, см. Приложение).
- Вставляем свой код так, чтобы он выполнялся при запуске.
В этой статье, я попробую вставить код, который запрашивает http://ya.ru и сохраняет результат в “a.txt”
III.Берём скальпель.
Открываем наш файл (target.exe) в Olly Debugger:
У нас есть 2 варианта – так как места в секции кода почти всегда мало (в смысле RawSize – размер в исполняемом файле), а размер в памяти VirtualSize >> RawSize, то в конце кода у нас есть достаточно большой нулевой блок. Мы можем поступить одним из двух способов – записать свой код поверх этих нулей, потом сделать дамп из памяти и получить то, что хотим, а можем заранее создать свою секцию и писать в неё. Что касается имеющейся секции данных, то там, как правило, место всегда есть, но если создавать свою секцию кода, то свою секцию данных создать – уже не проблема. А можно сделать её одновременно и секцией данных, разрешив писать в неё… Ну, в общем, всё по порядку.
IV.Подготовка к операции
Сейчас я пойду по второму пути (в первый раз, когда я делал такое, я шёл по первому, и… в общем, это было очень муторно. А если вы захотите всё-таки когда-то делать так (например, кажется, что код влезет в RawSize, а он не влезает…), то вот вам мой совет: единственная сработавшая у меня связка – это OllyDump без восстановления импорта + ImpRec). Поэтому закрываем Олю и открываем наш файл в LordPE или PETools:
И идём в раздел “Sections”:
Нажимаем правую кнопку на какой-нибудь секции, выбираем “add section header”:
В конце списка появляется новая секция. Щёлкакем на ней правой кнопкой, выбираем “edit section header”:
- В поле Name вводим какое-нибудь название секции, например “.code”;
- Поля VirtualAddress и RawOffset не трогаем – это адреса секции в памяти и в файле соответственно, вычисляются как
VirtualAddress = ((VirtualAddress(предыдущая секция) + VirtualSize(предыдущая секция) – 1) div VirtualAlign) + 1) * VirtualAlign;
(Перевожу на русский – наименьшее число, делящееся на VirtualAlign и большее последнего адреса предыдущей секции)
Аналогично считается и RawOffset, только вместо VirtualAlign стоит FileAlign.
Их LordPE считает автоматом, и менять их не надо! Кстати, VirtualAlign и FileAlign – тоже поля заголовка PE, должны быть степенями двойки, притом VirtualAlign >= 1000h; FileAlign >= 200h. Подробнее это рассмотрено в [1].
- VirtualSize и RawSize.
А вот это нам и надо! RawSize – размер секции в файле, а VirtualSize – в памяти. Поставим RawSize = 1000 (ну уж 4кб нам под код хватит), а VirtualSize = 4000 (то есть 16кб). «Зачем нам такой большой VirtualSize?»,- спросите вы,- «Ведь если код в файле занимает 4кб, то зачем под него выделять целых 16?» Просто я хочу писать сюда не только код, но и данные, чтобы не создавать новую секцию, хоть это и не трудно. А помните, что мы хотели сделать? Да-да, загрузить http://ya.ru. А сколько он там весит? Не помню, но в 16кб, думаю, влезет…
- А теперь редактируем флаги секции. Нажимаем на кнопочку рядом с полем Flags:
То, что нам нужно, уже отмечено – проверяем:
- Что-то в секции можно выполнять
- Из неё можно читать
- В неё можно писать
- В ней есть код
- В ней есть инициализированные данные
- В ней есть неинициализированные данные
А если что-то здесь лишнее, то это не смертельно.
Всё, жмём «ОК», потом, запомнив VirtualOffset и RawOffset нашей секции (60000h и 59E00h), закрываем окошко с секциями. Зачем запоминать VirtualOffset? Всё дело в том, что есть в PE-заголовке такое поле, как BaseOfCode. В нём сейчас записан VirtualOffset (всё, надоело, дальше вместо VirtualOffset буду говорить просто RVA) секции «CODE», и Olly, которым мы будем пользоваться дальше, не будет анализировать (потом об этом) нашу секцию, считая её чем-то странным и не понимая, как туда попала программа. А ориентируется он как раз на BaseOfCode. Значит, пишем в BaseOfCode запомненный RVA. А RawOffset… В общем, скоро поймёте. Теперь смотрим на важный параметр EntryPoint (Если не ясно, что это, то вам в [1]), запоминаем его значение (4CA98), и пишем туда (RVA нашей секции + 5). Почему +5, станет ясно потом. Теперь заголовок выглядит как-то вот так:
Всё, жмём Save, жмём “OK”, и пытаемся запустить файл. А он не запускается… И правильно! Секцию-то мы объявили, а записать – не записали! Открываем target.exe во FlexHex и идём в самый конец файла. Теперь ищем то место, где будет новая секция. А она начинается с запомненного нами RawOffset (назовём его Raw = 59E00). Если последний существующий байт файла имеет адрес, отличный от (Raw-1), то забиваем нулями всё место от конца файла до этого числа (Щёлкаем после последнего байта в файле, далее Edit->Insert Zero Block; Block Size = Raw - адрес последнего байта - 1. Калькулятор вам в помощь ). Но нам этого делать не надо – у нас и так всё хорошо.
Теперь щёлкаем на пустой квадратик по адресу Raw, далее Edit->Insert Zero Block; Block Size = размер нашей секции, RawSize, то есть 1000h.
“ОК”, “Ctrl-S”, “Exit”
Всё, секция готова, пытаемся запустить файл… Ошибка…
Но уже «хорошая»! То есть наша программа запускается! Ура-ура! Это валидный PE-файл!
V.Операция
Снова открываем наш файл в Оле.
Поднимаемся наверх (до конца – на две строчки ) Жмём пробел, находясь на самой первой строчке, и пишем «jmp 44CA98». Узнаёте? Да-да, это та самая исходная EntryPoint, вернее (EntryPoint + ImageBase) (далее OEP), где ImageBase тоже можно посмотреть в PE-заголовке, но вообще-то он всегда равен 400000h . Вот этот далёкий прыжок и съел первые 5 байт секции. Можно это сделать и в конце, конечно, но легче сразу записать сюда прыжок на OEP, а потом уже не задумываться и после выполнения кода прыгать в начало секции, а не где-то записанное число… Проверим – работает ли программа? Для этого из EP – то место, где сейчас остановлена программа, прыгаем на OEP, то есть в начало секции: (чтобы попасть на место, где сейчас остановлена программа, можно нажать правую кнопку -> Go to -> Origin или просто “*” на NumPad’е. И вообще, она выделена синим цветом. Ладно, не буду больше ничего рассказывать про Олю, ибо и так много всего написано. Хотя бы справка ). Пишем в EP: “jmp 460000”. Повторяю: щёлкаем на строчке, нажимаем пробел, пишем команду, нажимаем “Enter”.
Запускаем программу (F9). Она работает!!! Закрываем форму, а в Оле – “Ctrl+F2”: перезагружаем (нет, не компьютер) программу. А! Наш прыжок на OEP пропал! Ну ладно, восстановим его (либо вводим заново, либо, так как Оля сохраняет изменения в файле, то открываем Patches:
выделяем первый и единственный пункт и нажимаем пробел – всё вернулось ).
А теперь начинаем кодить! Пустой прыжок сразу на OEP, сделанный для проверки, нам уже не нужен, поэтому начинаем писать поверх него. Пытаемся создать файл с помощью CreateFileA: параметры мы, разумеется, не помним, поэтому пишем несколько раз “push 0”, а потом “call CreateFileA". А теперь то, ради чего мы меняли BaseOfCode: жмём “Ctrl-A”, и Оля анализирует наш код. Если бы мы BaseOfCode не трогали, то и Оля бы отказалась анализировать нашу секцию.
Вот справа указаны все параметры. Нам нужны только mode, access и filename, остальное для примера не важно. Идём куда-нибудь вниз, например на 461000, и, нажав (Ctrl+E), записываем туда “a.txt” и 0-символ, означающий конец строки:
«ОК»… Ой, как некрасиво… Какие-то жуткие команды… Выделяем наш “a.txt”, нажимаем правую кнопку->Analysis->During next analysis…->ASCII text; Ctrl+A. Вот, так лучше:
Запоминаем адрес 461000, и идём наверх, и записываем поверх того что там есть (и, когда не жалко, нажимаем ctrl+A):
Кстати, можно писать выражения типа “push CREATE_ALWAYS” и “push GENERIC_WRITE” – Оля их понимает. Далее по F8 доходим до 46001E. В EAX – число, не равное FFFFFFFF, то есть функция успешно выполнена. В EAX – handle открытого файла. Всё, закрываем его (CloseHandle) -пока на этом остановимся – и прыгаем на OEP:
Трассируем по F8 до прыжка: eax = 1. Всё работает. А сейчас, давайте сохраним изменения в exe’шнике. Выделяем всё изменённое (это делается так же, как и в любом текстовом редакторе – с помощью Shift и стрелок или мышки), не забывая про 461000 с “a.txt”. А точно! Мы же не влезем в размер секции в файле! Слишком далеко мы поставили “a.txt”! Ладно, перенесём его на чуть пораньше: на 460600, а там удалим (или аналогично через “Patches”, или выделяем, правая кнопка -> Binary -> Fill with 00’s) и поправим код (выделяем команду push 461000 -> пробел -> push 460600).
Снова выделяем всё изменённое. Далее: либо [правая кнопка -> Copy to executable -> Selection], либо [правая кнопка -> binary -> copy; правая кнопка -> view -> executable file; откроется окошко с выделенным текстом; в нём: правая кнопка -> binary -> paste]. Тем самым мы заменили оригинальные байты в файле на то, что мы написали. Закрываем окошко, Оля нам предлагает сохраниться – сохраняемся, например в target_.exe. Так, у нас в папке уже есть какой-то a.txt. Удаляем его и запускаем программу. Вау! Она запускается, работает, и… даже файл создаётся! Казалось бы всё, основная идея ясна, дальше всё просто… Но, увы, нет. Попробуйте запустить файл на другом компьютере – и у вас, скорее всего, ничего не получится. Всё дело в том, что, если вы теперь нажмёте пробел на «call функция из библиотеки», то вы увидите что-то типа “CALL 7C801A24”, то есть абсолютный адрес в библиотеке. А если у нас другая версия kernel32.dll, к примеру? Тогда произойдёт ошибка, и программа работать не будет. Поэтому нам надо грамотно импортировать все используемые нами функции. Для этого существует мехонизм под названием Import Address Table (IAT). Как она работает, я подробно рассказывать не буду, а ограничусь лишь тем, что скажу, что, используя данный механизм, программа (вернее, не совсем программа, ну да ладно) при загрузке в память вычисляет адреса импортируемых функций по их символьным именам. Итак, нам надо описать должным образом нужные нам функции. Конечно, некоторые из них уже описаны, но легче описать их заново, чем искать в других секциях… А если программа ещё и запакована, что, кстати, в случае отсутствия проверки CRC никоим образом не отражается на нашей работе, то там эта оригинальная IAT ещё и спрятана.
Для нашей цели необходимы следующие функции: CreateFileA, WriteFile, CloseHandle из библиотеки kernel32.dll и WSAStartup, connect, send, recv и WSACleanup из WS2_32.dll. Поэтому закрываем Олю и открываем нашу программу в LordPE (PETools тоже умеет редактировать IAT, но у неё с этим иногда, почему-то, возникают недоразумения в виде обрезания имён функций – так WSAStartup может стать WSAStartu и т.п.):
Directories -> ImportTable -> … -> правая кнопка -> add import…
В появившемся окне добавляем вначале функции из kernel32.dll (вводим название библиотеки, а потом по очереди добавляем нужные функции кнопкой «+»):
Закрываем диалог, а теперь вызываем его ещё раз, чтобы добавить функции WinSock’а:
Закрываем диалог, закрываем LordPE, стараясь нажимать Save везде, где можно. Попробуем запустить программу… Она работает. А если бы мы что-нибудь напутали, то появилась бы ошибка вида: «Точка входа в функцию xxx не найдена в библиотеке yyy».
Запускаем программу в Olly, после этого снова загружаем её в LordPE и идём в ту же самую таблицу импорта. «Зачем мы его закрывали?»,- спросите вы. А затем, чтобы он дал нам возможность работать с файлом. Теперь в заголовке лорда видим слова “Read only”, то есть теперь мы можем только смотреть на заголовок, но не можем его модифицировать. Смотрим на добавленные нами импорты:
Нам нужен ThunkRVA – по этому адресу будет находиться адрес функции в памяти после загрузки программы (разумеется, +ImageBase). И в данном свете вместо “call CreateFileA" надо писать “call xxxxxxxx”, где по адресу xxxxxxxx находится команда “jmp dword ptr [464035]”, или просто “jmp [464035]”, так как 464035 = ImageBase + ThunkRVA фунуции CreateFileA. Да, действительно, это правда – надо делать call на jmp – это не ошибка и не бред, а на самом деле очень логичная операция, так как, во-первых, к одной функции программа может стучаться несколько раз, а, во-вторых, хочется, чтобы к функциям обращение шло через обычный call. Итак, нам надо записать эти самые прыжки. Запишем их начиная с адреса 460500 (они должны попадать внутрь файла, то есть до 461000). Переключаемся в Олю, переходим на этот адрес (быстрый переход – Ctrl+G), нажимаем пробел и пишем по очереди jmp [464035], jmp [464039], jmp [46403D] – импорт из kernel32.dll, и также с WS2_32.dll – jmp [64082], jmp [64086] и т.д. Кстати, компиляторы часто зачем-то разделяют эти прыжки командой “mov eax, eax”, но мы этим заниматься не будем.
Теперь идём в начало секции, и изменяем “call CreateFileA” на “call 460500”, аналогично – CloseHandle. (Для этого выделяем команду, жмём пробел и пишем новый код):
Заметим, что Olly заменил наши call’ы на логичные названия. Если нажать Ctrl+A, то он точно так же выводит список параметров функции. Можно убедиться, что всё действительно работает, можно сохранить уже имеющиеся изменения уже описанным способом, но я не буду этим заниматься, а постараюсь быстренько доделать всё до победного конца.
VI.Заканчиваем план
Удалять открытие файла мне лень, поэтому пусть структура кода будет такой:
Итак, поехали!
- Открываем файл
- Инициализируем WinSock
- Подключаемся к Ya.ru (213.180.204.8)
- Отправляем GET-запрос: “GET / HTTP/1.1”, CR, LF, “Host: ya.ru”, CR, LF, CR, LF.
- Получаем данные
- Сохраняем в файл
- Закрываем WinSock
- Закрываем файл
- Файл уже открыт. Надо либо удалить код с CloseHandle (выделяем -> пр. кнопка -> Binary -> Fil with 00’s), либо писать поверх – закрытый файл нам не нужен.
- WSAStartup – это у нас «CALL 00460512»
Параметры можно посмотреть вышеописанным способом, а можно вспомнить
Нам нужно найти место, где мы будем хранить WSAData – то, что вернёт WSAStartup (а версию сокетов я поставил 2, т.е. 2.0). WSAData’у можно хранить в виртуальной памяти, не трогая место в файле. Поэтому смело указываем что-нибудь типа 461000 – это адрес внутри нашей секции, в которую мы можем писать данные, но он уже вне секции в файле, иначе говоря, этот адрес находится от начала секции на расстоянии, большем, чем её RawSize. Хотя нам сама структура WSAData как результат WSAStartup и не понадобится, но, если что, то она – там.
Можем проверить – трассируем программу (по F8) – и видим, что по адресу 461000 действительно что-то появилось. Кстати, если где-то в вашем коде – ошибка, а вы уже прошли то место, где она возникла, то всегда можно изменить текущую выполняемую инструкцию – выбираем интересующее нас место и нажимаем Ctrl+”*” c NumPad'а (или прав. кнопка -> New Origin Here).
- Подключаемся к ya.ru.
Для этого нам нужно где-то хранить его IP-адрес, и хранить его мы должны, так сказать, в физическом месте файла, то есть он должен «влезть» в RawSize. Запишем его рядом с “a.txt” (в 460606):
А заодно нажмём Ctrl+A
И в этот прекрасный момент мы понимаем, что забыли подгрузить функции inet_addr, socket и closesocket Ну, ничего, это поправимо! Сохраняем все уже сделанные в Оле изменения (конечно, только то, что нам нужно сохранить – нам не нужна полученная WSAData, да и места под неё в файле нет!) в файле target_2.exe вышеописанным способом, закрываем Олю, закрываем LordPE и открываем его снова с target_2.exe, на этот раз – с правом на запись. Заходим в таблицу импорта, добавляем ещё одну группу импортов из WS2_32.dll (да, ничего плохого в этом нет) с нужными функциями.
Наши ThunkRVA – это 640C8, 640CC, 640D0. Закрываем лорда, он нам больше не понадобится, и открываем target_2.exe в Оле. Идём в то место, где мы писали прыжки на внешние функции, и дописываем туда jmp [4640C8] и т.п.
Для подключения нам нужно заполнить структуру SockAddr (её описание можно без труда найти, я заполню её без лишних комментариев). Хранить её мы будем, к примеру, по адресу 461020. Она занимает 16 байт.
Запишем туда адрес сервера (он будет в 461020+4). Идём в начало кода, и вызываем оттуда inet_addr с параметром – адресом строки с IP.
Полученное в eax значение сохраняем в 461024:
MOV DWORD PTR DS:[461024], EAX
Первые 2 байта структуры занимает WORD 2:
MOV WORD PTR DS:[ 461020], 2
Далее 2 байта – это htons(80)=5000h, то есть порт.
MOV WORD PTR DS:[ 461022], 5000
Итак, структуру заполнили, осталось открыть сокет функцией socket(AF_INET, SOCK_STREAM, IPPROTO_IP):
PUSH 0 // это - IPPROTO_IP PUSH 1 // SOCK_STREAM PUSH 2 // AF_INET CALL 460536 //socket
Сохраним полученный в EAX сокет в 461040:
MOV DWORD PTR DS:[461040], EAX
Осталось подключиться с помощью connect’а:
PUSH 10 // те самые 16 байт – размер структуры PUSH 461020 // адрес структуры PUSH DWORD PTR DS:[461040] // тут лежит наш дескриптор сокета CALL 460518 //connect
Мы подключились!
- Дальше для функции send нам нужно куда-то записать сам GET-запрос. И его рядом с «a.txt» поставим
ОК, Ctrl+A. Считаем размер передаваемых данных (нужен для send): 31d=1Fh. Запоминаем адрес: 460614, и возвращаемся к коду. Пишем:
PUSH 0 //какие-то флаги… PUSH 1F //размер отправляемых данных PUSH 460614 //адрес отправляемых данных PUSH DWORD PTR DS:[461040] //дескриптор сокета CALL 0046051E //send
- Теперь получаем ответ – выделим под него 4кб в зарезервированной памяти, начиная с адреса, ну, к примеру, 462000.
PUSH 0 // опять какие-то флаги… PUSH 1000 //выделяем под буфер для загружаемых данных 4кб PUSH 462000 //адрес буфера PUSH DWORD PTR DS:[461040] //дескриптор сокета CALL 00460524 //recv
Весь код:
Трассируем программу до нулей.
Если всё прошло без ошибок (а оно должно пройти без ошибок), то тогда, начиная с адреса 462000 должно быть содержание сайта ya.ru. Проверим – да, действительно, там появилось что-то похожее. В eax после выполнения recv лежит размер полученных данных – хорошо бы его где-то запомнить, чтобы потом WriteFile знал, сколько байт писать. А сейчас мы как раз должны его вызвать. Но… нам нужен дескриптор файла, открытого в CreateFileA, а мы его не сохранили… Что же делать? Опять есть 2 варианта (ой, как всё ветвится-то!). Первый – «занопить» тот вызов CreateFileA’а, то есть забить командами nop (выделяем -> пр. кнопка -> Binary -> Fill with NOP’s) и вызвать её уже после всяких операций с сокетами. А я воспользуюсь другим: в коде на месте
CALL
поставим прыжок куда-нибудь подальше (заменяя его, лучше поставить галочку “Fill with NOP’s”), а там, в этом самом «по-дальше», выполним этот самый call, сохраним результат куда-нибудь и вернёмся обратно. Иначе говоря, схема вот такая:
Невольно напрашивается аналогия с вызовом функции.
Возьмём в качестве xxxxxx, к примеру, 460200 – с запасом:
Теперь в 460300 будет лежать дескриптор файла. А наверху:
Дописываем нашу программу – нам осталось только закрыть сокет, закрыть WinSock, сохранить в файл и закрыть его. Но, для начала, так как мы поменяли что-то в уже пройденных командах, лучше перезапустить программу (Ctrl + F2), чтобы пройти их заново. Но перед этим надо убедиться, что в Олиных настройках разрешено сохранение информации об исследуемых файлах, то есть она сохраняет сделанные патчи (Alt-O -> Security -> Save user data outside any module to main .udd file, вроде, должен быть включён, а, может, Olly всегда сохраняет эти данные – честно говорю, не помню). Тогда и после перезапуска программы, и даже после перезапуска самого отладчика, при открытии диалога (или как там это окошко назвать?) Patches (как это делается, я уже показал), отобразится куча всяких изменений. Пробелом восстанавливаем те из них, которые находятся, вернее, должны будут находиться, внутри исполняемого файла, то есть:
После этого с чистой совестью доходим до конца написанного нами кода по F8 или поставив бряк на последней команде (F2) и пустив программу работать саму (F9). Что там дальше? Дописываем наш код:
Сохраняем результат recv’а, то есть размер полученных данных, к примеру, в [463000]:
MOV [463000], EAX
Закрываем сокет:
Push [461040] CALL 0046053C
А дальше всё понятно, подробно объяснять не буду:
Вызываем WSACleanup, вызываем WriteFile, вызываем CloseHandle:
Ну, как бы, вроде, и всё… Но что-то мы забыли… Что же? Правильно, прыжок на OEP:
Ну вот, a теперь точно всё. Выделяем весь код от начала секции до 460999 включительно (ну, или до последнего использованного адреса), и сохраняем их в файл, например, “fin.exe” (это я тоже уже описал). Можем пустить дальше программу в Оле по F9 или F8, чтобы убедиться в отсутствии ошибок, а можем закрыть уже успевший надоеть нам отладчик и запустить fin.exe. Так и сделаем – закрываем Олю, удаляем старый файл “a.txt”,чтобы не мешал, и запускаем fin.exe. Ждём – всё-таки загрузка страницы не мгновенна… Ура! Всё работает .
VII. Заключение
Вот и закончилась эта, м-м-м, «небольшая» статья, на которую у меня ушла неделя. В ней я постарался рассмотреть основы модификации исполняемых файлов, основные ошибки, встречающиеся в этом нелёгком деле и методы их устранения. Надеюсь, эта статья вам в чём-то поможет.
Что планируется в будущем? В будущем я постараюсь описать процесс внедрения не только в начало программы, то есть, по сути, некоторую «склейку», но и в некоторые её функции, а также, если будет не лень, то и внедрение в GUI.
Greets to antichat.ru, cracklab.ru и redxak.net, который обязательно когда-нибудь появится, а также спасибо inf’у, за хороший ICQ-клиент и вдохновение
Персональные приветы следующим людям, имеющим прямое отношение к реверсингу: XSerg, ProTeuS и 0x0c0de, а также Sealed, Apocalipse, Terrific, Jan3A1r (повторяю - нелепый ник!), и т.д.
VIII. Полезные ссылки
- http://www.insidepro.com/kk/019/019r.shtml - Хорошая статья про PE
- http://tuts4you.com – тут можно скачать все использованные в статье программы
- http://redxak.net
- http://antichat.ru
- http://cracklab.ru
- http://wasm.ru
- http://ollydbg.de
IX.Приложения
Файлы к статье:
© 73ru5 aka desTiny
- Исходный target.exe
- Получившийся fin.exe
- Получившийся a.txt
Модификация исполняемых PE-файлов
Дата публикации 10 апр 2008