SECRET CLUB

SQL Injecting FlyFF MMO


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

Client side

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

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

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.