14.5 Anti-Cheat และความปลอดภัย

เฟส 14 · เน็ตเวิร์กและ Live-Service · เวลาเรียน: 20–40 h

ปกป้องเกมออนไลน์ — server authority, การ validate input และมาตรการ anti-cheat และความปลอดภัยที่ใช้กัน

เกมออนไลน์ทุกเกมมีโปรแกรมสองฝั่งที่ไม่ได้เชื่อกันโดยอัตโนมัติ นั่นคือ client (สำเนาของเกมที่รันอยู่บนเครื่องของผู้เล่นเอง) กับ server (โปรแกรมที่เก็บสถานะเกมชุดจริงที่ทุกคนเชื่อร่วมกัน) ที่ผ่านมาในหลักสูตรนี้เราเขียนโค้ดราวกับว่าทั้งสองฝั่งเห็นความจริงชุดเดียวกันเสมอ แต่ในเกมมัลติเพลเยอร์หรือเกมออนไลน์จริง มันไม่ได้เป็นแบบนั้น — ผู้เล่นควบคุมเครื่องที่ client ของตัวเองรันอยู่ได้เต็มที่ และช่องว่างระหว่างสิ่งที่ client อ้างว่าเกิดขึ้นกับสิ่งที่เกิดขึ้นจริง คือจุดที่การโกงเกิดขึ้นพอดี บทนี้จะพูดถึงกฎข้อเดียวที่ทุกอย่างในบทนี้สร้างต่อยอดมาจากมัน, การตรวจสอบฝั่ง server แบบเป็นรูปธรรมที่บังคับใช้กฎนั้น, ประเภทของ cheat ที่พบบ่อยอธิบายในระดับที่พอจะเข้าใจวิธีป้องกัน (ไม่ใช่วิธีทำ), และส่วนที่อยู่รอบ ๆ ปัญหานี้ — การเข้ารหัส, ความปลอดภัยของบัญชี, เครื่องมือระดับ kernel

1. กฎทองคำ: อย่าเชื่อ Client เด็ดขาด

client กับ server แลกเปลี่ยนข้อมูลชิ้นเล็ก ๆ ที่เรียกว่า packet (packet คือหน่วยข้อมูลหนึ่งชุดที่ส่งผ่านเครือข่าย เหมือนซองจดหมายหนึ่งซอง) ตลอดเวลา packet จาก client อาจบอกว่า "ฉันกดปุ่มเดินหน้า" "ฉันยิงอาวุธ" หรือ "ตอนนี้ฉันยืนอยู่ที่ตำแหน่ง (120, 4, 88)"

ปัญหาคือ ผู้เล่นเป็นเจ้าของเครื่องที่ client รันอยู่ พวกเขาสามารถต่อ debugger แล้วหยุดเกมกลางเฟรมได้ เปิดหน่วยความจำของเกมด้วยเครื่องมือแล้วแก้ตัวเลขระหว่างที่เกมกำลังรันอยู่ได้ เขียนไฟล์ executable ของเกมใหม่บางส่วนได้ หรือเขียนโปรแกรมที่คั่นกลางระหว่างเกมกับเครือข่ายเพื่อแก้ packet ก่อนที่มันจะออกจากเครื่องได้ ทั้งหมดนี้ไม่ได้แตะ server เลยแม้แต่นิดเดียว — มันเกิดขึ้นบนฮาร์ดแวร์ที่ผู้เล่นควบคุมเต็มที่ทั้งสิ้น

นั่นหมายความว่า packet ทุกอันที่ client ส่งมาเป็นแค่ คำกล่าวอ้าง ไม่ใช่ข้อเท็จจริง "ฉันสร้างดาเมจ 9999" ไม่ใช่ดาเมจที่เกิดขึ้นจริงตามกฎของเกม — มันคือตัวเลขที่โปรแกรมบนเครื่องของผู้เล่นเองตัดสินใจส่งมา ถ้า server เอาคำกล่าวอ้างของ client มาใช้กับสถานะเกมที่ทุกคนใช้ร่วมกันโดยไม่ตรวจสอบเลย กฎของเกมก็จะแข็งแรงเท่ากับความเต็มใจของคนโกงที่จะทำตามกฎเท่านั้น ซึ่งแทบไม่แข็งแรงเลย

Tip อ่านคำว่า "อย่าเชื่อ client" แบบแคบ ๆ ให้ถูก — มันไม่ได้แปลว่าผู้เล่นทุกคนเป็นคนไม่ดี หรือคุณต้องระแวงผู้เล่นปกติ มันแปลว่า โค้ดฝั่ง server ต้องไม่สันนิษฐานว่าข้อความเป็นจริงเพียงเพราะมันส่งมาถึง มันต้องตรวจสอบข้อความนั้นกับสิ่งที่กฎของเกมอนุญาตจริง ๆ ทุกครั้ง สำหรับผู้เล่นทุกคน รวมถึงคนที่ไม่ได้โกงด้วย
CLIENT (เครื่องของผู้เล่นเอง ควบคุมได้เต็มที่โดยผู้เล่น) | | packet: "ฉันสร้างดาเมจ 9999 ใส่บอส" v SERVER (ความจริงที่ทุกคนใช้ร่วมกัน เครื่องที่สตูดิโอควบคุม) | | ดาเมจ 9999 เป็นไปได้จริงไหม เมื่อดูจาก | อาวุธ, เลเวล, และบัฟของผู้เล่นคนนี้ ณ ตอนนี้? v [ ไม่ -> เพิกเฉยคำกล่าวอ้าง / คำนวณดาเมจจริงใหม่ ] [ ใช่ -> นำไปใช้กับสถานะเกมที่ใช้ร่วมกัน ]

2. ก่อนและหลัง: Server แบบไร้เดียงสา กับ Server แบบตรวจสอบ

วิธีที่ชัดที่สุดที่จะเห็นว่าทำไม "อย่าเชื่อ client" ถึงสำคัญ คือดูฟีเจอร์เดียวกันสองเวอร์ชัน: การจัดการข้อความดาเมจจากผู้เล่นที่โจมตี ทั้งสองเวอร์ชัน compile ผ่านทั้งคู่ แต่มีแค่เวอร์ชันเดียวที่ปลอดภัยพอจะรันกับผู้เล่นจริง

เวอร์ชันไร้เดียงสา

handler ฝั่ง server นี้รับข้อความจาก client แล้วนำตัวเลขที่ client ส่งมาไปใช้ตรง ๆ:

// NAIVE SERVER CODE -- do not copy this pattern
class DealDamageMessage
{
    public int AttackerId;
    public int TargetId;
    public int DamageAmount; // taken straight from the client
}

void OnDealDamageReceived(DealDamageMessage msg)
{
    Entity target = World.FindEntity(msg.TargetId);
    target.Health -= msg.DamageAmount; // trusts the client completely

    if (target.Health <= 0)
        World.Kill(target);
}

