Cheat Engine 메모리 변조 대비: 인게임 핵심 변수 암호화·무결성 검증 설계

Cheat Engine으로 체력·재화·쿨다운이 변조되는 원리와 한계를 정리하고 Unity C# 환경에서 서버 권한·무결성 검증·탐지 로그를 조합해 핵심 변수를 보호하는 테스트 절차를 소개합니다.

✓ 복사 완료!
챕터데이터 저장 & 직렬화
난이도중급
상태● 검토 완료
대상 엔진
Engine Agnostic
최종 업데이트
Cheat Engine 메모리 변조 대비: 인게임 핵심 변수 암호화·무결성 검증 설계

핵심 요약

Cheat Engine은 클라이언트 프로세스 메모리에서 체력, 재화, 탄약처럼 값이 반복적으로 변하는 주소를 찾고 값을 덮어쓸 수 있다. 클라이언트 안의 변수 암호화만으로는 변조를 완전히 막을 수 없으므로 경쟁·경제·보상에 영향을 주는 최종 값은 서버가 계산하고 서버가 검증해야 한다.

실무적으로는 서버 권한 모델, 전송 데이터의 무결성 검증, 클라이언트 변조 신호 기록, 재현 가능한 QA 시나리오를 함께 운영하는 방식이 가장 효과적이다.

Cheat Engine 메모리 변조는 왜 발생하는가?

Cheat Engine은 실행 중인 게임 프로세스의 메모리를 검색한다. 예를 들어 현재 체력이 100일 때 해당 값을 검색하고 피해를 받아 85가 된 뒤 다시 검색하면 후보 주소를 빠르게 좁힐 수 있다. 주소를 찾은 뒤에는 값을 고정하거나 임의의 수치로 변경할 수 있다. (Cheat Engine 메모리 검색 문서)

클라이언트가 gold, hp, ammo, cooldownEndTime을 일반적인 정수나 부동소수점 변수로 보관하고 그 값을 그대로 게임 결과 또는 서버 요청에 사용하면 변조가 곧바로 부정 이득으로 이어질 수 있다.

보호 대상메모리 변조 영향권장 최종 권한
화면 표시용 체력UI 오표시클라이언트
PvE 즉시 반응용 체력일시적 판정 왜곡서버 또는 서버 재검증
재화·인벤토리경제 파괴서버
랭크 점수·보상경쟁 공정성 훼손서버
공격 속도·쿨다운전투 판정 조작서버

Cheat Engine 값 검색과 서버 검증 경로를 설명하는 게임 보안 개념 이미지

핵심 변수 암호화만으로 Cheat Engine을 막을 수 없는 이유는 무엇인가?

메모리 값을 XOR로 난독화하거나 AES로 암호화해 저장하면 단순한 값 검색 난이도는 올라간다. 하지만 공격자는 복호화 직후의 값, 값을 읽는 함수, 판정 결과, 네트워크 요청을 후킹하거나 수정할 수 있다. 복호화 키와 검증 로직도 같은 클라이언트 프로세스에 있으면 분석 대상이 된다.

따라서 클라이언트 암호화는 지연 장치이지 신뢰 경계가 아니다. 특히 클라이언트에서 생성한 비밀 키로 MAC을 계산해도 키와 계산 함수가 클라이언트에 있으므로 공격자가 요청을 재생성할 수 있다.

다음 식처럼 저장 값과 검증 태그를 분리하는 방식은 우발적 메모리 손상이나 단순 변조 탐지에는 도움이 된다.

stored=value⊕maskstored = value \oplus mask tag=HMACK(playerId∥sequence∥value)tag = HMAC_{K}(playerId \parallel sequence \parallel value)

그러나 K가 클라이언트에 상주하면 공격자를 신뢰하지 않는 보안 검증에는 사용할 수 없다. 이 HMAC은 서버가 보유한 키로 서버 내부 상태 또는 서버가 발급한 토큰을 검증할 때 의미가 있다.

Cheat Engine 대응은 어떻게 구현할까?

1. 서버를 핵심 게임 상태의 단일 권한자로 만든다

