세이브 파일을 보호하기

_ = F = _·2023년 10월 27일
1
post-thumbnail

게임에서 세이브 파일은 플레이어의 진행상황을 기록해주기 때문에 정말 중요한 파일입니다. 하지만 누군가가 이 파일에 접근해서 값을 수정해버린다면 손쉽게 아이템을 얻거나 게임의 일부 구간을 건너뛸 수 있습니다. 어쩌면 치명적인 버그가 발생할지도 모르죠.

저의 경우에는 도전과제와 같은 업적 시스템을 넣을 것이었기에 이러한 파일 수정을 막는 방법이 필요했습니다. 단순히 저만의 암호화 알고리즘을 만들어서 적용시킬까 싶기도 했지만 누군가 게임을 뜯어서 코드를 분석하면 금방 들통날 것이기에 해결방법이 되지 않았습니다.


일단 최소한의 보호 장치를 만들기 위해 다음과 같은 목표를 세웠습니다.

  1. 사람의 눈으로만 해석이 가능하면 안된다.
  2. 파일이 수정되었음을 감지할 방법이 있어야 한다.
  3. 세이브 파일의 공유를 막을 수 있어야 한다.
  4. 클라우드 기능을 제공할 수 있어야 한다.

1. 사람의 눈으로만 해석이 가능하면 안된다.

이것은 간단합니다. XOR 암호화나 Shift 암호화만 거쳐도 맨눈으로는 평문을 예측하기 힘들어집니다. 물론 이런 형태의 암호화는 손쉽게 복호화가 이루어집니다.

그럼 암호화에 의미가 있는가?

사실 몇 초도 안되서 복호화가 진행되어버리기에 암호화의 의미는 없습니다. 단지 파일을 메모장으로 여는 것만으로 어떤 내용이 담겨있는지 알아차리는 것은 원하지 않았을 뿐입니다. 파일을 뜯어보겠다는 의지를 가진 사람에게 조그만 귀찮음을 줄 순 있겠네요.

그리고 게임에 save, load 과정에서 많은 비용을 잡아먹는 것을 원하지 않았습니다. 아이템을 먹거나 맵을 이동하는 순간 매번 렉이 걸린다면 플레이어는 답답함을 느껴버릴 겁니다. 그렇기에 간단한 암호화만으로 처리하였습니다.

나는 아주 조금 더 복잡한 암호화를 원한다! 하시면 Affine 암호화나 Vigenere 암호화를 사용하시길 바랍니다. 물론 이것도 아주 조금의 노력이 들어간다면 금방 뚫립니다.

아니야, 나는 엄청 복잡해서 복호화가 절대 안되는 암호화를 원해!
포기하십시오. 게임의 코드를 뜯으면 어떤 암호화 알고리즘이 사용되었는지, 또는 어떤 암호키가 사용되었는지 바로 들통납니다. 따로 암호키를 관리하는 서버를 만들지 않는 한.. 완벽한 암호화 시스템을 구축하긴 어렵습니다.


2. 파일이 수정되었음을 감지할 방법이 있어야 한다.

어떤 플레이어가 파일을 복호화하는 방법을 알아내고 수정까지 했다고 가정합시다. 이때 게임에서 파일이 수정되었음을 감지할 수 있다면 발생할 수 있는 문제를 미리 예방할 수 있습니다.

이는 해싱(hashing) 알고리즘을 이용하면 쉽게 구현할 수 있습니다.

해시 함수(Hash function)
: 임의의 데이터를 고정된 길이의 데이터로 매핑하는 단방향 함수

해시 함수의 정의는 이 정도로 간단히 이해하고 넘어가시면 되겠네요.

해시 함수의 입력 값이 달라지면 출력되는 hash 값도 달라집니다. 입력되는 값이 아주 조금만 변하더라도 전혀 다른 hash 값이 나오게 되는 것이죠. 세이브 파일의 내용이 달라지면 그 세이브 파일의 hash 값도 달라지는 겁니다.

이 hash 값을 세이브 파일의 맨 뒤에 추가함으로써 게임은 파일이 수정되었음을 감지할 수 있습니다. 파일 내용으로 다시 계산된 hash 값과 세이브 파일에 적힌 hash 값이 서로 동일한지 비교하면 끝입니다. 만일 서로 동일하지 않다면 수정되었다는 것을 알아차릴 수 있죠.

//세이브 파일 데이터를 문자열로 가져오기
var _save_string = json_encode(save_map);

//hash 생성(sha1 해싱 방법을 이용)
//이 hash 값은 길이가 40인 문자열 형태로 반환됨
var _hash = sha1_string_utf8(_save_string);

