
int со значением 3.malloc. Растет вверх.EBP.ESP указывает на вершину стека, EIP на следующую исполняемую команду, EBP на базу кадра функции. Локальные переменные удобно адресовать относительно EBP.EIP. Стек при этом не очищается, указатель просто сдвигается.EIP хранит только один адрес, поэтому адрес возврата обязательно лежит в стеке. Если программа записывает данные за пределами своего буфера, она может испортить сохраненный адрес возврата, и тогда процессор перейдет по постороннему адресу. Защита это запрет исполнения команд из секций данных: в 64-битных системах и на процессорах с NX-битом это включено по умолчанию, а в 32-битных системах его нужно включать вручную через DEP.Полный текст первых минут лекции «Стек и Регистры процессора» из видеокурса PracticU – субтитры с временной меткой для каждого абзаца. Так можно прочитать и найти нужные темы ещё до начала обучения.
Добро пожаловать на лекцию про стэк и регистры. На этой лекции мы поймем, из чего составлен этот стэк, как он пополняется, какие у нас есть регистры в процессоре.
Мы это прямо увидим с помощью дебагера от латчика. Это будет очень интересная лекция. Давайте только напомним, что во время запуска программы, во время запуска экзешника, у нас сначала копируется код программы.
Вот этот вот текст, они его называют текст-сегмент или код-сегмент. Он копируется внутрь памяти оперативной, чтобы компьютер работал быстро.
И там сидит в принципе экзешник, то есть команды на машинном коде, на языке ассемблера, можно сказать. И процессор именно с этого места, с этого региона памяти запускает все команды, исполняет их.
И этот сегмент в read-only находится с правами доступа read-only и execute, то есть право исполнения. Далее, выше него находятся сегменты с памятью, то есть данными.
Есть разные виды данных. Первые данные, которые сидят сразу после кода, это данные фиксированного размера, которые доступны глобально для всей программы, для всех функций в этой программе.
И у них фиксированный размер и заранее известный. Например, переменная val, это ее название, ее могли назвать любым другим именем. назвали val от слова value наверняка значение просто какая-то переменная ячейка в памяти которая обозначается именем val и у него уже есть значение поэтому это
называется инициализированные данные у нее значение 3 в момент ее создания в нее уже сразу же записывается значение 3 потом можно будет поменять во время программы во время исполнения и самое важное что у нее есть фиксированный размер. То есть нам заранее известно размер и значение этой переменной. И
размер это int. int это значит 32 бита и это могут быть цифры. 32 бита это 32 единички нуля. Из них можно сделать 4 миллиарда вариаций, то есть 4 миллиарда разных чисел. Но в случае integer мы делаем, используем его от минус 2 миллиардов до плюс 2 миллиарда. Это
вот случай интеджера. Если вам не нужно такое количество битов. Если вам не надо 32 бита, не надо 4 байта в памяти, вы можете попросить другой тип данных, и тогда вам выделится меньше памяти и меньше будут траты.
Но мне самое главное вам показать, что в этом отсеке памяти, в этом разделе памяти, в этом сегменте памяти сидят переменные с фиксированным размером и заранее известным значением. То есть в момент инициализации, в момент создания переменной
у нее уже есть значение. А в BSS, который выше этого, следующий отсек, сидят тоже переменные с фиксированным размером, но у них нету значения во время инициализации, и в них вставляется нолик. Это называется Uninitialize, то есть не инициализированные
данные. У них фиксированный размер, но они не имеют значения в момент создания. Далее мы переходим на хип. В хипе сидят списки, динамичные списки, то есть списки, которые могут увеличиваться во время работы программы. Например, моя программа принимает из клавиатуры новых учеников в класс. И я не знаю, как программист, сколько учеников будут
в классе, кто запустит мою программу. И мой список все время растет и, например, увеличивается. И тогда нам нужна переменная, которая будет указывать на этот список, и она будет расти по мере надобности.
И для этого у нас есть динамичная память, размер которой не фиксирован заранее. И мы используем команды как malloc, чтобы взять еще кусок динамичной памяти. Это называется heap.
И эта динамичная память растет кверху, то есть с нулевых адресов кверху растет, растет, растет. И сколько будет использовано, столько будет использовано.
И теперь следующее, что я вам хочу рассказать, это про стек. Стек это память, которая предназначена для функций, для вспомогательных функций, как ее локальная память.
Например, у меня в программе на сайте есть вспомогательная функция для главной программы, которая говорит, проверь для этого номера паспорта, его абонемент еще действительен или нет.
Или другая функция, которая помогает главной программе проверить для этого абонемента, для этого номера паспорта, этот фильм разрешен или нет.
То есть главная программа вызывает вспомогательные куски кода, которые называются функции. Они там что-то делают и возвращают ответ главной программе.
Преимущество функции то, что, во-первых, все поделено на логические блоки и не одной кашей, не одними макаронами. То, что называется, все подряд идет, а все поделено на логические блоки.
Если мне надо в двух разных местах проверить абонемент, мне не надо писать код проверки абонемента по номеру паспорта 2, 3, 5 раз.
У меня есть функция, я каждый раз ее вызываю, даю ей номер паспорта, и она мне говорит, этот абонемент ок или не ок.
Он просроченный или можно ему смотреть фильмы. Вот это вот маленькие программы, эти маленькие функции. У них есть свои переменные, свои локальные переменные, своя локальная память свой черновик такой и вот этот вот черновик где он находится в памяти вот это вот память как раз называется стеком
они как раз сидят на этом оперативном стеке мы его прямо сейчас увидим внутри отладчика я вам его прямо покажу сейчас в реальной жизни хотел чтобы вы поняли что функция то мини программка у которой есть своя локальная память по окончанию функции
надо ее очистить и она обслуживает главную программу и выдает ей там какой-то ответ более Для этого функция может в середине своей работы вызвать другую функцию, чтобы она ей помогла там что-то сделать.
Давайте перейдем на настоящий Windows, на десятку, запустим дебаггер, отладчик и посмотрим, что происходит в настоящей памяти. Я запускаю Immunity Debugger, это бесплатный отладчик, бесплатный дебаггер, вы его можете скачать.
И мы что делаем с помощью этого дебаггера? Мы ему говорим, открой какую-то программу и после каждой его команды, после каждой команды, которую ты делаешь, возвращайся ко мне в отладчик.
То есть отладчик говорит, например, запускай команду, там, программу Notepad, но не запускай ее одним махом, а запускай каждую строчку отдельно, каждую команду отдельно и после каждой команды возвращайся ко мне в отладчик.
Я хочу посмотреть, что происходит в памяти. Вот мы загрузили Notepad, мы видим здесь список всех команд, который внутри этого экзешника, то есть мы смотрим сейчас на память, на этот машинный код, который загрузился с этого notepad, с этого блокнота в память нашего компьютера,
в оперативную память, и мы видим, что происходит. И мы видим первое, что я смотрю на CPU, видите? Это view, это вид на процессор, и здесь есть все команды.
У нас есть разные виды, я сейчас не буду переходить в другие и вас путать. Нас сейчас интересуют команды, мы хотим посмотреть на этот view CPU. И вот список всех команд.
Видите, E8 это то, что написано в памяти его. И расшифровывается этот E8, это значение E8, как команда call на такую-то функцию. То есть вызови такую-то функцию, которая находится в такой-то ячейке памяти.
Видите, она находится в ячейке памяти FF20C2. То есть вызови вспомогательную функцию с этой ячейки памяти. И мы здесь можем видеть то, что у нас есть колонка с оригинальными числами.
То есть i8 это то, что написано в экзешнике. И его интерпретация, и его перевод на языке ассемблера, чтобы нам было легче на это смотреть.
Это как бы такая расшифровка называется disassembler. И левая колонка это адрес памяти, в которой сидит эта команда. То есть команда i8, которая на языке ассемблера расшифровывается как код, это кодировка.
Она сидит в памяти, в нашей операционной памяти, по адресу ff1860. Запомните этот адрес 1860, потому что мы сейчас будем вызывать функцию и потом возвращаться обратно.
После того, как мы вызовем функцию, нам надо будет вернуться обратно с этой функцией и продолжить исполнение нашей главной программы.
Видите, она сейчас находится на паузе, то есть мы можем перезагрузить ее сначала. Он загружает все в память и перед тем, как делать первую команду, переходит на паузу.
То есть он все загрузил, готов к исполнению первой команды. И он ждет, чтобы мы ему дали команду либо play, либо сделать только один шаг.
Я не делаю play, я хочу сделать пошагово и посмотреть, что происходит внутри памяти после каждой команды в экзешнике. Так мы видим, что у нас адрес памяти 1860, и в нем есть команда call, вызвать функцию по адресу ff20c2.
Это адрес, который сидит от кода этой функции. Мы сейчас пройдем пошагово и посмотрим, что происходит в регистрах, что происходит в памяти, что происходит в стеке.
Мы с вами рассмотрим, как работает компьютер, и потом поймем, в чем проблема, и как хакеры этим воспользовались.
Это называется атака Buffer Overflow. Так, давайте сейчас вернемся к нашим делам. Нам надо сейчас запустить эту функцию, но нам надо запомнить, куда вернуться, потому что когда мы ее закончим, нам надо знать, куда вернуться.
И для этого у нас есть регистры и есть стэк. Вот справа мы видим вот эти вот регистры. Видите, процессор регистры. Это 64-битный процессор, и регистры – это маленькие его помощники.
Это маленькие разделы в памяти, в которых сидят информация, которая очень помогает процессору. Это его правая рука, это его помощники. Сюда он кладет результат своих вычислений.
Сюда он кладет ту информацию, которая ему нужна. Это как рабочий стол для вас. И эти регистры, их совсем немножко, и они вплотную рядом с памятью.
Например, видите адрес 1860, это где сделать следующую команду? Это прописано в регистре EIP. Регистр EIP указывает процессору, какую следующую команду сделать.
И вот, заметьте, что он всегда будет указывать этот регистр EIP на следующую команду. И процессор идет туда, в эту ячейку памяти, которая прописана в EIP, и исполняет команду, в этом случае команда E8 call на такую-то функцию.
Давайте сейчас вот нажмем на эту стрелочку, видите, это Step Into, то есть сделать один шаг, включая вход в функцию. То есть если есть вызов функции, сделать один шаг и войти в эту функцию, если она вызывается.
Кстати, рядом есть Step Over, это значит сделать один шаг, но если есть функция, быстро-быстро всю функцию исполнить и вернуться на главную программу.
То есть я не заинтересован смотреть пошагово внутри функции. В моем случае я заинтересован пошагово смотреть внутри функции, я нажимаю Step Into.
И заметьте, мы сейчас перепрыгнем на адрес ff20c2. Мы прямо на него перепрыгнем, то есть мы с адреса 1860 перепрыгнем на этот адрес. Но не просто перепрыгнем, а вызовем функцию.
И вот мы действительно перепрыгнули на этот адрес ff20c2. Это адрес в памяти, и здесь первая команда какая? Поместить в ECX в значение, которое есть вот в таком-то отделе памяти.
То есть мы в реестр ECX хотим поместить какое-то значение из памяти. Команда Move это копировать. Но меня сейчас интересует EIP. Реестр EIP.
Этот регистр, видите, указывает теперь на этот следующий адрес. Далее у нас есть команда Push ESI. Что это за Push ESI? Перед тем, как мы перейдем к ней, давайте предыдущую команду посмотрим.
Видите, ECX копировал то, что было в памяти 4.2.1.0. Видите, реестр ECX поменял свой цвет. Потому что у него значение поменялось, и мне дебаггер показывает то, что изменилось другим цветом.
Я сейчас ищу в дампе памяти, то есть в списке адресов памяти, вот этот 42100, ff42100. И вот я вижу, что в этом 42100 написано 11f0e3d4.
И вот это вот значение должно было скопироваться в реестр ECX. И заметьте, что он действительно скопировался только в обратном порядке.
И это что-то новое, что мы видим в первый раз. что данные копируются в обратном порядке в память. Почему это происходит?
Потому что мы работаем по стандарту, который называется Little Indian. По этому стандарту самые маленькие числа, например, 11, видите, самое маленькое число, записывается в самый маленький отсек памяти.
То есть нижний адрес несет себе нижнее число. Но с нашей точки зрения это выглядит наоборот. Есть стандарт Big Indian, и там это все выглядит намного лучше, как мы привыкли. Короче, знайте, что всегда то, что в памяти, сидит в обратном порядке по байтам.
Каждые две числа – это отдельный байт, и это сидит в обратном порядке. Так, мы с вами рассмотрели первую команду move внутрь реестра памяти. Мы увидели, что реестр поменялся. Это маленькая память рядом с процессором.
И мы увидели, что с главной памяти, вот с этого адреса в главной памяти, был скопирован в реестр какое-то значение. Еще стоит заметить, что он мне пытается помочь, и для каждого байта, вот для каждых двух цифрок, он пытается их расшифровать по стандарту ASCII.
То есть, ну это мне совершенно не помогает в этом случае, просто он пытается. Это в конце концов просто числа, которые сидят в памяти. Это оперативная память, да, это RAM.
Давайте только немножко это все увеличим, и мы можем, кстати, сказать ему по 16 байтов показывать мне. То есть он мне показывает сейчас восьмиричные системы измерения, то есть в 8 байтах, а я могу ему сказать, покажи мне в 16 байтах.
Видите, у нас есть теперь 16 байтов в каждой строчке, и адреса идут 10, 20, 30, потому что это эксодоцимальные, все числа в эксодоцимальной системе измерения. То есть у нас есть адрес и значение.
Я могу ему сказать, покажи мне это в каждой строчке на экране, там, например, Unicode, расшифруй это по Unicode, по стандарту Unicode.
Я могу ему сказать, возьми эти чиселки из памяти и расшифруй их там по стандартному, например, экзешника, как будто бы это хедер экзешника, то есть команды, как будто это команды для исполнения, как вот здесь вот наверху есть.
То есть это вся память, это в конце числа, и вопрос, как их расшифровывать. Например, я сделаю disassemble, и он меня их превратил в команды, то есть расшифровал по стандарту, каждое число значит какая-то команда.
То есть все зависит от нашего стандарта, как мы его расшифровываем. И в конце концов в памяти сидят просто числа. Это могут быть данные, это могут быть команды.
Все зависит от как мы их понимаем, по какому стандарту мы их расшифровываем. И внизу вот мы видим этот hexdump, то есть просто эти числа в памяти.
А наверху мы видим эту же память, уже расшифрованную по стандарту команд. Так, давайте перезагрузимся. Начнем сначала.
Так что у нас мы начинаем сначала, включили отладчик notepad и первая команда call И заметьте адрес, который у нас сейчас 1860, это наш текущий адрес И мы вызываем функцию по адресу F20C2 И вот у нас сейчас EIP указывает на текущую команду
Какие у нас еще есть интересные регистры У нас есть еще регистр ESP, который указывает нам на стэк, на адрес стэка Мы сейчас это все увидим с вами.
И внизу у нас есть еще флажки. Флажки – это результаты вычислений. Вот эти вот флажки – это, например, результат вычисления, был ли ноль или не был нулем.
Вышел он из границ разрешенного или не вышел из разрешенных границ. То есть вот эти вот флажки – это результаты разных сравнений и вычислений. Мы это скоро тоже увидим.
То есть у нас есть регистры, у нас есть флажки. Сказать, по правде, есть еще много чего, но мы сейчас не на уроке ассемблера. Я вам хотел показать, что такое регистры, как они работают, и мы рассмотрим 5 регистров, и рассмотрим стэк и обычную память.
Смотрите, что если я нажму на память, она превратится в доллар. Что значит доллар? Доллар это означает база. Вот я хочу считать этот адрес в памяти базовым, и тогда относительно него, я могу говорить относительно этой базы, возьми приплюсуй 5, приплюсуй 10.
То есть работать не с абсолютными числами, а с относительными числами. Это называется offset. То есть относительно какой-то базы я работаю.
И я могу решить, что база будет наверху, и тогда у меня моя текущая команда будет база плюс два, offset плюс два. И вот этот вот базовый адрес, он часто используется.
Потому что наша программа может сидеть каждый раз в другом отсеке памяти, в других числах. Никто нам не гарантирует, что это всегда будет одни и те же адреса памяти.
Так, и у нас есть EBP, видите, EBP, который указывает на начало, на базу для нашей текущей функции, для нашего текущего стека. Еще раз, у нас есть стек.
Стек это наш черновик, отдел памяти, в котором сидят переменные нашей функции. Вот мы сейчас зайдем в функцию, у нас откроется этот черновик, этот стек.
И в этом стеке, видите, сейчас адрес, давайте я вам сейчас покажу, какой у него адрес, здесь плохо видно, вы все поймете, не беспокойтесь. Видите, ESP указывает на адрес 53FEDC.
Вот этот вот адрес, это там, где стек, вот внизу мы его прямо и видим. 53FEDC, это адрес памяти, в котором сидят данные стека. На данный момент там что-то сидит в этом стеке, и в тот момент, когда мы перейдем внутрь функции, мы в этот стэк, в этот отдел памяти, положим число во время вызова функции, положим число не просто так, а в которое
нам надо вернуться потом. То есть видите стэк, заметьте, DC был, да, вот этот DC, и он взял и поднялся наверх на D8, то есть стэк уменьшается каждый раз, каждый раз, когда мы кладем в него число, он уменьшается, его адрес становится меньше, это все адреса в памяти.
И он сохраняет, заметьте, вот этот вот 1865, то есть команда call функции положила в стэк адрес возврата, в который надо прийти по окончанию этой функции.
Так функция знает, куда перейти потом, когда мы вернемся. Видите, и мы вернемся на 1865, а сейчас мы перепрыгнули на адрес 20c2. Давайте запустим все заново и еще раз посмотрим.
Видите, мы находимся в адресе 1860, и нам надо по окончанию функции, по окончанию кола, вернуться на 1865. Поэтому адрес возврата мы положим в стэк, чтобы знать, куда вернуться.
Стэк – это отдельный регион памяти, как бы наш черновик такой, в который мы кладем вещи, кладем информацию, потом забираем информацию. И на этот стэк всегда указывает регистр ESP, который указывает на адрес этого стэка.
Видите, мы прыгнули, сделали команду call, и в наш стэк попал адрес 1865, это адрес возврата. Я вам показываю, что происходит во время вызова функций.
И заметьте, что, кстати, адрес ESP тоже поменялся, то есть хедер стэка. Давайте я вам еще раз продемонстрирую. Я делаю каждый раз стрелочку налево и демонстрирую.
Мы начали с 1860. Вызываем функцию на ff20c2, эту функцию, которую мы вызовем. По окончанию этой функции мы должны вернуться на адрес 1865.
Чтобы не потерять место, куда нам надо вернуться, потому что у нас в регистре и EIP есть место только для одного адреса, чтобы EIP, который указывает на следующую команду, не потерялся, мы его сохраним в стеке.
Видите, в адресе D8 сохранился адрес возврата на 1865. Почему это надо? Потому что в компьютере в EIP, где исполняются следующие команды, То есть в регистре EIP есть место только для одного адреса.
И он может как бы забыть, откуда он пришел. Поэтому EIP прыгает внутрь функции и сохраняет внутри стека адрес для своего возврата в главную программу.
Далее мы видим команду move ECX. Видите, этот ECX мы уже рассматривали. Сейчас у нас, видите, стек дальше идет по нулям и он уменьшается. Его адреса уменьшаются, потому что стек растет в минус, то есть он уходит к нулевым адресам.
И мы видим команду push ESI. Что такое push ESI? Сохранив стэк, push в стэк, вот это вот команда на языке ассемблера, номер у него, видите, системный 56, push ESI, то есть сохранив стэк то, что есть в реестре ESI.
Почему он это сохраняет? Потому что он хочет испортить реестр ESI, сохранить там новое значение. Посмотрите, там через 2-3 строчки там есть новое значение зайдет в этот ESI.
И нам надо по возврату из этой функции все восстановить обратно. Мы не хотим главной программе портить ее регистры. Поэтому мы сохраняем значение регистра в стэк, то есть в свой черновик.
Потом, когда мы закончим все делать и перед тем, как возвращаться, мы возвратим и восстановим это значение ESI. Давайте посмотрим, как это все происходит.
То есть мы делаем push ESI. Вот наш ESI. Где он? Вот он. Видите? FF1860. Мы его прямо видим здесь. Это вот значение этого реестра, этого помощника, этой памяти, которая рядом с центральным процессором.
И она должна зайти вот в этот адрес FAD4. Давайте исполним одну команду и увидим, что действительно в FAD4 поместится значение, которое есть в ESI.
Вот этот вот 1860. И также ESP должен сдвинуться. ESP это первая значимая верхушка стека. Видите, он указывает ESP на FAD8. Вот этот FAD8 указывает процессору, где сейчас находится стэк, какой адрес в памяти.
Мы видим, что это вот этот вот адрес. Мы исполняем одну команду. Мы видим действительно, что ESP сдвинулся. Видите, у него другой цвет стал. И 1860 попал в этот адрес памяти, где были до этого нули.
Видите, то есть стэк сдвинулся вверх. Голова стэка сдвинулась вверх. Видите, он изменился, поэтому он таким цветом стал. Все, что изменяется в команде во время исполнения команды, меняет свой цвет.
И он поменял свой цвет, значит стэк сдвинулся наверх. Если вы помнили, до этого он был D8. И в него сохранилось значение.
И EIP сдвинулся на следующую команду 20C9. И следующая команда какая? Сохранить EDI. Следующая команда говорит ему сохранить