'2010/09/25'에 해당되는 글 1건

  1. 2010.09.25 게임 등의 어플리케이션 개발 시 메모리 해킹 방지 기법 1 (1)
2010.09.25 15:41

개요: 메모리 해킹이 일반적으로 어떻게 이루어지는지, 또 그에대한 대처책을 소개합니다. 난이도는 '하'이므로 간단한 방법 위주로 설명합니다.

필독: 치트엔진을 사용한 핀볼 해킹 예시 1

조건: C언어 사용 가능 혹은 다른 언어로의 프로그래밍에 능숙

난이도: 하

 

 *틀린 부분은 지적해주시면 바로 수정하겠습니다(코딩오류, 오타 등).

 

 

 '치트엔진을 사용한 핀볼 해킹 예시 1 문서'를 읽었다는 가정하에 진행합니다.

 어플리케이션 (특히 게임) 개발자로서 위와 같은 간단한 방법으로 내 프로그램 내의 변수를 수정할 수 있다는 것은 상당히 위협적입니다.

 실제로 온라인 게임을 작성하는 개발자들도 위와같은 방식의 해킹 기법을 간과하고 프로그램을 작성하는 경우가 많아 안타깝습니다. 약간의 지식만 있어도 게임가드, 핵실드 등의 비싼 보안 솔루션을 사용하지 않아도 될텐데 말입니다.

 실제로 이런 방법을 사용해 게임 내의 체력과 딜레이 등의 정보를 수정하여 쉽게 해킹이 가능했던 온라인 게임들이 수 개월만에 서비스를 종료하게 된 사례도 있습니다.

 

 개발자 입장에서 구현 단계에서부터 설명하겠습니다.

 당신은 바람의나라와 같은 방식으로 진행되는(4방향으로 1칸씩 움직이는 시스템에 주목) 온라인 RPG 게임을 제작하는 개발자라고 가정합니다.

 지금은 개발 초기단계입니다. 당신은 클라이언트가 서버에 접속해 로그인 후에 맵 상에 돌아다니는 잉여 몬스터를 때릴 수 있도록 서버와 클라이언트 간의 프로토콜을 구현하였습니다. 구현된 프로토콜은 다음과 같습니다.

-> 클라이언트는 현재 자신의 장비와 몬스터의 방어력 등을 계산해 결과 총 데미지를 계산하고, 자신이 타격한 몬스터의 오브젝트 번호를 전송합니다.

 

<- 서버는 클라이언트가 보내준 정보를 그대로 받아 해당 오브젝트 번호의 몬스터에 해당 데미지를 주고, 이벤트를 발생시킵니다.

 현명한 개발자라면 이 프로토콜에 상당한 문제점이 있다는 것을 쉽게 눈치챕니다. 하지만 현업 게임 개발자는 실제로 프로토콜을 이런식으로 설계하는 경우가 허다합니다.

 이 프로토콜을 그대로 사용한다면 해커는 쉽게 다음과 같이 게임을 해킹할 수 있습니다.

 

1. 데미지에 영향을 주는 변수(장비 등)를 찾아 값을 수정해 서버에 전송되는 데미지를 증가시킬 수 있다.

 

