网游动态反外挂的研究

Home / Article MrLee 2016-4-10 2375

新年第一帖。 前言: 。现在的网络游戏反外挂早已经不是固定的,静态的检测代码了,这样做会让外挂制作者比较容易的通过排除法知道是外挂哪个部分行为被检测出来。现在更流行一种动态的外挂检测方案,这种方案就是把众多检测外挂的代码作为ShellCode形式放在服务端上面,再定时随机发送代码给客户端进行校验,客户端执行完ShellCode后再把结果回传给服务端进行校验,如果超时或者结果异常,就进行相应的处理(封号,T下线等)。在此感谢下李佳处长给我的实习机会。 正文: 让我们先从客户端开始: 从连接到服务端的那一刻开始,客户端遍一直接受着来自服务端的网络消息,登陆消息啊,玩家状态同步消息阿等等,我们都不感兴趣。我们要看的是服务端发给客户端的动态执行反外挂代码的消息,它就是以下这几个消息
    ,MSG_CHECKCHEATREQ                      // 检查是否作弊
    ,MSG_CHECKCHEATACK                      // 检查是否作弊
    ,MSG_GETPROCESSLISTREQ                  // 请求获得客户端的进程列表
    ,MSG_GETPROCESSLISTACK                  // 回应进程列表
    ,MSG_REQNUMBERCODE                      // 请求客户端检测图片验证码
    ,MSG_ACKNUMBERCODE                      // 客户端应答图片验证码

我们可以大致把它们区分成3种: 1.  单纯的检测代码。 2.  有回传进程信息与模块信息的检测代码。 3.  图形验证码的验证。 其中1跟2大同小异,唯一的区别就是传给ShellCode的参数,与回传的数据不一样,第3条就是在游戏里面常常遇见的的验证码识别了,这个被外挂制造者研究的也是挺多了。 让我们从MSG_CHECKCHEATREQ开始分析起
case MSG_CHECKCHEATREQ:
    {
      playerMgr->OnMsgCheckCheatReq( pMsg );
    }
    break;

收到消息后,调用OnMsgCheckCheatReq函数,我们继续看
void CPlayerMgr::OnMsgCheckCheatReq( Msg* pMsg )
{
  MsgCheckCheatReq* pkMsg = ( MsgCheckCheatReq* )pMsg;
  int nValue = CheckCheatStatic( pkMsg );
  MsgCheckCheatAck msg;
  msg.nResult = nValue;
  GettheNetworkInput().SendMsg( &msg );
}

这里就是把包含ShellCode的消息包交给CheckCheatStatic函数处理,并把函数的返回值回传给服务端。CheckCheatStatic函数就是简单的申请了一块内存,然后执行下ShellCode的代码并返回结果。
static int CheckCheatStatic( MsgCheckCheatReq* pMsg )
{
  LPVOID lpAddress = 0;
  try
  {
    uint32 nRet = 0;
    lpAddress = VirtualAlloc(
      NULL,                 // system selects address
      4096, // size of allocation
      MEM_COMMIT,          // allocate reserved pages
      PAGE_EXECUTE_READWRITE );
    if ( lpAddress == NULL )
    { return 0; }
    SIZE_T nWriteSize = 0;
    if ( WriteProcessMemory( GetCurrentProcess(), lpAddress, (LPVOID)pMsg->szCode, pMsg->nLength, &nWriteSize ) == FALSE )
    {
      int nError = GetLastError();
      VirtualFree( lpAddress, 0, MEM_RELEASE );
      return 1; 
    }
    _asm
    {
      //lea eax,acTempCode;
      call lpAddress;
      mov nRet,eax;
    }
    VirtualFree( lpAddress, 0, MEM_RELEASE );
    return nRet;
  }
  catch( const char* )
  {
    VirtualFree( lpAddress, 0, MEM_RELEASE );
  }
  return 2;
}

客户端的真相已经被查清了,接着我们深入研究下服务端,来看看服务端是怎么处理的。 服务端发送检测代码的函数是这一个:
/////////////////////////////////////////////////////////////////////////////////////////
// 发送检测代码给客户端
bool GamePlayer::ProcessSendCheckCheatData( CodeData* pCodeData, unsigned char uchCodeType )
{
    if ( pCodeData == NULL || pCodeData->GetCodeLength() == 0 )
    { return false; }
    bool bSendSuccess = false;
    switch ( uchCodeType )
    {
    case RabotDefine::EGP_CheckCode:
        {
            MsgCheckCheatReq xReq;
            bSendSuccess = xReq.AddCode( pCodeData->GetCheckCode(), pCodeData->GetCodeLength() );
            if ( bSendSuccess )
            { SendMessageToClient( &xReq ); }
        }
        break;
    case RabotDefine::EGP_GetProcessList:
        {
            MsgGetProcessListReq xReq;
            bSendSuccess = xReq.AddCode( pCodeData->GetCheckCode(), pCodeData->GetCodeLength() );
            if ( bSendSuccess )
            { SendMessageToClient( &xReq ); }
        }
        break;
    default:
        break;
    }
    return bSendSuccess;
}

