Язык программирования C/C++ и ему подобные, можно по праву назвать «высокоуровневым ассемблером», благодаря их гибкости и свободе. Но у чрезмерной свободы существуют и свои недостатки, следует неустанно следить, за тем, чтобы свобода одного не мешала свободе другого. Именно поэтому на программистов «свободных»(C/C++/Assembler)языков ложится все бремя ответственности за правильный ход выполнения программы (в других языках программирования за многим следит компилятор и не позволяет программисту допускать ту или иную ошибку). Сегодня мы разберем уязвимости, к которым может привести неправильное использование указателей и ссылок. Указатель представляет из себя, адрес определенной переменной в памяти, на которую он указывает. Такой подход во многом упрощает программирования, экономит «такты процессора», позволяет более быстро обращаться к большим участкам памяти, без их копирования. Приведем пример на языке C с использованием указателей, да и перейдем сразу к делу: Листинг программы на C Листинг программы на C Итак, по коду не сложно догадаться, что программа выведет в консоль число 25. Давайте скомпилируем данный пример, дабы убедиться в этом на практике. Представляю, как удивляться некоторые из вас, увидев, что p=10. Могу вас уверить, код выполнился, как ему и было положено, просто он содержит грубую ошибку, сейчас мы рассмотрим ее поподробнее. Для этого нам нужно забраться в самое сердце программы, взглянуть на нее в дизассемблированном виде. Код, который будет приведен ниже, немного исправлен мной, для лучшего понимания. Листинг дизассемблированной программы Листинг дизассемблированной программы В данном примере все кроется в глобальных и локальных переменных. Глобальные переменные, это такие переменные которые «видны» всей программе сразу, к ним можно обратиться из любой функции или процедуры, они инициализируются при запуске программы. Локальные переменные, видны только той процедуре или функции, в которой они объявлены. Локальные переменные инициализируются при запуске той или иной функции, место для них выделяется в стеке: Code: PUSH [COLOR=Cyan]EBP[/COLOR] MOV [COLOR=Cyan]EBP[/COLOR],[COLOR=Cyan]ESP[/COLOR] SUB [COLOR=Cyan]ESP[/COLOR],4 Выделяем 4-байта (одна переменная типа integer). После выполнения функции или процедуры, кадр стека закрывается (leave): Code: MOV [COLOR=Cyan]ESP[/COLOR],[COLOR=Cyan]EBP[/COLOR] POP [COLOR=Cyan]EBP[/COLOR] Но суть не в этом, а в том, что закрывая кадр стека, мы уничтожаем все переменные. То есть фактически, в памяти они остаются и не обнуляются, но обратиться к ним нельзя. Если раньше до уничтожения мы присвоили адрес переменной указателю (после уничтожения объекта, он станет висячим указателем), то обратиться к ней можно. Но никто не гарантирует, что ее значение останется прежним, ведь программа уже не учитывает, что место занято и может спокойно перезаписать значение на любое другое (как случайное, так и не очень). После выполнения функции test(), как раз это и произошло, P указывает на тот адрес, где раньше была переменная test. После, когда мы вызвали функцию test2(), открылся новый кадр стека, как раз в том же месте где существовал кадр стека для функции test(), и мы присвоили X значение 10, а X находился по тому же адресу, по которому когда-то располагалась переменная test. Таким образом, мы и получили, в итоге 10. Такого рода ошибка, называется Висячим указателем. Висячий указатель - указатель, ссылающийся на уже удалённый объект. Чем чревата подобная ошибка, спросите вы? Первое, если бы я продолжал использовать (проводить арифметические и другие действия) над указателем на test, думая, что test у меня равна 25, то в итоге программа выдавала бы просто непредсказуемые результаты, вплоть до полного отказа ее работы. Второе, при выполнение некоторых условий, можно сознательно манипулировать значением переменной, а вдруг такая ошибка будет находиться в особо важном фрагменте кода, например в функции авторизации пользователя, а я смогу изменить значение ключевой переменной и авторизироваться без пароля! Замените функцию test2() на: Code: [COLOR=DeepSkyBlue]int[/COLOR] test2() { [COLOR=DeepSkyBlue]int[/COLOR] x; scanf([COLOR=DarkRed]“%d”[/COLOR],&x); } И убедитесь сами. Для того чтобы исправить ошибку нужно объявить переменную test глобально, или же использовать так называемые умные указатели. Умный указатель— класс (обычно шаблонный), имитирующий интерфейс обычного указателя и добавляющий некую новую функциональность, например проверку границ при доступе или очистку памяти. Говоря попроще умный указатель, уничтожает сам себя, как только уничтожается объект, на который он указывает, что препятствует появлению висячих указателей. По мимо указателей существуют еще и понятие ссылки на объект. По своей сути они очень похожи на указатели, ссылка, можно сказать второе имя переменной(псевдоним), по которому к ней можно обращаться, но она не хранит адреса в отличие от указателя и считается более безопасной, но это не спасает ее от существования такого понятия, как висячие ссылки. А не спасает вот почему, ради чистого любопытства я решил проверить одну из своих догадок, и написал два экземпляра кода, с использованием указателей и ссылок: Листинг программы на C Листинг программы на C Скомпилировав, первый и второй варианты, затем дизассемблировав их, посмотрел, какой код выходит в итоге: Листинг дизассемблированной программы Листинг дизассемблированной программы Причем в первом и втором случае ассемблерный код оказался совершенно одинаковым! Это говорит о том, что на низком уровне ссылки и указатели это одно и тоже. Отличия можно наблюдать лишь на высоком уровне, дело в том, что если вы попробуете использовать ссылку, как обычной указатель, то компилятор просто откажется компилировать, хотя на уровне ассемблерных команд реализация ссылок и указателей одинакова. Листинг неправильный код Листинг неправильный код Я думаю именно потому, и существуют – «висячие ссылки». Нужно быть предельно осторожным при проектирование своих программ и особое внимание уделять указателям и ссылкам, ведь падение или неправильное выполнение программы еще не самое страшное, что может случиться, это еще одна, дополнительная лазейка для взломщика, которая может помочь ему в осуществление коварных планов. 13.11.2009 ©Kerny SASecurity gr.
ЯзыкИ программирования C И C++ и ИМ подобные. Это совершенно разные языки, и в них общего почти ничего нет.
я вот только не понимаю зачем так всё расписывать? И так ясно что все локальные переменные расположены в стеке и следовательно верны до тех пор пока процедура / функция не завершит свою работу. Т.е. после завершения работы процедуры адрес остаётся валидным, но значение может быть уже любым. 2 nerezus ты так говоришь как будто сравниваешь ассемблер с бейсиком. в C И C++ очень много похожего. И практически всегда С программа может быть скомпилированная как С++. А вот С++ прога не всегда может компилиться как С Различия есть но не настолько уже и серьезные, если судить по синтаксису.
Ну, это тебе ясно, но смысл был в том, что бы показать это на низком уровне, что бы все поняли, как в памяти затирается переменная.
D тоже обратно совсестим с C, значит что, теперь писать C/D/C++? ) Разного ГОРАЗДО больше, чем общего. Из общего ТОЛЬКО часть синтаксиса.
А что, старый Borland Pascal или Delphi (и прочие алголоподобные языки) по мнению автора не поддерживают работу с указателями?
Поддерживают, но я не рассматривал их, у меня нет сомнений если бы я привел в статье пример для delphi или другого языка, то тут началась бы дискуссия, что мол автор сравнивает языки, что лучше делфи или си.