สิ่งที่เกิดขึ้นกับ client ที่ถูกแก้ไข: คนโกงแก้ไข client ของตัวเอง (หรือเขียนโปรแกรมปลอม packet เครือข่าย) ให้ทุก DealDamageMessage ที่ส่งออกไปมี DamageAmount = 9999 เสมอ ไม่ว่าอาวุธที่ถืออยู่จริงจะเป็นอะไรก็ตาม

อาวุธจริงของผู้โจมตี: มีดเริ่มต้น โจมตีปกติ = 8 ดาเมจ Client ที่ถูกแก้ไขส่ง: DamageAmount = 9999 OnDealDamageReceived ทำงาน: target.Health (200) -= 9999 target.Health = -9799 target.Health <= 0 -> World.Kill(target) ผลลัพธ์: ศัตรูตัวไหนก็ตาย รวมถึงบอสที่มี 200 health ตายในหนึ่งฮิตจากอาวุธเริ่มต้น

สิ่งนี้ไม่ต้องแตะ server เลยแม้แต่น้อย คนโกงแตะแค่คอมพิวเตอร์ของตัวเองเท่านั้น

เวอร์ชันตรวจสอบ

วิธีแก้ไม่ใช่การเพิ่มเช็ค "ตัวเลขนี้ใหญ่เกินไปไหม?" ให้กับ DamageAmount — คนโกงที่มีวินัยแค่เลือกตัวเลขที่ต่ำกว่าเพดานที่คุณตั้งไว้ก็จบ วิธีแก้ที่แท้จริงคือเลิกเชื่อตัวเลขนั้นไปเลย: server ไปดูอาวุธและสเตตัสจริงของผู้โจมตีจากข้อมูลที่ตัวเองเป็นเจ้าของ แล้วคำนวณดาเมจเอง

// VALIDATING SERVER CODE
class DealDamageMessage
{
    public int AttackerId;
    public int TargetId;
    // no DamageAmount field at all -- the client cannot send a
    // number the server would need to trust
}

void OnDealDamageReceived(DealDamageMessage msg)
{
    Entity attacker = World.FindEntity(msg.AttackerId);
    Entity target = World.FindEntity(msg.TargetId);

    if (attacker == null || target == null) return;
    if (!attacker.IsAlive || !target.IsAlive) return;
    if (!InAttackRange(attacker, target)) return;   // sanity check
    if (!OffCooldown(attacker, "attack")) return;    // rate limit

    // server computes the real number itself, from data
    // only the server is allowed to change
    int damage = CombatRules.ComputeDamage(attacker.Weapon,
                                            attacker.Level,
                                            attacker.Buffs,
                                            target.Armor);

    target.Health -= damage;
    StartCooldown(attacker, "attack");

    if (target.Health <= 0)
        World.Kill(target);
}

ไล่ trace ด้วย client ที่ถูกแก้ไขตัวเดิม: client ของคนโกงยังส่ง DealDamageMessage ได้เร็วเท่าที่ต้องการ แต่ DamageAmount ไม่มีอยู่เป็นฟิลด์ที่ server อ่านอีกต่อไป server คำนวณดาเมจ 8 สำหรับมีดเริ่มต้นทุกครั้งไม่มีเปลี่ยนแปลง ไม่ว่า client จะอยากให้มันพูดว่าอะไรก็ตาม การแก้ client ไม่เปลี่ยนผลลัพธ์เลยแม้แต่นิดเดียว

นี่คือการเปลี่ยนแปลงหลัก: server แบบตรวจสอบไม่ได้แค่เช็คว่าข้อมูลจาก client ดูสมเหตุสมผล — มันปฏิบัติต่ออินพุตจาก client เป็นแค่ความตั้งใจ ("ฉันกำลังโจมตีเป้าหมายนี้") แล้วคำนวณผลลัพธ์เองจากข้อมูลที่ server เป็นเจ้าของ การเปลี่ยนแค่จุดเดียวนี้ปิดโอกาสโกงทั้งหมวดหมู่ได้ในคราวเดียว

ข้อผิดพลาดที่พบบ่อย การเพิ่มเช็คขอบเขตแบบ if (msg.DamageAmount > 500) reject(); แล้วคิดว่าแก้ปัญหาแล้ว วิธีนี้หยุดการโกงที่โจ่งแจ้งที่สุด (ดาเมจ 9999) ได้ แต่หยุดคนโกงที่มีวินัยที่ส่งดาเมจ 400 ทุกฮิตไม่ได้ — ซึ่งยังสูงกว่าอาวุธจริงมาก แต่อยู่ใต้เพดาน เช็คขอบเขตเป็นเลเยอร์ที่สองที่มีประโยชน์ แต่ไม่ควรเป็นเลเยอร์เดียว สำหรับค่าที่ไม่ควรเชื่อ client ตั้งแต่แรกอยู่แล้ว

3. Server Authority คือแนวป้องกันหลัก

รูปแบบจากข้อ 2 มีชื่อเรียกว่า server authority (กฎที่ว่า server เก็บสถานะเกมชุดจริงเพียงชุดเดียว และ client แต่ละตัวถือแค่สำเนาที่ไม่มีสิทธิ์บังคับให้เปลี่ยนแปลงได้) เกือบทุกเทคนิคเฉพาะเจาะจงในบทนี้คือแนวคิดเดียวกันนี้ที่นำไปใช้กับข้อมูลคนละประเภท: ตำแหน่ง, health, inventory, เงินในเกม, cooldown

เปรียบเทียบสถาปัตยกรรมสองแบบ:

CLIENT-AUTHORITATIVE (อ่อนแอ) client คำนวณผลลัพธ์ -> ส่งผลลัพธ์ -> server เก็บไว้เฉย ๆ เช่น "ตอนนี้ฉันอยู่ที่ (500, 0, 500)" -> server เชื่อไปตรง ๆ SERVER-AUTHORITATIVE (แข็งแรง) client ส่งความตั้งใจ -> server จำลองผล -> server ส่งผลลัพธ์กลับ เช่น "ฉันกำลังกดปุ่มเดินหน้า" -> server เคลื่อนผู้เล่นเอง ด้วย (ความเร็ว * เวลา) แล้วบอกผลลัพธ์ให้ทุกคนรู้

ภายใต้สถาปัตยกรรมแบบ client-authoritative client ที่ถูกแก้ไขสามารถอ้างว่าอยู่ที่ไหนก็ได้ มี health เท่าไหร่ก็ได้ หรือเป็นเจ้าของไอเทมอะไรก็ได้ และ server ไม่มีทางรู้ได้เองว่ามันผิด ภายใต้สถาปัตยกรรมแบบ server-authoritative client ส่งแค่ อินพุต (การกดปุ่ม, ทิศทางเล็ง, ไอเทมที่ต้องการใช้) และ server เป็นโปรแกรมเดียวเท่านั้นที่มีสิทธิ์เปลี่ยนอินพุตเหล่านั้นให้กลายเป็นสถานะเกมใหม่ ส่วน client ก็แค่แสดงผลสถานะที่ server ส่งกลับมา