我们继续往上面跟,可以看到这个函数里面,就是朝玩家发送检测消息了,其中还设置了相关的参数(正确的答案,代码长度,超时的时间等.)
template< class T >
unsigned short RabotManager< T >::SendCheckCodeToPlayer( CheckData* pCheckData, CodeData* pCodeData, unsigned int dwSendTime )
{
    if ( pCheckData == NULL )
    { return RabotDefine::EGP_Delete; }
    T* pPlayer = GetPlayerByID( pCheckData->GetPlayerID() );
    if ( pPlayer == NULL )
    { return RabotDefine::EGP_Delete; }
    // 需要实现ProcessSendCheckCheatData函数
    if ( pPlayer->ProcessSendCheckCheatData( pCodeData, RabotDefine::EGP_CheckCode ) )
    {
        pCheckData->SetRightResult( pCodeData->GetRightResult() );
        pCheckData->SetCheckStatus( RabotDefine::EGP_WaitCheckResult );
        pCheckData->StartOperateTimer( dwSendTime, RabotDefine::EGP_ResultTimeOut );
        return RabotDefine::EGP_Success;
    }
    pCheckData->Initialize();
    return RabotDefine::EGP_Failed;
}

相关的常量:
enum EGameProtectConstDefine
    {
        EGP_MaxSystemErrorCount = 2,        // 返回的系统级别错误次数,超过此次数认为作弊,T下线( 如果以后确定是作弊,则封号处理 )
        EGP_ResultTimeOutCount  = 2,        // 超时未回应服务器次数
        EGP_ResultTimeOut       = 12010,    // 发送检测函数以后,超过该时间未响应,累计次数超过 EGP_ResultTimeOutCount 认为作弊
        EGP_CheckSpaceTime      = 300000,   // 进入游戏后, 4分钟后开始检测
        EGP_UpdateSpaceTime     = 5000,     // 5秒update一次
        EGP_FuncNameLength      = 41,       // 执行函数名的长度
        EGP_DoNothing           = 0,
        EGP_SendCheckCode       = 1,
        EGP_KickPlayer          = 2,
        EGP_Delete              = 0,
        EGP_Success             = 1,
        EGP_Failed              = 2,
        EGP_InitResult          = 0,
        EGP_MaxCodeLength       = 4096,     // 检测代码最大长度
        EGP_ChangeCodeSpaceTime = 60 * 60 * 1000,  // 两小时切换一次检测代码
        EGP_CheckCode           = 1,
        EGP_GetProcessList      = 2,
        EGP_NotResult           = 0xFFFF,
        EGP_CheckFirstTime      = 0,
        EGP_CheckStatusInit     = 1,
        EGP_WaitSendCode        = 2,
        EGP_WaitCheckResult     = 3,
};

看到这里,你已经对整个体系有一个大致的了解了,我们再接着看
template< class T >
void RabotManager< T >::RunCheckCheatUpdate( unsigned int dwCurrentTime )
{
    if ( !GetCheckOpen() )      // 开关没有打开
    { return; }
    if ( !m_xUpdateTimer.DoneTimer( dwCurrentTime ) )
    { return; }
    ProcessChangeCheckCode( dwCurrentTime );
    if ( !IsHaveCodeData() )
    { return; }

    for ( MapCheckDataIter iter = m_mapPlayerCheckData.begin(); iter != m_mapPlayerCheckData.end(); ++iter )
    {
        switch( iter->second->Update( dwCurrentTime ) )
        {
        case RabotDefine::EGP_SendCheckCode:     // 发送检测代码
            {
                CodeData* pCodeData = GetCodeDataByRand();
                if ( pCodeData != NULL )
                {
                   SendCheckCodeToPlayer( iter->second, pCodeData, dwCurrentTime );
                }
            }
            break;
        case RabotDefine::EGP_KickPlayer:       // T掉某玩家
            {
                KickPlayerByErrorResult( iter->second->GetPlayerID(), RabotDefine::EGP_NotResult );
            }
            break;
        default:
            break;
        }
    }
}

