#include "StdAfx.h" #include "TickSpeedChecker.h" #include #include #include "PacketStruct_CG.h" #ifdef _NA_20100802_SPEED_HACK_DETECTION #define FOCUS_LOG_BEGIN(log_level) if (focused_) { \ SUNLOGEX(GetSettings().GetLog(), CSunLog::c##log_level, CSunLog::cOUTPUT_FILE, CSunLog::cCATE_ABUSE, #define FOCUS_LOG_END ); } // ¶ó¿îµå ½ÃÀÛ »çÀÌÀÇ ±âÁØ ½Ã°£ °£°Ý (ÃÊ ´ÜÀ§) // ÃÖ¼ÒÇÑ (Update() ½Ã°£ ÇØ»óµµ (1ÃÊ) * NUM_REPETITIONS_PER_ROUND) º¸´Ù´Â Å©µµ·Ï // ·£´ý ¹üÀ§¸¦ °¨¾ÈÇØ ¼³Á¤µÇ¾î¾ß ÇÑ´Ù. const unsigned int TickSpeedChecker::Settings::kMillisecsBetweenRoundStart = 60; // ÆÇÁ¤¿¡ »ç¿ëÇÒ ÃÖ±Ù ¼º°øÇÑ ¶ó¿îµåÀÇ °³¼ö const size_t TickSpeedChecker::Settings::kNumRecentRounds = 5; // ÆÇÁ¤ ÀÓ°è°ª delta ºñÀ²Ç¥ float TickSpeedChecker::Settings::kThresholds[MAX_ASSESSMENT_GRADE - 1] = { \ 1.1f, // DOUBTFUL 1.2f, // SUSPECT 1.4f, // KEY_SUSPECT 1.8f // SECURITY_RISK }; // ÃÖ±Ù nȸÀÇ ¼º°øÀûÀÎ ¶ó¿îµå¿¡¼­ Çã¿ëÇÒ µî±Þº° °³¼ö »óÇÑ float TickSpeedChecker::Settings::kGradeLimits[MAX_ASSESSMENT_GRADE - 1] = { \ 4, // DOUBTFUL 3, // SUSPECT 2, // KEY_SUSPECT 1 // SECURITY_THREAT }; // ÆÇÁ¤ µî±Þ ¹®ÀÚ¿­ Ç¥Çö const char* TickSpeedChecker::Settings::kGradeString[MAX_ASSESSMENT_GRADE] = { \ "GOOD", "DOUBTFUL", "SUSPECT", "KEY_SUSPECT", "SECURITY_RISK" }; TickSpeedChecker::TickSpeedChecker() : user_(NULL) , enabled_(false) , excepted_(false) , focused_(false) { } TickSpeedChecker::~TickSpeedChecker() { } void TickSpeedChecker::Init(User* user) { user_ = user; ASSERT(user_ != NULL); last_round_start_tick_ = 0; PickMillisecsToNextRound(); // ´ÙÀ½ ¶ó¿îµå±îÁöÀÇ ½Ã°£ °£°Ý ÃʱâÈ­ num_total_rounds_ = 0; num_successful_rounds_ = 0; assessment_.clear(); Reset(); } void TickSpeedChecker::SetUserID() { const TCHAR* user_id = user_->GetUserID(); excepted_ = GetSettings().IsException(user_id); focused_ = GetSettings().IsFocus(user_id); const TCHAR* status = _T("Normal"); if (excepted_) { status = _T("Excepted"); } else if (focused_) { status = _T("Focused"); } SUNLOG(eCRITICAL_LOG, "[SpeedCheck] activated on account '%s' : %s", user_id, status); } bool TickSpeedChecker::TryActivate(DWORD current_tick) { if ( GetDifference(last_round_start_tick_, current_tick) > millisecs_to_next_round_ ) { // ÃÖÃÊ 1ȸ´Â °Ç³Ê¶Ú´Ù if (last_round_start_tick_ == 0) { last_round_start_tick_ = current_tick; PickMillisecsToNextRound(); return false; } last_round_start_tick_ = current_tick; PickMillisecsToNextRound(); if (repetition_ >= 0) { Assess(); // ÀÌÀü ¶ó¿îµå°¡ Á¾·áµÇÁö ¾Ê¾ÒÀ¸¸é °­Á¦ Æò°¡/Á¾·á } repetition_ = 0; FOCUS_LOG_BEGIN(FULL_LOG) "[%s][%s] »ØºÏ#%u¿ªÊ¼£ºÏÂÒ»ÂÖ°²ÅÅÔÚ%uºÁÃëºó", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), millisecs_to_next_round_ FOCUS_LOG_END return true; } return (repetition_ >= 0); } void TickSpeedChecker::Update() { if (!IsEnabled()) { return; } ASSERT(repetition_ >= 0); if (repetition_ < 0 || repetition_ >= kNumRepetitionsPerRound) { return; } // ¼­¹öÀÇ ÇöÀç Æ½ Ä«¿îÆ®¸¦ ÀúÀåÇØ µÎ°í server_tick_[repetition_] = GetTickCount(); // Ŭ¶óÀÌ¾ðÆ®·Î Æ½ ÁúÀÇ ÆÐŶÀ» Àü¼ÛÇÑ´Ù. MSG_CG_CONNECTION_TICK_QUERY_SYN syn; syn.round = static_cast(num_total_rounds_ + 1); syn.index = static_cast(repetition_++); user_->SendPacket(&syn, sizeof(syn)); FOCUS_LOG_BEGIN(DEV_LOG) "[%s][%s] »ØºÏ#%uÖØ¸´%d:̽²â·þÎñÆ÷¼ÇºÅ%u", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), (repetition_-1), server_tick_[repetition_-1] FOCUS_LOG_END } void TickSpeedChecker::HandleClientResponse(DWORD round, BYTE index, DWORD client_tick, DWORD server_tick2) { if ( round != static_cast(num_total_rounds_ + 1) ) { FOCUS_LOG_BEGIN(FULL_LOG) "[%s][%s] Òì³£¿Í»§¶ËÂֺţº%u£¨Ó¦Îª%u£©", user_->GetUserID(), user_->GetCharName(), round, (num_total_rounds_+1) FOCUS_LOG_END return; } ASSERT(index < kNumRepetitionsPerRound); if (index >= kNumRepetitionsPerRound) { FOCUS_LOG_BEGIN(FULL_LOG) "[%s][%s] ¿Í»§¶ËÖØ¸´Ë÷ÒýÒì³££º%d", user_->GetUserID(), user_->GetCharName(), index FOCUS_LOG_END return; } DWORD server_tick = server_tick_[index]; client_tick_[index] = client_tick; DWORD rtt = GetDifference(server_tick, server_tick2); // rtt = server_tick2 - server_tick server_tick_[index] = server_tick + (rtt/2); FOCUS_LOG_BEGIN(DEV_LOG) "[%s][%s] »ØºÏ#%uÖØ¸´%d:¿Í»§¶Ë¹´Ñ¡%u£¬·þÎñÆ÷¹´Ñ¡%u£¨sTm=%u£©", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), index, client_tick, server_tick2, server_tick_[index] FOCUS_LOG_END if (index == kNumDeltaPerRound) { Assess(); } } void TickSpeedChecker::Assess() { int i, count; float delta_ratio[kNumDeltaPerRound]; for (i = 0; i < kNumDeltaPerRound; ++i) { delta_ratio[i] = 0.0f; // delta ºñÀ² °ø°£ ÃʱâÈ­ } // delta ºñÀ² °è»ê for (i = 0, count = 0; i < kNumDeltaPerRound; ++i) { if ( (client_tick_[i] == 0) || (client_tick_[i + 1] == 0) ) { continue; } DWORD client_delta = GetDifference(client_tick_[i], client_tick_[i + 1]); DWORD server_delta = GetDifference(server_tick_[i], server_tick_[i + 1]); if (client_delta > 0x7fffffff) { // Á¤»óÀûÀÎ wrap-around°¡ ¾Æ´Ï¶ó ´ÙÀ½ °ªÀÌ ÀÌÀü °ªº¸´Ù ¾à°£ ¾ÕÀ¸·Î µ¹¾Æ°¡ ÀÖ´Ù´Â °ÍÀÌ´Ù // Ŭ¶óÀÌ¾ðÆ® Ãø¿¡¼­ÀÇ ºñÁ¤»óÀûÀÎ Á¶ÀÛÀÌ ¾Æ´Ï¶ó¸é ºÒ°¡´ÉÇÑ °ªÀÌ´Ù // ÃÖ°í µî±ÞÀÇ Á¦À縦 °¡ÇÒ ¼ö ÀÖ´Ù FOCUS_LOG_BEGIN(CRITICAL_LOG) "[%s][%s] Abnormal client tick: [%d]=%u, [%d]=%u", user_->GetUserID(), user_->GetCharName(), i, client_tick_[i], i + 1, client_tick_[i + 1] FOCUS_LOG_END TCHAR description[MAX_ADDINFO_SIZE + 1]; _sntprintf(description, MAX_ADDINFO_SIZE, "Òì³£¿Í»§¶Ë¹´ºÅ[%d]=%u, [%d]=%u", \ i, client_tick_[i], i + 1, client_tick_[i + 1]); description[MAX_ADDINFO_SIZE] = '\0'; GAMELOG->LogSpeedHackSuspect(user_, description); DisconnectUser(); return; } // ÆÇÁ¤ ±âÁØÀÌ µÇ´Â delta ºñÀ²Àº (Ŭ¶óÀÌ¾ðÆ® Æ½ Â÷ÀÌ)/(¼­¹ö ƽ Â÷ÀÌ)ÀÌ´Ù delta_ratio[i] = (float)client_delta / server_delta; FOCUS_LOG_BEGIN(DEV_LOG) "[%s][%s]µÚ#%uÂÖÆÀ¹À£º %d-%d %.2f (%u/%u)", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), i, (i+1), delta_ratio[i], client_delta, server_delta FOCUS_LOG_END } // Æò±Õ °è»ê¿¡¼­ ¹èÁ¦ÇÒ ÃÖ´ë/ÃÖ¼Ò°ªÀ» °áÁ¤ÇÑ´Ù // ´Ü, ÃÖ´ë/ÃÖ¼Ò°ªÀÌ µÎ ¹ø ÀÌ»ó ³ªÅ¸³­´Ù¸é ¹èÁ¦ÇÏÁö ¾Êµµ·Ï ÇÑ´Ù. float max_ratio = 0.0f; float min_ratio = 1000000.0f; for (i = 0, count = 0; i < kNumDeltaPerRound; ++i) { float ratio = delta_ratio[i]; if (ratio < 0.9f) { continue; } if (ratio >= max_ratio) { max_ratio = (ratio != max_ratio ? ratio : 1000000.0f); } if (ratio <= min_ratio) { min_ratio = (ratio != min_ratio ? ratio : 0.0f); } } // ÃÖ´ë/ÃÖ¼Ò°ªÀ» Á¦¿ÜÇϰí delta ºñÀ²ÀÇ 1Â÷ Æò±ÕÀ» °è»êÇÑ´Ù float sum = 0.0f; for (i = 0, count = 0; i < kNumDeltaPerRound; ++i) { float ratio = delta_ratio[i]; // °ÔÀÓ¿¡¼­ ½Ã°£À» ´À¸®°Ô ÇÔÀ¸·Î½á ¾òÀ» ¼ö ÀÖ´Â À̵æÀÌ ¾ø°í // Ŭ¶óÀÌ¾ðÆ®°¡ ¸ô¾Æ¼­ ÀÀ´äÇÏ´Â °æ¿ì client_delta°¡ 0¿¡ °¡±î¿ÍÁú ¼ö ÀÖÀ¸¹Ç·Î // delta ºñÀ² Á¤»ó°ª(1)ÀÇ 90% ¹Ì¸¸ °ªµéÀº ¹ö¸°´Ù if (ratio < 0.9f) { continue; } if (ratio <= min_ratio || max_ratio <= ratio) { continue; } sum += ratio; ++count; } float average = sum / count; float lower_bound = average * 0.67f; float upper_bound = average * 1.33f; // 1Â÷ Æò±Õ °ªÀ» 33% ÀÌ»ó ³Ñ¾î¼­°Å³ª ¹Øµµ´Â ¿¹¿Ü°ªµé°ú ÃÖ´ë/ÃÖ¼Ò°ªÀ» Á¦¿ÜÇÏ°í ´Ù½Ã Æò±ÕÀ» ±¸ÇÑ´Ù sum = 0.0f; for (i = 0, count = 0; i < kNumDeltaPerRound; ++i) { float ratio = delta_ratio[i]; if (ratio < 0.9f) { continue; } if (ratio <= min_ratio || max_ratio <= ratio) { continue; } if (ratio < lower_bound || upper_bound < ratio) { continue; } sum += ratio; ++count; } average = 0.0f; AssessmentGrade grade = SUSPENDED; // À¯È¿ÇÑ delta Ç¥º» ¼ö°¡ ¶ó¿îµå¿¡¼­ ¾òÀ» ¼ö ÀÖ´Â Á¤»ó °ªÀÇ 50% ÀÌÇÏÀÎ °æ¿ì if ( count <= (kNumDeltaPerRound / 2) ) { // ÀÌ ¶ó¿îµå µ¥ÀÌÅÍ´Â ÆÇÁ¤¿¡ »ç¿ëÇÏÁö ¾Ê´Â´Ù FOCUS_LOG_BEGIN(FULL_LOG) "[%s][%s] µÚ#%uÂÖʧ°Ü£ºÔöÁ¿Ñù±¾²»×ã %d", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), count FOCUS_LOG_END } else { // ÃÖÁ¾ Æò±Õ average = sum / count; grade = GetAssessmentGrade(average); assessment_.push_back(grade); while (assessment_.size() > Settings::kNumRecentRounds) { assessment_.pop_front(); } FOCUS_LOG_BEGIN(FULL_LOG) "[%s][%s] »ØºÏ#%u³É¹¦£º%s£¨Æ½¾ùÔöÁ¿±ÈÂÊ%.2f£©", user_->GetUserID(), user_->GetCharName(), (num_total_rounds_+1), Settings::kGradeString[grade], average FOCUS_LOG_END ++num_successful_rounds_; } ++num_total_rounds_; Dispose(grade, average); Reset(); } void TickSpeedChecker::Dispose(AssessmentGrade grade, float average_delta_ratio) { // °Ë»ç ½ÇÆÐÇÑ ¶ó¿îµåÀÇ °æ¿ì if (grade == SUSPENDED) { // °Ë»ç ¶ó¿îµå ¼º°øÀ²ÀÌ ±ØÈ÷ ºñÁ¤»óÀûÀÎ °æ¿ì º¸¾È À§ÇùÀ¸·Î ó¸® float round_success_ratio = (float)num_successful_rounds_ / num_total_rounds_; if (round_success_ratio < 0.01f && num_total_rounds_ >= 100) { FOCUS_LOG_BEGIN(CRITICAL_LOG) "[%s][%s]Òì³£ÂÖÍê³ÉÂÊ£º³É¹¦%u/×ÜÊý%u", user_->GetUserID(), user_->GetCharName(), num_successful_rounds_, num_total_rounds_ FOCUS_LOG_END // ½ÃµµµÈ ÃÑ ¶ó¿îµå ¼ö°¡ 100ȸ°¡ ³Ñ´Âµ¥ 1% ¹Ì¸¸ÀÇ ¼º°øÀ²À» º¸Àδٸé // ÃÖ°í µî±ÞÀÇ Á¦À縦 °¡ÇÒ ¼ö ÀÖ´Ù TCHAR description[MAX_ADDINFO_SIZE + 1]; _sntprintf(description, MAX_ADDINFO_SIZE, "Òì³£ÂÖÍê³É%u/%u", \ num_successful_rounds_, num_total_rounds_); description[MAX_ADDINFO_SIZE] = '\0'; GAMELOG->LogSpeedHackSuspect(user_, description); DisconnectUser(); } return; } // ¼º°øÇÑ ¶ó¿îµå ¼ö°¡ ±âÁØ¿¡ ¹ÌÄ¡Áö ¸øÇÑ´Ù¸é ÆÇÁ¤À» À¯º¸ÇÑ´Ù if (assessment_.size() < Settings::kNumRecentRounds) { return; } for (int i = SECURITY_RISK; i > GOOD; --i) { size_t n = CountGrades(i); if (n >= Settings::kGradeLimits[i - 1]) { FOCUS_LOG_BEGIN(CRITICAL_LOG) "[%s][%s] ÒÉËÆ¼ÓËÙ: %s", user_->GetUserID(), user_->GetCharName(), Settings::kGradeString[i] FOCUS_LOG_END TCHAR description[MAX_ADDINFO_SIZE + 1]; _sntprintf(description, MAX_ADDINFO_SIZE, "%u %s last_d_ratio=%.2f", \ n, Settings::kGradeString[i], average_delta_ratio); description[MAX_ADDINFO_SIZE] = '\0'; GAMELOG->LogSpeedHackSuspect(user_, description); assessment_.clear(); // °­Á¦ Á¢¼Ó Á¾·á¸¦ »ç¿ëÇÏÁö ¾Ê´Â °æ¿ì¸¦ À§ÇØ ÃÖ±Ù Æò°¡ µî±Þ ±â·Ï ÃʱâÈ­ DisconnectUser(); return; } } } void TickSpeedChecker::Reset() { repetition_ = -1; for (int i = 0; i < kNumRepetitionsPerRound; ++i) { server_tick_[i] = 0; client_tick_[i] = 0; } } void TickSpeedChecker::PickMillisecsToNextRound() { // ±âÁØ ½Ã°£ °£°ÝÀÇ -33% ~ +33% »çÀÌ·Î ´ÙÀ½ ¶ó¿îµå ½ÃÀÛ±îÁöÀÇ ½Ã°£ °£°Ý °áÁ¤ millisecs_to_next_round_ = static_cast( \ (Settings::kMillisecsBetweenRoundStart * 1000) * frand(0.67f, 1.33f) ); } void TickSpeedChecker::DisconnectUser() { if (GetSettings().UsingForcedDisconnection() == false) { return; } GAMELOG->LogSpeedHackSuspectDisconnection(user_); MSG_CG_CONNECTION_DISCONNECT_CMD cmd; cmd.m_dwErrorCode = RC::RC_CONNECTION_REQUESTFROMGAMESERVER; user_->SendPacket(&cmd, sizeof(cmd)); user_->SetDisconnectCode(RC::RC_DISCONNECT_SPEED_HACK_SUSPECTED); user_->DisconnectUser(); } struct GTE { GTE(int value) : value_(value) {} bool operator()(int value) { return (value >= value_); } private: int value_; }; size_t TickSpeedChecker::CountGrades(int grade) { return count_if(assessment_.begin(), assessment_.end(), GTE(grade)); } TickSpeedChecker::AssessmentGrade TickSpeedChecker::GetAssessmentGrade(float delta_ratio) { for (int i = SECURITY_RISK; i > GOOD; --i) { if (delta_ratio >= Settings::kThresholds[i - 1]) { return static_cast(i); } } return GOOD; } TickSpeedChecker::Settings::Settings() : enabled_(false) , force_disconnection_(false) , log_(NULL) { } TickSpeedChecker::Settings::~Settings() { SAFE_DELETE(log_) } void TickSpeedChecker::Settings::Load(const TCHAR* filename) { enabled_ = (GetPrivateProfileInt(_T("GLOBAL"), _T("ENABLE"), 0, filename) != 0 ? true : false); force_disconnection_ = (GetPrivateProfileInt(_T("GLOBAL"), _T("FORCE_DISCONN"), 0, filename) != 0 ? true : false); #ifdef _CHINA if (GetPrivateProfileInt(_T("GLOBAL"), _T("ENABLE"), UINT_MAX, filename) == UINT_MAX) { enabled_ = true; // Áß±¹¿¡ ÇÑÇØ Ưº°È÷ ÁöÁ¤µÇÁö ¾ÊÀ¸¸é Ȱ¼ºÈ­ } #endif SUNLOG(eCRITICAL_LOG, "[SpeedCheck] GLOBAL:ENABLE = %d", enabled_); SUNLOG(eCRITICAL_LOG, "[SpeedCheck] GLOBAL:FORCE_DISCONN = %d", force_disconnection_); TCHAR buffer[2048+1]; // ÀÐ¾î µéÀÌ´Â ¼³Á¤ ¹®ÀÚ¿­ ÃÖ´ë ±æÀ̸¦ 2048·Î Á¦ÇÑ DWORD result = GetPrivateProfileString(_T("USERS"), _T("EXCEPT"), _T(""), buffer, _countof(buffer), filename); buffer[_countof(buffer) - 1] = '\0'; if (result > 0 && _tcslen(buffer) > 0) { SUNLOG(eCRITICAL_LOG, "[SpeedCheck] USERS:EXCEPT = %s", buffer); char* token = strtok(buffer, ","); while (token != 0) { except_users_.insert(token); token = strtok(NULL, ","); } } result = GetPrivateProfileString(_T("USERS"), _T("FOCUS"), _T(""), buffer, _countof(buffer), filename); buffer[_countof(buffer) - 1] = '\0'; if (result > 0 && _tcslen(buffer) > 0) { SUNLOG(eCRITICAL_LOG, "[SpeedCheck] USERS:FOCUS = %s", buffer); char* token = strtok(buffer, ","); while (token != 0) { focus_users_.insert(token); token = strtok(NULL, ","); } } } void TickSpeedChecker::Settings::CreateLog(const TCHAR* path) { if (log_ != NULL) { return; } log_ = new CSunLog(); log_->Init(OUTPUT_BOTH, eDEV_LOG, path, "Agent.TickSpeedChecker"); } // EOF TickSpeedChecker.cpp #endif // #ifdef _NA_20100802_SPEED_HACK_DETECTION