server authority ไม่ได้มาฟรี ๆ มันกิน CPU ของ server เพิ่ม — เพราะ server ต้องจำลองเกมทั้งหมด ไม่ใช่แค่ตัดสินเหมือนกรรมการ — และมันเพิ่มดีเลย์เล็กน้อยก่อนที่ client จะเห็นผลลัพธ์ที่ยืนยันแล้วของอินพุตตัวเอง นี่คือเหตุผลที่เกมมักใช้ client-side prediction (ให้ client เดาผลลัพธ์ที่น่าจะเป็นทันที แล้วค่อยแก้เงียบ ๆ ถ้า server ไม่เห็นด้วยภายหลัง) เพื่อซ่อนดีเลย์นั้น ต้นทุนนี้เองคือเหตุผลว่าทำไม server authority ถึงเป็นแนวป้องกันหลัก ไม่ใช่ของฟรี สตูดิโอต้องจ่ายแรงงานวิศวกรรมจริง ๆ เพื่อสิ่งนี้ เพราะทางเลือกอื่นคือเกมที่เชื่อถือไม่ได้เลย

4. ตรวจสอบการเคลื่อนที่: เช็คความเร็วและการเทเลพอร์ต

ตำแหน่งเป็นหนึ่งในค่าที่ถูกโกงบ่อยที่สุด เพราะ "อยู่ในที่ที่ไม่ควรไปถึงได้ตอนนี้" มีประโยชน์โดยตรง — จบเรซก่อนเวลา, ยืนอยู่ในกำแพงเพื่อหลบไม่ให้โดนตี, หรือไปถึงทรัพยากรหายากก่อนคนอื่น ระบบการเคลื่อนที่แบบ server-authoritative ยังต้องมีเช็คที่เป็นรูปธรรม เพราะอินพุตปลอม (ตำแหน่งที่อ้าง, delta เวลาที่อ้างว่าใหญ่มาก) ยังหลุดผ่านไปได้ ถ้า server ไม่ตรวจซ้ำผลลัพธ์กับความเป็นจริง

เช็คนี้ง่ายมาก: จากตำแหน่งล่าสุดที่รู้ของผู้เล่นและเวลาจริงที่ผ่านไป มีระยะทางสูงสุดที่เป็นไปได้ที่พวกเขาจะเคลื่อนที่ได้จริง ตำแหน่งที่อ้างเกินกว่าค่าสูงสุดนั้นเป็นไปไม่ได้ตามกฎของเกมเอง

class PlayerMoveState
{
    public Vector3 LastPosition;
    public float LastServerTime;
    public float MaxSpeed; // units per second, from this player's stats
}

bool TryMove(PlayerMoveState state, Vector3 claimedPosition, float serverNow)
{
    float dt = serverNow - state.LastServerTime;
    if (dt <= 0f) return false; // out-of-order or replayed packet

    float distance = Vector3.Distance(state.LastPosition, claimedPosition);
    float maxDistance = state.MaxSpeed * dt * 1.15f; // +15% tolerance for lag

    if (distance > maxDistance)
    {
        Flag(state, "movement speed exceeded"); // impossible -- reject
        return false; // client is snapped back to LastPosition
    }

    state.LastPosition = claimedPosition;
    state.LastServerTime = serverNow;
    return true;
}

ไล่ trace ผู้เล่นปกติ: ความเร็วสูงสุด 6 unit/วินาที ผ่านไป 0.1 วินาทีนับจากอัปเดตล่าสุด -> ระยะทางสูงสุด = 6 * 0.1 * 1.15 = 0.69 unit การก้าวปกติ 0.5 unit ผ่านสบาย ๆ

ไล่ trace การโกงเทเลพอร์ต: ตำแหน่งที่อ้างของผู้เล่นคนเดิมกระโดดไป 500 unit ภายในหน้าต่างเวลา 0.1 วินาทีเดียวกัน

เวลา -> t0 t0 + 0.1 วินาที ตำแหน่ง: (0,0,0) อ้างว่า (500,0,0) ระยะทางสูงสุดที่เป็นไปได้ใน 0.1 วินาที ที่ 6 unit/s (+tolerance) = 0.69 ระยะทางที่อ้างจริง = 500 500 > 0.69 => ถูกปฏิเสธ ตำแหน่งถูกดึงกลับไปที่ (0,0,0)

ค่าความคลาดเคลื่อนที่ยอมรับได้ (1.15f ด้านบน) มีอยู่เพราะดีเลย์เครือข่ายทำให้ server ไม่มีทางเห็นภาพที่อัปเดตล่าสุดแบบสมบูรณ์แบบตลอดเวลา — ต้องมีช่องว่างเล็กน้อยเพื่อไม่ให้ผู้เล่นที่มี lag ปกติ (ไม่ได้โกง) ถูกปฏิเสธตลอดเวลา ตั้งค่านี้แน่นเกินไปจะทำให้ผู้เล่นที่ lag แต่ไม่ได้โกงถูกปฏิเสธผิด ๆ หลวมเกินไปก็จะจับได้แค่การเทเลพอร์ตที่สุดโต่งที่สุดเท่านั้น

5. Rate Limiting การกระทำต่าง ๆ

ความเร็วและตำแหน่งไม่ใช่สิ่งเดียวที่ client ที่ถูกแก้ไขสามารถขยายผลได้ การกระทำใดก็ตามที่มี cooldown หรือความถี่จำกัด — ยิงอาวุธ, ร่ายเวท, เปิดร้านค้า, รับรางวัลประจำวัน — สามารถถูกละเมิดได้โดย client ที่ส่งข้อความ "ทำสิ่งนี้" เร็วกว่าที่ client จริง (และมนุษย์ที่กดปุ่มจริง) จะทำได้มาก นี่คือญาติที่หยาบกว่าของ speed hack: แทนที่จะเคลื่อนที่เร็วขึ้น cheat นี้กระทำการเร็วขึ้นแทน

วิธีป้องกันคือ cooldown ต่อการกระทำที่เก็บไว้บน server ทั้งหมด ถูกตรวจสอบก่อนทุกการกระทำจะได้รับอนุญาตให้เกิดขึ้น โดยไม่ขึ้นกับสิ่งที่ client บอกเกี่ยวกับเวลาของตัวเองเลย:

class ActionCooldowns
{
    Dictionary<string, float> lastUsedServerTime = new Dictionary<string, float>();

    public bool TryUse(string actionName, float cooldownSeconds, float serverNow)
    {
        if (lastUsedServerTime.TryGetValue(actionName, out float last))
        {
            if (serverNow - last < cooldownSeconds)
                return false; // still on cooldown -- refuse the action
        }

        lastUsedServerTime[actionName] = serverNow;
        return true;
    }
}

