안녕하세요 주인장입니다.
게임을 좋아하신다면 한 번쯤은 치트엔진이나 치트오매틱 같은 프로그램을 다뤄보신 적 있으실 겁니다.
이런 툴들은 게임 메모리를 직접 수정해서 체력, 골드, 경험치 등을 마음껏 조작할 수 있게 해주죠.
그래서 오늘은 클라이언트 측에서 치트엔진 같은 툴을 통한 데이터 변조를 방지하는 방법에 대해 다뤄보겠습니다.
물론, 안티치트의 핵심은 서버 검증 입니다.
서버와의 데이터 동기화 및 무결성 검증이 가장 확살한 방법이지만,
이번 글에서는 클라이언트 단에서 할 수 있는 최소한의 방어 기법을 중심으로 살펴보겠습니다.
핵심 기능 코드
아래는 제가 주로 사용하는 메모리 변조 방지 스크립트 입니다.
using System;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
public class ProtectedValue
{
private byte[] SecretValue; // 원본 데이터
private byte[] encryptedSecret; // 암호화된 데이터
private byte[] key; // AES 암호화에 사용할 키
private byte[] iv; // 초기화 벡터(IV)
public ProtectedValue(int value)
{
key = GenerateKey(); // 암호화 키 생성
iv = GenerateIV(); // 초기화 벡터 생성
SecretValue = BitConverter.GetBytes(value);
encryptedSecret = Encrypt(SecretValue, key, iv); // 값 암호화
}
public ProtectedValue(bool value)
{
key = GenerateKey();
iv = GenerateIV();
SecretValue = Encoding.ASCII.GetBytes(value.ToString());
encryptedSecret = Encrypt(SecretValue, key, iv);
}
public ProtectedValue(string value)
{
key = GenerateKey();
iv = GenerateIV();
SecretValue = Encoding.ASCII.GetBytes(value);
encryptedSecret = Encrypt(SecretValue, key, iv);
}
public void ApplyNewValue(bool value)
{
SecretValue = Encoding.ASCII.GetBytes(value.ToString());
encryptedSecret = Encrypt(SecretValue, key, iv);
}
public void ApplyNewValue(int value)
{
SecretValue = BitConverter.GetBytes(value);
encryptedSecret = Encrypt(SecretValue, key, iv);
}
public void ApplyNewValue(string value)
{
SecretValue = Encoding.ASCII.GetBytes(value);
encryptedSecret = Encrypt(SecretValue, key, iv);
}
public bool CompareValue(bool value)
{
return value == GetBool();
}
public bool CompareValue(int value)
{
return value == GetInt();
}
public bool CompareValue(string value)
{
return value == GetString();
}
public bool GetBool()
{
byte[] bytes = Decrypt(encryptedSecret, key, iv);
return bool.Parse(Encoding.ASCII.GetString(bytes));
}
public int GetInt()
{
byte[] bytes = Decrypt(encryptedSecret, key, iv);
return BitConverter.ToInt32(bytes, 0);
}
public string GetString()
{
byte[] bytes = Decrypt(encryptedSecret, key, iv);
return Encoding.ASCII.GetString(bytes);
}
// AES 암호화 함수
private byte[] Encrypt(byte[] data, byte[] key, byte[] iv)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (System.IO.MemoryStream msEncrypt = new System.IO.MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
csEncrypt.Write(data, 0, data.Length);
csEncrypt.FlushFinalBlock();
return msEncrypt.ToArray();
}
}
}
}
// AES 복호화 함수
private byte[] Decrypt(byte[] cipherText, byte[] key, byte[] iv)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = key;
aesAlg.IV = iv;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (System.IO.MemoryStream msDecrypt = new System.IO.MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
byte[] decrypted = new byte[cipherText.Length];
int decryptedCount = csDecrypt.Read(decrypted, 0, decrypted.Length);
Array.Resize(ref decrypted, decryptedCount);
return decrypted;
}
}
}
}
// 키와 초기화 벡터(IV) 생성 함수
private byte[] GenerateKey()
{
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
byte[] key = new byte[32]; // 256비트 키
rng.GetBytes(key);
return key;
}
}
private byte[] GenerateIV()
{
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
byte[] iv = new byte[16]; // 128비트 IV
rng.GetBytes(iv);
return iv;
}
}
}
이 클래스는 게임 내에서 중요한 값을 암호화해서 메모리에 저장하는 역할을 합니다.
즉, 외부에서 치트엔진으로 스캔해도 값이 암호화된 상태로 보이기 때문에 직접 수정이 어렵습니다.
핵심아이디어
- 실제 값(int, bool, string)을 바이트 배열로 변환
- 그 값을 AES 알고리즘으로 암호화
- 메모리에 저장될 때는 암호화된 데이터만 존재
- 값을 읽을 때만 복호화해서 원래 데이터로 복원
주요 동작 원리
- 값 저장시
SecretValue = BitConverter.GetBytes(value); encryptedSecret = Encrypt(SecretValue, key, iv);
평문 값을 AES로 암호화하여 encryptedSecret 에 저장합니다 - 값 읽을 때
byte[] bytes = Decrypt(encryptedSecret, key, iv); return BitConverter.ToInt32(bytes, 0);
메모리에 저장된 암호문을 복호화해서 원래 값을 반환합니다. - 매번 다른 키와 IV
key = GenerateKey();
iv = GenerateIV();
각 인스턴스마다 다른 AES 키와 초기화 벡터를 사용합니다
실제 사용 예시
예시를 위해 간단한 스크립트를 만들어 보았습니다
using UnityEngine;
using UnityEngine.UI;
public class NewMonoBehaviourScript : MonoBehaviour
{
public GameObject cheaterPopup; // 치트 탐지 시 표시할 팝업
public Text hpText; // 체력 UI 표시용 텍스트
public int Health = 100; // 로컬 체력 값
private ProtectedValue protectedhealth; // 암호화된 체력 저장소
void Awake()
{
// 1) ProtectedValue 초기화 및 정상 체력 저장
protectedhealth = new ProtectedValue(Health);
// 2) 치트 팝업 초기 비활성화
if (cheaterPopup != null) cheaterPopup.SetActive(false);
}
void Update()
{
// 3) 무결성 체크: 외부 변조 여부 확인
if (!protectedhealth.CompareValue(Health))
{
// 4) 변조 발견 시 즉시 복원
Health = protectedhealth.GetInt();
hpText.text = $"HP : {Health}";
// 5) 탐지 알림: 팝업 표시
if (cheaterPopup != null) cheaterPopup.SetActive(true);
// TODO: 서버 로그 전송, 강제 로그아웃 등 추가 조치 가능
}
}
// 6) 정상 체력 감소 처리
public void TakeDamage(int damage)
{
int currentHP = Health;
int newHP = currentHP - damage;
// 7) ProtectedValue와 로컬 값 동기화
Health = newHP;
protectedhealth.ApplyNewValue(newHP);
// 8) UI 갱신
hpText.text = $"HP : {newHP}";
}
}
사용 영상:
https://youtu.be/QNappXCaZ-U?si=wur5HYYDFCchwuMC
TakeDamage 함수를 통해 정상적으로 체력이 감소할 때는 아무 문제가 없지만,
치트 엔진을 사용해 값을 변조하면 치트 탐지 팝업이 활성화되는 것을 확인할 수 있습니다.
이번 예제에서는 탐지 로직을 Update()에서 처리했지만,
실제 프로젝트에서는 이벤트 기반 방식으로 처리하거나,
탐지 후 서버 로그 전송, 세션 차단 등 추후 조치를 어떤 방식으로 할지는
프로젝트의 성격과 요구사항에 맞게 자유롭게 응용하면 됩니다.
오늘의 기록은 여기까지. 주인장은 이만 로그아웃 합니다, 모두 평안한 밤 보내세요!
'개발 > Unity' 카테고리의 다른 글
| [Unity] Camera Stacking 사용법과 간단한 응용 (0) | 2025.11.12 |
|---|---|
| [에셋/툴] Build Report Tool (0) | 2025.11.06 |
| [Unity] 유니티에서 다른 앱 호출 방법 (0) | 2025.10.27 |
| [Unity] 재사용 스크롤 리스트 (0) | 2025.10.24 |
| [Unity] 스트링 파싱 (0) | 2025.10.22 |