2. 리버싱을 통해 몬스터가 아무리 멀리 떨어져 있어도 공격에 맞도록 한다. 서버는 오브젝트 번호만을 받아 유효성 검사 없이 무조건 처리하기 때문에 해커는 사정거리 제한 없이 몬스터에게 데미지를 줄 수 있다.

 *이 글은 프로토콜에 대하여 이야기 하는 것이 아니므로, 심화된 내용은 '게임 등의 어플리케이션 개발 시 바람직한 프로토콜 설계 1 문서'를 참고하여 주시기 바랍니다.

 

 '데미지에 영향을 주는 변수'에 포커스를 맞추어 봅시다.

 개발자는 데미지 계산 루틴을 다음과 같이 작성했습니다.

 int CalcDmg(~){

  int Calc = (User->무기->데미지 + DEFAULT_DMG) - Monster->방어력;

  return  Calc >= 0 ? Calc : 0 ;

 }

 데미지 변수에 대한 아무런 검사가 이루어지지 않는 상태입니다. 이상태에서 해커가 유저의 무기 데미지혹은 몬스터의 방어력을 수정하면 쉽게 데미지가 올라갈 것입니다. 가장 중요한 일은 스캔 혹은 메모리 수정을 어렵게 하거나 불가능하게 만드는 것입니다.

 이를 방지하기 위한 방법은 여러가지가 있습니다.

 

 1번째로 변수의 내용을 암호화해 스캔이 어렵도록 하는 방법이 있습니다. 해커는 반드시 수정할 변수를 찾아야 합니다. 찾는 과정을 어렵게 만들고, 찾은 후에도 값의 수정을 어렵도록 만드는 원리입니다.

 아래는 간단한 암호화를 적용한 뒤의 코드입니다.

 void InitWeapons(){

  int i;

  for(i=0; i < WEAPONNUM ; i++){

    무기[i].데미지 = 10 ^ 0x12345678; // xor을 이용한 간단한 암호화

  }

 }

 

 int CalcDmg(~){

  int Calc = ((User->무기->데미지 ^ 0x12345678) + DEFAULT_DMG) - Monster->방어력;

  return  Calc >= 0 ? Calc : 0 ;

 }

 

 이 방법을 사용하면 무기의 데미지가 암호화되었으므로 스캐닝 및 변수를 다루는 과정이 어려워지게 됩니다. 뉴비 해커들은 이 단계에서 포기합니다. 하지만 암호화 과정을 거쳤어도 스캐닝 자체는 가능하므로 암호화는 완벽한 방법이라고 볼 수는 없습니다.

 

 2번째 방법은 메모리 수정 자체를 감지하는 방법입니다. 해커는 반드시 변수에 값을 써 넣야아 하는 입장이기 때문에 이를 적절히 이용할 수 있습니다. 변수에 값을 써 넣는 시점에서 '체크섬'을 따로 저장해놓고, 읽는 시점에서 이와 메모리 내용을 비교하여 같지 않다면 해킹에 대한 시도로 보고 클라이언트를 종료한다면 도중에 프로그램 내의 루틴을 거치지 않고 수정된 메모리가 있는지 검사가 가능합니다.

 

 void InitWeapons(){

  int i;

  for(i=0; i < WEAPONNUM ; i++){

    무기[i].인덱스 = i;

    무기[i].데미지 = 10;

    무기체크섬데미지[i] = &무기[i].데미지 ^ 10 ^ 0x12345678; // 간단한 체크섬 값을 만들어놓는다.

  }

 }

 

 int CalcDmg(~){

  if ( (&User->무기->데미지 ^ User->무기->데미지 ^ 0x12345678)

       != 무기체크섬데미지[User->무기->인덱스]; ){

         MessageBoxA(0,"해킹하지마 바보녀석아!",~~);

         ExitProcess();

      }

  int Calc = ((User->무기->데미지) + DEFAULT_DMG) - Monster->방어력;

  return  Calc >= 0 ? Calc : 0 ;

 }

 

 이런 방법으로 메모리 수정 시도를 감지하는 경우에는 해커 입장에서 리버싱을 할 줄 모른다면 해킹을 포기해야합니다. 99%의 해킹 시도를 차단할 수 있습니다.

 하지만 스캔 과정에서 체크섬의 값이 '변수와 함께' 스캔되는 경우가 있습니다. 그러므로 이 방법도 완벽하다고 할 수는 없습니다.

 

 3번째로, 아예 스캔 자체가 되지 않도록 변수를 보호하는 루틴을 작성하는 방법이 있습니다. 변수에 대한 스캔은 찾으려는 변수가 해커의 입장에서 마음대로 바꿀 수 있거나, 변수가 바뀐 시점과 바뀐 내용을 알 수 있다는 가정 하에 가능해집니다.

 이번 방법은 변수가 바뀌지 않아도 계속 수정되게 함으로써 스캔이 불가능하도록 만듭니다. 별도의 스레드를 생성하여 시간마다 달라지는 키를 사용해 변수 내용을 계속하여 바꿔주기 때문에 변수 자체가 제대로 검색되지 않습니다.

 long LoopThread(void* tmp){

  while(1){

    EnterCriticalSection(~);

    int i;   무기데미지키 = GetTickCount();

    for(i=0; i < WEAPONNUM ; i++){

      무기[i].데미지 = 무기[i].데미지 ^ 무기데미지키;

    }

    LeaveCriticalSection(~);

    Sleep(1);

  }

 }

 

 

 void InitWeapons(){

  int i;

  무기데미지키 = GetTickCount(); // 시간마다 키를 다르게한다.

  for(i=0; i < WEAPONNUM ; i++){

    무기[i].인덱스 = i;

    무기[i].데미지 = 10 ^ 무기데미지키;

  }

  CreateThread(~,LoopThread,~);

 }

 

 

 int CalcDmg(~){

  EnterCriticalSection(~);

  int Calc = ((User->무기->데미지 ^ 무기데미지키) + DEFAULT_DMG) - Monster->방어력;

  LeaveCriticalSection(~);

  return  Calc >= 0 ? Calc : 0 ;

 }

 

 

 제가 소개해드린 방법 중 가장 완벽한 방법입니다. 변수 자체가 검색되지 않으니 리버서 입장에서 리버싱할 루틴을 찾기 난해하고, 검색이 안되니 변수 수정도 불가능합니다. 만약 변수를 찾았다고 해도 1ms를 주기로 암호화 key가 바뀌기 때문에 변수를 제대로 수정할 수 없고,  '변수 수정 감지'와 병행하여 사용한다면 더욱 강력한 메모리 해킹 방지 기법이 될것입니다.

 정말 뛰어난 리버서가 ThreadLoop() 스레드를 죽여버리지 않는다면 이 방법은 해커로부터 거의 안전합니다.

 

이번 글은 이쯤에서 마칠까 합니다.

 

 

 

 

 

 

 

 

 

 

 

 

어쩌다보니 블로그에 글을 쓰고있습니다..
-선린고에 재학중인 어느 한 블로거-

Posted by 라이에

티스토리 툴바