ไล่ trace: เวทไฟบอลมี cooldown 2 วินาที client ที่ถูกแก้ไขยิงข้อความ CastSpell("fireball") 40 ครั้งในหนึ่งวินาที TryUse ปล่อยให้ครั้งแรกผ่านไปได้ แล้วปฏิเสธอีก 39 ครั้ง เพราะยังไม่ผ่านไป 2 วินาทีนับจากการใช้ครั้งล่าสุดที่ยอมรับ — ไม่ว่าจะมีข้อความมาถึงกี่อันก็ตาม เอฟเฟกต์บนหน้าจอของผู้เล่นอาจสแปม แต่มีไฟบอลเพียงลูกเดียวเท่านั้นที่มีอยู่จริงในสถานะเกมที่ใช้ร่วมกัน

Tip Rate limiting ไม่ใช่แค่เครื่องมือ anti-cheat เท่านั้น — มันยังหยุด client ที่มีบัค (ไม่ได้ตั้งใจโกง) ไม่ให้ถล่ม server โดยไม่ได้ตั้งใจ และหยุดอาการเครือข่ายกระตุกของผู้เล่นเอง (packet ที่ถูกคิว ดีเลย์ แล้วมาถึงพร้อมกันเป็นชุด) ไม่ให้ถูกตีความผิดว่าเป็นคำขอจริง 40 ครั้ง เช็คตัวเดียวกันนี้ป้องกันทั้งการโกงและบัคปกติในเวลาเดียวกัน

6. Pipeline การตรวจสอบแบบทั่วไป

ข้อ 2, 4, และ 5 แต่ละข้อแสดงเช็คเฉพาะเจาะจงหนึ่งอย่าง — คำนวณดาเมจเองแทนที่จะเชื่อ, จำกัดระยะทางการเคลื่อนที่, จำกัดอัตราการกระทำ ในเกมจริงเช็คเหล่านี้มีรูปแบบเดียวกันทุกครั้ง นี่คือรูปแบบนั้นในรูป pseudocode ที่ทั่วไปพอจะใช้ได้กับแทบทุกการกระทำที่ client ร้องขอ:

function HandleClientAction(playerId, action, serverNow):
    player = LookupPlayer(playerId)

    // 1. Identity and ownership -- is this really this player's
    //    connection, and do they own what they claim to act on?
    if player == null or not player.IsConnected:
        return Reject("unknown or disconnected player")

    if action.RequiresOwnership and not player.Owns(action.TargetItemId):
        return Reject("does not own target item")

    // 2. State preconditions -- is this action even legal right now?
    if not player.IsAlive:
        return Reject("dead players cannot act")

    if not player.IsInRange(action.TargetId, action.MaxRange):
        return Reject("target out of range")

    // 3. Rate limiting -- has enough real server time passed?
    if not player.Cooldowns.TryUse(action.Name, action.CooldownSeconds, serverNow):
        return Reject("action on cooldown")

    // 4. Compute the result from server-owned data -- never from
    //    any numeric value the client itself supplied
    result = ComputeResult(player, action)  // e.g. damage, loot, currency

    // 5. Apply to the one shared game state, then tell every
    //    relevant client what actually happened
    ApplyToWorld(result)
    BroadcastToRelevantClients(result)

    return Accept(result)

แต่ละขั้นตอนที่มีเลขกำกับปิดโอกาสการโกงหนึ่งหมวดหมู่: ขั้นที่ 1 หยุดคนโกงที่แสร้งเป็นคนอื่นหรือกระทำกับไอเทมที่ตัวเองไม่ได้เป็นเจ้าของ ขั้นที่ 2 หยุดการกระทำที่ผิดลำดับกฎของเกมเอง (โจมตีทั้งที่ตายแล้ว, ตีสิ่งที่อยู่ไกลสุดขอบแผนที่) ขั้นที่ 3 หยุดการละเมิดความเร็วและอัตรา และขั้นที่ 4 คือบทเรียนจากข้อ 2 ที่ขยายให้ทั่วไปขึ้น — server เป็นสิ่งเดียวเท่านั้นที่มีสิทธิ์ตัดสินว่า "ผลลัพธ์" คืออะไรจริง ๆ

CLIENT SERVER (ส่งแค่ความตั้งใจเท่านั้น) (ตรวจสอบ แล้วค่อยตัดสิน) "ฉันต้องการโจมตี ส่งผ่านเครือข่าย เป้าหมาย #42" ------------------------> | v +-------------------------+ | 1 identity / ownership | | 2 state preconditions | | 3 rate limit | | 4 compute result | +-------------------------+ | นำไปใช้กับสถานะที่ใช้ร่วมกัน แล้วกระจายผล v client ทุกตัวได้รับ: "โจมตี #42 โดน, -8 hp"

7. หมวดหมู่การโกง: การแก้ไขหน่วยความจำและค่าตัวเลข

การตั้งชื่อหมวดหมู่การโกงจริง ๆ ที่ข้อก่อนหน้านี้ป้องกันไว้ ในระดับที่พอจะเข้าใจวิธีป้องกัน — ไม่ใช่คู่มือวิธีทำ จะช่วยให้เข้าใจภาพรวมได้ หมวดหมู่แรกคือ memory manipulation (การแก้ไขหน่วยความจำ): เครื่องมือที่มองเข้าไปในหน่วยความจำของโปรเซสเกมเอง (ตัวเลขที่โปรแกรมที่กำลังรันใช้อยู่ตอนนั้น เช่น กระสุนปัจจุบัน, health ปัจจุบัน, หรือเงินในเกมปัจจุบัน) แล้วแก้ค่าตรง ๆ ขณะที่เกมกำลังรันอยู่ ทั้งหมดนี้เกิดขึ้นบนเครื่องของคนโกงเองเท่านั้น เทคนิคที่เกี่ยวข้องกันคือการต่อ debugger เข้ากับ client ที่กำลังรันแล้วแก้ค่าหรือข้ามโค้ดในลักษณะเดียวกัน

ฟังดูทรงพลัง และในเกมแบบ client-authoritative มันก็ทรงพลังจริง ๆ: ถ้าสำเนาของ client เกี่ยวกับ "กระสุน = 30" เป็นตัวเลขเดียวกับที่ server เชื่อ การแก้เป็น 999 ตรง ๆ ก็ชนะทันที แต่ลองดูสิ่งที่มันเปลี่ยนแปลงจริง ๆ: ไบต์ข้างในโปรเซส client การแก้หน่วยความจำในคอมพิวเตอร์ของตัวเองไม่มีทางไปถึงเครือข่ายแล้วเปลี่ยนสำเนาแยกต่างหากของ server ที่เก็บค่าเดียวกันนี้ไว้เลย

