Загрузка...

Эксплуатация бинарных уязвимостей: часть 1 Основы

Тема в разделе Статьи создана пользователем LockBit 27 май 2025. (поднята 7 июн 2025 в 01:51) 817 просмотров

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

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

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

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

    Опкод (с английского: opcode, код операции) - числовое представление ассемблерных инструкций. допустим, инструкция 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] => 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 - не рассматривается.
    используется для функций, связанных с классами в C++, где первый аргумент - указатель на экземпляр класса:
    void classFunc(void* this, int arg1, ...);


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

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

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

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

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

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

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

    Допустим, где-то в недрах уязвимого приложения мы нашли функцию, которая заканчивается так:
    ASM

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

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

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

    Адрес возврата можно перезаписать несколько раз "подряд", в таком случае адреса должны следовать друг за другом.
    Пример (как это выглядит в стеке):
    CPP

    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
    ret
    (берёт значение из 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).

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

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

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

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


    Правда в том, что всё, чем мы пользуемся, в той или иной мере уязвимо, и вы даже не представляете, насколько.

    Благодарю за прочтение статьи!​
     
    Этот материал оказался полезным?
    Вы можете отблагодарить автора темы путем перевода средств на баланс
    Отблагодарить автора
    27 май 2025 Изменено
  2. Самомнение
    Самомнение 27 май 2025 Рекламный креативщик - https://lolz.live/threads/6238236 6664 23 окт 2018
    То что нужно, буду ебланов в кс крашить
     
    1. Посмотреть предыдущие комментарии (4)
    2. Пингвиниак
      gonome, у билла гейтса от такого хуй к потолку встанет
  3. kipsad
    kipsad 27 май 2025 16 119 24 дек 2018
    топ 1 пример
    топ 5 статей
    топ 1 непонимание, че к чему, я тупой для такого еще, сорянчик
     
  4. exepert
    exepert 27 май 2025 Заблокирован(а) 4921 25 ноя 2018
    однозначно годная инфа
     
  5. Кастория
    Кастория 27 май 2025 СТАТЬ МИЛЛИОНЕРОМ ЗА МЕСЯЦ - https://lolz.live/threads/8241684/ 3210 9 окт 2020
    LockBit в cs2 есть чит, и у него была такая функция что он крашил тебе игру когда на тебя выходит)
     
  6. CodeRed
    CodeRed 27 май 2025 366 4 янв 2019
    еще когда ксго была, что-то похожее было, только еще можно было посылать на сервак много пакетов и он начинал лагать у всех. Отдельные луашки и скрипты были с крашером для читов. Помню фризил сервак, у всех все лагало и они бомбили угарно.
    Мб кто знает, есть ли такие скрипты из-за которых сервак начинает именно лагать, а не крашится
     
    1. LockBit Автор темы
      CodeRed, да, банально выявляются пакеты которые сильнее всего влияют на стабильность сервера (долго обрабатываются), затем происходит мини DDoS такими пакетами
  7. tonwarden
    tonwarden 28 май 2025 T.me/twitchpaty - новости twitch 288 27 апр 2025
    Грамотно пишешь, читать приятно. Аллегории с Васей прям в точку. Я вспомнил про баг, когда можно было порно в меню голосования в кс2 поставить. Годно :pepeNewYear:
     
    1. LockBit Автор темы
      tonwarden, это была уязвимость HTML Injection, никнеймы отображались не как текст, а как HTML разметка)
      по моему уязвимую библиотеку оттуда потом вырезали, если не ошибаюсь.
  8. DaleCooper
    DaleCooper 28 май 2025 гость, walk with me.:pepetorch: 3525 19 янв 2020
    Классная статья! Скажем так, вектор развития для изучения данного направления задан :smine_nice:
     
  9. drwhooper
    drwhooper 29 май 2025 Заблокирован(а) 0 5 янв 2025
    Для чего используются регистры RAX и RDX ? в игре контер страйк
     
    1. LockBit Автор темы
      drwhooper, конкретно в игре - не используются.
      они нужны для функций и передачи параметров
  10. Smex
    Smex 29 май 2025 смешно. ха... ха.... ха...... 604 15 сен 2020
    классная статья, а будет слив 0day уязвимостей по active directory? мне так чисто для школьного реферата надо, информатик сказал что ток такие принимает
     
  11. pamef
    помню в майнкрафте обнаружили уязвимость, через которую можно было запускать код на чужом компьютере.
    достаточно было просто ввести нужное сообщение в чат
     
    2 июн 2025 в 16:36 Изменено
    1. LockBit Автор темы
      pamef, уязвимость была в библиотеке log4j, клиент игры (да и сервер) при логировании сообщений движка (в том числе чата), подгружал вредоносный класс из пейлоада)
Top