FlyFF (Fly For Fun) is an old MMO game developed by the korean AeonSoft company and released in 2005. The game has been exploited and abused by hackers many times in the past due to it’s low security and due to the multiple source code leaks that let hackers analyse the server side code for possible exploits. Today we will mostly focus on a SQL injection vulnerabilty and we will also talk about the possible ways to prevent this kind of attack. We will use the v15 source leak to be able to explain the exploit in details.
The client and server’s structure
Server side
- AccountServer.exe
- CacheServer.exe (Port 5400)
- CoreServer.exe
- CertifierServer.exe (Port 23000)
- DatabaseServer.exe
- LoginServer.exe (Port 28000)
- WorldServer.exe
Client side
- Neuz.exe (main game executable)
As you can see the game connects to 3 different servers which allows us to find multiple exploits.
The exploit
During the login phase the game connects to the LoginServer
if( !g_dpLoginClient.ConnectToServer( lpAddr, PN_LOGINSRVR, FLXORProtocol::GetInstance(), TRUE ) )
{
// Can't connect to server
g_WndMng.OpenMessageBox( _T( prj.GetText(TID_DIAG_0043) ) );
CNetwork::GetInstance().OnEvent( LOGIN_CONNECT_FAIL );
break;
}
CNetwork::GetInstance().OnEvent( LOGIN_CONNECTED );
and thus inits the g_dpLoginClient class to send packets directly to that server. This means that if we manage to find the g_dpLoginClient pointer in game you will be able to craft / edit malicious packets.
Let’s take a look at how g_dpLoginClient is used inside a packet send function
void CDPLoginClient::SendCreatePlayer(
BYTE nSlot, LPCSTR lpszPlayer, BYTE nFace,
BYTE nCostume, BYTE nSkinSet, BYTE nHairMesh,
DWORD dwHairColor, BYTE nSex, BYTE nJob,
BYTE nHeadMesh, int nBankPW, BYTE bySelectPage )
{
BEFORESENDSOLE( ar, PACKETTYPE_CREATE_PLAYER, DPID_UNKNOWN );
ar.WriteString( g_Neuz.m_bGPotatoAuth?g_Neuz.m_szGPotatoNo: g_Neuz.m_szAccount );
ar.WriteString( g_Neuz.m_szPassword );
ar << nSlot;
if( strlen( lpszPlayer ) > 16 )
FLERROR_LOG( PROGRAM_NAME, _T( "%s" ), lpszPlayer );
ar.WriteString( lpszPlayer );
if( strlen( lpszPlayer ) > 16 )
FLERROR_LOG( PROGRAM_NAME, _T( "%s" ), lpszPlayer );
ar << nFace << nCostume << nSkinSet << nHairMesh;
ar << dwHairColor;
ar << nSex << nJob << nHeadMesh;
ar << nBankPW;
ar << g_Neuz.m_dwAuthKey;
ar << bySelectPage;
SEND( ar, this, DPID_SERVERPLAYER );
}
We can see that we can easily manipulate the packet buffer by either hooking the function and changing directly the params or just by crafting manually a CAr
buffer and calling the SEND
function which will automatically do all the encryption/checksum stuff for us.
Here’s where the exploit comes into play. We have 2 string variables we can modify
g_Neuz.m_szAccount[42]
g_Neuz.m_szPassword[42]
And now let’s follow up the packet path till it’s recevied by the LoginServer.
ON_MSG( PACKETTYPE_CREATE_PLAYER, OnCreatePlayer );
void CDPLoginSrvr::OnCreatePlayer( CAr & /*ar*/, DPID dpid, LPBYTE lpBuf, u_long uBufSize )
{
if( g_tSlotActionFlag.bNotCreate == true )
{
SendError( ERROR_SLOT_DONOT_CREATE, dpid );
FLINFO_LOG( PROGRAM_NAME, _T( "." ) );
return;
}
LPDB_OVERLAPPED_PLUS lpDbOverlappedPlus = g_DbManager.AllocRequest();
g_DbManager.MakeRequest( lpDbOverlappedPlus, lpBuf, uBufSize );
lpDbOverlappedPlus->dpid = dpid;
lpDbOverlappedPlus->nQueryMode = CREATEPLAYER;
PostQueuedCompletionStatus( g_DbManager.m_hIOCPGet, 1, NULL, &lpDbOverlappedPlus->Overlapped );
}
We found something very interesting here. Take a deep look at the function and we find out that it takes the packet buffer directly to the DatabaseServer using g_DbManager.MakeRequest( lpDbOverlappedPlus, lpBuf, uBufSize );
without doing any security checks.
Let’s follow where the packet ends up in the DatabaseServer.
case CREATEPLAYER:
CreatePlayer( pQuery, lpDbOverlappedPlus );
break;
void CDbManager::CreatePlayer( CQuery *qry, LPDB_OVERLAPPED_PLUS lpDbOverlappedPlus )
{
CAr arRead( lpDbOverlappedPlus->lpBuf, lpDbOverlappedPlus->uBufSize );
arRead.ReadString( lpDbOverlappedPlus->AccountInfo.szAccount, _countof( lpDbOverlappedPlus->AccountInfo.szAccount ) );
arRead.ReadString( lpDbOverlappedPlus->AccountInfo.szPassword, _countof( lpDbOverlappedPlus->AccountInfo.szPassword ) );
The function reads directly the buffer we have input inside szAccount
and szPassword
with only a buffer overflow check inside ReadString
function.
Deep down in this function we only find a check about the szPlayer
variable which we didn’t touch anyway
if( prj.IsInvalidName( lpDbOverlappedPlus->AccountInfo.szPlayer ) ||
prj.IsAllowedLetter( lpDbOverlappedPlus->AccountInfo.szPlayer ) == FALSE )
{
return;
}
But here’s the most interesting part
char szQuery[QUERY_SIZE] = { 0,};
DBQryCharacter( szQuery, "I1", 0, g_appInfo.dwSys,
lpDbOverlappedPlus->AccountInfo.szAccount,
lpDbOverlappedPlus->AccountInfo.szPlayer,
nSlot, dwWorldID, dwIndex, vPos.x, vPos.y,
vPos.z, '\0', nSkinSet, nHairMesh,dwHairColor,
nHeadMesh, nSex )
The DatabaseServer directly executes an MsSQL query using the szAccount
variable that we can manipulate directly from the Neuz.exe
without a single check.
Pwn
Let’s go back to the client-side
ar.WriteString( g_Neuz.m_bGPotatoAuth?g_Neuz.m_szGPotatoNo: g_Neuz.m_szAccount );
Now imagine setting g_Neuz.m_szAccount
to
;' DROP pwn_table;--
Limitations and mitigations
- We can’t execute an enourmous query because we are limited to 42 chars only.
- A simple fix would be just using SQLBindParameter which seems to be already used in the recent versions of the game.
Conclusion
We realize that not only web apps but also games are vulnerable to SQL injection attacks. A simple packet modification in the memory of the client can have a big impact on the server side if there isn’t enough layers of security. This was just a simple showcase of a possible exploit in this game, but there’s a plenty of other exploits possible and especially in MMO games because they rely on a heavy client-server communication so having every single networking component secured is a must. The previous post also shows how with just messing with the networking of the game you are able to break things.