เครื่องของคนโกงเอง เครื่องของสตูดิโอ +----------------------+ +----------------------+ | โปรเซส CLIENT | | โปรเซส SERVER | | กระสุน = 30 (ค่าล่าสุด | | กระสุน = 30 (ค่าจริง | | ที่ server ส่งมา) | | server เป็นเจ้าของ) | | | | | | เครื่องมือโกงแก้ไข | ไม่มีทาง | | | หน่วยความจำ client ตรง ๆ: | ไปถึง | server ไม่เคยอ่าน | | กระสุน = 999 (แสดงผล | server | หน่วยความจำของ client | | แค่เครื่องนี้เครื่องเดียว) | | ตัวนี้เลยแม้แต่นิดเดียว | +----------------------+ +----------------------+ การอัปเดตครั้งต่อไปของ server จะเขียนทับค่า "999" ปลอมของ client กลับไปเป็นค่าที่ server บอกว่ากระสุนมีจริง ๆ

วิธีป้องกัน: นี่คือ server authority จากข้อ 3 นั่นเอง ถ้า server เป็นสิ่งเดียวที่ลดกระสุนเมื่อมีการยิง และการอัปเดตครั้งต่อไปของ server เขียนทับสิ่งที่ client กำลังแสดงอยู่เฉย ๆ การแก้หน่วยความจำของ client จะเปลี่ยนแค่สิ่งที่หน้าจอของคนโกงเองแสดงชั่วขณะหนึ่งเท่านั้น และไม่เปลี่ยนแปลงอะไรเลยกับสถานะเกมที่ใช้ร่วมกันซึ่งคนอื่นทุกคนเห็น ค่าไหนก็ตามที่สำคัญต่อความยุติธรรม — health, เงินในเกม, inventory, cooldown, ตำแหน่ง — ต้องอยู่บน server จริง ๆ ไม่ใช่แค่ถูกสะท้อนไว้ที่นั่นเฉย ๆ

8. หมวดหมู่การโกง: Speed Hack

speed hack พยายามทำให้ client เชื่อว่าเวลาผ่านไปมากกว่าที่เกิดขึ้นจริง — เร่งนาฬิกาของ client เอง หรืออ้าง deltaTime (เวลาที่ผ่านไปนับจากอัปเดตล่าสุด ปกติวัดโดย game engine เอง) ที่ใหญ่กว่าความจริงตรง ๆ — เพื่อให้อะไรก็ตามที่คำนวณจากเวลา (การเคลื่อนที่, ตัวจับเวลา cooldown, แอนิเมชัน) วิ่งเร็วกว่าที่ควรจะเป็น

วิธีป้องกันมีอยู่แล้วในบทนี้: ข้อ 4 และ 5 ต่างก็คำนวณเวลาจากนาฬิกาของ server เอง ไม่เคยใช้ค่าที่ client รายงานเกี่ยวกับตัวเองเลย

// VULNERABLE: trusts a client-reported time delta
void OnMoveRequest(MoveMessage msg)
{
    // msg.ClientDeltaTime is whatever the client's cheat tool wants it to be
    player.Position += player.InputDirection * player.Speed * msg.ClientDeltaTime;
}

// SAFE: server measures elapsed time itself
void OnMoveRequest(MoveMessage msg)
{
    float dt = ServerClock.Now - player.LastUpdateServerTime; // server's own clock
    player.Position += player.InputDirection * player.Speed * dt;
    player.LastUpdateServerTime = ServerClock.Now;
}

เวอร์ชันที่มีช่องโหว่ปล่อยให้ client ที่ถูกแก้ไขแค่รายงาน ClientDeltaTime = 1.0 ในทุก ๆ network tick ทำให้ผู้เล่นเคลื่อนที่เป็นระยะทางของหนึ่งวินาทีเต็มหลายสิบครั้งต่อวินาที เวอร์ชันที่ปลอดภัยไม่มีทางถูกหลอกแบบนี้ได้ เพราะตัวเลขที่มันเอาไปคูณมาจากนาฬิกาของ server เอง ซึ่งเครื่องของคนโกงไม่มีทางเอื้อมเข้าไปแตะได้เลย นี่คือหลักการเดียวกับการจำกัดระยะทางในข้อ 4 แต่เอาไปใช้กับอินพุตเวลาแทนที่จะเป็นเอาต์พุตตำแหน่ง — เกมจริงมักใช้เช็คทั้งสองแบบร่วมกัน

9. หมวดหมู่การโกง: Wallhack และ Aimbot — และทำไม Interest Management ถึงเอาชนะมันได้

wallhack แสดงศัตรู (หรือไอเทม หรือเป้าหมาย) ทะลุกำแพงและ geometry อื่น ๆ ที่ควรจะบังมันไว้ aimbot ล็อกเป้าเล็งของผู้เล่นไปที่ศัตรูโดยอัตโนมัติ มักใช้ข้อมูลพื้นฐานตัวเดียวกับที่ wallhack ใช้ ทั้งสองอย่างนี้ไม่ได้ปลอมตัวเลขแบบที่ damage hack ทำ — ทั้งคู่แค่อ่านข้อมูลที่อยู่บนเครื่องของคนโกงอยู่แล้ว แล้วเอามาแสดงผลหรือใช้กระทำการ

นี่คือรายละเอียดสำคัญ: ในเกมที่สร้างมาแบบไม่ระมัดระวัง server ส่งตำแหน่งของทุกเอนทิตีในแมพทั้งหมดให้ client ทุกตัวตลอดเวลา แล้วพึ่งพา renderer ของ client เองให้ตัดสินใจว่าจะวาดอะไรบนหน้าจอ — สิ่งที่ "ถูกซ่อน" ถูกซ่อนแค่เพราะโค้ดเกมปกติเลือกที่จะไม่วาดมันเท่านั้น client ที่ถูกแก้ไขสามารถแค่วาดมันออกมาเฉย ๆ หรือป้อนตำแหน่งเหล่านั้นตรงเข้าไปในระบบช่วยเล็งได้เลย ข้อมูลนี้ไม่เคยเป็นความลับจริง ๆ เลย มันนั่งอยู่ในหน่วยความจำของ client ตลอดเวลา ถูกส่งไปที่นั่นโดย server เอง

ไม่มี interest management (เอื้อต่อ wallhack) server ส่ง: [player A, player B อยู่หลังกำแพง, player C, item D...] ไปให้ client ทุกตัว ตลอดเวลา renderer ปกติของ client: วาดแค่สิ่งที่มองเห็นได้เท่านั้น client ที่โกง: วาดทุกอย่างที่ได้รับมาแล้ว -- wallhack มี interest management (เอาชนะ wallhack ได้) server คำนวณแยกต่อ client: client ตัวนี้เห็นอะไรได้บ้างจริง ๆ? server ส่งให้ player A: [player A, player C] (B อยู่หลัง กำแพง ไม่ถูกส่งเลย) client ที่โกงของ player A: ไม่มีอะไรให้วาดสำหรับ player B เพราะไม่เคยถูกบอกด้วยซ้ำว่า player B มีอยู่

