본문 바로가기
[Unity]

[Unity] 간단히 메모리 변조 방지하기 - 안티 치트(Anti Cheat)

by 김기승 2023. 8. 20.

 이 글에서 소개하는 내용은 본인의 경험을 토대로 솔루션을 제시하는 것이므로,

실제로 사용하기에 부적절할 수도 있음을 인지하여 주시길 바랍니다.

 

 

유니티로 게임을 만들었다고 하더라도 불법 프로그램을 사용하여 데이터를 조작하는 악의적인 사용자는 있다.

만약, 그 데이터가 화폐 또는 능력치 같은 중요한 데이터라면 피해가 매우 크다.

이러한 데이터를 조작하는 예시를 한번 보자.

 

int형의 money 변수가 있다고 해보자.

그렇게 되면 메모리에 1000이라는 숫자가 들어갔을 것이다.

 

그 메모리 주소를 Cheat Engine이라는 프로그램으로 찾아볼 것이다.

현재는 1000이라는 값을 가진 메모리 주소를 모두 검색했다.

 

이번에는 950으로 변경하고 검색을 했더니, 이전의 메모리 주소들 중에서 950으로 변경된 값을 남겨주었다.

 

어느 정도 줄어들었을 때,

여기서 첫 번째 메모리 주소의 값을 500으로 변경했더니,

 

변수의 값이 외부의 조작만으로 500으로 변경되었다.

 

이 방식은 메모리를 변조하는 기본적인 방식이다.

그러나, PC 뿐만 아니라 모바일 환경에서도 적용 가능한 방식이기도 하다.

이렇게 외부에 자유롭게 노출되는 변수를 간단한 방법으로 숨겨보도록 하자.


원리

사물함에 좌물쇠가 있고 이를 여는 가 있다고 가정해보자.

열기 위해서는 좌물쇠 + 키 상태가 되어야 한다.

시간이 지나면서 좌물쇠와 키가 마모되므로,

사용할 때마다 교체해준다.

 

이를 변환하면 아래와 같다.

1) 두 개의 변수를 활용하여 암호화한다. (Lock, Key)
2) 값을 얻기 위해서는 두 변수를 더해야한다. (Lock + Key)

3-1) 값을 지정하면 Lock은 무작위 값을 부여받는다. (Lock = ?)
3-2) Key는 지정한 값만큼 차이가 나도록 변경한다. (Key = 새로운 값 - Lock)

 

위 원리를 적용한 변수를 나타낸 이미지이다.

이렇게 되면 실제 값을 찾기가 매우 어려워진다.

값을 직접 탐색할 수도 없고, 무작위로 값이 부여되기에 대소 구분 탐색도 불가능하다.


소스 코드

using UnityEngine;
using System.Collections.Generic;

public class AntiCheatInt : MonoBehaviour
{
    /** 선언부 **/
    private static readonly int minNewLock = -10000; //새로운 좌물쇠의 최소 값
    private static readonly int maxNewLock = 10000; //새로운 좌물쇠의 최대 값

    private Dictionary<string, AntiCheatValue> values; //실제로 안티 치트가 적용된 값들

    private class AntiCheatValue
    {
        private int varLock; //좌물쇠
        private int varKey; //열쇠

        public int value
        {
            get //값을 얻고자하면
            {
                return varLock + varKey; //좌물쇠 + 열쇠 = 실제 값
            }
            set //값을 지정하고자 하면
            {
                varLock = Random.Range(minNewLock, maxNewLock); //좌물쇠는 무작위 값으로 변경
                varKey = value - varLock; //열쇠는 지정한 값만큼 차이가 나도록 변경
            }
        }
        public AntiCheatValue(int value = default) //생성자에서는
        {
            this.value = value; //초기 값을 반영한다
        }
    }

    /** 내부용 **/
    private void Awake()
    {
        values = new();
    }

    /** 외부용 **/
    public bool Add(string name, int value = default) //변수를 추가하는 함수
    {
        if (values.ContainsKey(name)) //이미 존재하는 변수 이름이면
        {
            return false; //추가할 필요가 없으므로 추가하지 못했다고 알림
        }
        else //없는 변수 이름이면
        {
            values.Add(name, new AntiCheatValue(value)); //초기 값을 지정하면서 추가
            return true; //추가했다고 알림
        }
    }
    public bool Remove(string name) //변수를 제거하는 함수
    {
        if (values.ContainsKey(name)) //변수 이름이 존재하면
        {
            values.Remove(name); //제거
            return true; //제거했다고 알림
        }
        else //변수 이름이 없으면
        {
            return false; //제거할 필요가 없으므로 제거하지 못했다고 알림
        }
    }
    public bool Exist(string name) //변수가 존재하는지 판단하는 함수
    {
        return values.ContainsKey(name);
    }
    public int this[string name] //인덱서 선언부
    {
        get => values[name].value;
        set => values[name].value = value;
    }
}

