Универсальный прототип функции

This post is also available in: Английский

Еще один шаг на пути к фаззингу произвольной внутренней функции программы. Правильный прототип функции — точки начала фаззинга, нужен для того, что бы осуществить ее правильный вызов. Под правильным прототипом понимается такой прототип, который позволит передать функции параметры, так, что бы функция смогла их обработать. Так же важно, что бы после вызова функции стабильность программы не пострадала.

__stdcall, __cdecl, __fastcall, __pascal и т.д. Распространенных способов вызова функции не много, но никто не может гарантировать, что в исследуемой функции будет использован именно он. Для __fastcall (который не так и редко встречается) вообще не оговаривается однозначно, какие регистры используются[]. Встречал я в дизассемблированном коде даже __usercall. Это вообще вызов, который не описывается никакими общедоступными правилами.

Итак, восстановить прототипы такими, какие они были до компиляции не получится. В исполняемом коде не достаточно для этого данных. Но это и не нужно. Достаточно определить следующие характеристики функции:

  • Кто освобождает стэк (вызывающая функция или вызываемая)
  • Сколько параметров передается через стэк
  • Какие регистры задействованы для передачи параметров

В принципе, описание функции в любой из convention содержит именно информацию из выше представленного списка. Информацию о признаках функции можно описать, например, двойным словом:

12222222333333333333333333333333

, где каждый бит имеет следующее значение:

1 — какая функция освобождает стек ( 1 — вызываемая, 0 — вызывающая )

2 — количество параметров, передаваемых через стек ( 7-ми битное число)

3 — карта использования регистров. Регистр используется в качестве параметра, если установлен бит. Соответствие битов регистрам такое же как и BeaEngine.

Следующий вопрос — как из подобной информации построить прототип, пригодный для использования в коде, выполняющем фаззинг. Можно использовать способ, описанный ниже. Рассмотрим на примере. Пусть о функции известно следующее:

  1. функция получающет на вход 5-ть параметров.
  2. Стек освобождается в функции.
  3. Первый параметр передается через ecx, второй через  edx
  4. Параметры 3-5 предаются через стек

В бинарном виде то же самое:

0x83000006

Для генерации кода ( для последующего ручного связывания («линковки»)  ) можно использовать автосгенерированный из информации о функции «интерфейс».

int __cdecl foo__ ( int b, int c, int d )
{
    __asm {
    jmp fooaddr;
    }
}

// Функция принимает 5-ть параметров. 1-ый через ecx, 2-ой через edx.
// Параметры с 3-го по 5-ый передаются через стек
int __cdecl foo ( int a, int b, int c, int d, int e )
{
   // Поместим нужные параметры в регистры
   __asm {
          mov ecx, a
          mov edx, b
   }

   // Остальное через стек
   foo__( c, d, e);
}

Во время компиляции не известен адрес fooaddr. Его значение нужно заполнить во время связывания. Описанная функция изначально была написана как __fastcall. По этой причине первые два параметра передаются через ecx и edx. Однако предложенный прототип эквивалентен. Параметры передаются через регистры и через стек. Стек очищает вызываемая фукния.

Все описанное выше еще требует тщательной проверки и предоставлено здесь для обсуждения для того что бы как можно раньше узнать о том, что я возможно делаю не верно. Так что предложения и критика приветсвуюется.

Вот немного ссылок по function conventions ( Спасибо Ивану Судакову ): [1], [2], [3], [4].

