Jump to content

Recommended Posts

  • Bot

As you probably know, there's an exploit that players use to change channels by abusing the LoginByKey function (a hack function). This function lacks any limitations, any connection can use it at any time. When combined with poorly written systems, it can be used to duplicate items or yang.

I have already shared the fix privately with many people. At one point, someone even posted it on a forum, but it appears that the content has since been deleted. I’ve now been informed that this same user is selling the “fix”, despite the fact that it isn’t even theirs.

The fix isn’t perfect, but it works. Essentially, it ensures that only players with a valid logout can use LoginByKey, as it should be.

// common/tables.h
// 1. Search:
typedef struct SLogoutPacket
{
        char login[LOGIN_MAX_LEN + 1];
        char passwd[PASSWD_MAX_LEN + 1];
} TLogoutPacket;

// 1. Replace with:
typedef struct SLogoutPacket
{
        char login[LOGIN_MAX_LEN + 1];
        char passwd[PASSWD_MAX_LEN + 1];
        bool bCanUseLoginByKey;
} TLogoutPacket;




// db/ClientManagerLogin.cpp 
// 1. Search: (void CClientManager::QUERY_LOGIN_BY_KEY)
    if (memcmp(pkLoginData->GetClientKey(), p->adwClientKey, sizeof(DWORD) * 4))
    {
        const DWORD* pdwClientKey = pkLoginData->GetClientKey();
        sys_log(0, "LOGIN_BY_KEY client key differ %s %lu %lu %lu %lu, %lu %lu %lu %lu", r.login, p->adwClientKey[0], p->adwClientKey[1], p->adwClientKey[2], p->adwClientKey[3], pdwClientKey[0], pdwClientKey[1], pdwClientKey[2], pdwClientKey[3]);
        pkPeer->EncodeReturn(HEADER_DG_LOGIN_NOT_EXIST, dwHandle);
        return;
    }
	
// 1. Add after:
    if (!pkLoginData->IsAllowLoginByKey())
    {
        sys_err("LOGIN_BY_KEY without request %s", r.login); // To track them down
        sys_log(0, "LOGIN_BY_KEY without request %s %lu", r.login, p->dwLoginKey);
        TPacketDGLoginAlready ptog;
        strlcpy(ptog.szLogin, szLogin, sizeof(ptog.szLogin));
        pkPeer->EncodeHeader(HEADER_DG_LOGIN_ALREADY, dwHandle, sizeof(TPacketDGLoginAlready));
        pkPeer->Encode(&ptog, sizeof(TPacketDGLoginAlready));
        return;
    }

    pkLoginData->SetAllowLoginByKey(false);
	
// db/ClientManagerLogin.cpp 
// 2. Search: (void CClientManager::QUERY_LOGOUT)
    CLoginData* pLoginData = GetLoginDataByLogin(packet->login);
    if (pLoginData == NULL)
        return;
		
// 2. Add after:
    pLoginData->SetAllowLoginByKey(packet->bCanUseLoginByKey);
	
	
	
// db/LoginData.cpp
// 1. Inside constructor add: (CLoginData::CLoginData())
    m_bAllowLoginByKey    = true; // We allow loginByKey by default (so the first login doesn't fail)
	

// db/LoginData.h
// 1. Add:
    public:
        bool IsAllowLoginByKey() const { return m_bAllowLoginByKey; }
        void SetAllowLoginByKey(bool bFlag) { m_bAllowLoginByKey = bFlag; }
		
    private:
         bool m_bAllowLoginByKey;
	


// game/char.cpp
// 1. Somwhere inside void CHARACTER::Initialize() add:
	m_bCanUseLoginByKey     = false;
	

// 2. Inside your warp functions search: (WarpSet, ChangeChannel, etc)
    Stop();
    Save();
	
// 2. Add before that:
    m_bCanUseLoginByKey = true;
	
	
// game/char.h
// 1. Add:
    public:
        bool CanUseLoginByKey() const { return m_bCanUseLoginByKey; }

    protected:
        bool m_bCanUseLoginByKey;
		
		
		
// game/desc.cpp
// 1. Search: (void DESC::Destroy()) 
	if (m_lpCharacter)
	{
		m_lpCharacter->Disconnect("DESC::~DESC");
		m_lpCharacter = NULL;
	}
	
// 1. Replace with:
    bool bCanUseLoginByKey = true;

    if (m_lpCharacter)
    {
        bCanUseLoginByKey = m_lpCharacter->CanUseLoginByKey();

        m_lpCharacter->Disconnect("DESC::~DESC");
        m_lpCharacter = NULL;
    }
	
	