int형 변수를 숨기는 전체 코드이다.

무작위로 값을 받는 Random 함수를 사용해야 되기에 MonoBehaviour 클래스를 상속받았다.

따라서, 게임 오브젝트에 붙여서 사용해야한다.

 

※ 원래는 제네릭 타입으로 자료형을 유동적으로 처리하고자 했으나, '+, -' 연산자가 적용되어야 하기에 불가능했다.

※ 아래에 float형 코드도 따로 정리해두었다.

더보기

using UnityEngine;
using System.Collections.Generic;

public class AntiCheatFloat : MonoBehaviour
{
    /** 선언부 **/
    private static readonly float minNewLock = -10000f; //새로운 좌물쇠의 최소 값
    private static readonly float maxNewLock = 10000f; //새로운 좌물쇠의 최대 값

    private Dictionary<string, AntiCheatValue> values; //실제로 안티 치트가 적용된 값들

    private class AntiCheatValue
    {
        private float varLock; //좌물쇠
        private float varKey; //열쇠

        public float value
        {
            get //값을 얻고자하면
            {
                return varLock + varKey; //좌물쇠 + 열쇠 = 실제 값
            }
            set //값을 지정하고자 하면
            {
                varLock = Random.Range(minNewLock, maxNewLock); //좌물쇠는 무작위 값으로 변경
                varKey = value - varLock; //열쇠는 지정한 값만큼 차이가 나도록 변경
            }
        }
        public AntiCheatValue(float value = default) //생성자에서는
        {
            this.value = value; //초기 값을 반영한다
        }
    }

    /** 내부용 **/
    private void Awake()
    {
        values = new();
    }

    /** 외부용 **/
    public bool Add(string name, float value = default) //변수를 추가하는 함수
    {
        if (values.ContainsKey(name)) //이미 존재하는 변수 이름이면
        {
            return false; //추가할 필요가 없으므로 추가하지 못했다고 알림
        }
        else //없는 변수 이름이면
        {
            values.Add(name, new AntiCheatValue(value)); //초기 값을 지정하면서 추가
            return true; //추가했다고 알림
        }
    }
    public bool Remove(string name) //변수를 제거하는 함수
    {
        if (values.ContainsKey(name)) //변수 이름이 존재하면
        {
            values.Remove(name); //제거
            return true; //제거했다고 알림
        }
        else //변수 이름이 없으면
        {
            return false; //제거할 필요가 없으므로 제거하지 못했다고 알림
        }
    }
    public bool Exist(string name) //변수가 존재하는지 판단하는 함수
    {
        return values.ContainsKey(name);
    }
    public float this[string name] //인덱서 선언부
    {
        get => values[name].value;
        set => values[name].value = value;
    }
}


사용 방법

using UnityEngine;

public class AntiCheatManager : MonoBehaviour
{
    public AntiCheatInt antiInt; //게임 오브젝트의 AntiCheatInt 컴포넌트를 할당

    private void Start()
    {
        string moneyText = "money";
        string powerText = "power";

        antiInt.Add(moneyText); //변수 생성
        antiInt.Add(powerText, 999); //변수 생성2

        if (antiInt.Exist(moneyText))  //변수 존재 확인
        {
            antiInt[moneyText] = 1004; //변수 지정
            Debug.Log(antiInt[moneyText]); //변수 읽기
        }
        antiInt.Remove(moneyText); //변수 제거
    }
}

별로 어렵지 않다.

위의 예제에서 사용하는 문법이 전부다.

antiInt 변수 초기화는 Inspector에서 했다.


결과

▼ 기본 Int.GIF

 

▼ 안티치트 적용 Int.GIF

직접적인 값 참조가 불가능해진다.


이 방법이 완벽히 메모리 변조를 막을 수 있다고 보장할 수는 없지만,

정말 간단하게 변수 은닉이 가능하다는 점에서 도움이 되었으면 한다.

댓글