Сначала PUSH затолкнёт в стек значение 401256. Далее, следующий за ним RET воспримет это значение как адрес возврата из текущего CALL, хотя это вовсе не так, и передаст управление на адрес 401256. Таким образом, этот код отработает точно также как JMP 401256.
Далее мы рассмотрим ещё один пример использования CALL/RET. Перезагружаем крэкми CrueHead'а. "Go to" - "Expression" - вводим 401364.
По этому адресу нажимаем F2, устанавливая таким образом точку останова (об этом мы подробно поговорим позже). Olly прервёт исполнение как только начнёт выполняться инструкция по адресу, на который установлена точка останова.
Адрес выделяется красным цветом и это значит, что точка останова (BREAKPOINT) установлена. Нажимаем на F9 (RUN), чтобы приложение запустилось...
Появилось окно крэкми. Если Вы его не видите, поищите хорошенько через Alt+Tab
Приложение ещё не выполнило код, на который мы поставили точку останова. В окне крэкми вызовите меню и выберите опцию "Help" - "Register".
Появилось окно, через которое нужно ввести имя и серийник.
Пишем что угодно.
Нажимаем OK.
Выскакивает сообщение о том, что нам не повезло, т.е. крэкми не понравились наши имя и серийник (было бы просто удивительно, если бы мы сразу угадали правильные регистрационные данные Если закрыть это сообщение, активизируется наконец наша точка останова.
Мы находимся прямо посреди выполнения кода программы, но кое-какую базовую информацию можем почерпнуть например из стека:
Видим сразу несколько RETURN TO... Видимо, мы находимся внутри процедуры и самый верхний RETURN TO содержит адрес возврата, на который передаст управление ближайший RET. Этот адрес, вероятно, окажется на верхушке стека, перед выполнением RET и управление будет передано на адрес 40124A.
Если присмотреться, можно заметить также, что перед нами уже знакомый код. Но на этот раз он вызван не нами, а в ходе нормального выполнения программы.
Жмём на F8, чтобы добраться до RET, как и раньше. Только в этот раз при выполнении последнего CALL сразу перед RET должно появиться сообщение, которое нужно закрыть, чтобы продолжить трассирование.
Закрываем его.
Вот мы и добрались до инструкции RET и верхушка стека теперь содержит адрес возврата, как мы и предполагали.
В прошлый раз мы выполнили процедуру самостоятельно, принудительно изменив значение EIP. В этот раз мы попали в код процедуры в следствии нормального выполнения программы. Достаточно снова нажать на F9, чтобы программа продолжила выполняться как ни в чём не бывало.
В общем, я хотел показать, что при срабатывании точек останова в процессе свободного выполнения программы можно почерпнуть из стека немало полезной информации. В частности, беглого взгляда по содержимому стека обычно хватает, чтобы узнать откуда была вызвана текущая процедура и куда она вернёт управление после завершения. Если же в стеке видно несколько ячеек с комментарием RETURN TO, то, очевидно, текущая процедура была вызвана из другой процедуры, которая в свою очередь тоже была откуда-то вызвана - это называется вложенностью процедур.
Рассмотрим ещё один закрепляющий пример. Для этого нужно перезагрузить крэкми в отладчике, сразу нажать на клавишу пробела и ввести следующую инструкцию: CALL 401245.
Готово. Далее воспользуемся опцией FOLLOW, чтобы попасть во внутрь функции.
Процедура начинается по адресу 401245 и заканчивается по адресу 401288 (где находится инструкция RETN 10, которая немного отличается от знакомого нам уже RET, но об этом позже). Обратите внимание на вложенный CALL (первая инструкция процедуры является вызовом другой процедуры).
Нажмите МИНУС, чтобы выйти из FOLLOW. Теперь нажмите F7, чтобы войти в процедуру, фактически передав на неё управление.
Вот мы и внутри, о чём свидетельствует значение EIP: оно указывает на адрес 401245.
В верхушке стека хранится адрес возврата к следующей за нашим свежевписанным CALL инструкции.
Адрес возврата имеется, но OllyDbg не выделил его как RETURN TO 401005. Чтобы понять почему он в этот раз не проявил сообразительность, нужно принять во внимание, что мы вписали CALL после того, как OllyDbg выполнил предварительный анализ кода. Представьте себе, что OllyDbg - это Чапаев, пересекающий реку верхом Прямо посреди реки мы модифицируем его скакуна, чем, мягко говоря, дезориентируем Василия Ивановича. Точно также в растерянность впадает и OllyDbg. (В общем, OllyDbg в воде не тонет, но пословица тут не при чём - прим. переводчика ;-) )
Чтоб исправить ситуацию, достаточно нажать на правую кнопку мыши в любом месте окна дизассемблера - "Analysis" - "Analyse code". Таким образом, отладчик снова пробежится по коду анализатором и скорректирует подсказки в стековом кадре.
Теперь адрес возврата в верхушке стека корректно распознан анализатором:
Адрес возврата в данном случае обозначен относительно стартовой точки программы: MODULE ENTRY POINT + 5 = 401000 + 5 = 4010005.
В общем, не забывайте запускать анализатор после внесения изменений в код программы (это касается также изменений кода самой программой). Иногда анализ кода может оказаться ошибочным и его следует сбросить опцией "Analysis" - "Remove analysis from module".
Давайте вернёмся к нашему примеру.
Жмём F7 и "погружаемся" в следующий CALL.
Над предыдущим адресом возврата теперь появился ещё один. В общем случае, между адресами возврата вложенных процедур могут находиться произвольные значения, попавшие в стек в результате промежуточных инструкций PUSH и других операций.
Если Вы попадёте в подобную ситуацию, например, в результате срабатывания точки останова, не имея представления о ходе выполнения программы до останова, взглянув на стек можете сориентироваться:
Ниже в окне дизассемблера виден RET, что наталкивает на мысль о том, что мы находимся внутри процедуры.
Смотрим в стек, чтобы удостовериться:
Сверху вниз первый RETURN TO указывает на возможный адрес возврата из текущей процедуры. В данном конкретном случае - это адрес 40124A.
К тому же, ниже есть ещё один RETURN TO, а это может значить, что текущая процедура была вызвана из другой процедуры.
Второй RETURN TO говорит нам, что в конечном итоге программа должна вернуться к адресу 401005 и непосредственно над этим адресом находится CALL, который инициировал всю эту цепочку вызовов. Кстати, вот он:
Привыкайте реконструировать таким образом цепочки вызовов, т.к. в подобной ситуации вместо 2х вложенных процедур может оказаться, например, 30 и трассировать их индивидуально ни у кого не хватит терпения.
Если что-то непонятно - спрашивайте. До следующей главы! © Рикардо Нарваха, пер. Quantum