Содержание статьи
- Беззнаковая арифметика
- Эксперимент с test_unsigned.cpp
- Дополнительный код (Two’s Complement)
- Беззнаковое переполнение (wrap-around)
- Флаги процессора: как CPU принимает решение
- Размеры регистров
- Флаги состояния (status flags)
- CMP — вычитание без сохранения результата
- Возвращаемся к крякми:
SUB—CMP—JBE - Прогон GDB для test
- Прогон GDB для example
- Хакерские приемы с затиранием
CMP - Проверка диапазона через вычитание
- Раскладка веток: JA и JBE
- Сравнение с экспериментальной программой
- Таблица условных переходов
- Почему в крякми именно
JBE - Как быстро определить тип сравнения
- Сохраняем результат анализа в Ghidra
- Поясняем условие в декомпиляторе
- Поясняем инструкции в листинге
- Заключение
SUB, CMP и JBE, проследим за регистрами и флагами процессора в GDB, а затем подтвердим выводы в Ghidra. Заодно разберемся с переполнением, дополнительным кодом и особенностями условных переходов x86-64.Когда Ghidra показывает 5 < , хочется сразу перевести условие на привычный язык. Но одна строка псевдокода не расскажет, что произошло с числом при вычитании и куда ведет переход. Проследим значение от поля std:: до инструкций SUB, CMP и JBE, проверим флаги в GDB и запишем подтвержденный вывод в Ghidra.
Для этого мы возьмем крякми Getting Started Keygen от автора Mazzotti — небольшую программу‑головоломку для Linux x86-64, написанную на C++. Она просит ввести строку, затем число и, если число совпадает с результатом хеш‑функции, поздравляет с победой. Исходного кода нет, отладочных символов тоже. В твоих руках только бинарник, Ghidra и GDB.
В статье «Байт за байтом. Учимся восстанавливать структуры данных с помощью Ghidra и GDB» мы заметили, что декомпилятор показывает в main три переменные: local_78, local_70 и local_68. Они лежали рядом на стеке и участвовали в чтении строки через std::. Мы предположили, что это части одного объекта std::, а затем проверили гипотезу в GDB.
Для проверки мы написали на Python скрипт, который запускал программу со строками разной длины и считывал значения из памяти через GDB. Серия из ста запусков показала изменение емкости 30 — 60 — 120; дополнительная проверка подтвердила значение 240. Мы также увидели, что строки длиной до 15 символов хранятся внутри объекта, а начиная с 16 символов используется память в куче. Так ведет себя примененная здесь реализация std:: из libstdc++. По результатам эксперимента мы восстановили структуру std::, назвали ее std_string и применили в Ghidra.
Если ты не читал предыдущую статью, для дальнейшего разбора достаточно знать главное: после реконструкции у нас появилось поле _string_length, содержащее длину введенной строки. В восстановленной структуре мы задали ему тип unsigned . В исследуемой сборке он занимает 8 байт и хранит только неотрицательные значения. Это результат нашего анализа, а не известное нам объявление из исходного кода — исходников, напоминаю, у нас нет.
Теперь посмотри на псевдокод функции main после реконструкции структуры:
undefined8 main(void){ // ... std_string enter_string; // ... enter_string._string_ptr = (char *)&enter_string._string_storage; enter_string._string_storage._string_buf[0] = '\0'; enter_string._string_length = 0; poVar2 = std::operator<<((ostream *)std::cout,"Enter a string of characters (no spaces): "); // ... std::operator>>((istream *)std::cin,(string *)&enter_string); if (5 < enter_string._string_length - 5) { std::operator<<((ostream *)std::cout,"Bro, what are you trying to do?"); FUN_00101430(); exit(0); } poVar2 = std::operator<<((ostream *)std::cout,"Enter correct number (no spaces): "); // ...}Программа читает строку, а затем проверяет ее длину. Если длина не в правильном диапазоне, программа выводит сообщение «Bro, what are you trying to do?» и завершается. Теперь посмотри на конструкцию if и условие в ней:
if (5 < enter_string._string_length - 5) { ... }До реконструкции структуры Ghidra показывала проверку 5 < , а после реконструкции вместо local_70 появилось поле _string_length. Суффикс U сообщает тип литерала, но смысл всего вычитания зависит и от типа поля.
Сначала проверим беззнаковое вычитание на отдельной программе. Потом вернемся к крякми: сопоставим псевдокод с SUB, CMP и JBE, пройдем участок в GDB и сохраним вывод в Ghidra. Так странное условие превратится в проверку с понятными границами и ветками.
Беззнаковая арифметика
Итак, перед тобой условие:
if (5 < enter_string._string_length - 5) { std::operator<<((ostream *)std::cout,"Bro, what are you trying to do?"); FUN_00101430(); exit(0);}Напомню, что до реконструкции структуры std:: наше условие выглядело так:
if (5 < local_70 - 5U) { ... }В прежнем листинге поле длины обозначалось local_70. После реконструкции объекта мы назвали его _string_length. Вот структура, с которой дальше мы будем читать условие:
#include <cstddef>struct std_string { char* _string_ptr; // Offset 0x00, указатель на данные (8 байт) unsigned long _string_length; // Offset 0x08, длина строки (8 байт) union union_string_storage { char _string_buf[16]; // SSO-буфер для коротких строк (offset 0x10) unsigned long _string_capacity; // Емкость кучи для длинных строк (offset 0x10) } _string_storage;};// Общий размер структуры — 32 байта (0x20)static_assert(sizeof(std_string) == 32, "std_string must be 32 bytes");После реконструкции структуры мы задали полю _string_length тип unsigned . В исследуемой 64-битной сборке он занимает 8 байт и хранит значения от 0 до 2^64 , то есть до 18446744073709551615. Поэтому вычитание _string_length нужно разбирать как беззнаковую арифметику.
Теперь проверим вычитание с восстановленным типом поля. Возьмем небольшое значение, вычтем 5 и посмотрим, что получится. Затем сопоставим результат с машинным кодом и наблюдениями в GDB.
Эксперимент с test_unsigned.cpp
Открой редактор и напиши простую программу test_unsigned., которая повторит такое же условие, которое ты видел в декомпилированном псевдокоде:
if (5 < enter_string._string_length - 5) { ... }Перепиши исходный код test_unsigned.:
#include <iostream>#include <string>int main() { unsigned long num; std::cout << "Enter a number: "; do { std::cin >> num; if (5 < num - 5) { // При TRUE длина выходит за диапазон 5–10 std::cout << "Condition TRUE! (5 < num - 5)" << std::endl; } else { std::cout << "Condition FALSE! (5 >= num - 5)" << std::endl; } std::cout << "num = " << num << std::endl; std::cout << "num - 5 = " << (num - 5) << std::endl; } while (num != 0); return 0;}Пример рассчитан на корректный числовой ввод: вводи неотрицательные целые числа в диапазоне unsigned , а для завершения — 0. Ошибки чтения здесь не обрабатываются. Если закрыть поток после ненулевого значения, программа может зациклиться, поэтому в этом опыте мы завершаем ввод именно нулем.
Я использую тип unsigned — такой же, как у поля _string_length в восстановленной структуре std_string. Это важно: от типа переменной зависит, как вычисляется вычитание.
Компилируй без оптимизаций, чтобы получить максимально простой для анализа машинный код:
g++ -O0 -o test_unsigned test_unsigned.cpp
Я собираю пример с -O0, чтобы изменения кода при оптимизации не мешали сопоставлять исходное условие с листингом. Это не гарантия конкретной последовательности инструкций: ее все равно придется проверить в получившемся бинарнике. С -O2 или -O3 компилятор может с большей вероятностью перестроить сравнение и ветки.
Для интереса посмотри на псевдокод test_unsigned в Ghidra. Код условия может отличаться от крякми и быть, например, таким:
if (local_10 - 5 < 6) { // FALSE} else { // TRUE}Ghidra представила условие в инвертированной форме и поменяла местами блоки TRUE и FALSE. Логика от этого не изменилась: при num выполняется блок FALSE, в остальных случаях — TRUE. Само расположение веток в машинном коде мы определим по дизассемблерному листингу. Пока запомни: запись условия в декомпиляторе может отличаться от той, которую ты написал в C++, даже когда обе записи описывают одно поведение.
Запусти программу и вводи числа, имитируя длину строки. Ниже показаны фрагменты отдельных запусков с разными значениями. Внутри одного запуска приглашение Enter появляется только один раз; следующие числа можно вводить без повторного приглашения.
Enter a number: 1
Condition TRUE! (5 < num - 5)
num = 1
num - 5 = 18446744073709551612
...
Enter a number: 4
Condition TRUE! (5 < num - 5)
num = 4
num - 5 = 18446744073709551615
Enter a number: 5
Condition FALSE! (5 >= num - 5)
num = 5
num - 5 = 0
...
Enter a number: 10
Condition FALSE! (5 >= num - 5)
num = 10
num - 5 = 5
Enter a number: 11
Condition TRUE! (5 < num - 5)
num = 11
num - 5 = 6
...
Что ты здесь видишь? При вводе чисел от 1 до 4 условие истинно, от 5 до 10 — ложно, а от 11 — снова истинно. В крякми на месте num стоит длина введенной строки. Когда условие истинно, программа выводит «Bro, what are you trying to do?» и завершает работу. При длине от 5 до 10 проверка ложна, поэтому выполнение продолжается и программа просит ввести число. Но откуда берутся огромные значения в строке num для чисел меньше пяти? Разберемся с беззнаковым вычитанием.
Для 0 условие тоже истинно: результат беззнакового вычитания 0 оказывается в верхней части диапазона. В нашей экспериментальной программе этот результат выводится, после чего цикл завершается из‑за while (.
Для unsigned выражение 4 дало 18446744073709551615. Посмотрим на биты этого результата: так станет видно, почему та же последовательность в знаковой интерпретации означает -1.
Дополнительный код (Two’s Complement)
Процессор не хранит в регистре отдельный признак «это знаковое число» или «это беззнаковое число». Для него регистр содержит только последовательность битов. Типы int и unsigned существуют на уровне языка и компилятора, а при выполнении машинного кода интерпретация результата зависит от используемых инструкций.
Одно значение можно описать на двух уровнях. На уровне C++ переменная имеет тип unsigned и вычитание подчиняется правилам беззнаковой арифметики. На уровне машинного кода в регистре находятся биты результата. Отдельной метки «знаковое» или «беззнаковое» рядом с ними нет. Если следующая инструкция проверяет флаги после вычитания, условный переход определяет, какую семантику сравнения использует программа.
Например, такие инструкции, как SUB и CMP, изменяют флаги состояния процессора. Беззнаковые и знаковые условные переходы используют разные комбинации этих флагов, поэтому одна и та же последовательность битов может участвовать как в беззнаковом, так и в знаковом сравнении.
Когда ты пишешь на C++
unsigned long a = 4;unsigned long b = 5;unsigned long c = a - b;компилятор не записывает в SUB отдельный признак знакового типа. Инструкция выполняет вычитание заданной разрядности и устанавливает флаги. Дальнейший код может читать биты результата из регистра или проверять флаги условным переходом. В нашем примере JBE не читает RAX: он проверяет CF и ZF, установленные предшествующей инструкцией CMP.
Одна и та же последовательность битов может интерпретироваться по‑разному в зависимости от того, какой условный переход проверяет установленные флаги. Беззнаковые переходы (JB, JBE, JA, JAE) используют CF, а JBE и JA дополнительно проверяют ZF. Переход JB проверяет CF=1, JBE — CF=1 или ZF=1, JA — CF=0 и ZF=0, а JAE — CF=0. Знаковые переходы (JL, JLE, JG, JGE) используют SF и OF, а JLE и JG дополнительно учитывают ZF.
Если в регистре только биты, как вообще получается отрицательное число? Через дополнительный код (Two’s Complement) — способ представления знаковых целых чисел, при котором старший бит определяет знак: 0 — неотрицательное число, включая ноль; 1 — отрицательное. В арифметике фиксированной разрядности вычитание a можно реализовать как сложение a . Здесь нет «отрицательного нуля» — в отличие от более старых способов представления (прямого и обратного кода), где ноль имел два представления: +0 и −0.
Проще говоря, чтобы получить -5, нужно взять число 5 в двоичном виде (00000101). Затем ты инвертируешь все биты и получаешь 11111010. Далее прибавляешь единицу. В результате ты получишь число 11111011 — это -5 в 8-битном дополнительном коде. Если прочитать его как беззнаковое, получится 251.
Давай немного посчитаем для закрепления. Возьми лист бумаги и вычисли 4 для 8-битного беззнакового целого.

Результат 11111111 — это -1, если читать как знаковое, и 255, если читать как беззнаковое значение.
Аналогично посчитай результат 4 для 64-битного unsigned , который мы использовали в эксперименте:

Результат 0xFFFFFFFFFFFFFFFF — это -1 в знаковом виде и 18446744073709551615 в беззнаковом.
Когда ты видишь в GDB, что в регистре лежит 0xFFFFFFFFFFFFFFFF, не спеши решать, что это «минус один» или «огромное число». Это одна и та же последовательность битов, а ее интерпретация зависит от контекста. Если после операции выполняется беззнаковый переход вроде JBE, нужно смотреть на флаги CF и ZF, которые определяют результат этого перехода. Для знакового перехода вроде JLE используются другие флаги: SF, OF и ZF.
Одна и та же последовательность битов имеет два разных смысла. Поэтому нам нужны и значение в регистре, и инструкция, установившая флаги, и условный переход, который эти флаги проверяет.
Теперь посмотрим, что происходит при вычитании большего числа из меньшего в беззнаковой арифметике и почему результат «заворачивается» через ноль.
Беззнаковое переполнение (wrap-around)
В предыдущем опыте значение 4 после вычитания 5 оказалось у верхней границы диапазона unsigned . Разберем, как получается этот результат.
Например, если переменная num типа unsigned содержит 4, то выражение num дает 18446744073709551615. Когда из беззнакового числа вычитают значение, которое больше самого числа, результат выходит за нижнюю границу диапазона. Для беззнаковых типов арифметика выполняется по модулю 2^N, где N — количество битов типа. Поэтому результат не становится отрицательным: он «заворачивается» через ноль и оказывается в верхней части доступного диапазона. Для нашего 64-битного типа это арифметика по модулю 2^64.
Рассмотрим для наглядности старый механический счетчик электроэнергии: у него всего пять цифр, и после 99999 он снова показывает 00000. Если счетчик стоит на 00003, а ты «отматываешь» его на пять единиц назад, он не покажет -2. Он покажет 99998, потому что прошел через нулевое значение.

Этот пример хорошо показывает принцип переполнения (wrap-around): диапазон значений замкнут в кольцо. Если двигаться ниже минимального значения, ты не останавливаешься на нуле, а переходишь к максимальному значению диапазона и продолжаешь отсчет оттуда.
В двоичной арифметике происходит такое же «заворачивание». Возьмем для простоты 8-битное беззнаковое число. Его диапазон — от 0 до 255. Если из 3 вычесть 5, математический результат равен -2, но отрицательное значение для беззнакового типа представить нельзя.
Процессор не получает сначала «настоящее» отрицательное число -2, а затем преобразовывает его в 254. При работе с фиксированной разрядностью операция выполняется над доступным количеством битов, поэтому после вычитания 3 получаются биты 11111110. Уже дальнейшая интерпретация этих битов определяет, увидим мы 254 как беззнаковое значение или -2 как знаковое.
В двоичном виде это выглядит так.

Получившиеся 11111110 — это 254, если интерпретировать биты как беззнаковое значение. Те же биты в знаковом 8-битном дополнительном коде соответствуют -2.
Это и есть переполнение: при выходе за границу диапазона значение не становится отрицательным, а переходит на противоположный конец доступного диапазона.
Флаги процессора: как CPU принимает решение
При беззнаковом вычитании одних битов результата нам мало: нужно понять, получился ли ноль и был ли заем (так называют ситуацию, когда уменьшаемое было меньше вычитаемого и результат вышел за нижнюю границу диапазона). SUB и CMP записывают эту информацию во флаги регистра RFLAGS; затем условный переход проверяет нужные ему флаги. Для нашего JBE это CF и ZF. Прежде чем проследить их в GDB, разберемся с размером регистров и операндов.

Сначала посмотрим на регистры, с которыми работают инструкции крякми.
Размеры регистров
Регистры — небольшие сверхбыстрые ячейки внутри процессора. Их немного, зато они находятся непосредственно в процессоре и используются для хранения данных, с которыми работают инструкции. Доступ к регистрам значительно быстрее, чем доступ к оперативной памяти.
Отдельно стоит упомянуть регистр флагов. RFLAGS — 64-битный регистр. В 64-битном режиме его младшие 32 бита соответствуют EFLAGS, а младшие 16 бит — FLAGS. Все нужные нам флаги находятся в младших битах RFLAGS.

В архитектуре x86-64 регистры имеют разные размеры. Один и тот же регистр можно адресовать по‑разному в зависимости от того, сколько битов мы хотим использовать.

Обозначения RAX, EAX, AX и AL — это разные способы обращения к частям одного и того же регистра. EAX — младшие 32 бита RAX, AX — младшие 16 бит, а AL — младшие 8 бит.
Например, если RAX , то регистр будет выглядеть так.

В коде крякми ты увидишь RAX — 64-битный регистр. Именно туда инструкция MOV загружает длину строки — 8-байтовое значение, которому мы назначили тип unsigned в восстановленной структуре.
Ширина операнда влияет на то, что останется в RAX после записи. MOV обнулит весь RAX: запись в его младшие 32 бита автоматически обнуляет старшие 32. MOV изменит только младшие 16 бит, а старшие 48 сохранит. Поэтому в листинге обращай внимание, в какую часть регистра пишет инструкция.
Когда Ghidra в одном месте пишет RAX, а в другом — EAX, это не два разных регистра, а два способа обращения к одному и тому же регистру. Если ты видишь MOV , это не значит, что где‑то отдельно живет RAX, который не обнулился. Инструкция записывает значение в EAX, а при записи в 32-битный регистр верхние 32 бита соответствующего RAX автоматически обнуляются. Поэтому после MOV весь RAX содержит ноль.
Флаги состояния (status flags)
Посмотрим на флаги, которые меняют арифметические инструкции. Для нашего JBE понадобятся CF и ZF, а остальные помогут прочитать полный вывод GDB после каждого шага.
Ассемблерная инструкция SUB выполняет вычитание и записывает получившийся результат в первый операнд, одновременно устанавливая флаги OF, SF, ZF, AF, PF и CF в RFLAGS. CMP выполняет такое же вычитание для установки этих флагов, но саму разность не сохраняет. В нашем фрагменте кода получившееся состояние флагов затем проверяет JBE.
Инструкции SUB и CMP изменяют шесть перечисленных ниже флагов. Сведем в таблицу условия установки этих шести флагов после вычитания.
| Флаг | Название | Когда установлен |
|---|---|---|
| CF | Carry Flag (флаг переноса) | При вычитании был заем. Для беззнаковых чисел это означает, что уменьшаемое было меньше вычитаемого и результат вышел за нижнюю границу диапазона |
| PF | Parity Flag (флаг четности) | Младший байт результата содержит четное число единичных битов |
| AF | Auxiliary Carry Flag (вспомогательный флаг переноса) | При вычитании произошел заем через границу младшего полубайта — между битами 3 и 4. Используется, в частности, при операциях с BCD-числами |
| ZF | Zero Flag (флаг нуля) | Результат равен нулю |
| SF | Sign Flag (флаг знака) | Старший бит результата равен 1 |
| OF | Overflow Flag (флаг переполнения) | Произошло знаковое переполнение: математический результат не помещается в знаковый тип соответствующего размера |

Тебе нужно запомнить, что для беззнаковых сравнений важен флаг CF (и иногда ZF), а для знаковых — комбинация SF и OF (и иногда ZF). Именно поэтому в ассемблере существуют разные инструкции условного перехода: одни используют CF и ZF (например, JA, JBE), другие — SF и OF (например, JG, JL). Так, JBE выполняет переход, если CF=1 или ZF=1, а JA — если CF=0 и ZF=0. Подробную таблицу со всеми переходами мы разберем позднее.
Сами по себе условные переходы эти флаги не вычисляют. Они только проверяют их состояние. Значит, чтобы понять, почему JBE или другой условный переход принимает то или иное решение, нужно разобраться, какая инструкция перед ним устанавливает флаги и что именно она при этом вычисляет.
CMP — вычитание без сохранения результата
CMP — это инструкция сравнения, которая для установки флагов выполняет такое же вычитание, что и SUB, но с одним отличием: результат не сохраняется в регистр. Обновляются только флаги.
CMP a, b ; Вычисляет a - b, устанавливает флаги, но не записывает результат
С точки зрения вычисления флагов это соответствует
SUB a, b ; a = a - b, устанавливает флаги
После CMP регистр a остается прежним, а после SUB меняется. При одинаковых операндах флаги в обоих случаях устанавливаются одинаково.
SUB вычитает 5 из длины и сохраняет length в RAX. Затем CMP сравнивает это значение с 5; обе инструкции меняют флаги, но JBE проверяет состояние, оставленное последней из них — инструкцией CMP. Если убрать SUB и оставить только CMP , сравнение пойдет с исходной длиной и проверка диапазона изменится.
Но чтобы понять, почему для установки флагов нельзя просто использовать любую инструкцию, нужно разобраться, какие инструкции вообще изменяют состояние флагов.
Познакомлю тебя с арифметическими и логическими инструкциями ассемблера, которые обновляют флаги:
ADD,SUB,ADC,SBB— сложение и вычитание;CMP,TEST— выполняют вычисление для установки флагов, но не сохраняют результат;AND,OR,XOR— логические операции;INC,DEC— инкремент и декремент. В x86 они обновляютOF,SF,ZF,AFиPF, но намеренно не изменяютCF.
Есть и инструкции, которые выполняют вычисления, но флаги не изменяют. Например, NOT выполняет побитовое отрицание, но не обновляет флаги.
А вот пересылочные и управляющие инструкции флаги не обновляют:
MOV,LEA,XCHG— пересылка данных;PUSH,POP— работа со стеком;JMP,CALL,RET— управление потоком выполнения;NOP— ничего не делает.
Поэтому MOV не может использоваться для установки флагов. MOV просто копирует значение и не изменяет ни CF, ни ZF, ни SF. Эти флаги остаются в том состоянии, которое им задала предыдущая инструкция, изменяющая их.
Таким образом, для успешного решения нашей задачи проверяй, что вычисляет инструкция и какие флаги она изменяет. MOV может перенести значение в регистр, но состояние RFLAGS при этом останется прежним. А вот SUB и CMP устанавливают флаги по результату вычитания — именно их состояние затем проверяет условный переход JBE.
Чтобы увидеть, как все это работает вместе, вернемся к тому месту в крякми, с которого мы начали разбор.
Возвращаемся к крякми: SUB — CMP — JBE
В нашем коде SUB нужен для получения значения length , которое затем используется как первый операнд следующего сравнения. SUB сохраняет это значение в RAX, после чего CMP устанавливает флаги состояния, на которые ориентируется JBE.
Посмотри на ассемблерный код крякми в этом месте. Стрелкой показан условный переход, который выполняется, когда длина введенного слова находится в диапазоне [.

На схеме видно три инструкции: SUB вычитает 5 из длины строки, CMP сравнивает результат с 5, а JBE решает, куда идти дальше. JBE — это Jump if Below or Equal (прыгнуть, если ниже или равно). Да, именно ниже, а не меньше — такая терминология, к которой мы еще вернемся в дальнейшем. Это условный переход для беззнакового сравнения: он выполняется, если CF=1 или ZF=1.
Если условие JBE истинно, мы попадаем на LAB_0010128a (Enter ). Если ложно, выполняем следующую инструкцию по адресу 0010126c (Bro, ).
У тебя может возникнуть вопрос, почему при ложном JBE мы не доходим до LAB_0010128a, выполняя инструкции по порядку. Ведь LAB_0010128a расположен ниже по адресу.
Ответ находится между адресами 0010126c и 0010128a — там стоит вызов exit(. Этот вызов завершает программу насовсем. Никакие инструкции после него — включая 0010128a — выполнены не будут. Поэтому при ложном JBE программа выводит насмешливое сообщение и завершается, а при истинном прыгает на 0010128a и просит число.
Мы разобрали, как программа принимает решение на уровне инструкций SUB — CMP — JBE. Теперь проверим этот разбор непосредственно во время выполнения с помощью GDB. Сначала разберемся, как сопоставить адреса из Ghidra с адресами в памяти, и настроим GDB на привычный нам Intel-синтаксис. После этого остановимся перед SUB и пошагово проследим, как меняются RAX и флаги после SUB и CMP.
Прогон GDB для test
Пора запустить программу под отладчиком. Но прежде чем ставить первую точку останова, нам нужно разобраться с парочкой нюансов, которые могут тебя сбить с намеченного пути: почему адреса в GDB отличаются от адресов в Ghidra и как правильно указать адрес в команде break.
PIE и ASLR
Ты уже знаешь, что адреса в GDB отличаются от адресов в Ghidra. Причина в том, что наша крякми собрана как PIE-бинарник и при запуске с ASLR может загружаться по разным базовым адресам. Поэтому адрес из Ghidra нужно пересчитать с учетом реального адреса загрузки.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