Комментарии 6

  • Реализовал автоматическое построение прототипов. Получилось следующее. Один параметр через стек:

    Dataflow:Dataflow, (-,426) GetPrototypeInfo(): Registers than has not been initialized before use: 00000000( )
    Dataflow:Dataflow, (-,442) GetPrototypeInfo(): Func: sub_110AA, func free call: 0, stack params: 1

    int __stdcall sub_110AA__( int a )
    {
    __asm{
    jmp 0x4110aa
    }
    }

    int __stdcall sub_110AA( int a )
    {
    __asm{
    }

    sub_110AA__( a );
    }

    Четыре параметра через стек:
    Dataflow:Dataflow, (-,426) GetPrototypeInfo(): Registers than has not been initialized before use: 00000000( )
    Dataflow:Dataflow, (-,442) GetPrototypeInfo(): Func: sub_111EF, func free call: 1, stack params: 4

    int __stdcall sub_111EF__( int a, int b, int c, int d )
    {
    __asm{
    jmp 0x4111ef
    }
    }

    int __stdcall sub_111EF( int a,int b,int c,int d )
    {
    __asm{
    }

    sub_111EF__( a, b, c, d );
    }

    Вместо линковки во время загрузки сразу же вставил адреса. Позже проверю можно ли это скомпилировать и запустить 🙂

  • про __fastcall немного неточно. Для каждого компилятора соглашение своё, но четко описанное. К примеру для ms cl -> http://msdn.microsoft.com/ru-ru/library/6xa169sk.aspx. Вроде сводную таблицу по фастколлу можно найти у КК в книге философия дизассемблирования.
    Но это никак не спасет, тк четкой идентификации компилятора не получится, да и __usercall настоящая проблема. С возвращаемыми значениями я как понял вы не заморачивались? Впрочем это и не нужно в контексте задачи.
    Кстати. самое интересное вы не написали. Как на данный момент вы идентифицируете параметры переданные через регистры? Просто запоминаете по ходу анализа состояния регистров? Но это слишком труднозатратно по памяти, да и общие проблемы построения cfg типа непрямых переходов и вызовов функций затрудняет решение проблемы, а обратное дизассемблирование в общем случае невозможно.

    • Да, определение регистров-параметров было самым сложным местом. Если коротко, то я определял входные параметры каждого блока ( об этом я писал http://artem.ufoctf.ru/?p=230 ), строил матрицу достижимости блоков. Если в блоке используется не инициализированный регистр, то смотрю из каких блоков в него может прийти управление. Если ни в одном таком блоке не было инициализации регистра, то считается, что регистр инициализирован за пределами функции. Т.е. это параметр. Более подробно опишу, когда всю схему реализую.

  • тьфу. Написал, а потом вспомнил, что вам надо только восстановить прототип, а не искать, что туда передается =) В принципе необходимо отыскать лишь использование неинициализированных регистров в функции.
    Да кстати, вполне возможно в прототип вносить и данные о глобальных переменных, потому что это ещё одни вид передачи данных в функцию, что в свете вашей проблематики я думаю важно =)

    • По поводу глобальных переменных согласен. У меня была такая мысль. Они действительно являются данными из вне. Интересно, что тот же Hex-rays отслеживает использование глобальных переменных. Вот к примеру начало одной стандартной функции, которая была прикреплена cl к моему коду:

      .text:00411A60 sub_411A60 proc near ; CODE XREF: start_0+Ap
      .text:00411A60
      .text:00411A60 var_2C = dword ptr -2Ch
      .text:00411A60 var_28 = dword ptr -28h

      .text:00411A60
      .text:00411A60 mov edi, edi
      .text:00411A62 push ebp
      .text:00411A63 mov ebp, esp
      .text:00411A65 push 0FFFFFFFEh
      .text:00411A67 push offset unk_416B50
      .text:00411A6C push offset sub_41106E
      .text:00411A71 mov eax, large fs:0
      .text:00411A77 push eax
      .text:00411A78 add esp, 0FFFFFFE4h
      .text:00411A7B push ebx
      .text:00411A7C push esi
      .text:00411A7D push edi
      .text:00411A7E mov eax, dword_41702C
      .text:00411A83 xor [ebp+var_8], eax

      Видно, что eax сначала eax используется для своих целей, а потом в нее загружается значение глобальной переменной. Hex-rays интересно этот интерпретирует:

      signed int __usercall sub_411A60(int a1)

      Не могу объяснить такой выбор.

      На глобальных переменных я пока решил не заморачиваться, потому как нужен анализ указателей … ввобщем, пока что не хочется. И без того есть что развивать.

      • Из-за особенностей встроенного ассемблера cl пришлось изменить прототипы. Теперь они выглядят так:

        int ( __cdecl *fzsub_11014__)( void ) = ( int ( __cdecl * ) ( void) ) 0x411014;

        inline int __cdecl fzsub_11014( int a,int b,int c,int d,int e,int f )
        {
        __asm{
        mov EAX, a
        mov ECX, b
        mov EDX, c
        mov EBX, d
        mov ESI, e
        mov EDI, f
        }

        fzsub_11014__( );
        }

        int ( __cdecl *fzRaiseException__)( void ) = ( int ( __cdecl * ) ( void) ) 0x8181ec;

        inline int __cdecl fzRaiseException( void )
        {
        __asm{
        }

        fzRaiseException__( );
        }

        Так же пришлось прилепить префикс fz. Иначе есть конфликты со стандартными функциями.

Добавить комментарий для izlesa Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *