Бесполезный Perl и общая теория улучшения мира
Как-то недавно покупал я на Петровке книжку по Perl, и
один пипл спросил, как может сейчас пригодиться perl здравомыслящему
человеку. Насчет здравомыслящих не знаю, а кое-кому пригодится - и даже
очень:
Расскажу вам историю о том, как использовать старый как
мир perl для такой же извечной задачи - реверса исходного кода.
Для начала о самом реверсе. Некоторые считают, что это
незаконно, другие же уверены, что весь код должен быть открытым и
преступно как раз скрывать исходники. Моя позиция в этом вопросе - это
полное отсутствие всякой позиции. То есть я ломаю все, что мне интересно,
но не получаю от этого никакой прибыли и, соответственно, не имею правовых
проблем.
Зачем это может быть нужно? Зачем ломать код? Первый (и
никогда не соответствующий действительности) мотив - исправление ошибок в
неправильно работающих программах. Обычно единственной исправляемой
"ошибкой" оказывается требование ввести серийный номер - ну, или что-то в
этом роде.
Вторая, более реальная причина - желание вскрыть механизм
вредоносного кода (типа вируса или троянского коня), дабы впоследствии
построить против него защиту. Вариация: вы вскрываете код трояна с целью
проследить, куда и что он отправляет, и получить таким образом возможность
либо вычислить пимпера, либо, по крайней мере, "пропатчить" его трафик.
Смешная ситуация: удаленный хакер троянит ваш комп - и вместо списка
файлов видит ваш портрет в ASCII-графике :-).
Третий мотив - уже совсем реальный - это собственно
"evaluation purposes". Постоянное созерцание кода, ковыряние во всяком
бинарном хламе - от ассемблера до псевдокода Java - сделает из вас
законченного Code Warriora'а и, помимо прочего, поможет писать быстрые,
надежные и безопасные программы.
Граничные условия, вход/выход, etc.
Будем считать, что с UseCase разобрались. Теперь немного
архитектурных решений. Для начала - о том, что у нас есть или что считать
входными данными.
Итак, есть исполняемый файл для Windows, написанный на
C++/MFC (предположительно - в формате PE). Если он был сжат/закодирован,
будем считать, что эта часть проблемы уже решена: сжатие/кодировка - это
отдельная тема, для которой имеется свой инструментарий, а в Сети
существуют даже специализированные ресурсы по этому вопросу.
Для того чтобы разговор был предметным, возьмем очень
известную в Сети программу (условно назовем ее СБ). Эта программа известна
всем, кто пытался получить деньги за серфинг. Причин для такого выбора
несколько: во-первых, она представляет собой хороший пример "кристально
чистого" кода MFC без каких-либо методов шифрования самого кода. С другой
стороны - это весьма интересный freeware-код для взлома, (по вполне
понятным причинам коммерческие программы мы с вами ломать не станем).
Говоря о взломе СБ, я не имею в виду те небольшие
"отчисления", которые вы можете получить, сломав этот код. Вовсе нет - я
бы даже не советовал вам этого делать, поскольку массовый взлом может
вообще привести к "прикрытию" этой полезной конторы. Или же, что более
вероятно, на ваш регион просто перестанут высылать чеки - и вас порвут
ваши же "товарищи по оружию".
Интерес тут другого рода: программисты выложили в сеть
программу для получения легких денег - и вполне естественно, что все
киберпанки тут же бросились ломать клиентскую часть с целью получения
незаконных прибылей. Также понятно, что в программу встроена тысяча и одна
защита от таких взломов. Иными словами СБ - это пример открытого и
честного противостояния: группа кодеров vs. Большая Сеть, причем
противостояние это длится аж с 1999 года, что уже само по себе факт
примечательный.
Итак, есть наша программа - СБ.exe. Пропускаем ее через
IDA и получаем огромный листинг СБ.asm. То, что перед нами C++/MFC, вне
сомнений; но пока мы видим просто текст на ассемблере, причем размер его
составляет около 30Мб. Работать с ним неудобно, а некоторые текстовые
редакторы вообще виснут (тот же F4 в Far, например). Кроме того, нам видно
много лишних деталей: технические комментарии, код пролога и эпилога
функций, передача параметров по push и т.д. Короче говоря, полученный
текст приблизительно в 10-20 раз длиннее, чем нам нужно для понимания и
реставрации алгоритмов программы.
Это можно и нужно исправить. В Сети есть несколько
программ, преобразующих asm в C, но у них есть ряд недостатков. В
частности, эти программы делают все по-своему - в то время как наша
программа будет делать все "по-нашему", и, если нам что-то не понравится,
мы сможем сделать это на свой лад, не изучая при этом сотню страниц
документации по ключам и настройкам.
Сразу хочу заметить: если мы и будем пользоваться
подобием синтаксиса C, то только для большей наглядности. Полученный код
предназначен не для компиляции, а только для удобства чтения, так что,
если нам будет удобно, мы можем, например, не ставить точки с запятой.
Архитектура
Для преобразования текста в читабельный вид самыми
"горячими" и востребованными будут операции со строками, поскольку и
входной и выходной поток - суть последовательность строк. Поэтому вполне
естественно, что мы станем использовать Perl - он имеет ни с чем не
сравнимые средства для работы со строками и, вместе с тем, является мощным
языком высокого уровня, способным справиться и с другими задачами.
Поскольку наша программа дизассемблирована под Windows, то будем
использовать Windows-версию Perl ActiveState - при желании этот же код
потом можно обыграть под Linux.
К архитектуре нашего приложения относится также
количество проходов - то есть то, сколько раз наша программа будет
сканировать исходный текст. В одно время большим достоинством программы
(компилятора в частности) считалась возможность обрабатывать текст за один
проход. В результате появились forward-объявления типов и функций, файлы
хедеров, дополнительные препроцессоры и т.д. Сегодня, когда цена дисковой
и оперативной памяти не является критической, об оптимальном количестве
проходов можно спорить. Если программа оперирует одним проходом, она
неизбежно хранит много состояний для различных подсистем - и в результате
вы, что называется, теряете управление сложностью.
С другой стороны, несколько однопроходных модулей,
вызываемых последовательно и соединенных через программные каналы,
предоставят ту же производительность при значительно более простом коде.
Так мы и сделаем - будем иметь дело с несколькими однопроходными
операциями, каждая из которых достаточно проста, чтобы уместиться на одном
экране текстового редактора. Поскольку наши "подпрограммы" нужно вызывать
в определенном порядке, будем нумеровать файлы подобно тому, как раньше
нумеровали строки в qbasic (например, 5,10,20,30 и т.д.),- в случае чего
можно вставить в последовательность пару новых элементов, и при этом будет
ясно, что за чем идет.
Командный файл для запуска будет примерно таким: perl -n 1.pl СБ.asm | perl -n 2.pl | perl -n 3.pl 1>СБ.out
Ключ -n не просто экономит несколько строк кода - он
буквально принуждает нашу программу работать определенным образом, то есть
получать на входе в переменной $_ одну за одной строки asm-файла и также
по одной их обрабатывать. Конечно, никто не запрещает хранить строки во
внутреннем хранилище, например в массиве. После обработки каждый блок
должен возвращать все строки в исходном или модифицированном виде в
выходной поток для дальнейшей обработки. Тривиальный модуль, как минимум,
должен вернуть все строки без обработки: print;
По-разному можно решить вопрос о том, какой будет связь
между модулями - сильной или слабой. Сильная связь означает такое
поведение, когда один модуль корректно работает только в случае, если он
запущен после того, как отработало несколько предварительных модулей.
Последние, в свою очередь, могут оставлять в результате своей работы
некоторые артефакты, то есть вносить собственную разметку, рассчитанную на
дальнейшую интерпретацию следующим модулем.
Такое поведение, вообще говоря, позволяет делать более
сложные вещи, но мы хотим поддерживать код настолько простым, насколько
это возможно. В идеале порядок проходов совершенно не должен иметь
значения, но неявно мы все время будем использовать зависимости -
поскольку от перестановки проходов производительность будет меняться в
диапазоне от "отлично" до "неприемлемо". Например, после замены всех
"white spaces" на единичные пробелы мы можем везде вместо "\s+"
использовать " ", что значительно проще для regexp-движка, и так
далее.
Листинги и комментарии
Поскольку эта программка для меня что-то вроде хобби, к
которому я периодически возвращаюсь, то за последние два года она выросла
до 24-х проходов с весьма хитрыми "исподвывертами", длящимися несколько
минут даже на моем неслабом компьютере. Но поскольку в данном случае нам
важно всего лишь понять смысл программы, а не собрать компилируемый код,
то для начала достаточно нескольких проходов, которые, скажем, позволят
уменьшить размер файла в 10 раз.
Проход 05_stringer.pl - сборка строк: 01 if (m|db\s+..h\s+;\s+([0-9A-Za-z\@:\.\?/\-])|) {
02 $buff=(defined ($buff) && length
($buff)>0?"$buff$1":$1);
03} else {
04 if (length ($buff)) {
05 print (m/db\s+0/)?" db '$buff',
0\n":" db '$buff'\n$_";
06 $buff="";
07} else {
08 print;
09}
10}
Комментарий: IDA хорошо собирает строковые литералы и
даже дает им осмысленные имена, но только в случае, если на строку есть
ссылка. К сожалению, если этих ссылок нет, то строка так и остается
"толпой одиноких байт". Приведенный выше код исправляет ситуацию: если
встречается строка типа db 65h; A - мы начинаем накапливать строку в
переменной $buff. Все последующие символы добавляются в строку, до тех пор
пока не встретим что-то другое. "Что-то другое" может быть db 0 или нет -
в первом случае мы предполагаем, что строка "закрывается" этим символом, и
присоединяем его к строке. В противном случае наша программа выводит
строку, за которой идет то, что распознано нами как не-строка (поз 05).
Поскольку в следующем проходе мы будем "складывать" повторяющиеся db 0 в
один dup, то удобнее будет заранее "сложить" строки, чтобы потом не
приходилось выискивать db 0 в dup'ах. Это та причина, по которой данный
проход имеет номер 05 - он был добавлен сюда позже.
Проход 10_squezee.pl, упаковка 01 if (!m/^\s*(;|$)/
&&!m/(align|assume|model|segment|p386n|public)/) {
02 s/\s+/ /g;
03 if (m/unexplored/) {
04 $unxcnt=($unxcnt?$unxcnt+1:1);
05} elsif (m/db 0/) {
06 $zeros=($zeros?$zeros+1:1);
07} else {
08 if ($unxcnt) {print " db $unxcnt dup
(?)\n"; $unxcnt=0;}
09 elsif ($zeros) {print $zeros==1?"
db 0\n":" db $zeros dup (0)\n";
$zeros=0;}
10 s/;.... XREF:.*$//;
11 s/0(FFFF)?FFFFh/-1/;
12 print $_,"\n";
13}
14}
Комментарий: директивы в строке 01 имеют смысл только для
ассемблера, для нас же они - мусор на 100%, так что просто игнорируем их.
Строка 02 заменяет все "blank spaces" (то есть последовательности
пробелов, табуляций и прочих "невидимок") одним пробелом. Возможно, это
уменьшает читабельность, но она нам пока ни к чему. Следующий блок
"скатывает" последовательности db 0 и db?. Первое вхождение такой строки
начинает отсчет, и вся последовательность превращается в одну строку типа
db 100 dup (?). Слово unexplored ничего нам не дает, так как? именно это и
значит. Строка 10 "убивает" все перекрестные ссылки - поскольку у нас уже
нет адресов в левой части листинга, то они тоже мало что дают. В строке 12
добавляется перевод строки к строкам, которые мы пропускаем без обработки,
поскольку он тоже blank, и поэтому был удален в строке 02.
Проход 20_subrouter.pl, обработка определений функций: 01 if (!m/(mov esp, ebp|push ebp|mov ebp, esp|pop
ebp|push e [sd] i|pop e [sd] i)/&&!m/^(var|arg)/) {
02 if (m/ proc /) {
03 s/^(.+) proc.*$/\n$1 {/;
04}
05 if ($retn) {
06 if (m/endp/) {
07 s|^(.+) endp|}|;
08} else {print "}\n";}
09}
10 $retn=m/retn/;
11 if (!$retn) {print;}
12}
Комментарий: в первой же строке удаляются все стандартные
прологи и эпилоги функций. Вы можете представить ситуацию, когда эти
команды используются в другом контексте, но в нашем случае компилятор
этого не делает - поэтому тут все "чисто". Тут же я удаляю все определения
аргументов и переменных - они только мешают (можете считать, что в нашем С
все переменные определяются неявно в момент использования). Определения
функций заменяются на С-подобные в строке 03. Небольшие трюки связаны с
обработкой конца функции - правильно написанная функция всегда имеет одну
точку выхода, так что retn - наверняка конец функции, однако IDA в этом не
уверен.
Проход 30_caller.pl, обработка вызовов функций: 01 if (!m/add esp/) {
02 s/^ call (.+); (.+)$/ $3/;
03 s/ds://;
04 s/^ call (.+) / $1/;
05 print;
06}
Комментарий: первая строка отфильтровывает строки типа
add esp, 10h - компилятор использует их только для коррекции после вызова
функций из стандартных библиотек типа libc - и никак иначе. Команды call
заменяются на комментарии от IDA в случае "хорошо известных" вызовов (эти
комментарии нарочно не удалялись) или на имена функций нашей программы,
если IDA нам ничего не подсказывает. В более полном варианте в этом же
проходе можно "скатать" стек. При этом учитывается, какие переменные были
помещены в стек перед вызовом, и вместо пачки push аргументы перечисляются
в скобках.
Проход 40_reasm.pl, замена ассемблерных конструкций: s/\[ebp\+(.+)\]/$1/;
s/\]//;
s/\[//;
s/mov (.+), (.+)/$1=$2/;
s/lea (.+), (.+)/$1=&$2/;
s/xor (.+), $1/$1\=0/;
s/sub (.+), (.+)/$1-=$2/;
s/add (.+), (.+)/$1+=$2/;
s/offset /*/;
s/ptr //;
s/short //;
s/unknown_libname/lib/;
print;
Эти строки делают несколько замен общего характера,
отличительная особенность которых заключается в том, что после этого
прохода мы больше не имеем дело с кодом ассемблера.
В этом месте мы уже достигли определенного прогресса:
текст СБ.asm уменьшился ровно в 14 раз (с 28 Мб до 2 Мб), работать с ним
стало приятно и полезно. Не хочу утомлять вас более сложным кодом - сейчас
главное понять, как это делается, и почувствовать вкус к такого рода
"улучшению мира".
А как же СБ?!
Для тех, кого заинтересовала история взлома СБ, могу
только обозначить направляющие, которые могут привести вас к
положительному результату (а могут и не привести). Вообще, есть три
способа укрощения СБ и ему подобных: читинг, патч и эмуляция.
Читинг - это способ "удовлетворить" СБ, не модифицируя
его код. Для того чтобы СБ начислял вам деньги, вы должны просматривать
веб-страницы. На техническом уровне это значит, что на первом плане у вас
должен находиться Internet Explorer или Netscape Navigator (Opera
почему-то не поддерживается), вы должны периодически переходить с сайта на
сайт и время от времени шевелить мышкой. Переходы с сайта на сайт
регистрируются путем DDE-нотификаций: можно посылать их СБ в обход
настоящего браузера. Нажатия мышки регистрируются установкой глобального
хука на функцию в DLL (что самой Майкрософт вообще-то не
рекомендуется).
Существует 1000+1 программа "накрутки" СБ именно через
читинг, но все они ловятся и выкупаются так или иначе - сразу или со
временем. В частности, не вредно будет знать, что СБ может загружать
"антихакерские" модули, число которых постоянно растет.
Патч - это метод взлома оригинального бинарного кода.
Хочется сказать, что СБ.exe только этого и ждет, то есть весь код
просто-таки нашпигован разнообразными проверками. В частности, проверяются
контрольные суммы (по частям файла, а не целиком, так что "исправить" CRC
не удастся) самой exe, dll, вычисляются и проверяются дайджесты и
хеш-значения ресурсных строк (например, команд протокола и заголовки
браузеров). К тому же, существует, по крайней мере, несколько
runtime-проверок на работу под управлением (под отладкой) и дополнительно
- та же возможность загружать модули для выполнения. Все это можно
отключить, но я бы никогда не рискнул утверждать, что не осталось еще
одной проверки.
Эмуляция - это такой случай, когда СБ.exe вообще никогда
не получает управления, вместо этого работает ваша программа, выглядящая
для сервера как СБ. Естественно, что ваша программа не будет показывать
банеры и не будет делать ничего "вредного", вроде уведомлений о попытке
взлома. Задача рассыпается на три части: обнаружить команды интерфейса,
декодировать трафик и исследовать стохастические вероятности.
Первое сделать просто - запустите какой-то прокси-сервер
для порта 80, отслеживающий и записывающий трафик, настройте СБ на работу
через этот прокси (благо, такая возможность предоставляется штатно) и
поработайте пару часов "под СБ". Как вы скоро убедитесь, количество
"штатных" команд не очень велико. Другое открытие, уже менее приятное,-
трафик закодирован. Не пытайтесь использовать его повторно - в трафике
стоят time stamps, и такая попытка приведет к аннулированию вашего
счета.
Для кодирования в СБ используется два метода: первый -
"сжимающий" алгоритм - кодирует текстовые строки в другие текстовые
строки. Этот метод кодирования условно можно назвать x2. Поскольку x2
строит упорядоченные бинарные деревья, то найти его можно, поискав вызовы
функции qsort.
Кроме того, в уже декодированной строке есть два
комплиментарных дайджеста (aid и bid) - 11-байтных хеша, основанных на
идентификаторе пользователя и текущем системном времени.
(aid,bid)=function (id,time ()). Поскольку все кодировки в СБ основаны на
случайных числах, которые инициализируются srand (time ()), то вызов srand
() в начале функции безошибочно покажет вам, где идет кодирование или
создание дайджестов. Справедливости ради отметим, что, кроме x2 кодировки
и создания aid-bid дайджестов, в программе есть еще пару кодировок - в
частности, для шифрования файла настроек.
Стохастический анализ пригодится вам для генерации
правдоподобного трафика. Естественно, если вы будете каждую минуту
зарабатывать балл, это вызовет подозрение. Чтобы получить "красивый"
трафик, придется наблюдать собственную работу по категориям (например,
"активная работа в браузере", "просто работал", "выключен" и т.д.),
проследить плотность обмена в каждом режиме и само чередование режимов
(дважды или трижды модулированная вероятность) - и тогда ваш эмулятор
сможет работать в полностью автономном режиме под Linux.
Короче, как можно понять из всего сказанного выше, людям
здравомыслящим принадлежит весь мир, но зато остальным - все остальное.
Так что выбор есть.
Автор: Арсений Чеботарев
Источник: www.comizdat.com
|