Авторские статьи Игры со стабом

Discussion in 'Статьи' started by А®ТеS, 16 Oct 2007.

  1. А®ТеS

    А®ТеS Active Member

    Joined:
    25 Nov 2006
    Messages:
    198
    Likes Received:
    193
    Reputations:
    41
    [size=+2]Игры со стабом[/size]

    [size=-2]О оптимизации DOS stub'a в PE32 файлах[/size]​

    • 1. Зачем?
    • 2. Как?
    • 3. Реализация
    • 4. Заключение и благодарности добрым людям :)

    1. Зачем?
    Для начала давайте уясним что такое DOS-stub (также вы можете встретить имена "Old-exe заголовок", "DOS2Header", etc... Смысл не меняться). Буквально "stub" переводиться как "огрызок", но как всегда Microsoft все переиначило и с буквальным переводом реальное значение не имеет ничего общего. Когда разрабатывался формат PE (Portable Executable - Переносимый Исполняемый), стал вопрос о back-совместимости с DOS'овской средой, чтобы exe'шники под Windows могли корректно исполняться и под DOS'ом (точнее сказать корректно завершаться =)). Рассмотрим вообщем как выглядит обычный PE файл:

    IMAGE_DOS_HEADER - эта стуктура как раз и есть наш DOS совместимый заголовок, подробнее расмотрим позже

    IMAGE_NT_HEADERS - PE заголовок, включает в себя сигнатуру PE/x0/x0 и 2 структуры - IMAGE_FILE_HEADER & IMAGE_OPTIONAL_HEADER

    IMAGE_SECTION_TABLE - таблица секций (таблица объектов, object table)

    ...

    other section and directory - вообщем то остальная PE'шка, для нашей задачи не очень важна

    ...

    Теперь о том, что происходит когда файл запускаеться под DOS'ом:

    Загрузчик видит привычный для себя заголовок и начинает исполнять DOS-программку (скорее всего вызов int 21h, но говорят что встечаються экзотические софтины, где есть версия программы под ДОС). Про то, что дальше идет валидный файл для Win32 он не знает (да ему это нафиг не нужно :)). Я очень часто повторяю слова DOS-заголовок, но мы так его и не рассмотрели, давайте исправим этот факт:

    Code:
    IMAGE_DOS_HEADER STRUCT
      e_magic           WORD      ?
      e_cblp            WORD      ?
      e_cp              WORD      ?
      e_crlc            WORD      ?
      e_cparhdr         WORD      ?
      e_minalloc        WORD      ?
      e_maxalloc        WORD      ?
      e_ss              WORD      ?
      e_sp              WORD      ?
      e_csum            WORD      ?
      e_ip              WORD      ?
      e_cs              WORD      ?
      e_lfarlc          WORD      ?
      e_ovno            WORD      ?
      e_res             WORD   4 dup(?)
      e_oemid           WORD      ?
      e_oeminfo         WORD      ?
      e_res2            WORD  10 dup(?)
      e_lfanew          DWORD      ?
    IMAGE_DOS_HEADER ENDS
    
    Рассмотрим подробнее основные поля структуры:

    e_magic - это сигнатура "MZ", именно по ней ориентируеться загрузчик Windows что это валидная PE'шка и если там этой сигнатурки не будет, то в различных платформах загрузчик ведет себя крайне неоднозначно. Кстати, многие интересуютсья почему именно MZ? Ответ прост - это инициалы некоего Mark Zbinovski - главного архитектора PE формата... Ничего не скажешь, мужик увековечил себя в народной памяти :))).

    e_cparhdr - размер заголовка в параграфах (1 параграф == 16 байт)

    e_csum - контрольная сумма. Всем и всегда было на нее глубоко наплевать

    e_lfanew - самое важное для нас поле, хранит смещение к PE заголовку (забегая вперед скажу - загрузчик Windows из всей структуры IMAGE_DOS_HEADER интересуеться только двумя полями - e_magic & e_lfanew).

    у тебя наверное назрел вопрос - что я тут собрался оптимизирвать и главное зачем? ИМХО лучше один раз увидеть, чем 100 раз услышать и поэтому мы сейчас напишем минимальное приложение в MASM32 и рассмотрим его под микроскопом HIEW'a. Тогда все станет ясно. Итак, за дело, вот исходник, берем и компилируем:

    Code:
    .386
    
    .model flat, stdcall
    
    option CaseMap : none
    
    INCLUDE \masm32\include\kernel32.inc
    INCLUDE \masm32\include\user32.inc
    INCLUDE \masm32\include\windows.inc
    
    INCLUDELIB \masm32\lib\kernel32.lib
    INCLUDELIB \masm32\lib\user32.lib
    
    .code
    start:
    invoke MessageBox, 0h, NULL, NULL, MB_OK
    invoke ExitProcess, 0h
    end start
    
    Эта простейшая программуля должна выполнять WIN32API функция MessageBoxA() (впринципе можно и без нее, но для наглядности необходимо). Теперь берем в наши захапущие лапы HIEW и открываем софтинку... Если нет HIEW'а, то смотрим на картинку ниже:
    [​IMG]

    Во первых, DOS-стаб занимает довольно много места (для своей ничтожной функции), а во-вторых (красный прямоугольник) добрый линкер от M$ запихал в стаб ID железа!!! (естественно не в плэйн текстом =)). Должен сказать что Borland недалеко ушел вперед и инфу о линкере (детально версию и т.п.) тоже можно найти в стабе. По моему не есть гуд, что в любой момент любой дятел может узнать чем собрана PE'шка (а в некоторых случаях это фатально) и тем более если ты написал супер-пупер малваря, то ID железа автора в нем не будет большим плюсом :). Можно сделать просто и почистить все вручную, например так:
    [​IMG]

    Но это как то не по хекерски :), да и размер остаеться прежним (хотя программа работает как ни в чем не бывало и иногда так поступить можно). Вообщем, то что в ряде случаев так оставлять ни в коем случае нельзя я вас надеюсь убедил, и поэтому мы плавно переходим ко второй части.

    2. Как?

    Основой для этой части будет утверждение, что для Windows загрузчика важны только два поля DOS заголовка - e_magic (сигнатура "MZ") и e_lfanew (смещение к реальному PE заголовку). Впервые такая идея появилась в далеком 2001 на http://assembler.ru/. Суть ее вот в чем:
    В PE заголовке полным полно не используемых полей, почему бы не передвинуть его к началу файла так, чтобы DWORD по смещению 0x3C (поле e_lfanew) попадал на какое нибудь из этих полей, которые даром никому не нужны? Ведь это очень просто. Оптимальным вариантом здесь являеться использование поля Base Of Data:

    Описание поля из руководства по PE формату от Hard Wisdom'a.

    Тогда начало PE заголовка будет всего лишь на 0xC от начала файла и DOS-стаб будет занимать всего лишь 12 байт (против 128 и более дефолтовых). Однако не все так сладко, размер ФАЙЛА нам трогать никак нельзя, он должен остаться таким, каким был до отпимизации. Это вызвано двумя факторами - файл может содержать какой нибудь оверлей с жескто прописанным оффсетом к нему и есть такое понятие как файловое выравнивание (File Alignment) заморачиваться с которым мне не хочеться :). Итак, вот какие плюсы мы получаем от оптимизации:

    - Затираються сигнатуры чего-бы там ни было
    - Если в DOS заголовке сидит какая нибудь дрянь (имееться ввиду малварь) то мы его почистим
    - После таблицы секций появляеться больше (на размер e_lfanew - 0xC байт) свободного места (как я говорил размер файла мы должны оставить прежним) и туда можно записать какую либо свою дрянь =).

    Еще небольшой нюанс - если мы аккуратные люди (а мы ведь аккуратные люди), то мы должны отредактировать еще два поля PE заголовка - Size Of Headers (общий размер заголовков, вычисляеться как PE Header Size + DOS2Header Size + Object Table Size) и File Checksumm (контрольная сумма файла, которая меняеться, расчет новой контрольной суммы выходит за рамки данной статьи и поэтому мы просто ее обнулим). Если мы не сделаем вышеперечисленного, то впринципе ничего страшного не произойдет, но если нарвемся на какого нибудь дотошного юзера, которого хлебом не корми дай контрольную сумму сравнить, или на какойнибудь сильно злой антивирус, который решит пересчитать ее самостоятельно то мы можем сильно встрять. Впрочем это крайность, врядли это случиться.

    3. Реализация

    Языком реализации я сделал C#. Почему? Просто он мне нравиться :). Итак, исходник в студию:

    Code:
    using System;
    using System.IO;
    
    #region intro
    /*
    Оптимизация DOS 2 Header'a до 0xC байт с подитранием сигнатур линкера и прочей вредной инфы которую туда записали. 
    Размер файла при этом не меняеться (что немаловажно, если к файлу прилеплен оверлей), при этом появляеться больше (на размер старого стаба - 0xC байт) пустого места за таблицей секций.
    (С) king Artas 2007 year
    Grtz: Аленка, =HALK=, XSP, Shturm0vik, Dronga, Draco, All Antichat, All WAsm.Ru, All phreak.ru, All http://Assembler.Ru (RIP)
    Contact to me: ICQ 903521, private message to user A®TeS on / or user A®TeS on http://phreak.ru/
    */
    
    #endregion intro
    
    namespace StubOptimizer
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.Out.WriteLine("DOS2Header optimizer (C) king Artas 2007 year\r\n");
                try
                {
                    BinaryReader FromStream = new BinaryReader(new FileStream(args[0], FileMode.Open, FileAccess.Read));
                    BinaryWriter ToStream = new BinaryWriter(new FileStream(args[1], FileMode.OpenOrCreate, FileAccess.Write));
                    Console.Out.WriteLine("Streams maked...");
                    FromStream.BaseStream.Seek(0x3C, SeekOrigin.Begin);
                    uint PEHeaderOffset = FromStream.ReadUInt32();
                    Console.Out.WriteLine("Offset to PE header is {0}", PEHeaderOffset);
                    FromStream.BaseStream.Seek(PEHeaderOffset + 0x54, SeekOrigin.Begin);
                    uint SizeOfHeaders = FromStream.ReadUInt32();
                    Console.Out.WriteLine("Size of Headers (DOS stub + PE header + Object Table) is {0}", SizeOfHeaders);
                    FromStream.BaseStream.Seek(PEHeaderOffset, SeekOrigin.Begin);
                    ToStream.Write((ushort)0x5A4D);
                    ToStream.BaseStream.Seek(0xC, SeekOrigin.Begin);
                        for (uint i = 0; i < SizeOfHeaders - PEHeaderOffset; i++)
                        {
                            ToStream.Write(FromStream.ReadByte());
                        }
                        for (uint i = 0; i < PEHeaderOffset - 0xC; i++)
                        {
                            ToStream.Write((byte)0);
                        }
                        FromStream.BaseStream.Seek(SizeOfHeaders, SeekOrigin.Begin);
                        for (uint i = 0; i < FromStream.BaseStream.Length - SizeOfHeaders; i++)
                        {
                            ToStream.Write(FromStream.ReadByte());
                        }
                    ToStream.BaseStream.Seek(0x3C, SeekOrigin.Begin);
                    ToStream.Write((uint)0xC);
                    //Теперь обнулим чексумм и поставим правильный размер заголовков
                    FromStream.BaseStream.Seek(PEHeaderOffset + 0x54, SeekOrigin.Begin);
                    ToStream.BaseStream.Seek(0x60, SeekOrigin.Begin);
                    ToStream.Write((uint)FromStream.ReadUInt32() - PEHeaderOffset + 0xC); //Самому мне расчитывать размер заголовков фпадлу, я беру старый размер и провожу нехитрые арифметические операции. Работает только при условии что старый размер установлен правильно (как правило это так).
                    ToStream.Write((uint)0);
                    Console.Out.WriteLine("\r\nComplete\r\n");
                }
                catch (Exception e)
                {
                    Console.Out.WriteLine(e.Message + "\r\n" + e.ToString());
                    return;
                }
            }
        }
    }
    
    Здесь думаю пояснять уже нечего... Кто внимательно читал теорию, тот без труда разбереться. З.Ы. Чтобы не отвлекаться от сути приложение сделал консольное.

    Заключение.
    [​IMG]
    Вот результат трудов наших праведных :) (это все тот же МАСМовский "хеллоуворлд", но уже оптимизированный). Конечно далеко не всегда нужно так поступать, и если ты написал например калькулятор, то тереть сигнатуры вовсе нет нужды. НО, даже если вы нигде не станете использовать полученные знания (и все таки дочитали до этих строк), то хочу сказать, что практика никогда не бывает лишней, порой в жизни пригождаеться то, что казалось бы нафиг не нужно было вчера.

    Я подумал, что чем отвечать на кучу дурацких вопросов в теме, лучше подумать над ними заранее и написать ответы =). Вот что у меня получилось:

    Q: Что такое HIEW?
    A: Шестнадцатеричный редактор, имеет кучу полезных фичей для работы с PE/NE форматами

    Q: Где взять компилятор MASM'a?
    A: MASM32 - это некоммерческий проект, все можно скачать с http://masm32.com/ совершенно бесплатно

    Q: Как скомпилировать программу написанную на C#?
    A: Как минимум поставь .NET Framework и воспользуйся csc.exe. А еще лучше юзай Microsoft Visual Studio.

    Q: Я все сделал и отнес программу к другу Васе, но там она не работает, в чем дело?
    A: Скорее всего на компьютере Васи не установлен .NET Framework

    Q: Я скомпилировал, но что делать дальше?
    A: Программа консольная. Если заметил потоки создаються с именами файлов взятых из командной строки. 1 агрумент - имя оптимизируемового файла, 2 - конечный файл. Например можно запустить ее так: c:/programms/stuboptimizer.exe source.exe dest.exe

    Q: Будет ли работать эта фича с PE64?
    A: Нет, не будет. В PE64 все указатели 64-битные и все оффсеты другие, тут нужно думать отдельно.

    Пока что на ум больше не приходит, что может быть непонятным, вообщем если что не ясно - берете книжки и в бой. Если заинтересовались PE форматом, то рекомендую почитать руководство от Hard Wisdom'a (найти можно на Краклабе), уроками от IceZelion'a (найти можно на WASM.RU), книги Криса Касперски и вообще лучший друг и советчик - Google :).

    А теперь начинаеться раздача слонов, точнее приветов:
    • Благодарю Shturm0vika, именно из за него я начал заниматься данной темой.
    • Благодарю http://assembler.ru/ - некоторые идеи позаимствованны оттуда.
    • Спасибо Аленке, просто за то что есть.
    • Grtz: =HALK=, XSP, Draco, Dronga, Pete®, All Antichat, All WASM.Ru and other good ppl =).

    (C) Artes 2007 year
     
    1 person likes this.
  2. Ni0x

    Ni0x Elder - Старейшина

    Joined:
    27 Aug 2006
    Messages:
    338
    Likes Received:
    157
    Reputations:
    37
    Зачем такой изврат? Неужто не знаешь, что можно слинковать программу со своим стабом без всяких извращений со сторонними программами. На примере msvc:
     
    2 people like this.
  3. А®ТеS

    А®ТеS Active Member

    Joined:
    25 Nov 2006
    Messages:
    198
    Likes Received:
    193
    Reputations:
    41
    Знаю. ПОзволь процитировать тебе уже упоминвашийся текст от http://assembler.ru/ частично разумееться:
    до 12 байт ты стаб свой по любому не сократишь, к тому же я сказал - практика =).