วิธีป้องกัน: interest management (เรียกอีกอย่างว่า network-level visibility culling) หมายถึง server คำนวณแยกกันสำหรับ client แต่ละตัวว่า client ตัวนั้นมีสิทธิ์รู้เกี่ยวกับเอนทิตีไหนได้บ้างในตอนนี้ — โดยอิงจาก line-of-sight (มีเส้นตรงที่ไม่ถูกกีดขวางด้วย geometry ไปถึงเอนทิตีนั้นหรือไม่), ระยะทาง, หรือการออกแบบที่ตั้งใจอย่าง fog of war — แล้วใส่เฉพาะเอนทิตีเหล่านั้นในข้อมูลที่ส่งให้ client ตัวนั้น ถ้าตำแหน่งของศัตรูไม่เคยถูกส่งให้ client เลย ก็ไม่มีอะไรในหน่วยความจำของ client ให้ wallhack อ่านหรือวาดได้เลย ไม่ว่าจะสแกนหน่วยความจำยังไงก็ตาม

List<Entity> BuildVisibleSnapshot(Player viewer, List<Entity> allEntities)
{
    List<Entity> visible = new List<Entity>();

    foreach (Entity e in allEntities)
    {
        if (e == viewer.Entity) { visible.Add(e); continue; }

        float dist = Vector3.Distance(viewer.Position, e.Position);
        if (dist > viewer.MaxViewDistance) continue;        // too far

        if (!HasLineOfSight(viewer.Position, e.Position)) continue; // blocked

        visible.Add(e); // only entities that pass both checks are sent
    }

    return visible;
}

ไล่ trace: player A ยืนอยู่ในทุ่งโล่งกับศัตรูตัวหนึ่งที่ห่างออกไป 20 unit (line of sight โล่ง อยู่ในระยะ MaxViewDistance) และศัตรูอีกตัวห่างออกไป 5 unit แต่อยู่อีกฝั่งของกำแพงทึบ BuildVisibleSnapshot จะรวมศัตรูตัวแรกและตัดตัวที่สองออก — packet เครือข่ายที่ส่งให้ client ของ player A ไม่มีตำแหน่งของศัตรูตัวที่สองอยู่จริง ๆ เลย wallhack ที่รันอยู่บนเครื่องของ player A จึงไม่มีข้อมูลให้เปิดเผยเลย เพราะตัวกรองฝั่ง server ทำงานไปแล้วก่อนที่จะมีอะไรถูกส่งออกไปด้วยซ้ำ

aimbot ถูกแก้ได้แค่บางส่วนจาก interest management มันเอาข้อมูลของศัตรูที่ผู้เล่นไม่ควรเห็นเลยออกไปได้ แต่ตำแหน่งของศัตรูที่มองเห็นได้จริงตามกฎยังต้องถูกส่งไปเพื่อให้เกมที่เล่นตรง ๆ วาดมันได้ และ aimbot ก็ล็อกเข้ากับตำแหน่งนั้นได้ การจับ aimbot ที่เล็งไปที่เป้าหมายที่มองเห็นได้จริงตามกฎต้องใช้วิธีป้องกันอีกแบบหนึ่ง ซึ่งจะพูดถึงต่อไป

10. จับสิ่งที่จับไม่ได้: การตรวจจับความผิดปกติและทางสถิติ

ไม่ใช่ทุกการโกงที่จะปิดกั้นได้ด้วยกฎการตรวจสอบ aimbot ที่เล็งไปที่ศัตรูที่ผู้เล่นมีสิทธิ์เห็นได้จริงตามกฎ ไม่ได้ทำอะไรที่กฎของ server จะปฏิเสธได้ตรง ๆ เลย — "เล็งแม่นยำไปที่เป้าหมายที่มองเห็นได้" ก็เป็นสิ่งที่ผู้เล่นมนุษย์ที่เก่งมาก ๆ ทำได้เช่นกัน สิ่งเดียวกันนี้ใช้ได้กับการโกงที่อ่านข้อมูลที่มองเห็นได้เร็วกว่าที่มนุษย์จะตอบสนองได้ โดยไม่ได้แตะค่าใด ๆ ที่ server เป็นเจ้าของเลย

วิธีป้องกันตรงนี้ต่างออกไปโดยพื้นฐาน: แทนที่จะเช็คการกระทำหนึ่งครั้งกับกฎตายตัว server (หรือบริการวิเคราะห์ข้อมูลแยกต่างหาก) บันทึกสถิติพฤติกรรมของผู้เล่นตลอดช่วงเวลา — เปอร์เซ็นต์ความแม่นยำ, อัตรายิงหัว, ความสม่ำเสมอของการเล็งที่ตามเป้าหมายที่เคลื่อนที่ผ่าน recoil ของอาวุธ, เวลาตอบสนองนับจากศัตรูปรากฏจนถึงผู้เล่นยิงครั้งแรก — แล้วตั้งค่าสถานะผู้เล่นที่ตัวเลขอยู่นอกช่วงที่มนุษย์จริงทำได้มาก ๆ

class AccuracyTracker
{
    int shotsTaken = 0;
    int shotsHit = 0;

    public void RecordShot(bool hit)
    {
        shotsTaken++;
        if (hit) shotsHit++;

        if (shotsTaken >= 50) // only judge once there is enough data
        {
            float accuracy = (float)shotsHit / shotsTaken;

            // 92%+ sustained accuracy over 50+ shots is far above
            // typical human performance for this weapon type
            if (accuracy > 0.92f)
                FlagForReview(accuracy, shotsTaken);
        }
    }
}

ไล่ trace: ผู้เล่นที่เก่งจริง ๆ อาจรักษาความแม่นยำ 35-55% กับอาวุธยิงเร็วตลอดแมตช์จริงได้ ผู้เล่นที่ความแม่นยำที่บันทึกไว้อยู่ที่ 96% จาก 200 นัด ไม่ได้เป็นไปไม่ได้ตามกฎเดี่ยว ๆ ที่ server เช็คได้ทันที แต่มันอยู่นอกช่วงปกติของมนุษย์มากพอที่จะควรตรวจสอบเพิ่มเติม — ไม่ว่าจะเป็นแบบอัตโนมัติ (การตรวจสอบที่เข้มขึ้นเริ่มทำงาน) หรือผู้ตรวจสอบที่เป็นมนุษย์ดูวิดีโอย้อนหลังของแมตช์นั้น

ข้อผิดพลาดที่พบบ่อย ปฏิบัติต่อธงสถิติเหมือนเป็นหลักฐานแล้วแบนอัตโนมัติทันที ผู้เล่นที่เก่งจริง ๆ, สถิติที่ดีเป็นช่วงสั้น ๆ จากโชค, หรือการตั้งค่าอาวุธที่แปลกแต่ถูกกฎ ก็สามารถสร้างตัวเลขที่เกินเกณฑ์ง่าย ๆ ได้เหมือนกัน สตูดิโอมักปฏิบัติต่อการตรวจจับแบบนี้เป็นสัญญาณที่เพิ่มความสงสัยและกระตุ้นการตรวจสอบเพิ่มเติม (ดูรีเพลย์, ตรวจสอบเข้มขึ้น, บางครั้งใช้ผู้ดูแลที่เป็นมนุษย์) ไม่ใช่คำตัดสินอัตโนมัติด้วยตัวมันเอง

