해킹방지를 위한 코딩 백과사전

흔히 소프트웨어를 보호하기 위해 하드웨어 동글(통상 키락이라고도 부른다) 또는 소프트웨어적인 라이선싱 제품을 구매하여 사용한다. 이러한 상용 하드웨어 동글 또는 라이선싱 제품은 소프트웨어에 안전한 잠금 장치를 제공한다. 하지만 현관을 잠근 열쇠를 우편함에 두고 가는 실수를 범해서는 안 된다. 즉 상용 하드웨어 동글이나 라이선싱 제품을 사용한다고 하더라도 구현시에 여러 가지를 고려해야 완벽히 소프트웨어를 보호할 수 있는 것이다.

최장욱 jerry.choi@safenet-inc.com|다년간 임베디드 소프트웨어 개발자로 일했으며, 대표적 상용 리눅스인 몬타비스타 리눅스 한국 지사에서 다수의 프로젝트를 진행했다. 현재는 암호화 및 소프트웨어 라이선싱 전문 업체인 세이프넷 한국 지사에서 근무하고 있다.


소프트웨어를 보호하기 위해 사용할 수 있는 기술들이 있다. 이러한 기술들은 하드웨어 동글 또는 소프트웨어 라이선싱과 함께 사용하면 한층 더 강력한 보호막을 칠 수 있다. 여기에서 설명하는 기술들이 모든 개발 환경, 컴파일러에 적용되지 않는다. C 언어처럼 기계어로 컴파일되는 언어가 있는가하면, 자바 언어처럼 중간 단계의 바이트 코드가 있는 언어도 있다.
하지만 이번 컬럼에서 설명되는 해킹 방어 기술들은 대부분의 소프트웨어에 보안을 적용할 때 가이드로 사용할 수 있다.

자체 개발보다는 전문 업체의 솔루션 사용

대부분의 소프트웨어는 특정 라이선스 내에서만 동작하는 형식을 취하고 있다. 이렇게 해야만 상용 소프트웨어로서 라이선스를 구매해 사용하는 조건이 만족되는 것이다.

이러한 라이선싱 구현은 하드웨어 동글 또는 소프트웨어 기반의 라이선싱, 또는 개발사나 개발자가 직접 개발한 코드로도 가능하다.

대부분의 소프트웨어 개발사는 소프트웨어 라이선싱이나 해킹방지에 대해 경험이 많지 못하다. 또한 해킹 프로그램이 어떻게 동작하는지, 그리고 이러한 해킹 프로그램에 대해 자신들의 소프트웨어를 보호하려면 어떻게 해야 하는지 잘 알지 못하는 경우도 많다. 암호학에서 그런 것처럼, 비전문가는 자신들만의 새로운 방법을 만들어내고, 크래커는 쉽게 그 방법을 간파한다.
소프트웨어 보안전문 기업의 경우, 이 분야에 많은 경험을 가지고 있고 해킹방지를 위한 전문기술을 갖춘 팀을 구성하고 있다. 이들은 오랜 시간에 걸쳐 크래커들을 관찰해 왔고 크래커들의 코드 분석 패턴, 디버깅, 디스어셈블리, 패치, 에뮬레이션 기법을 알고 있다.

보안기업들은 최신의 디스어셈블리, 디버거, 크래킹 도구들로 자신들의 제품을 테스트해 본다. 보안 업체들은 오래 시간에 걸쳐 축적해 온 전문성으로 크래커를 막을 수 있는 제품을 만들어 왔다. 전문 보안 업체들이 보호한 소프트웨어는 크래커에게 난이도가 훨씬 더 높아지는 것이다.

단순한 라이선스 확인을 넘어
소프트웨어 보호를 위해서는 어떤 시점에서 라이선스 조건이 확인돼야한다. 유효한 라이선스(하드웨어 동글의 형태이건, 소프트웨어 라이선스의 형태이건)가 존재하면, 보호된 소프트웨어는 실행이 허가된다. 만약 라이선스가 없다면 어찌할 것인가. 물론 소프트웨어는 실행되지 않거나 제한된 모드로 실행된다.

많은 개발자들이 단순히 프로그램 구동 초반에 라이선스를 확인하는 함수를 호출함으로써 라이선스 확인을 하고는 한다.