// game/desc.cpp
// 2. Search: (void DESC::Destroy()) 
	if (!g_bAuthServer)
	{
		if (m_accountTable.login[0] && m_accountTable.passwd[0])
		{
			TLogoutPacket pack;

			strlcpy(pack.login, m_accountTable.login, sizeof(pack.login));
			strlcpy(pack.passwd, m_accountTable.passwd, sizeof(pack.passwd));

			db_clientdesc->DBPacket(HEADER_GD_LOGOUT, m_dwHandle, &pack, sizeof(TLogoutPacket));
		}
	}
	
// 2. Change it like this:
	if (!g_bAuthServer)
	{
		if (m_accountTable.login[0] && m_accountTable.passwd[0])
		{
			TLogoutPacket pack;

			strlcpy(pack.login, m_accountTable.login, sizeof(pack.login));
			strlcpy(pack.passwd, m_accountTable.passwd, sizeof(pack.passwd));
			pack.bCanUseLoginByKey = bCanUseLoginByKey;

			db_clientdesc->DBPacket(HEADER_GD_LOGOUT, m_dwHandle, &pack, sizeof(TLogoutPacket));
		}
	}
	
	
	
// game/input_db.cpp
// 1. Search: (void CInputDB::LoginSuccess)
	if (!d)
	{
		sys_log(0, "CInputDB::LoginSuccess - cannot find handle [%s]", pTab->login);

		TLogoutPacket pack;

		strlcpy(pack.login, pTab->login, sizeof(pack.login));
		db_clientdesc->DBPacket(HEADER_GD_LOGOUT, dwHandle, &pack, sizeof(pack));
		return;
	}

	if (strcmp(pTab->status, "OK"))
	{
		sys_log(0, "CInputDB::LoginSuccess - status[%s] is not OK [%s]", pTab->status, pTab->login);

		TLogoutPacket pack;

		strlcpy(pack.login, pTab->login, sizeof(pack.login));
		db_clientdesc->DBPacket(HEADER_GD_LOGOUT, dwHandle, &pack, sizeof(pack));

		LoginFailure(d, pTab->status);
		return;
	}
	
// 1. Replace with:
	if (!d)
	{
		sys_log(0, "CInputDB::LoginSuccess - cannot find handle [%s]", pTab->login);

		TLogoutPacket pack;

		strlcpy(pack.login, pTab->login, sizeof(pack.login));
		pack.bCanUseLoginByKey = false;
		db_clientdesc->DBPacket(HEADER_GD_LOGOUT, dwHandle, &pack, sizeof(pack));
		return;
	}

	if (strcmp(pTab->status, "OK"))
	{
		sys_log(0, "CInputDB::LoginSuccess - status[%s] is not OK [%s]", pTab->status, pTab->login);

		TLogoutPacket pack;

		strlcpy(pack.login, pTab->login, sizeof(pack.login));
		pack.bCanUseLoginByKey = false;
		db_clientdesc->DBPacket(HEADER_GD_LOGOUT, dwHandle, &pack, sizeof(pack));

		LoginFailure(d, pTab->status);
		return;
	}



 

  • Metin2 Dev 7
  • Good 4
  • muscle 2
  • Love 9

english_banner.gif

Link to comment
https://metin2.dev/topic/33869-loginbykey-exploit/
Share on other sites

  • 2 weeks later...
  • Premium

These days I was trying to create a REST API for the auth, eliminating the need for the auth server and I got to fight with the login key (well, I am not using the old key but a JWT but the problem still stands since the whole flow is.. quite something I gotta say). During logout I set the deletion of LoginData but this would then reject the old expired JWT so I had to make some changes for the warp, like you did here. Problem is, I've had to do it for the change character as well because of the logout change.

 

This is making me question then, for everyone using the original login, should this fix be applied in the changing character phase? Something like:
 

			case SCMD_PHASE_SELECT:
				{
					if (d) {
                      				d->SetCanUseLoginKey(true);
                			}
					ch->Disconnect("timed_event - SCMD_PHASE_SELECT");

					if (d) {
						d->SetPhase(PHASE_SELECT);
					}
				}
				break;

Technically changing character should trigger DESC::Destroy where bCanUseLoginByKey would be false and selecting one would send again LOGIN_BY_KEY again. Am I tripping or what?