재화 증감, 아이템 지급, 랭크 점수, 전투 결과 확정은 서버가 처리한다. 클라이언트는 “재화가 500이다”라고 전달하지 않고 “상점 상품 potion_01 구매를 요청한다”처럼 의도를 보낸다.

서버는 플레이어의 재화, 상품 가격, 구매 가능 횟수, 요청 순번을 자체 데이터로 확인한 뒤 결과만 반환한다. 이 구조에서는 클라이언트 메모리의 gold를 999999로 바꿔도 서버 잔액이 바뀌지 않는다.

// Unity 클라이언트 예시: 값을 보내지 말고 행동만 요청한다.
public async Task BuyPotionAsync()
{
    var request = new PurchaseRequest
    {
        ProductId = "potion_01",
        ClientSequence = ++clientSequence
    };

    var result = await api.PurchaseAsync(request);
    walletView.SetGold(result.AuthoritativeGold);
}

서버에서는 다음 검증을 한 요청 처리 안에 묶는다.

  1. 인증된 플레이어 ID와 요청 순번을 확인한다.
  2. 서버 DB 또는 인메모리 권한 상태에서 가격과 보유 재화를 조회한다.
  3. 재화 차감과 아이템 지급을 원자적으로 기록한다.
  4. 확정된 잔액과 지급 결과를 응답한다.

2. 전투 요청을 값이 아닌 사건으로 검증한다

실시간 전투에서도 클라이언트가 피해량이나 남은 쿨다운을 확정해서 보내면 안 된다. 서버는 장비, 스킬 레벨, 버프, 서버 시간, 마지막 시전 시각을 기준으로 피해와 쿨다운을 재계산한다.

// 서버 의사 코드: 클라이언트의 damage 값은 신뢰하지 않는다.
public SkillResult CastSkill(PlayerId playerId, SkillId skillId, TargetId targetId)
{
    var state = playerStateRepository.Get(playerId);
    var skill = skillCatalog.Get(skillId);
    var now = clock.UtcNow;

    if (now < state.GetCooldownEnd(skillId))
        return SkillResult.Rejected("CooldownActive");

    if (!rules.CanTarget(state, targetId, skill))
        return SkillResult.Rejected("InvalidTarget");

    var damage = combatCalculator.CalculateDamage(state, skill, targetId);
    state.SetCooldownEnd(skillId, now.AddSeconds(skill.CooldownSeconds));
    combatRepository.ApplyDamage(playerId, targetId, damage);

    return SkillResult.Accepted(damage);
}

3. 클라이언트 난독화와 변조 감지는 보조 계층으로 둔다

오프라인 게임이나 서버 응답 전의 임시 상태에는 난독화 저장과 일관성 검사를 적용할 수 있다. 목적은 보안을 보장하는 것이 아니라 단순 편집을 어렵게 만들고 조사에 필요한 신호를 수집하는 데 있다.

public sealed class ObscuredInt
{
    private readonly int mask;
    private int encoded;
    private uint checksum;

    public ObscuredInt(int initialValue, int randomMask)
    {
        mask = randomMask;
        Set(initialValue);
    }

    public void Set(int value)
    {
        encoded = value ^ mask;
        checksum = ComputeChecksum(value);
    }

    public bool TryGet(out int value)
    {
        value = encoded ^ mask;
        if (checksum != ComputeChecksum(value))
        {
            value = default;
            return false;
        }

        return true;
    }

    private static uint ComputeChecksum(int value)
    {
        unchecked { return (uint)value * 2654435761u; }
    }
}

이 예시는 암호학적 보호 수단이 아니다. TryGet 실패 시 즉시 계정을 제재하기보다 세션 ID, 빌드 버전, 변수 종류, 발생 시점, 서버 검증 실패 여부를 기록하고 서버 권한 결과를 유지하는 편이 안전하다. 메모리 오류나 호환성 문제를 부정행위로 오인할 가능성이 있기 때문이다.

QA는 메모리 변조 방어를 어떻게 검증해야 하는가?

테스트 범위와 기대 결과를 먼저 분리한다