이 함수는 소프트웨어 보안전문 업체가 제공한 보호 API이거나 개발자가 자체 개발한 코드일 수 있다. 이 함수는 보통 적합한 라이선스가 없으면 리턴하지 않거나 또는 참 혹은 거짓의 결과로 리턴한다.

참/거짓일 경우 보통 TRUE/FALSE의 불(Boolean) 데이터거나, 정수형(integer)으로써 0이면 참, 0이 아니면 거짓을 의미하게 된다. 크래커는 보통 쉽게 이 함수를 찾아내 이 함수가 늘 참을 리턴하도록 프로그램을 바꾼다. 즉 패치하게 된다. 이렇게 패치된 프로그램은 앞으로는 라이선스 유무를 전혀 확인하지 않고 언제든지 정상 동작하게 된다.

단순한 라이선스 확인보다 더 발전된 형태는 ‘간접 확인(indirect verification)’이다. 즉 챌린지(Challenge) 값을 하나 준비하고 이 챌린지 값을 보호기기(protection device)로 보내 응답값(response)을 받는다. 여기서 보호 기기의 가장 대표적인 예는 하드웨어 동글이다. 하드웨어 동글은 자체에 암호화 키와 프로세서를 가지고 있어 입력된 값을 암호화한 값을 리턴해 줄 수 있다.

개발자는 챌린지 값이 보호기기에 보내지면 어떤 응답값이 나오리라는 것을 알고 있다. 따라서 이 챌린지 값을 보호기기에 보내 나온 응답값이 맞다면 라이선스가 있다고 판단할 수 있다. 하지만 이것은 너무 단순한 방법이며 이보다는 응답값의 특징을 이용하는 것이 더 발전된 기법이다. 예를 들면, 응답값의 ‘1’ 비트의 개수라거나, 응답값의 바이트를 모두 더한 값, 상수 D로 나눈 나머지 등의 값을 사용할 수 있다. 이러한 값은 소프트웨어가 이후에 실행되는 동안에도 계속하여 확인될 수 있다.

크래커는 보통 두 대의 컴퓨터로 크래킹을 시도한다. 한 대에는 적합한 라이선스가 있는 상태에서, 나머지 한 대에는 라이선스가 없는 상태에서 같은 프로그램을 실행하면서 어떤 시점에서 프로그램이 다르게 분기하는지 디버거로 잡아낸다.

좋은 소프트웨어 보호 기법이란 이러한 단순한 공격을 방지하는 것이다. 예를 들어 라이선스가 없었을 때 단순히 소프트웨어가 동작을 멈출 수도 있지만, 동작을 계속할 수도 있다. 다만 잘못된 데이터를 계산하며 오동작하는 것이다.

함수 숨기기 - 로드된 DLL에서 직접 함수 주소 얻기
크래커는 흔히 프로그램에서 어떤 함수를 사용하는지, 그 함수는 어디에 있는지 분석한다. Win32 실행파일에는 임포트 섹션(import section)이 있고 여기에는 함수 이름이 나열되어 있다. 크래커는 단순히 이러한 임포트 테이블(import table)만 살펴보고 레지스트리를 사용하는지의 여부나 CryptoAPI 함수를 사용하는지 등의 중요한 정보들을 얻을 수 있다.

코드를 디스어셈블 해 보아도 함수가 어디서 호출되는지 알 수 있다. 크래커는 이런 식으로 소프트웨어 보호와 관련된 함수들이 소프트웨어 내에서 어디서 호출되는지 분석한다. 이것을 방지하는 방법 중 한 가지는 함수 주소를 실시간으로 얻어 임포트 테이블에는 나타나지 않도록 하는 것이다.

다른 간단한 방법은 LoadLibrary와 GetProcAddress 함수를 사용하는 것이다. 하지만 이러한 함수들을 사용하면 크래커에게 의도를 파악당하게 된다. 대신 DLL의 익스포트 테이블(export table)에서 함수를 검색해 함수 주소를 알아내는 것이 더 바람직하다. 물론 코드를 만드는데 더 많은 시간이 소요되지만 충분히 그럴 가치가 있다.

