Форум АНТИЧАТ

Форум АНТИЧАТ (https://forum.antichat.xyz/index.php)
-   Статьи (https://forum.antichat.xyz/forumdisplay.php?f=30)
-   -   Ссылка в никуда или сломанный указатель. (https://forum.antichat.xyz/showthread.php?t=159802)

Kerny 28.11.2009 20:57

Ссылка в никуда или сломанный указатель.
 
Язык программирования C/C++ и ему подобные, можно по праву назвать «высокоуровневым ассемблером», благодаря их гибкости и свободе. Но у чрезмерной свободы существуют и свои недостатки, следует неустанно следить, за тем, чтобы свобода одного не мешала свободе другого. Именно поэтому на программистов «свободных»(C/C++/Assembler)языков ложится все бремя ответственности за правильный ход выполнения программы (в других языках программирования за многим следит компилятор и не позволяет программисту допускать ту или иную ошибку). Сегодня мы разберем уязвимости, к которым может привести неправильное использование указателей и ссылок. Указатель представляет из себя, адрес определенной переменной в памяти, на которую он указывает. Такой подход во многом упрощает программирования, экономит «такты процессора», позволяет более быстро обращаться к большим участкам памяти, без их копирования. Приведем пример на языке C с использованием указателей, да и перейдем сразу к делу:

Листинг программы на C
Цитата:

#include <cstdlib>
#include <iostream>


int *p; //объявляем глобальный указатель типа integer

int test()
{
int test; //объявляем локальную переменную test
test=25; //присваиваем ей значение 25
p=&test; //присваиваем указателю p, адрес переменной test
}

int test2()
{
int x=10; //объявляем и присваиваем значение локальной переменной X=10.
}

int main()
{
test(); //вызываем функцию test
test2(); //вызываем функцию test2
printf( "*P=%d",*p); //выводим на экран значение, которое находится по адресу в указателе p
return(0);
}
Листинг программы на C

Итак, по коду не сложно догадаться, что программа выведет в консоль число 25. Давайте скомпилируем данный пример, дабы убедиться в этом на практике. Представляю, как удивляться некоторые из вас, увидев, что p=10. Могу вас уверить, код выполнился, как ему и было положено, просто он содержит грубую ошибку, сейчас мы рассмотрим ее поподробнее.


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

Листинг дизассемблированной программы
Цитата:

Функция test:
PUSH EBP ; открываем кадр стека
MOV EBP,ESP
SUB ESP,4 ; Резервируем место для локальных переменных
MOV DWORD PTR SS:[EBP-4],19 ; скопируем 19h=25d по адресу EBP-4 (test=25)
LEA EAX,DWORD PTR SS:[EBP-4] ; сохраняем в EAX адрес EBP-4(по сути адрес пер. test)
MOV DWORD PTR DS:[443010],EAX ; сохраняем содержимое EAX в памяти(p=&test)
LEAVE ; закрываем кадр стека
RETN ; выходим из функции

Функция test2:
PUSH EBP ; открываем кадр стека
MOV EBP,ESP
SUB ESP,4 ; Резервируем место для локальных переменных
MOV DWORD PTR SS:[EBP-4],0A ; скопируем 0Ah=10d по адресу EBP-4 (x=10)
LEAVE ; закрываем кадр стека
RETN ; выходим из функции

Функция main:
PUSH EBP ; открываем кадр стека
MOV EBP,ESP
SUB ESP,18 ; Резервируем место для локальных переменных
CALL Project#.00401390 ; вызываем функцию test
CALL Project#.004013A8 ; вызываем функцию test2
MOV EAX,DWORD PTR DS:[443010] ; в EAX значение указателя p
MOV EAX,DWORD PTR DS:[EAX] ; в EAX содержимое, по адресу в p
MOV DWORD PTR SS:[ESP+4],EAX ; передаем printf значение на которое указывает p
MOV DWORD PTR SS:[ESP], 00440000 ; передаем printf строку *P=%d
CALL PRINTF ; вызываем printf
LEAVE ; закрываем кадр стека
RETN ; выходим из функции
Листинг дизассемблированной программы

В данном примере все кроется в глобальных и локальных переменных. Глобальные переменные, это такие переменные которые «видны» всей программе сразу, к ним можно обратиться из любой функции или процедуры, они инициализируются при запуске программы. Локальные переменные, видны только той процедуре или функции, в которой они объявлены. Локальные переменные инициализируются при запуске той или иной функции, место для них выделяется в стеке:

Код:

PUSH EBP
MOV EBP,ESP
SUB ESP,4

Выделяем 4-байта (одна переменная типа integer). После выполнения функции или процедуры, кадр стека закрывается (leave):

Код:

MOV ESP,EBP
POP EBP

Но суть не в этом, а в том, что закрывая кадр стека, мы уничтожаем все переменные. То есть фактически, в памяти они остаются и не обнуляются, но обратиться к ним нельзя. Если раньше до уничтожения мы присвоили адрес переменной указателю (после уничтожения объекта, он станет висячим указателем), то обратиться к ней можно. Но никто не гарантирует, что ее значение останется прежним, ведь программа уже не учитывает, что место занято и может спокойно перезаписать значение на любое другое (как случайное, так и не очень). После выполнения функции test(), как раз это и произошло, P указывает на тот адрес, где раньше была переменная test. После, когда мы вызвали функцию test2(), открылся новый кадр стека, как раз в том же месте где существовал кадр стека для функции test(), и мы присвоили X значение 10, а X находился по тому же адресу, по которому когда-то располагалась переменная test.


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

Первое, если бы я продолжал использовать (проводить арифметические и другие действия) над указателем на test, думая, что test у меня равна 25, то в итоге программа выдавала бы просто непредсказуемые результаты, вплоть до полного отказа ее работы.
Второе, при выполнение некоторых условий, можно сознательно манипулировать значением переменной, а вдруг такая ошибка будет находиться в особо важном фрагменте кода, например в функции авторизации пользователя, а я смогу изменить значение ключевой переменной и авторизироваться без пароля! Замените функцию test2() на:

Код:

int test2()
{
int  x;
scanf(“%d”,&x);
}

И убедитесь сами. Для того чтобы исправить ошибку нужно объявить переменную test глобально, или же использовать так называемые умные указатели.

Умный указателькласс (обычно шаблонный), имитирующий интерфейс обычного указателя и добавляющий некую новую функциональность, например проверку границ при доступе или очистку памяти.

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

Листинг программы на C
Цитата:

Пример первый:
int main()
{
int a=7; //объявляем переменную типа integer, присваиваем ей значение 7
int* b=&a; //объявляем указатель, присваиваем ему адрес переменной a
}

Пример второй:
int main()
{
int a=7; //объявляем переменную типа integer, присваиваем ей значение 7
int& b=a ;//создаем псевдоним для переменной a
}
Листинг программы на C

Скомпилировав, первый и второй варианты, затем дизассемблировав их, посмотрел, какой код выходит в итоге:

Листинг дизассемблированной программы
Цитата:

MOV DWORD PTR SS:[EBP-4],7
LEA EAX,DWORD PTR SS:[EBP-4]
MOV DWORD PTR SS:[EBP-8],EAX
MOV EAX,0
LEAVE
RETN
Листинг дизассемблированной программы

Причем в первом и втором случае ассемблерный код оказался совершенно одинаковым! Это говорит о том, что на низком уровне ссылки и указатели это одно и тоже. Отличия можно наблюдать лишь на высоком уровне, дело в том, что если вы попробуете использовать ссылку, как обычной указатель, то компилятор просто откажется компилировать, хотя на уровне ассемблерных команд реализация ссылок и указателей одинакова.

Листинг неправильный код
Цитата:

int main()
{
int a=8;
int b=7;
int& c=b ;
*c=&a;
}
Листинг неправильный код

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

13.11.2009


Kerny 29.11.2009 01:48

Зачем перенесли? это вроде, как статья.

Huster 29.11.2009 01:50

Ух ты.. я читал про это на 2-ух сотой странице своей книги

nerezus 29.11.2009 06:46

Цитата:

Язык программирования C/C++ и ему подобные
ЯзыкИ программирования C И C++ и ИМ подобные.

Это совершенно разные языки, и в них общего почти ничего нет.

Kerny 29.11.2009 12:04

Цитата:

Сообщение от Huster
Ух ты.. я читал про это на 2-ух сотой странице своей книги

я очень за тебя рад, что ты хочешь этим сказать?

slesh 29.11.2009 12:05

я вот только не понимаю зачем так всё расписывать?
И так ясно что все локальные переменные расположены в стеке и следовательно верны до тех пор пока процедура / функция не завершит свою работу.
Т.е. после завершения работы процедуры адрес остаётся валидным, но значение может быть уже любым.

2 nerezus ты так говоришь как будто сравниваешь ассемблер с бейсиком.
в C И C++ очень много похожего. И практически всегда С программа может быть скомпилированная как С++. А вот С++ прога не всегда может компилиться как С
Различия есть но не настолько уже и серьезные, если судить по синтаксису.

Kerny 29.11.2009 12:09

Цитата:

Сообщение от slesh
я вот только не понимаю зачем так всё расписывать?
И так ясно что все локальные переменные расположены в стеке и следовательно верны до тех пор пока процедура / функция не завершит свою работу.

Ну, это тебе ясно, но смысл был в том, что бы показать это на низком уровне, что бы все поняли, как в памяти затирается переменная.

nerezus 29.11.2009 14:39

Цитата:

в C И C++ очень много похожего. И практически всегда С программа может быть скомпилированная как С++. А вот С++ прога не всегда может компилиться как С
Различия есть но не настолько уже и серьезные, если судить по синтаксису.
D тоже обратно совсестим с C, значит что, теперь писать C/D/C++? )

Разного ГОРАЗДО больше, чем общего. Из общего ТОЛЬКО часть синтаксиса.

t04 29.11.2009 16:45

А что, старый Borland Pascal или Delphi (и прочие алголоподобные языки) по мнению автора не поддерживают работу с указателями?

Kerny 29.11.2009 17:06

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


Время: 15:19