테스트 케이스조작기대 결과확인 로그
재화 값 변경클라이언트 표시 재화 수정구매 결과는 서버 잔액 기준purchase_rejected 또는 서버 잔액
쿨다운 값 변경로컬 종료 시각 수정서버가 시전 거부CooldownActive
피해량 요청 변경패킷의 피해 관련 필드 변경서버 재계산 결과만 적용damage_mismatch
순번 재전송동일 요청 재전송중복 지급 없음duplicate_sequence
이전 응답 재사용오래된 토큰·요청 재생만료 또는 순번 검증 실패expired_token

재현 가능한 검증 절차

  1. 관측 기준을 고정한다. 테스트 계정, 서버 빌드, 클라이언트 빌드, 시작 재화, 스킬 레벨, 서버 시간을 테스트 기록에 남긴다.
  2. 클라이언트 값만 수정한다. Cheat Engine으로 화면에 보이는 재화나 체력을 수정한 뒤 구매·보상 수령·스킬 시전 같은 서버 판정 행동을 수행한다.
  3. 서버 결과를 대조한다. 서버 DB의 확정 상태, API 응답, 이벤트 로그를 비교한다. 클라이언트 표시가 바뀌었더라도 서버 상태와 보상이 변하지 않아야 통과다.
  4. 재전송과 순서 변경을 시험한다. 동일 요청을 반복 전송하고 요청 순번을 역순으로 보내 중복 처리 방지와 멱등성(idempotency)을 확인한다.
  5. 탐지 신호를 점검한다. 변조 추정 이벤트가 관측되었는지 확인하되 탐지 이벤트 자체를 자동 영구 제재 조건으로 두지 않았는지 함께 검토한다.

QA 담당자가 클라이언트 변조 시도와 서버 로그를 대조하는 테스트 화면

운영 로그와 패치 판단 기준

변조 방어의 품질은 “차단했다”는 메시지보다 서버에 남는 근거로 판단한다. 이벤트에는 최소한 익명화된 플레이어 식별자, 세션 ID, 서버 시각, 빌드 버전, 요청 종류, 거부 사유, 요청 순번을 포함한다. 원본 패킷이나 개인정보는 보존 정책과 접근 권한에 맞춰 최소화한다.

다음 상황은 우선순위 높은 패치 대상으로 분류할 수 있다.

  • 서버가 클라이언트의 재화, 피해량, 쿨다운을 그대로 반영한다.
  • 중복 요청으로 보상이나 아이템이 두 번 지급된다.
  • 클라이언트 시계만으로 기간제 보상 또는 쿨다운을 판정한다.
  • 변조 탐지 실패 시 서버 권한 검증까지 우회된다.

반대로 클라이언트 UI 숫자만 달라지고 서버 결과가 일관되게 유지된다면 보안 취약점보다는 표시 불일치 버그로 분리해 대응할 수 있다.

자주 묻는 질문 (FAQ)

클라이언트 변수에 AES를 적용하면 Cheat Engine을 막을 수 있나?

아니다. 메모리 검색 난이도는 높아질 수 있지만 클라이언트의 복호화 시점과 판정 로직은 여전히 수정 또는 후킹될 수 있다. 경제와 경쟁 결과는 서버가 확정해야 한다.

오프라인 싱글플레이 게임도 서버 권한 모델이 필요한가?

플레이 경험만이 목적이면 반드시 필요하지 않다. 다만 온라인 리더보드, 업적 보상, 클라우드 저장을 신뢰해야 한다면 해당 결과를 제출받는 서버에서 검증해야 한다.

변조 탐지 이벤트가 한 번 발생하면 바로 밴해야 하나?

권장하지 않는다. 디버그 도구, 메모리 오류, 호환성 이슈도 유사 신호를 만들 수 있다. 서버 검증 실패, 반복성, 비정상 경제 이득 등 여러 근거를 결합해 검토하는 편이 안전하다.

정리

Cheat Engine 대응은 클라이언트 메모리를 완전히 숨기는 데서 끝나지 않는다. 핵심 변수의 최종 권한을 서버로 옮기고 행동 요청을 재검증하며, QA에서 변조 후에도 서버 상태가 유지되는지 반복 확인해야 한다.

클라이언트 암호화와 무결성 체크는 방어 심도를 높이는 보조 수단으로 활용하되 라이브 서비스의 재화·보상·랭크·전투 판정은 반드시 서버의 권한 상태를 기준으로 확정하자.