11. ป้องกันช่องทางระหว่าง Client กับ Server

วิธีป้องกันทุกอย่างที่พูดมาสมมติว่า packet ที่ server ได้รับคือ packet ที่ client ส่งมาจริง ๆ ไม่ถูกแก้ไขระหว่างทาง และ packet ที่ server ส่งออกไปถูกรับโดย client ตัวที่ตั้งใจส่งให้เท่านั้น สมมติฐานนี้ต้องมีวิธีป้องกันของตัวเอง: เส้นทางเครือข่ายระหว่าง client กับ server เป็นช่องทางที่บุคคลที่สามอาจอ่านหรือแทรกแซงได้ — จาก Wi-Fi เครือข่ายเดียวกัน, เราเตอร์ที่ถูกแฮก, หรือเครื่องมือ proxy ที่คนโกงรันบนเครื่องตัวเองโดยตั้งใจ

มีการป้องกันสองอย่างแยกกันที่สำคัญตรงนี้:

วงจรชีวิตของข้อมูลลับ แบบที่ทำอย่างปลอดภัย: SERVER: เก็บสถานะที่ซ่อนไว้ทั้งหมด (แมพ, มือคนอื่น, ผล loot) | | ส่งเฉพาะสิ่งที่ viewer.Player มีสิทธิ์รู้เท่านั้น | ผ่านช่องทางที่ถูก ENCRYPT v CLIENT: ได้รับแค่พอที่จะวาดมุมมองของตัวเองอย่างถูกต้อง (ไม่มีอะไรเหลือให้ขโมย แม้จะตรวจหน่วยความจำ เพราะความลับไม่เคยถูกส่งมาตั้งแต่แรก)
Tip "เข้ารหัสทุกอย่าง" กับ "ส่งให้น้อยลง" ไม่ใช่ทางเลือกที่แข่งกัน — มันป้องกันผู้โจมตีคนละแบบกัน การเข้ารหัสหยุดบุคคลที่สามบนเส้นทางเครือข่าย การลดปริมาณข้อมูลที่ส่งหยุดตัวโปรแกรม client เอง (หรือ cheat ที่ฝังอยู่ในนั้น) ไม่ให้มีความลับให้รั่วไหลได้เลยตั้งแต่ต้น ไม่ว่าจะปกป้องมันดีแค่ไหนระหว่างทางก็ตาม

12. ความปลอดภัยของบัญชี: การยืนยันตัวตน, 2FA, และการจำกัดอัตราการล็อกอิน

ทุกอย่างที่พูดมาป้องกันตัวเกมเองจากการถูกแทรกแซงระหว่างแมตช์ พื้นผิวการโจมตีอีกแบบหนึ่งคือ บัญชี ของผู้เล่น (ข้อมูลตัวตนและความคืบหน้าที่ผูกกับการล็อกอิน) — ผู้โจมตีที่ไม่ได้พยายามโกงในแมตช์เลย แต่พยายามเจาะเข้าบัญชีของคนอื่นเพื่อขโมยไอเทมที่ซื้อไว้, ขายต่อ, หรือใช้มันแพร่การโกงต่อไป

การป้องกันในทางปฏิบัติสามอย่างครอบคลุมพื้นผิวนี้ได้ส่วนใหญ่:

class LoginRateLimiter
{
    Dictionary<string, List<float>> recentAttempts = new Dictionary<string, List<float>>();
    const int MaxAttemptsPerWindow = 5;
    const float WindowSeconds = 60f;

    public bool AllowAttempt(string account, float nowServerTime)
    {
        if (!recentAttempts.ContainsKey(account))
            recentAttempts[account] = new List<float>();

        List<float> attempts = recentAttempts[account];
        attempts.RemoveAll(t => nowServerTime - t > WindowSeconds); // drop old ones

        if (attempts.Count >= MaxAttemptsPerWindow)
            return false; // too many recent attempts -- block for now

        attempts.Add(nowServerTime);
        return true;
    }
}

ไล่ trace: สคริปต์หนึ่งลองรหัสผ่าน 200 ตัวกับบัญชีเดียวใน 10 วินาที การพยายาม 5 ครั้งแรกภายในหน้าต่าง 60 วินาทีจะผ่านเข้าไปถึงการเช็ครหัสผ่านจริง (แล้วล้มเหลว เพราะเดาผิด) ครั้งที่ 6 เป็นต้นไปถูกบล็อกโดย AllowAttempt ก่อนที่จะไปถึงการเช็ครหัสผ่านด้วยซ้ำ ไม่ว่าสคริปต์จะส่งมาอีกกี่ครั้งก็ตาม ผู้เล่นจริงที่พิมพ์รหัสผ่านผิดสองครั้งแทบไม่ได้รับผลกระทบเลย

13. การแข่งขันที่ไม่มีวันจบ: Kernel Anti-Cheat, การแบน, และ Telemetry

วิธีป้องกันทุกอย่างในบทนี้สามารถ และในที่สุดก็จะ ถูกหาทางเลี่ยงได้โดยคนที่ตั้งใจมากพอ — มีตลาดจริง ๆ ของคนที่สร้างและขายเครื่องมือโกง และพวกเขาอัปเดตเครื่องมือเหล่านั้นเมื่อสตูดิโอปะช่องโหว่ Anti-cheat ไม่ใช่ปัญหาที่สตูดิโอแก้ครั้งเดียวจบ มันเป็นงานต่อเนื่อง ใกล้เคียงกับการที่ธนาคารคอยป้องกันการฉ้อโกงมากกว่าการแก้บัคตัวเดียว

ความจริงสามข้อของการแข่งขันที่ไม่มีวันจบนี้ พูดตรง ๆ:

สรุปตรง ๆ: ไม่มีเทคนิคไหนในนี้เลย ไม่ว่าจะใช้เดี่ยว ๆ หรือรวมกัน ที่ทำให้เกมกันโกงได้ถาวร เป้าหมายที่เป็นจริงคือทำให้การโกงมีต้นทุนและความเสี่ยงสูงพอ และการตรวจจับเร็วพอ ที่ผู้เล่นส่วนใหญ่จะได้แมตช์ที่ยุติธรรม — ไม่ใช่การไปถึงศูนย์การโกง ซึ่งไม่มีเกมมัลติเพลเยอร์ที่วางขายจริงเกมไหนเคยทำได้จริง

14. อภิธานศัพท์

15. แบบฝึกหัด

แบบฝึกหัดที่ 1 — หาบัคความเชื่อใจ handler ฝั่ง server ต่อไปนี้ให้ผู้เล่นซื้อไอเทมจากร้านค้าในเกม หาปัญหาความเชื่อใจที่ถูกโกงได้ และอธิบาย (เป็นคำพูดหรือโค้ด) ว่าคุณจะแก้ยังไง
class BuyItemMessage
{
    public int ItemId;
    public int Price;      // sent by the client
    public int PlayerGold; // sent by the client, "for convenience"
}

