Загрузка...

Фываasdf asdkfa ksdfka skd a

Тема в разделе Тестовый раздел создана пользователем LockBit 11 май 2025. 88 просмотров

Загрузка...
  1. LockBit
    LockBit Автор темы 11 май 2025
    Привет дорогие читатели!

    В этой статье я поведую вам, как при помощи специального набора символов опытные хакеры способны крашить игру противнику, выполнять код удалённо и тд, А ИМЕННО - по какому принципу это работает.

    Прежде чем начать, для начала дадим определение этому феномену.

    Переполнение стека (с английского: Stack Overflow) - Явление, при котором процессор, записывая данные туда - куда не нужно (по вине разработчика программы), это вызывает неопределённое поведение в программе, что может привести к зависанию, вылету программы или даже к выполнению кода, контролируемого хакером.

    Опкод (с английского: opcode, код операции) - HEX-представление ассемблерных инструкций, допустим, инструкция NOP имеет опкод 0x90.

    Шеллкод (с английского: shellcode, он же payload) - Полезная нагрузка, представляющая собой набор опкодов, ассемблерных инструкций.

    В данной статье рассматривается переполнение буфера в стеке на примере 32 битного ассемблера (некоторые понятия применимы к 64 битному ассемблеру).

    Стек
    Стек (c английского: stack, в контексте нашей статьи) - это структура, которая хранит различные данные, будь то значения регистров, адреса возврата и прочее.

    Подчиняется эта структура правилу LIFO (Last In First Out), что дословно переводится как "Последний зашёл - первым выйдет", пример:​
    1. Мы засунули в стек число 0xA (первый зашёл)
    2. Затем мы поместили туда число 0xF (последний зашёл)
    После чего мы обратились к стеку, чтобы вытащить оттуда значение, и получили 0xF (первый вышел).

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

    Регистр ESP хранит адрес вершины этой тарелки.
    Регистр EBP хранит адрес дна этой тарелки.

    Наглядная демонстрация (адрес => значение):
    // выше всех идут локальные переменные, это следует запомнить
    [ESP] или [EBP-0x8] => 0xF < последнее число в стеке
    [ESP+0x4] или [EBP-0x4] => 0xA <- первое число в стеке
    // сюда ещё записывается значение регистра EBP, но это мы опустим
    [ESP+0x8] или [EBP-0x4] => SAVED EBP <- базовый адрес текущего кадра стека
    [ESP+0x12] или [EBP+0x4] => RETURN <- адрес возврата
    // тут аргументы функции, если бы они были (ARG 1, ARG2)
    // допустим [EBP+0x8] хранит число 0x100 - это первый аргумент функции ARG 1


    То есть, данные помещаются в стек как-бы сверху вниз (из-за адресации, от меньшего адреса к большему):
    [IMG]

    Регистры

    Регистры - это участки памяти непосредственно на самом процессоре (да-да), которые могут хранить 8, 16, 32, и 64 битные числа.

    Небольшая оговорка.
    В архитектуре x86 существуют 32 битные регистры, EAX, EBX, ECX, EDX, ESP, EBP и тд.
    В архитектуре x64 помимо упомянутых, так-же существуют 64 битные регистры, отличие в том, что называются они RAX, RBX, RCX (по аналогии с 32 битными) и занимают они 64 бита.

    Так-же в х64 архитектуре, регистры EAX<->RAX, EBX<->RBX, ECX<->RCX (и тд) взаимосвязаны. при изменении значения регистра EAX, будут изменены 32 бита (половина) значения регистра RAX:
    [IMG]

    То же самое относится и к 16, и к 8 битным регистрам, например AX (16), AH (8) и AL (8).
    Их в данной статье я рассматривать НЕ буду.


    У процессора есть определённый набор регистров, с которыми он взаимодействует:
    - 32 битный регистр ESP (Extended Stack Pointer) - указывает на последний элемент в стеке (то есть содержит адрес, куда будет записан следующий параметр при вызове PUSH), когда мы записываем в стек что-нибудь - его значение уменьшается.
    - 32 битный регистр EBP (Extended Base Pointer, "глубина" стека) - содержит адрес, начиная с которого в стек вносится та или иная информация, можно даже сказать противоположен регистру ESP (то есть [ESP+X] = [EBP] и наоборот, [EBP-X] = [ESP] с некоторыми оговорками), аргументы функции имеют положительный сдвиг по отношению к нему (EBP+X), а локальные переменные - отрицательный (EBP-X).
    - 32 битный регистр EIP (Extended Instruction Pointer) - указатель на адрес следующей инструкции для выполнения (тот самый адрес возврата, куда мы переходим после завершения выполнения функции).
    - остальные 32 битные регистры общего назначения - EAX, ECX, EDX, EBX, ESI, EDI.
    - SSE регистры - xmm0, xmm1, ...xmm15. Используются для хранения чисел с плавающей точкой.
    Регистры делятся на два вида (согласно Microsoft Windows ABI):
    Изменяемые регистры (volatile) - они могут свободно изменять свои значения, после выхода из функции не нужно беспокоиться о том, чтобы они хранили своё первоначальное значение, сюда относятся EAX, ECX, EDX, xmm0 - xmm5.
    Неизменяемые регистры (non-volatile) - эти регистры должны сохранять информацию после выхода из функции (возврата), сюда относятся EBX, EBP, EDI, ESI, ESP, xmm6 - xmm15 (не знаю, соблюдает ли кто-то сейчас это правило).​
    О передаче параметров

    Если вкратце, во время компиляции все параметры делятся на три типа:
    INTEGER - интегральные типы, которые хранятся в регистрах общего назначения, сюда относятся типы данных bool, char, short, int и подобные, а так-же указатели (потому что указатель - это такой-же int, он хранит адрес, который в целом тоже является числом).
    SSE - числа с плавающей точкой, сюда относятся типы float и double.
    MEMORY - объекты, которые передаются через стек, сюда относится всё остальное.

    Несколько правил (МОГУТ МЕНЯТЬСЯ в зависимости от соглашения о вызове, об этом чуть позже):
    1 - Параметры всегда представляют собой 32 битные значения (справедливо для 32 битного ассемблера), будь это int8_t или int32_t.
    2 - Код должен зарезервировать определённое кол-во байт (32 байта для 64 битного ассемблера) в стеке для параметров (даже если это место не будет использовано), при этом первый параметр будет лежать по адресу ESP, второй - ESP+4, третий - ESP+8 и тд.
    3 - INTEGER параметры передаются через следующий свободный регистр в следующем порядке: EDI, ESI, EDX, ECX.
    4 - SSE параметры передаются через следующий свободный регистр в следующем порядке: xmm0, xmm1, ...xmm15.

    Пример:
    CPP

    void func1(int a, int b);
    // для func1 параметры передаются так:
    // b - EDI
    // a - ESI

    void func2(int a, double b);
    // для func2 параметры передаются так:
    // a - EDI
    // b - xmm0

    void func3(char a, float b, double c);
    // для func3 параметры передаются так:
    // a - EDI
    // b - xmm0
    // c - xmm1
    Соглашения о вызовах

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

    Рассмотрим некоторые из них:

    __cdecl (с английского: c-declaration) - Соглашение о вызове, по умолчанию используемое компиляторами для языка Си, согласно которому:
    1 - Аргументы расширяются до 4 байт, даже если это char, short, bool.
    2 - За сохранение регистров EAX, ECX, EDX и стека отвечает вызывающая функция, за остальные - вызываемая функция.
    3 - Очистку стека производит вызывающая функция.

    Это основной способ вызова функций с неизвестным кол-вом параметров, va_list (к примеру - функция printf).

    __stdcall - Соглашение о вызове, применяемое в Windows с их гадким WinAPI:
    1 - Аргументы функций передаются через стек, справа налево.
    2 - Очистку стека производит вызываемая функция.

    Возможно есть люди, которые думают, что есть какая-то разница в C++ между WINAPI и __stdcall.
    Обратимся к определению этого макроса, и узнаем, что разницы толком-то и нет (это одно и то же):
    CPP

    // minwindef.h
    #define WINAPI __stdcall
    __fastcall - не рассматривается.

    __safecall - не рассматривается.

    __thiscall - не рассматривается.

    После того, как разобрались с базой, можем приступить к самой сути.

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

    ROP гаджеты - состоят из набора опкодов, которые завершаются опкодом 0xC3 (инструкция ret).​
    Картинка из интернета.

    На ней изображён ROP гаджет, находящийся по адресу 0x000ED7CB, который помещает значение регистра EBX в регистр EAX, затем значение из стека помещается в регистр EBX, после чего следующее значение из стека помещается в регистр ESI.

    При написании эксплоитов стоит учитывать инструкции POP, после адреса возврата в стеке должен быть любой мусор, которым мы забьём эти регистры.

    В противном случае, в регистры попадут адреса ROP гаджетов, и наша ROP цепочка отработает не так, как планировалось.
    [IMG]

    Различные ROP гаджеты и их сочетание позволяют делать самые разные вещи, начиная от установки конкретных значений в регистр (через mov и константу, или через push и тот же стек), заканчивая вызовом любых функций.

    Допустим, где-то в недрах уязвимого приложения мы нашли функцию, которая заканчивается так:
    // ...
    // пусть инструкция mov будет расположена по адресу 0x100
    mov eax, 1h
    xor ecx, ecx
    ret

    0x100 - это адрес ROP гаджета, который поместит в регистр eax единицу, в ecx - ноль (xor сам на себя обнулит значение регистру), и выполнит переход на следующий адрес возврата.

    Если мы перезапишем текущий адрес возврата в стеке числом 0x100, то после выполнения уязвимой функции, нас перебросит на указанный адрес - выполнение будет продолжаться, начиная с адреса 0x100.

    Далее, в регистр eax будет записано число 1, после чего значение регистра ecx будет обнулено, и мы попадём туда, откуда пришли (как описывал выше), то есть ROP гаджет отработал, выполнение идёт дальше.

    Адрес возврата можно перезаписать несколько раз "подряд", в таком случае адреса должны следовать друг за другом.
    Пример (как это выглядит в стеке):
    0x100 // первый адрес
    0x200 // за ним следует второй адрес, сюда мы попадём после выполнения инструкции ret предыдущего ROP гаджета
    0x300 // следующие адреса

    ROP гаджеты можно искать руками, а можно использовать специальные утилиты.
    Две основные из них:
    ⁡ropgadget --binary vulnerableApp

    ⁡ropper --file vulnerableApp


    Эти команды выведут огромный список из ROP гаджетов, рекомендую сразу перенаправлять вывод в файл, для упрощения последующей работы с ними.

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

    Рассмотрим несколько техник, которые помогут приблизиться к выполнению кода.​
    Чтобы записать строки в память, для начала стоит узнать адрес, куда мы можем записать наше значение.
    Обнаружив пустую область в программе, записываем её адрес (пусть будет 0x2000 для наглядности), он нам понадобится.

    Далее наша задача - найти ROP гаджеты, которые позволят контролировать значения некоторых регистров, допустим, 0x12345 pop ecx, pop edx, ret.

    Находим гаджет, который позволит записать данные по указанному адресу, допустим, 0x54321 mov dword ptr [edx], ecx (берёт значение из ecx и записывает по адресу edx).

    Допустим, мы хотим записать в память строку "/bin/sh" (привет любителям линукса).
    Любую строку можно представить в виде чисел, деля её на строки по 4 символа в каждой:
    "/bin" - 4 байта, которые можно представить в виде числа, пусть это будет число XXXX.
    "//sh" - здесь я добавил ещё один символ /, чтобы заполнить недостающий четвёртый байт в числе (на путь в строке это никак не повлияет), пусть это будет число YYYY.

    Записываем в стек адрес возврата, затем значение для ECX, затем значение для EDX, и следующие адреса из ROP цепочки:
    0x12345 // первый гаджет, задаём значения регистрам
    XXXX // значение регистра ECX, т.е первая часть строки
    0x2000 // значение регистра EDX, т.е адрес
    0x54321 // второй гаджет, записываем строку в память
    0x12345 // первый гаджет, задаём значения для второй половины строки
    YYYY // значение ECX, вторая часть строки
    0x2004 // значение EDX, адрес
    0x54321 // второй гаджет, дозаписываем вторую часть строки

    На данном этапе нам удалось записать строку в память:
    0x2000 "/"
    0x2001 "b"
    0x2002 "i"
    0x2003 "n"
    0x2004 "/"
    0x2005 "/"
    0x2006 "s"
    0x2007 "h"
    Вызывать функции мы будем на примере линукса.
    В линуксе есть замечательная функция execve, которая идеально подходит для раскручивания нашей уязвимости и последующего выполнения кода.

    Плюс этой функции заключается в том, что вызвать её можно через syscall (системный вызов), то есть достаточно просто указать номер функции (для execve это 11) в регистр EAX, затем передать адрес строки ("/bin//sh") в регистр EBX (то есть значение 0x2000, если ориентироваться на прошлый спойлер), после чего заполнить регистры ECX и EDX нулями (это остальные параметры функции. нули - потому что их мы не используем, argv, envp).

    Номера функций для системного вызова можно посмотреть в заголовочном файле asm/unistd_32.h, если кому интересно)
    Они представлены в виде макросов

    Подготовив параметры для вызова функции, можно вызвать её.
    Делается это при помощи прерывания:
    int 80h

    После выполнения данной инструкции будет вызвана оболочка bash, чего более чем достаточно для того, чтобы доказать факт наличия уязвимости максимально наглядно (Proof-of-Concept).


    Про Impact | Как это может повлиять на меня?

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

    После детального изучения выяснилось, что краш происходит из-за перезаписи буфера и последующей перезаписи адреса возврата в функции cs2.exe+0x12345.
    Спустя пару дней, методом проб и ошибок, при помощи ROP гаджетов Вася сформировал пейлоад, который позволяет ему записывать вредоносную команду в память, затем вызывать функцию system, передавая эту команду в качестве аргумента.

    Итог? Вредоносная команда выполнена, Васе удалось заполучить контроль над компьютером игроков.

    Правда в том, что всё, чем мы пользуемся, в той или иной мере уязвимо (вы даже не представляете, насколько), и будет очень обидно, если Вася Пупкин найдёт критическую уязвимость в каком-нибудь SSH, который используется буквально ВЕЗДЕ.

    P.S. Даже великий и могучий лолзтим допускает в себе дыры (я много интересного здесь находил), никто не застрахован, одним словом.

    Благодарю за прочтение статьи!​
     
    11 май 2025 Изменено
  2. eretik
    eretik 13 май 2025 Следить за будущей звездой - https://t.me/lolzofollio 273 31 мар 2019
    ahahhahaaha
     
Top