브레이크포인트 감지
크래커들은 흔히 특정 함수가 어디서 호출되고 인자는 무엇인지 분석한다. 만약 개발자가 함수 주소를 로드된 DLL에서 동적으로 얻는 방식으로 이를 숨겼다면, 그래서 크래커가 실행 파일의 임포트 테이블에서 함수 레퍼런스들을 찾을 수 없다면 어떻게 될까.

크래커는 이런 상황에서도 자신이 분석하고자 하는 API에 브레이크 포인트를 걸어 크래킹을 시도할 수 있다. 예를 들어 크래커는 CreateFile이라는 함수에 브레이크 포인트를 걸어 언제 파일 이름이 라이선스 파일과 같은 만들어지는지 확인하려고 할 수 있다는 것이다.

하지만 개발자 역시 API에 브레이크 포인트가 걸렸는지 여부를 알아낼 수 있다. 브레이크 포인트는 보통 인터럽트 3번이며 코드로는 0xcc이기 때문에 이것이 발견되면 프로그램이 지금 디버깅되고 있다는 증거이다. 따라서 브레이크 포인트가 감지되면 소프트웨어 실행을 중단할 수 있다.

인텔 x86 머신에서는 하드웨어 디버그 브레이크 포인트가 사용될 수 있다. 하드웨어 브레이크 포인트는 “INT 3” 기계어 명령을 사용하지 않기 때문에 단순한 코드 확인으로 쉽게 감지할 수 없다. 하드웨어 브레이크 포인트는 기계어 명령 대신 프로세서의 디버그 레지스터를 사용한다. 하지만 여전히 소프트웨어에서 하드웨어 브레이크 포인트를 감지하거나 무력화할 수 있는 기술들이 몇 가지 존재한다.

문자열 숨김


<화면 1> 애플리케이션에 포함된 문자열이 디버거에 잡힌 모습

해킹을 피하기 위해 함수 주소를 DLL에서 동적으로 얻는다고 하더라도 어쨌든 함수이름이 한 번은 확인되어야 한다. 즉 DLL에서 함수 주소를 얻기 위해 함수 이름으로 주소를 찾아야 한다는 것이다.

개발자가 함수 이름을 문자열로 사용하면(예를 들어 “RegOpenKey”), 이 문자열은 코드 또는 데이터 영역에서 쉽게 볼 수 있고 이 문자열이 참조하는 지점도 쉽게 알아낼 수 있다.

문자열을 숨기는 방법은 여러 가지가 있다. 문자열을 인코딩한 뒤 필요할 때만 디코딩한 뒤 사용한 다음에는 다시 인코딩할 수도 있다. 다른 방법은 지역 변수(local variable)와 같은 임시저장 공간에 함수 이름을 한 자씩 거꾸로 넣어 두는 것이다. 임시 저장 공간에 저장된 함수 이름은 임시저장 공간이 없어질 때 같이 지워지고 덮어씌워진다.

일반적으로 함수이름뿐만 아니라 라이선싱과 관련된 파일 이름이나 에러 메시지 등 모든 문자열이 숨겨지는 것이 소프트웨어 보호 측면에서 바람직하다. 실행 파일에서 뿐만 아니라 프로그램 실행시 할당되는 메모리에서도 가급적 문자열은 숨겨져야 한다.

여러 개의 라이선스 확인 함수 작성
개발자들은 보통 라이선스를 확인하기 위해 프로그램 초기 구동 시뿐만 아니라 프로그램이 구동된 이후에도 주기적으로 적합한 라이선스 존재 여부를 확인한다. 하지만 라이선스 존재 여부를 확인하는 함수는 오로지 한 개 뿐인 경우가 많다. 즉 아무리 여러 번 라이선스 존재 여부를 확인하더라도 라이선스 확인함수 자체가 패치되어 버린다면 아무런 소용이 없다. 따라서 라이선스 확인 함수를 여러 개를 만들고 그 내용도 상당히 다르게 만드는 것이 필요하다. 크래커가 한 두 개의 확인 함수를 찾아서 패치한다고 해도 여전히 남아 있는 확인 함수가 라이선스 존재 여부를 확인할 것이기 때문이다.