void OnBuyItem(BuyItemMessage msg, Player player)
{
    if (msg.PlayerGold >= msg.Price)
    {
        player.Inventory.Add(msg.ItemId);
        player.Gold = msg.PlayerGold - msg.Price;
    }
}
Show answer

handler นี้เชื่อค่าสองตัวที่ไม่ควรเชื่อเลย: msg.Price (client อ้างได้ว่าไอเทมไหนก็ตามราคา 0 เงิน) และ msg.PlayerGold (client อ้างได้ว่ามีเงินเท่าไหร่ก็ได้ แล้ว server ก็เขียนทับบันทึกเงินของผู้เล่นเองด้วยสิ่งที่ client บอก) นี่แย่กว่า handler ดาเมจแบบไร้เดียงสาในข้อ 2 เสียอีก เพราะมันไม่ได้แค่ใช้ค่าผิด — มันปล่อยให้ client กำหนดยอดเงินทั้งหมดของผู้เล่นไปข้างหน้าเลย

วิธีแก้เป็นแบบเดียวกับข้อ 2: ไปดูข้อมูลจริงบน server และไม่รับตัวเลขที่ client เสนอเกี่ยวกับตัวเองเลย

class BuyItemMessage
{
    public int ItemId; // only the intent -- which item -- is needed
}

void OnBuyItem(BuyItemMessage msg, Player player)
{
    ItemDef item = ItemDatabase.Lookup(msg.ItemId); // server-owned price
    if (item == null) return;

    if (player.Gold < item.Price) return; // server's own gold record

    player.Inventory.Add(msg.ItemId);
    player.Gold -= item.Price; // computed from server state, not received
}

ตอนนี้ client บอกได้แค่ว่าต้องการไอเทมไหน ราคาและเงินปัจจุบันของผู้เล่นทั้งคู่มาจากข้อมูลที่ server เป็นเจ้าของ ไม่เหลืออะไรให้ client ที่ถูกแก้ไขโกหกได้เลย

แบบฝึกหัดที่ 2 — เขียนเช็ค Cooldown เขียนฟังก์ชันฝั่ง server bool TryUsePotion(PlayerState player, float serverNow) ที่อนุญาตให้ใช้ potion ได้แค่ครั้งเดียวทุก 3 วินาทีจริง โดยใช้นาฬิกาของ server เอง จากนั้นไล่ trace ว่าจะเกิดอะไรขึ้นถ้า client ที่ถูกแก้ไขส่งคำขอ UsePotion 10 ครั้งภายในหน้าต่างเวลา 0.2 วินาทีเดียวกัน
Show answer
class PlayerState
{
    public float LastPotionServerTime = -999f; // far in the past initially
}

bool TryUsePotion(PlayerState player, float serverNow)
{
    const float cooldownSeconds = 3f;

    if (serverNow - player.LastPotionServerTime < cooldownSeconds)
        return false; // still on cooldown, reject

    player.LastPotionServerTime = serverNow; // record using server's clock
    // ... apply the potion's real effect here ...
    return true;
}

ไล่ trace: คำขอ 10 ครั้งมาถึงระหว่าง serverNow = 10.0 ถึง serverNow = 10.2 คำขอแรก (ที่ 10.0) ผ่าน เพราะ 10.0 - (-999) >= 3 และตั้งค่า LastPotionServerTime = 10.0 อีก 9 คำขอที่เหลือมาถึงก่อนที่ serverNow จะไปถึง 13.0 ดังนั้นแต่ละคำขอจะไม่ผ่านเช็คและถูกปฏิเสธ มี potion ถูกใช้แค่ลูกเดียวเท่านั้น ไม่ว่า client ที่ถูกแก้ไขจะส่งคำขอมากี่ครั้งก็ตาม เพราะเช็คนี้อิงกับนาฬิกาของ server เองทั้งหมด ไม่ใช่สิ่งที่ client อ้างเกี่ยวกับเวลาเลย

แบบฝึกหัดที่ 3 — ขยาย Interest Management เกมหนึ่งมีกลไก stealth: ผู้เล่นที่หมอบอยู่ในหญ้าสูงควรมองไม่เห็นสำหรับศัตรูที่อยู่ไกลกว่า 8 unit แม้จะมี line of sight ที่โล่งก็ตาม โดยใช้รูปแบบ BuildVisibleSnapshot จากข้อ 9 ร่างว่าคุณจะขยายเช็คการมองเห็นยังไงเพื่อซ่อนผู้เล่นที่หมอบในหญ้าที่อยู่เกิน 8 unit ด้วย และอธิบายในหนึ่งหรือสองประโยคว่าทำไมสิ่งนี้ถึงหยุด wallhack (หรือการโกงที่คล้ายกัน) ไม่ให้เปิดเผยผู้เล่นที่ซ่อนอยู่ได้เลย ไม่ว่า cheat จะทำอะไรบน client ก็ตาม
Show answer
bool CanSee(Player viewer, Entity target)
{
    float dist = Vector3.Distance(viewer.Position, target.Position);

    if (target.IsCrouchedInGrass && dist > 8f)
        return false; // hidden by the stealth rule, regardless of line of sight

    if (dist > viewer.MaxViewDistance) return false;
    if (!HasLineOfSight(viewer.Position, target.Position)) return false;

    return true;
}

List<Entity> BuildVisibleSnapshot(Player viewer, List<Entity> allEntities)
{
    List<Entity> visible = new List<Entity>();
    foreach (Entity e in allEntities)
        if (e == viewer.Entity || CanSee(viewer, e))
            visible.Add(e);
    return visible;
}

สิ่งนี้ได้ผลด้วยเหตุผลเดียวกับที่ interest management เอาชนะ wallhack ทั่วไปได้: เช็คนี้ทำงานทั้งหมดบน server ก่อนที่ packet เครือข่ายที่อธิบายสิ่งที่ viewer มองเห็นได้จะถูกสร้างขึ้นด้วยซ้ำ ผู้เล่นที่หมอบอยู่เกิน 8 unit ไม่ใช่หนึ่งในเอนทิตีที่ถูกรวมอยู่ใน packet นั้นเลย ดังนั้นตำแหน่งของพวกเขาจึงไม่มีทางไปถึง client ของศัตรูเลย ไม่ว่าจะแก้หน่วยความจำ, อ่าน packet, หรือแก้ไข client บนเครื่องของศัตรูมากแค่ไหน ก็ไม่มีทางเปิดเผยข้อมูลที่ไม่เคยถูกส่งไปที่เครื่องนั้นตั้งแต่แรกได้เลย — ความลับถูกเก็บไว้บน server ไม่ใช่ถูกซ่อนแต่ยังอยู่บน client

← กลับไปหน้ารวมบท