Авторские статьи Недостатки форматной строки.

Discussion in 'Статьи' started by Kerny, 19 Nov 2009.

  1. Kerny

    Kerny Member

    Joined:
    18 Nov 2009
    Messages:
    37
    Likes Received:
    9
    Reputations:
    1
    Недостатки форматной строки.

    Строка форматирования в языке С++, и ему подобных используется множеством программистов, по всему миру, вообще это одно из самых удобных средств форматирования выводимых данных, но и за удобство приходится платить, как и все остальное это решение придумано и реализовано человеком, а посему не лишено недостатков (как сказали в одном фильме: «Наше совершенство в нашем несовершенстве»)

    http://ru.wikipedia.org/wiki/Printf

    printf - обобщённое название семейства функций или методов стандартных или широкоизвестных коммерческих библиотек, или встроенных операторов некоторых языков программирования, используемых для форматного вывода — вывода в различные потоки
    значений разных типов, отформатированных согласно заданному шаблону. Этот шаблон определяется составленной по специальным правилам строкой (форматной строкой).



    Немного о вводе.

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

    char user[13];
    char pass[13];
    gets(&user[0]);
    gets(&pass[0]);


    Фрагмент кода кажется безопасным и правильным, но на самом деле его выполнение может привести к критическим последствиям, как минимум отказ в обслуживание, как максимум захват контроля на ПК, который использует уязвимое приложение(если оно сетевое, и выше приведенный фрагмент кода , есть ничто иное как просьба авторизоваться), все дело тут в том что функция Gets, никак не проверяет количество символов введенных пользователем, для переменных выделяется по 13 байт, а если пользователь вводит больше символов, чем запланировано, то они выходят за границы буфера и затирают находящиеся за ним значения (Другие переменные, иногда даже участки кода, если user и pass, объявлены локально, то они хранятся в стеке, а как следствие, можно затереть и адрес возврата из функции, что позволяет, выполнить атаку Срыв стека). Ошибка тут вовсе не программиста, а непосредственно разработчиков функции (не сделали проверки).


    Вот аналог выше приведенного кода на Delphi (Object Pascal)

    var
    user:string[13];
    pass:string[13];
    begin
    readln(user);
    readln(pass);
    end;

    Этот код полностью безопасен в отличие от своего собрата на языке С++, функция которая читает введенные пользователем данные(readln) получает в регистре ecx значение размера буфера, затем сохраняет его и сравнивает с количеством введенным пользователем символов, и берет только первые символы пока не кончится буфер. Что - бы защититься от данной «проблемы» достаточно использовать безопасный аналог функции gets, а именно fgets.


    char user[13];
    char pass[13];
    fgets(&user[0],13,stdin);
    fgets(&pass[0],13,stdin);


    Защита от переполнения функции Read/Readln в Delphi

    [​IMG]


    Читаем стек.

    Теперь перейдем непосредственно к форматной строке:

    #include "stdafx.h"
    int main()
    {
    char str[17];
    fgets(&str[0],17,stdin);
    printf(&str[0]);
    }

    С первого взгляда все в порядке, но тут кроется одна большая неприятность, если запустить
    проект и ввести в поле ввода %x %x %x, то в консоль выведется вовсе не это, а 241fe4 12f7bc 7ffdd000


    (У вас результат может отличаться и даже должен, это как повезет)

    [​IMG]


    Обратим внимание на текст, который мы вводим, вообще — то он эквивалентен записи:

    printf(“%x %x %x”);

    Откомпилируйте проект и снова увидите нечто подобное первому результату, получается что мы как — бы «обманываем программу» (не находите, очень похоже на sql – инъекцию)

    %x — говорит о том, что нужно достать значение из стека, и записать в буфер, а из буфера вывести на экран (ну а вообще-то переводит переменную типа int в строку).

    Нормальный расклад таков:

    printf(“%x %x %x”,a,b,c);

    Где а,b,c – некоторые переменные, тогда на экран будут выведены значения этих переменных.

    Состояние стека при нормальном раскладе:​

    [​IMG]


    Погрузившись в отладчике в printf, мы понимаем, что произошло, когда мы не передали ни каких аргументов, но передали три спецификатора, это значит ей нужно распечатать три аргумента, она «не знает», что аргументов ей не дали и поэтому достает их с того места где по ее мнению они должны быть.

    [​IMG]




    Состояние стека, когда аргументы не были переданы:

    [​IMG]

    На месте Случайных данных, должны быть переменные, но мы их не передали, следовательно, там «валяется» разная информация - значение других переменных, адреса возврата, и т.д. Таким образом, мы получаем возможность читать значения в стеке, где при чрезмерном везение оказываются пароли и всякая полезная для взломщика информация.

    Читаем код программы.

    Помимо %x, у printf существует еще огромная куча спецификаторов, один из них %s, спецификатор %s указывает на строку, то есть в переменной хранится адрес, нате данные, которые нужно вывести, запустите предыдущий пример и введите %s. Посмотрим, что произошло.

    [​IMG]

    Мы считали данные по адресу 7C910208, потому что функция «подумала» - это аргумент. А теперь задумаемся, что если — бы мы могли заменить этот аргумент, на свой собственный, например на адрес точки входа в программу? Да мы получили бы код программы до первого попавшегося ноля, т.к ноль завершает строку, но это не всегда возможно, как повезет.

    Перезапись ячейки памяти.

    Спецификатор %n, для этого как раз и предназначен, запускайте и вводите, 2222%%x%x%x%x%n, %x - «возьмут на себя» те ячейки, в которые запись запрещена, если все же попытаться записать туда, то произойдет исключение и программа «упадет», мы же записываем по адресу 12ff50, а туда запись разрешена...

    [​IMG]

    Спецификаторы очень опасная вещь, хотя и очень удобна в использование, в Object Pascal ничего подобного нет и от этого программирование на нем уже более безопасно, вместо спецификаторов там используются функции, например строка

    printf(“%x ”,a);

    На Delphi выглядит так:

    write(inttostr(a));

    Можно понять, что тут используется функция inttost(), и по-моему такое решение гораздо удачливее. Как же защититься? Можно, например, фильтровать вводимые пользователем данные, как это делается в случае с sql-инъекцией, но это было - бы полным идиотизмом, лучше

    printf(str);

    Заменить на

    printf(”%s”,str);

    И все будет ок...

    Сегодня мы рассмотрели недостатки строки формата, конечно с помощью них врядли удастся получить контроль над удаленной тачкой, но то что они становятся верными помощниками и друзьями взломщика сомневаться не приходится.

    sa-sec.org
    (с)Kerny​

    Жду ваших отзывов.

    p.s Здесь я не сравнивал Delphi и C так, что базар по этому поводу не разводить.
     
  2. _nic

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

    Joined:
    5 May 2006
    Messages:
    651
    Likes Received:
    54
    Reputations:
    3
    Автор путает С и С++ ,в последнем есть такая штука как #include <iostream>
     
  3. Kerny

    Kerny Member

    Joined:
    18 Nov 2009
    Messages:
    37
    Likes Received:
    9
    Reputations:
    1
    языке С++, и ему подобных