크래커 입장에서 볼 때 프로그램의 시작 부분만 디버깅하는 것이 프로그램 전체를 디버깅하는 것보다 훨씬 쉽다. 라이선스 확인 함수는 여러 곳에서 호출될 수 있다. 만약 프로그램 사이즈가 상당히 크다면 크래커가 그 모든 프로그램 곳곳을 디버깅한다는 것은 상당히 어려운 작업이 된다. 또한 프로그램이 여러 개의 DLL과 모듈을 사용한다면 각각의 DLL과 모듈에 라이선스 확인 함수를 흩어놓는 것도 좋은 방법이 된다.

사일런트 코드 사용
사일런트 코드(Silent code)는 프로그램 내부에서만 실행되는 연속된 명령어를 뜻한다. 사일런트 코드라 부를 수 있으려면 연속된 특정 구간 내 코드에서 외부 DLL이나 시스템 함수, 디바이스 드라이버, 인터럽트 등을 호출하지 않아야 한다.

만약 이 사일런트 코드 구간이 상당히 길다면 크래커가 이 사일런트 코드를 찾아내 분석할 가능성은 매우 낮다. 만약 사일런트 코드의 앞쪽 또는 뒤쪽에 외부로 호출되는 부분이 바로 인접되지 않았다면 말이다. 크래커에게 최대한 숨겨야 할 보안코드 즉, 라이선싱 함수는 이러한 사일런트 코드에 위치해야 한다.

동글 쿼리 값을 사용


<화면 2> 애플리케이션은 동글에 질의를 보내고 그에 대한 응답을 받는다.

소프트웨어 복제 방지를 위해 사용하는 하드웨어 동글에는 일반적으로 내부에 암호화 키를 생성해 저장하는 기능이 있다. 이 동글 내의 암호화 키를 사용하면 어떤 값을 암호화할 수 있고, 이렇게 암호화된 결과값을 사전에 알 수 있다.

하드웨어 동글 내에는 암호화 키 뿐 아니라 암호화 알고리즘도 내장되어 있다. 개발자는 하드웨어 동글에 요청(즉 암호화할 값)를 보내 결과값(암호화된 값)을 받을 수 있다. 보통 4바이트에서 16바이트 정도의 값을 동글에 보내 역시 같은 크기의 결과값을 받는다.

하드웨어 동글을 사용해 해킹 방지를 하는 프로그램은 테이블을 두 개 사용한다. 하나는 암호화할 값들의 테이블이고 나머지 하나는 암호화할 값들을 동글에 보냈을 때 예상되는 결과값들의 테이블이다. 프로그램은 암호화할 값들을 모아 놓은 테이블에서 하나를 골라 동글에 보내고 암호화된 값을 돌려받아 예상된 결과값들의 테이블에서 이 값이 맞는지 확인한다. 물론 이 방식이 “동글이 있는가?”만을 확인하는 가장 단순한 방식보다는 더 발전된 방식이기는 하다. 하지만 이 역시 암호화 전과 후를 비교해 맞는지 확인하는 비교적 단순한 보안 방식이다. 만약 크래커가 코드에서 비교 명령어를 찾아내기만 하면 결과값이 언제나 참이 되도록 코드를 바꿔 넣을 수 있다.

이러한 단순한 보안 루틴을 넘어, 프로그램은 동글에서 돌려받은 결과값을 어느 정도의 시간이 지난 뒤에 참, 거짓의 확인이 아닌 다른 중요한 계산에 사용할 수 있다. 이렇게 동글에서 돌려받은 결과값을 조금 더 어렵게 이용하는 것은 그다지 어려운 일은 아니다.

한 가지 예를 들어 보자. 만약 프로그램 실행시 어떤 타이밍에 상수 C를 이용해 연산해야 한다고 가정하다. 개발자는 X라는 입력값을 하나 정하고 이것을 하드웨어 동글에 보내 암호화된 결과값 즉 Y를 얻는다고 하자. Y XOR C를 계산하면 C*이 나오는데 이 때 X와 C* 값을 프로그램 코드에 저장한다. (하드 코딩할 수도 있고 파일 같은 곳에 저장할 수도 있다.) 프로그램 실행 시에는 상수 C가 필요하기 한참 전에 X를 동글에 보내 Y 값을 얻어 둔다. 그리고 얼마의 시간이 흐른 뒤 Y XOR C* 계산을 해서 C를 얻은 후 잠시 저장해 둔다. 그리고 다시 얼마의 시간이 흐른 뒤 C값이 필요한 시점에서 C를 꺼내 본래 연산에 사용한다.