//문자열 뒤에 해시 값 추가
_save_string += "#" + _hash + "#";

//이 문자열을 세이브 파일에 저장
var _file = file_text_open_write(filename);
file_text_write_string(_file, _save_string);
file_text_close(_file);
//세이브 파일 불러오기
var _file = file_text_open_read(filename);
var _save_string = file_text_read_string(_file);
file_text_close(_file);

//기록된 hash 값만 잘라내서 저장
var _expected_hash = string_copy(_save_string, string_length(_save_string)-40, 40);

//hash 값을 제외한 내용만 가져오기
var _hashless_string = string_copy(_save_string, 1, string_length(_save_string)-42);

//세이브 파일 내용을 바탕으로 새로 hash 값을 계산
var _new_hash = sha1_string_utf8(_hashless_string);

//hash 값이 서로 동일한지 확인
if(_expected_hash == _new_hash){
    //세이브 파일은 무사하네요! 어서 세이브 파일을 읽어오도록 합시다.
    load_map = json_decode(_hashless_string);
}
else{
    show_error("세이브 파일 무결성 검사 실패", false);
}

이렇게 게임메이커에서 제공해주는 해시 함수를 이용하여 쉽게 구현할 수 있습니다. 하지만 아직 완벽한 것은 아닙니다. 해커가 해싱 알고리즘을 알아낸다면 몇 분안에 hash 값을 다시 계산하여 세이브 파일을 수정할 수 있죠. 그렇게 되면 게임에서 파일이 수정되었는지 확인할 길은 없습니다.

HMAC(Hash-based Message Authentication Code)
: 해시 기반 메시지 인증 코드. 해시 함수와 비밀 키를 사용하는 암호화 인증 기술이다.

바로 HMAC를 이용해서 hash를 암호화할 것입니다. 누군가 파일을 수정해서 hash 값을 계산하면 게임에서 예상하는 hash 값과 다르게 나오게끔 할 수 있습니다.

우리가 사용할 HMAC 알고리즘의 정의는 아래와 같습니다. 물론 이해할 필요는 없습니다.

HMAC-SHA1(Key, Message) = SHA1((Key' ^ OuterPadding) |+| SHA1((Key' ^ InnerPadding) |+| Message))

sha1_string_utf8(string) 함수를 sha1_string_utf8_hmac(key, string) 형태로 바꿔서 작성해야 합니다. 키를 바탕으로 hash 값이 계산되게끔 말이죠. 이 스크립트는 아래 프로젝트 파일에서 가져와서 사용하실 수 있습니다. (Juju Adams님께서 올리신 프로젝트 파일입니다.)
https://www.dropbox.com/s/4yujkgcjdg5pss7/protect%20your%20savefiles.yyz?dl=0


3~4. 세이브 파일의 공유를 막을 수 있어야 한다. 그리고 클라우드 기능을 제공할 수 있어야 한다.

지금까지 파일의 수정은 막을 수 있었지만 공유는 막을 수 없었습니다. 코드에 있는 동일한 키를 바탕으로 계산되기 때문이죠. 그럼 HMAC 알고리즘에 사용되는 secret key를 수정해서 공유를 막으면 되지 않을까? 하고 생각했습니다.

'그럼 컴퓨터에 있는 시스템 정보를 바탕으로 키를 생성하면 되겠구나! 그러면 모든 장치에서 각자 고유의 키를 생성할 수 있겠어!' 하고 생각했습니다. 파일이 생성된 장치에서만 올바른 hash 값을 만들어낼 수 있기에 다른 사용자들간의 파일 공유는 막을 수 있습니다. 하지만 이렇게 되면 클라우드 기능을 제공할 수 없게 됩니다. '왜 같은 계정으로 게임을 즐기는데 기기가 다르다고 세이브를 그대로 이용할 수 없는거야?' 라며 불평을 할 수 있습니다. GMS에서 제공해주는 ds_map_secure_save(id, filename), ds_map_secure_load(filename) 함수를 이용하지 못하는 이유이기도 합니다.

그래서 키 값을 유저의 id를 이용해서 만드는 것으로 결정하였습니다. 유저의 id는 중복되지 않는 고유의 번호이기 때문이죠. 아직 개발 초기단계라 테스트는 못해보았지만 스팀에서 제공해주는 API같은 것을 이용한다면 충분히 가능할거라고 생각됩니다.



제 프로젝트에 맞게끔 찾은 정보와 제 생각은 담은 글이므로 모든 게임에 해당되는 내용들은 아니니 참고용으로만 봐주세요!


참고자료
https://gamemaker.io/en/blog/protect-your-savefiles

profile
Developer who loves pixels and makes game :D

0개의 댓글