Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:c-tricks-16h [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-tricks-16h ======
 +<​sub>​{{c-tricks-16h.odt|Original file}}</​sub>​
 +
 +====== сишные трюки\\ (16h выпуск) ======
 +
 +крис касперски ака мыщъх, a.k.a. souriz, a.k.a. nezumi, no-email
 +
 +**сегодня у нас несколько необычный выпуск. своеобразный юбилей. если перевести номер в шестнадцатеричную систему (забыв о том, что он _уже_ записан в ней), мы получим число 10****h****,​ в "​круглости"​ которого сомневаться не приходится. ошибка?​! конечно! вот и поговорим об ошибках,​ которые только с виду ошибки,​ а на самом деле интересные хакерские трюки, срывающие крышу даже опытным программистам! короче,​ мы немного похулганим… не вздумайте показывать описанные трюки ни преподавателям,​ ни работодателям!!!**
 +
 +===== трюк #​1 –возврат указателей на локальные переменные =====
 +
 +Рассмотрим следующий (см. листинг 1) исходный код, вполне типичный для начинающих,​ и попробуем ответить — что в нем неправильно?​
 +
 +char *foo(int a, int b)
 +
 +{
 +
 +char buf[69];
 +
 +
 +
 +if (a - b) strcpy(buf,"​nezumi"​);​ else strcpy(buf,"​souriz"​);​
 +
 +**return buf;****// <<<​ ****трюк?​ или… ошибка?​ или все-таки трюк?​!**
 +
 +}
 +
 +main(int c, char **v)
 +
 +{
 +
 +char *s, *p; if (c < 3) return 0;
 +
 +**s = foo(atol(v[1]),​ atol(v[2]));​**
 +
 +
 +
 +if (strcmp(s, "​souriz"​)) p = "​japlish";​ else p = "​franglais";​
 +
 +printf("​%s - it's %s\n",​s,​ p);
 +
 +
 +
 +******// ****мы не освобождаем ****s****, т.к. она указываем на локальную переменную**
 +
 +}
 +
 +Листинг 1 _рабочий_ пример с возвратом указателя на локальную переменную
 +
 +Ага! Уже раздаются крики: //​**возвращать указатели на локальные переменные (строка **////​**"​return buf"​ **////​**выделенная полужирным) ни в коем случае нельзя,​ поскольку они автоматически уничтожаются при выходе из функции**//​. Это же в каждом букваре по Си написано! Ну сколько можно говорить…
 +
 +Хм, тогда кто рискнет объяснить почему же несмотря ни на какие буквари,​ данный код стабильно работает независимо от версии компилятора и совместим со всеми операционными системами из линейки NT, Linux, BSD?!
 +
 +Фокус в том, что при завершении функции локальные переменные не уничтожаются,​ а освобождаются. Указатель стека опускается вниз и они оказываются в свободной зоне, которую может использовать кто угодно,​ например,​ обработчик аппаратного прерывания,​ однако,​ NT, Linux и BSD сконструированы так, что на стек потока никто не покушается — только он сам. При возникновении прерывания регистры сохраняются на стеке ядра. Стек потока остается в неприкосновенности,​ а потому после завершения функции содержимое пользовательского стека не может быть "​стихийно"​ разрушено (к тому же каждый поток имеет свой стек и друг другу они не мешают). Исключение составляет 9x, "​засоряющей"​ пользовательский стек без его ведома и согласия,​ что, кстати говоря,​ осложняет разработку некоторых видов exploit'​ов.
 +
 +Естественно,​ при вызове любой функции,​ сохранность освобожденных переменных уже не гарантируется и тут все зависит от того сколько стекового пространства "​кушает"​ очередная вызываемая функция,​ причем,​ некоторые функции могут вызывается неявно (мало ли что захочется воткнуть в код компилятору!),​ к тому же стек активно используется для временного сохранения регистров,​ заталкиваемых туда компилятором. То есть, гарантий,​ что освобожденные переменные не будут уничтожены у нас все-таки нет, однако,​ если предпринять ряд предосторожностей,​ то риск не так уж и велик. Стек растет вверх, а локальные буфера вниз. Выделяя локальный буфер с запасом хотя бы в пару килобайт мы на 99% обезопасим себя от затирания актуальных данных.
 +
 +Конечно,​ в "​промышленном"​ коде подобные трюки недопустимы и нужно выделять память из кучи (благополучно забывая ее потом освободить),​ но… возврат указателей на локальные переменные во многих случаях происходит по ошибке и такие ошибки могут годами дремать в коде, неожиданно пробуждаясь при модификации программы или перекомпиляции другим компилятором или с новой версией такой-то библиотеки (скажем,​ одна из библиотечных функций увеличила свою потребность в стеке и стала затирать освобожденные переменные,​ приводя программу к краху, источник которого зачастую не так-то просто обнаружить).
 +
 +===== трюк #2 – выделение памяти из стека =====
 +
 +Учебники по Си упоминают о трех основных типах памяти,​ доступных программисту:​ автоматическая стековая память,​ динамическая память (куча) и статическая память (секция данных). Автоматическая память хороша тем, что гарантированно освобождается компилятором по выходе из функции,​ исключая возможность утечек,​ однако,​ стековый кадр формируется в момент вызова функции и потому размеры локальных буферов задаются на стадии компиляции,​ что не позволяет обрабатывать данные заранее неизвестного размера,​ к тому же мы не можем (легальным образом) возвращать указатели на автоматические переменные материнской функции. Куча снимает эти ограничения,​ но перекладывает заботы по освобождению памяти на плечи программиста и малейшая небрежность ведет к трудноуловимым утечкам. Статическая память наследует худшие черты кучи и стека — размеры буферов задаются на стадии компиляции и не могут быть увеличены во время исполнения программы.
 +
 +Но есть еще и четвертый тип памяти,​ о котором умалчивают учебники. Это память — лежащая выше указателя стека. Почему бы ее не использовать для хранения динамических данных?​! Естественно,​ со всеми предосторожностями,​ упомянутыми выше. А ниже приведен код функции,​ выделяющей заданное количество килобайт стековой памяти и возвращающей указатель на обозначенный блок памяти:​
 +
 +char* stack_alloc(int s_z)
 +
 +{
 +
 +char buf[1024]; if (s_z) return stack_alloc(s_z - 1);return buf;
 +
 +}
 +
 +Листинг 2 динамический стековый аллокатор (упрощенный "​макетный"​ вариант)
 +
 +Несколько замечаний по ходу. Во-первых,​ никакой это не аллокатор,​ поскольку реального выделения памяти не происходит и она остается свободной. Повторный вызов функции "​выделит"​ новый блок поверх старого (естественно,​ при желании этот недочет легко обойти,​ передав функции базовый адрес с которого начинается "​выделение"​ очередного блока).
 +
 +Во-вторых,​ размер выделенного блока всегда чуть больше требуемого,​ т. к. в стеке кроме буфера сохраняются регистры и адреса возврата,​ но это не есть проблема. Напротив,​ определенный запас по размеру снижает риск "​стихийного"​ затирания данных.
 +
 +В-третьих,​ оптимизирующие компиляторы наверняка избавятся и от хвостовой рекурсии и от реально неиспользуемого буфера buf, а потому данная функция никакой памяти выделять вообще не будет и вернет указатель черт знает на что (точнее сказать невозможно,​ это уже от типа компилятора и ключей компиляции зависит!). Значит,​ нужно переписать функцию так, чтобы компиляторы не смогли "​развернуть"​ рекурсию и не трогали буфер buf (для этого достаточно "​загрузить"​ его работой по хозяйству,​ имитируя бурную деятельность).
 +
 +И последнее — не стоит принимать стековой аллокатор всерьез. Это шутка! Но иногда она оказывается очень полезной ("​заложить"​ ее в "​промышленном"​ коде перед увольнением с работы,​ чтобы кому-то потом сильно аукнулось — не предлагать).
 +
 +===== трюк #3 – неявная инициализация стековых переменных =====
 +
 +А вот этот трюк можно использовать для запутывания кода, что полезно при создании защитных механизмов. Идея заключается в следующем:​ вызываем функции foo(), которая что-то записывает в _свои_ собственные локальные переменные,​ а потом завершается. Указатель стека опускается,​ но содержимое самих переменных остается нетронутым. Если теперь запустить функцию bar(), то в _ее_ локальных переменных (неинициализированных,​ конечно) окажутся значения,​ оставленные функцией foo().
 +
 +В большинстве случаев это происходит по ошибке,​ но если немного подумать и все рассчитать — лучшего трюка для скрытой передачи данных,​ пожалуй,​ и не придумать. Основная сложность в том, что мы не можем управлять размещением переменных в стеке. Обычно компиляторы располагают их в порядке обращения к ним (не объявления!) при этом часть переменных попадает в регистры,​ а часть нет. Другими словами,​ если у нас больше одной переменной — жди проблем или же… закладывайся на особенности поведения конкретной версии компилятора с заданным набором ключей трансляции.
 +
 +Приведенный ниже код достаточно надежен и дружит с оптимизаторами,​ правда для этого пришлось круто извратится с глобальными переменными,​ расплачиваясь наглядностью кода, зато теперь можно быть на 99% уверенным,​ что компилятор не создаст никаких "​служебных"​ локальных переменных,​ смещающих кадр стека — ведь нам надо добиться,​ чтобы переменная buf функции bar() легла в аккурат поверх переменной buf функции foo(), но увы, никакие извращения не дают 100% гарантии. Компилятор — это черный ящик и никто не знает, что у него на уме.
 +
 +int a, b, c, d;
 +
 +#define S "​nezumi has you!\n"​
 +
 +// функция foo() инициализирует переменную buf,
 +
 +// а затем завершает свое выполнение
 +
 +foo()
 +
 +{
 +
 +char buf[0x60]; d = strlen(S);
 +
 +for (a = 0; a <= d; a++) { c = S[a]; buf[a] = c; }
 +
 +return buf[a];
 +
 +}
 +
 +// функция bar(), вызываемая следом за функцией foo(),
 +
 +// объявляет переменную buf и выводит ее на экран,
 +
 +// "​подхватывая"​ содержимое,​ оставленное в стеке
 +
 +// функцией foo(), создавая иллюзию того, что
 +
 +// переменная buf не инициализирована
 +
 +bar()
 +
 +{
 +
 +char buf[0x60];
 +
 +printf(buf);​
 +
 +}
 +
 +main()
 +
 +{
 +
 +foo(); bar();
 +
 +printf("​***\n"​);//​ если убрать этот вызов то оптимизатор
 +
 +// можем заменить call bar на jmp bar,
 +
 +// что сдвинет стековый фрейм функции bar
 +
 +}
 +
 +Листинг 3 рабочий пример с неявной инициализацией локальных переменных
 +
 +