만약 크래커가 동글에 쿼리하는 코드를 찾아내 이 부분을 패치한다고 가정하자. 크래커는 프로그램에서 X를 동글에 보내 Y를 얻는 부분을 찾아낼 수도 있다. 그리고 이 부분을 프로그램에서 제거할 수 있다. 이럴 경우, 프로그램은 Y가 없어 제대로 된 C값을 계산해 낼 수 없기 때문에 결국 어디선가 오동작하게 된다. 이 상황에서는 명확히 참 혹은 거짓을 비교하는 부분이 없기 때문에 크래커는 쉽게 프로그램을 해킹할 수 없게 된다.

변수 주소 숨기기
애플리케이션 내부의 여러 함수가 참조하는 중요 변수가 있다고 하자. 크래커는 보통 이런 변수들의 주소를 알아내고 이 변수가 어떤 함수에서 어떤 방식으로 사용하는지 분석한다. 어떤 중요한 변수가 있을 때 이 변수를 여러 함수에서 같은 주소로 참조하게 되면 크래커는 이 변수를 쉽게 주목하게 된다. 그리고 평범한 방식으로 변수를 사용하면 디스어셈블된 코드 또는 바이너리 그 자체에서도 그 주소를 쉽게 찾을 수 있다.


<그림 1> 애플리케이션 관점에서 전역 변수와 스택의 주소를 보여주는 메모리 레이아웃

개발자는 크래커에 대항해 변수 주소를 숨겨야 한다. 다음과 같은 정수형 전역 변수가 있다고 하자.

Int Var ;

보통 변수를 위와 같이 지정해 코드 내에서Var로 그대로 참조한다. 이렇게 되면 컴파일 후 바이너리에는 항상 Var의 주소가 그대로 나타난다.
이렇게 단순하게 변수를 사용하는 대신 배열을 잡도록 한다. 예를 들어 크기가 50인 배열을 잡는다.

#define SIZE 50
Int Var[SIZE]

그리고 변수를 사용할 때 다음과 같은 포인터로 참조한다.

Int *pVar;

여기서 한 걸음 나아가 위 포인터를 처음에는 배열 내 다른 위치를 가리키게 한 뒤 변수를 사용하기 직전에 제대로 된 위치를 가리키게 한다.


#define X 36
pVar = &Var[X]


처음 위치를 배열 내 36번째로 지정했는데 실제로 값을 참조할 때 36만큼 위치를 앞으로 이동시켜 준다.

하지만 대부분의 컴파일러들은 최적화 옵션을 사용하면 개발자가 크래커에 대해 대응 수단으로 코딩한 부분을 축약해 코드를 만들어 버릴 수 있다. 이렇게 되면 소용이 없으므로 보통,

1) 실제로는 큰 의미가 없는 어셈블리 코드를 삽입해 최적화를 막거나
2) 배열 내 위치를 변화시켜 주는 함수를 사용한다.


배열 내 위치를 변화시켜 주는 함수를 간단히 표현하면 다음과 같다. 이 함수는 배열 p의 위치를 n만큼 이동시켜 준다.


Int* MoveArrayLoc (int *p, int n) {
While (n--)
p--;
return p;
}

여러 함수에서 같은 배열 인자를 사용하는 것보다는 함수마다 배열의 다른 위치를 참조하는 것이 크래킹에 대응하기에는 더 좋다. 따라서 Var[X] 변수에서 X가 늘 다른 것이 더 좋다. X를 매번 다른 값으로 사용하기 위해 “__LINE__ % SIZE”와 같은 값을 X로 쓰는 것도 좋은 방법이다.

“__LINE__”은 C++ 컴파일러가 이 코드가 사용된 소스 코드 내의 라인 넘버로 대체하기 때문에 늘 다른 값이 나올 것이다. 그리고 “__LINE__ % SIZE” 값은 늘 SIZE보다는 작은 값인 0~49가 된다. 처음 배열 내 위치를 이처럼 지정하고 그 값만큼 이후 이동시켜 주면 된다.