这里就可以很清楚的看到服务端对所有玩家定时(2小时换一次)的在发送随机的检测代码,再根据结果进行相应的处理。那ShellCode是怎么来的呢,我们接着看:
template< class T >
void RabotManager< T >::ReleaseCheckCodeAddress()
{
    m_vecCodeData.clear();
}
typedef int ( * __GetCheckFuncCount )();                                  // 得到检测函数数量
typedef char* ( * __GetCheckFuncName )( int nIndex );                     // 得到检测函数的名字
typedef int ( * __GetCheckFuncLength )( void* pFunc, int nMaxLength );    // 得到检测函数长度
typedef unsigned int ( * __GetFuncRightResult )( const char* pszFunc );   // 得到检测函数的正确答案
typedef int ( * __GetMaxSystemError )();                                  // 最大系统错误
typedef int ( * __GetVersion )();                                         // 获得版本号
template< class T >
bool RabotManager< T >::LoadCheckCheatConfig( const char* pszFileName )
{
    if ( pszFileName == NULL || pszFileName[0] == 0 )
    { return false; }
    HMODULE hHandle = ::LoadLibraryA( pszFileName );
    if ( hHandle == NULL )
    { return false; }
    __GetVersion MyGetVersion = ( __GetVersion )::GetProcAddress( hHandle, "GetCheckVersion" );
    if ( MyGetVersion == NULL )
    { 
        FreeLibrary( hHandle );
        return false;
    }
    //if ( MyGetVersion() == GetConfigVersion() )         // 版本没变就不读取了
    //{ 
    //    FreeLibrary( hHandle );
    //    return true;
    //}
    
    __GetMaxSystemError MyGetMaxSystemError = ( __GetMaxSystemError )::GetProcAddress( hHandle, "GetMaxSystemError" );
    if ( MyGetMaxSystemError == NULL )
    { 
        FreeLibrary( hHandle );
        return false;
    }
    __GetCheckFuncCount MyGetCheckFuncCount = ( __GetCheckFuncCount )::GetProcAddress( hHandle, "GetCheckFuncCount" );
    if ( MyGetCheckFuncCount == NULL )
    { 
        FreeLibrary( hHandle );
        return false;
    }
    __GetCheckFuncName MyGetCheckFuncName = ( __GetCheckFuncName )::GetProcAddress( hHandle, "GetCheckFuncName" );
    if ( MyGetCheckFuncName == NULL )
    { 
        FreeLibrary( hHandle );
        return false;
    }
    __GetCheckFuncLength MyGetCheckFuncLength = ( __GetCheckFuncLength )::GetProcAddress( hHandle, "GetCheckFuncLength" );
    if ( MyGetCheckFuncLength == NULL )
    {
        FreeLibrary( hHandle );
        return false;
    }
    __GetFuncRightResult MyGetFuncRightResult = ( __GetFuncRightResult )::GetProcAddress( hHandle, "GetFuncRightResult" );
    if ( MyGetFuncRightResult == NULL )
    { 
        FreeLibrary( hHandle );
        return false;
    }
    // 还要把所有的错误码对应的外挂名称读取出来
    //......
    SetMaxSystemError( MyGetMaxSystemError() );
    SetConfigVersion( MyGetVersion() );      //保存版本号
    // 读取所有的检测代码
    int nCount = MyGetCheckFuncCount();
    m_vecCodeData.resize( nCount );
    for ( int i = 0; i < nCount; ++i ) 
    {
        char* pszFuncName = MyGetCheckFuncName( i );
        if ( pszFuncName == NULL )
        { continue; }
        void* pCheckFunc = ::GetProcAddress( hHandle, pszFuncName );
        if ( pCheckFunc == NULL )
        { continue; }
        int nCodeLength = MyGetCheckFuncLength( pCheckFunc, RabotDefine::EGP_MaxCodeLength );
        if ( nCodeLength == 0 )
        { continue; }
        unsigned int nRightResult = MyGetFuncRightResult( pszFuncName );
        if ( nRightResult == 0 )
        { continue; }
        m_vecCodeData[ i ] = CodeData( nRightResult, pCheckFunc, nCodeLength, pszFuncName );
    }
    static const char* szGetProcessListFuncName = "GetProcessList";
    void* MyGetProcessList = ::GetProcAddress( hHandle, szGetProcessListFuncName );
    if ( MyGetProcessList != NULL )
    {
        int nLength = MyGetCheckFuncLength( MyGetProcessList, RabotDefine::EGP_MaxCodeLength );
        if ( nLength != 0 )
        {
            m_xGetProcessList.SetCodeFuncName( szGetProcessListFuncName );
            m_xGetProcessList.SetCheckCode( MyGetProcessList, nLength );         
            m_xGetProcessList.SetRightResult( nLength );
        }
    }
    m_strLoadFile = pszFileName;        // 保存当前加载的文件名字
    FreeLibrary( hHandle );
    return true;
}

这样就很清楚了,原来所有的检测代码都是服务端通过读取dll来获取的,

3


所有的检测函数其实都是DLL的导出函数,先通过调用DLL的GetCheckFuncName与GetCheckFuncCount导出函数来得到所有检测函数的导出名字,我们可以用IDA随便加载一个反外挂的dll,就可以清楚的看到
和检测外挂的ShellCode了

1


漂漂亮亮的ShellCode阿。

2

本文链接:https://www.it72.com/9013.htm

推荐阅读
最新回复 (0)
返回