Nvm the last part, my brain is completely fried. There's indeed bool bCanUseLoginByKey = true; and it's also initialized in CLoginData.

This still makes me question if the problem was even for a character change (and your solution fixes it anyway btw). I am completely ignorant about whatever exploit is used with it, so I am just speculating.

Edited by Intel
  • Love 1
Link to comment
https://metin2.dev/topic/33869-loginbykey-exploit/#findComment-171685
Share on other sites

  • Bot

 

1 hour ago, Intel said:

This is making me question then, for everyone using the original login, should this fix be applied in the changing character phase?

No, as you can see in LoginData.cpp constructor we have: 

m_bAllowLoginByKey = true; // We allow loginByKey by default (so the first login doesn't fail)

This is exactly for that. There is not problem because as far as i've read in the code that part can't be exploited, correct me if I'm wrong.
 

Also I want to say this is not a common exploit everyone can use. I posted it because there is a turkish hack that has a function called "Channel changer" that use exactly this exploit to change channel and skip the time checks.



Didn't want to post a demonstration because last time I posted an exploit people abused on some servers and hurt them.
This is an exception because not everyone knows how to implement this from outside the binary 😅
This is how the exploit works (this is only for demonstration, you need extra steps to reproduce on a live server) :

1. Create a functions like this:

void CPythonNetworkStream::SendLoginByKeyExploitPacket(BYTE bCharacterSlot, const char* c_szAddr, uint32_t uPort)
{
    __DirectEnterMode_Set(bCharacterSlot, true);
    Connect(c_szAddr, uPort);
}

2. Call it like this:
- bCharacterSlot = your character slot, eg. 0
- szAddr = server ip
- uPort = the port in which you want to go

3. Go on a map1 and open general store on ch1
4. After you buy an item from the general store, you imediatly call the login by key function with the port for ch2
5. You will be teleported to ch2 and all the checks after buying an item will be skipped
6. If the server does not extra checks you might see you got the item but you still have the money 😅

 

english_banner.gif

Link to comment
https://metin2.dev/topic/33869-loginbykey-exploit/#findComment-171686
Share on other sites

  • Premium
19 minutes ago, Abel(Tiger) said:

 

No, as you can see in LoginData.cpp constructor we have: 

m_bAllowLoginByKey = true; // We allow loginByKey by default (so the first login doesn't fail)

This is exactly for that. There is not problem because as far as i've read in the code that part can't be exploited, correct me if I'm wrong.
 

Also I want to say this is not a common exploit everyone can use. I posted it because there is a turkish hack that has a function called "Channel changer" that use exactly this exploit to change channel and skip the time checks.



Didn't want to post a demonstration because last time I posted an exploit people abused on some servers and hurt them.
This is an exception because not everyone knows how to implement this from outside the binary 😅
This is how the exploit works (this is only for demonstration, you need extra steps to reproduce on a live server) :

1. Create a functions like this:

void CPythonNetworkStream::SendLoginByKeyExploitPacket(BYTE bCharacterSlot, const char* c_szAddr, uint32_t uPort)
{
    __DirectEnterMode_Set(bCharacterSlot, true);
    Connect(c_szAddr, uPort);
}

2. Call it like this:
- bCharacterSlot = your character slot, eg. 0
- szAddr = server ip
- uPort = the port in which you want to go

3. Go on a map1 and open general store on ch1
4. After you buy an item from the general store, you imediatly call the login by key function with the port for ch2
5. You will be teleported to ch2 and all the checks after buying an item will be skipped
6. If the server does not extra checks you might see you got the item but you still have the money 😅

 

Yeah I realized the LoginData constructor thing. I wonder why the FindLogonAccount check wouldn't be sufficient, but honestly I've had enough to investigate the login flow lol

 

Btw I guess at the end of the day, if I am not mistaken, it boils down to badly written systems that don't do the needed checks anyway (better safe than sorry though ahah)

Edited by Intel
  • Love 1
Link to comment
https://metin2.dev/topic/33869-loginbykey-exploit/#findComment-171687
Share on other sites

  • 1 month later...

Don't use any images from : imgur, turkmmop, freakgamers, inforge, hizliresim... Or your content will be deleted without notice...
Use : https://metin2.download/media/add/

Please use https://metin2.download/ when uploading files smaller than 100MB, otherwise the approval will take longer due to manual upload.

Please sign in to comment

You will be able to leave a comment after signing in



Sign In Now
×
×
  • Create New...

Important Information

Terms of Use / Privacy Policy / Guidelines / We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.