이처럼 중요한 변수는 크래커가 바이너리를 분석할 때 최대한 그 실제 주소를 추측하기 어렵게 하는 것이 좋다.

데이터 브레이크 포인트 피하기
이번 절에서 설명하는 방법은 앞서 소개한 방법을 한 단계 발전시켜 변수주소를 숨겨줄 뿐 아니라 변수 자체를 프로그램 시동 때마다 다른 위치에 위치시키는 방법이다.

다시 말하면, 앞서 설명한 방법을 사용하면 개발자는 변수 주소를 숨길 수 있다. 하지만 크래커는 중요한 변수 위치를 파악하기 위해 디버거에서 데이터 브레이크 포인트를 이용할 수 있다. 또한 데이터 브레이크 포인트를 이용하면 크래커는 의심이 가는 데이터 영역들에 브레이크 포인트를 걸어 놓고 이 데이터에 대한 접근 횟수를 보고 그 데이터가 얼마나 중요한 것인지 추측할 수 있다.


<화면 3> 데이터 브레이크 포인트 설정 모습

중요 변수는 프로그램이 시동될 때마다 늘 다른 주소를 사용한다. 물론 크래커가 프로그램을 한 번 시동하고 디버깅하는 동안에는 중요 변수의 주소가 같겠지만, 크래커가 프로그램을 여러 번 시동해서 디버깅해야 한다면 중요 변수의 주소가 계속 달라지기 때문에 혼란을 가져다 줄 수 있다. 또한 크래커는 프로그램이 시동될 때마다 매번 중요 데이터로 추측되는 위치로 데이터 브레이크 포인트를 늘 이동시켜야 한다.

이처럼 중요 변수를 동적 할당된 메모리에 잡는다면, 배열은 순차적이 아니라 각각의 인자가 동적 할당되어 여기저기 흩어져 존재하게 된다.


Int* pVar[SIZE];
pVar[0] = malloc(sizeof(int));
int** ppVar = &pVar[X];


코드로 보면 다소 복잡할 수 있지만 앞서 설명한 방법은 하나의 배열에 대한 포인터를 이리 저리 이동시키는 것이라면 이번 절에서 설명한 방법은 하나의 배열에 대한 포인터를 이리 저리 이동시키는 것은 동일하고 배열 내 각 요소에 int가 있는 것이 아니라 int형 포인터가 있어서 실제 값은 다른 동적 할당 위치에 있는 것이다.

CPU 디버그 레지스터 피하기
크래커가 애용하는 크래킹 도구 중 하나는 인텔 x86 CPU의 디버그 레지스터이다. 크래커는 CPU의 디버그 레지스터를 사용해 메모리 브레이크포인트를 걸고 해당 주소가 언제 접근되는지 살펴본다. 단 CPU 디버그 레지스터는 네 개 밖에 없고 따라서 크래커는 동시에 네 개의 주소만 모니터링할 수 있다. 우리는 이것을 이용해 크래커가 디버그 레지스터를 사용해 크래킹하려는 시도를 무산시켜야 한다.


<그림 2> DR0~3은 디버그 레지스터다.

우리는 중요한 변수를 수많은 위치로 복사한 뒤 코드에서 여러 군데의 위치를 참조하도록 하며 크래커를 혼란시킬 수 있다. 이 방법은 데이터가 특히 읽기 전용일 때 더 좋다. 어차피 값은 같기 때문에 결과는 다르지 않다. 다만 크래커가 여러 개의 변수를 모니터링하게 하는 것일 뿐이다.

변수를 여러 위치로 복사하는 것에서 더 나아가 메모리를 동적 할당해 사용하면 이 기법을 더 발전시킬 수 있다. 즉 변수를 동적 할당된 메모리로 복사하고, 다시 메모리를 동적 할당해 여기에 한 카피 더 복사하고, 이것을 계속 반복한다. 하나의 변수만 복사하는 것이 아니라 복수 개의 변수를 마찬가지로 동적 할당된 메모리에 복사한다. 이렇게 하면 크래커가 모니터링 해야 하는 변수는 점점 더 많아지는 반면 디버그 레지스터는 네 개이기 때문에 크래커의 분석이 점점 더 힘들어지는 것이다.

메모리 브레이크 포인트 혼란시키기
크래커가 변수를 모니터링 하는 것을 방해하는 다른 방법도 있다. 크래커는 변수 위치에 메모리 브레이크 포인트를 잡고 이것이 접근될 때 살펴본다. 따라서 이번 방법은 단순히 메모리 브레이크 포인트에 지나치게 자주 걸리게 코드를 만드는 것이다.

크래커는 자신이 브레이크 포인트를 걸어 놓은 변수가 한, 두 번만 걸린다면 역시 한, 두번만 유심히 살펴보면 된다. 하지만 만약 브레이크 포인트를 걸어 놓은 변수가 100번 걸린다면 100번을 살펴봐야 하는 것이고 이렇게 되면 크래킹 작업이 아주 힘들어지게 된다.

만약 중요 변수가 언제 값이 설정, 또는 수정되는지 숨기고 싶을 경우에는 값이 0인 전역 변수를 하나 만들어 중요 변수에 더하기를 해 준다.


Int g_zero = 0;

Var1 += g_zero;
Var2 += g_zero;


중요 변수에 0을 더 해 준 것이기 때문에 아무런 의미가 없지만 Var1과 Var2에 브레이크 포인트를 걸어 놓은 크래커는 이것이 과연 어떤 의미가 있는지 분석하느라 시간을 소비하게 된다. 위 덧셈 연산은 단순히 크래커를 혼란시키고 시간을 소모시키기 위한 것인데도 말이다.


<화면 6> OllyDbg로 메모리 브레이크 포인트를 설정하는 모습

매크로 사용으로 코딩을 쉽게
위와 같이 크래커를 혼란시키기 위한 코드는 개발자에게는 추가적인 타이핑 작업이 된다. 하지만 편리하게도 매크로란 것이 있다. 코드를 하나 하나 곳곳에 다 붙여 넣기 할 필요 없이 간단히 매크로를 선언한 뒤 짧은 매크로 명령이면 충분하다.


#define TOUCH {Var1 += g_zero; Var2 += g_zero;}


이제 코드에는 TOUCH 라고 써 주기만 하면 된다. 만약 성능과 속도가 중요한 코드 부분에서는 매크로를 넣지 않으면 된다. 하지만 위 코드 같은 경우에는 무시해도 될 만큼의 CPU 시간을 사용하는 코드이기 때문에 성능에는 별반 차이가 없이 크래커를 혼란시킬 수 있다.

프로그램 구동시 랜덤한 메모리 영역을 할당
크래커는 프로그램을 분석하기 위해 디버거를 이용해 여러 번 프로그램을 구동시킨다. 이렇게 여러 번 프로그램을 구동할 때 프로그램이 매번 같은 주소에 변수와 함수가 위치하게 된다면 크래커의 작업은 아주 쉬워진다. 크래커는 반복되는 디버깅을 자동화할 수도 있다.

이를 방지하기 위해 프로그램 구동시 랜덤한 양의 메모리 영역을 할당하는 방법이 있다.

첫째, 프로그램을 구동했을 대에는 랜덤하게, 예를 들어 100 바이트를 동적 할당했다고 하자. 둘째, 프로그램을 구동했을 때에는 처음과는 다르게 이번에는 26바이트를 동적 할당한다. 이에 따라 프로그램 후반부에 할당되는 변수의 주소는 모두 달라지게 된다. 이런 작업은 크래커를 더욱 혼란스럽게 만들고, 혹시 크래커가 이런 작업의 의도를 눈치챈다고 하더라도 크래킹 작업 자체가 어려워지게 된다.

크래킹을 방지하기 위해 개발자들이 이제까지 사용해 온 수많은 코딩 기술들이 있다. 이러한 기술들을 모두 열거할 수는 없었지만, 필자는 이번 연재를 통해 몇 가지 기술을 정리해 보았다.

다소 부족하기는 했지만 의미 있는 작업이었다고 생각하며 3부에 걸쳐 소개한 소프트웨어 라이선싱 연재를 마친다.

Posted by 퓨전마법사
,