본문 바로가기

.Net Technology/.NET General

닷넷의 리버스 엔지니어링과 CIL

2009년 2월호로 월간 마이크로소프트웨어에 기고한 내용입니다. 
지면상 내용을 간추리려고 노력했습니다. 


리버스 엔지니어링은 소프트웨어 공학의 한 분야로 이미 만들어진 시스템을 역으로 추적하여 처음의 문서나 설계기법 등의 자료를 얻어 내는 일을 말한다. 역 어셈블리 공학이라고도 부르는 이 리버스 엔지니어링 기술을 보는 시각이 곱지 못하다. 왜냐하면 대부분 다른 이의 소프트웨어를 저작자의 동의 없이 제3의 프로그램을 만들어 내는 것이 가능하기 때문이다. 대표적인 예로 크랙과 같은 프로그램을 만드는 것이 여기에 해당된다. 하지만 리버스 엔지니어링을 좋은 방향으로 잘 이용한다면 여러 방면으로 유용할 수 있다. 먼저 닷넷의 경우 소스코드가 없는 어셈블리를 수정할 수 있을 뿐만 아니라 수정하고 싶지만 미숙한 닷넷 언어를 이용해서 컴파일 되었을 경우 CIL을 이용해서 코드를 수정할 수 있다. 그럼 닷넷의 리버스 엔지니어링에 대해서 자세히 살펴보도록 하자.


닷넷의 리버스 엔지니어링

닷넷은 CIL(Common Intermediate Language)로 중간 컴파일을 거치기 때문에 닷넷의 어셈블리어를 이해하는 것은 비교적 쉽다. 먼저 닷넷의 실행 과정을 살펴보자. 


[그림1]닷넷의 컴파일 과정


비주얼 스튜디오나 명령 프롬포트에서 컴파일을 하게 되면 그 파일은 MSIL형태로 컴파일 된다. (MSIL과 IL 그리고 CIL은 모두 같은 것을 지칭한다는 것을 알아두자.) 우리는 중간 MSIL형태로 컴파일 되는 코드를 IL 디스어셈블리를 이용해서 확인할 수 있다. 다음 [화면1]은 IL 디스어셈블리를 이용해서 코드를 열어봤을 때의 화면이다.
 


[화면1] IL 디스어셈블리


[화면1]을 보면 굉장히 낯선 코드들이 적혀있는 것을 볼 수 있을 것이다. 이것을 우리는 CIL이라고 부르고 닷넷의 어셈블리어라고 지칭하기도 한다. 이 CIL 언어를 이용해서 우리는 C#인 VB.NET과 같은 코드없이 재 컴파일 하는 것이 가능하다. 즉, 소스를 잃어 버렸다거나 바이너리 파일에서 원하는 기능을 제거하고 싶다거나 할 경우에 이 CIL을 편집하여 다시 제2의 프로그램을 만들어 낼 수 있다는 것이다. 

 

[그림2] 양방향 엔지니어링


물론 CIL을 이해하는 것은 상당히 어렵다. 하지만 CIL을 볼 수 있다면 닷넷이 내부적으로 어떻게 동작되는지 쉽게 이해할 수 있다. 예를 들어 foreach와 같은 구문이 내부적으로 어떻게 생성되고 어떻게 동작되는지 볼 수 있다는 것이다. 그리고 다른 언어의 어셈블리어보다는 CIL이 비교적 쉽다. 여기서는 CIL의 간단한 개념들에 대해서 살펴볼 것이고 또 간단한 프로그램을 만들어 그 프로그램을 수정해 보도록 할 것이다. 


CIL의 기본개념 이해하기

원래 어셈블리어는 0X58, 0X73와 같은 언어를 이용하기 때문에 사람이 알아보기 굉장히 힘들다. 하지만 CIL의 경우 이러한 어려운 문자들을 쉽게 읽을 수 있게 문자화 시켰다. 예를 들어 x+y 라는 문장이 있다면 여기서 +는 기계어로 0X58로 표현된다. 하지만 CIL에서는 이것을 읽기 쉽게 add로 지칭하여 사용한다는 것이다. 그럼 아래와 같은 아주 간단한 C#코드가 있다고 가정해보자.

static int Add(int x, int y)
{
  return x + y;
}


[코드1] 간단한 C#코드

이 코드를 ildasm.exe를 이용해서 살펴보면 다음과 같은 코드가 생성되는 것을 볼 수 있을 것이다.

  

.method private hidebysig static int32 Add(int32 x,
int32 y) cil managed
{
// 코드 크기 9 (0x9)
.maxstack 2
.locals init ([0] int32 CS$1$0000)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldarg.1
IL_0003: add
IL_0004: stloc.0
IL_0005: br.s IL_0007
IL_0007: ldloc.0
IL_0008: ret
}


[코드2] CIL 코드

코드를 차례대로 살펴보도록 하겠다. .maxstack 2의 경우 스택의 공간을 할당한 것이다. CIL은 모두 스택 기반으로 동작하게 되는데 이 부분은 지면상 생략하도록 하겠다. 그 다음 IL_0000과 같은 접두사가 코드 앞에 붙어있는 것을 볼 수 있을 것이다. 이것은 for나 foreach와 같은 반복문을 쓸 때 혹은 분기문장을 쓰게 될 경우에 어디로 갈지 지정해주기 위한 번호라고 보면 된다. ldarg의 경우 스택에 파라메터 값을 저장하기 위한 것이다. stloc의 경우 메모리에 그 값을 저장하기 위해서 사용하는 명령어이고 바로 호출되는 ldloc는 저장된 값을 다시 로드하여 반환하게 된다. 

두서없이 코드를 설명해서 머리속이 굉장히 복잡해졌을 것이다. CIL 코드는 CIL 지시자, CIL 애트리뷰트, CIL 명령어(동작코드)이렇게 크게 3가지로 구분된다. 먼저 닷넷 어셈블리 전체 구조를 설명하는데 사용되는 CIL 문법이 있는데 이러한 문법들은 지시자라고 부른다. CIL 지시자는 CIL 컴파일러가 어셈블리 안에서 사용되는 네임스페이스, 클래스, 멤버들을 어떻게 정의되었는지 살펴보기 위해서 사용된다. 그리고 대부분의 경우 CIL 지시자 자체로는 닷넷에 정의된 타입이나 멤버들을 자세하게 표현하기가 힘들다. 때문에 많은 CIL 지시자 들은 다양한 CIL 애트리뷰트들을 선언하여 실제로 어떻게 처리가 되는지 지정하게 된다. public, private와 같은 속성을 지정하기 위한 코드라고 보면 된다. 그리고 마지막으로 메서드 안에서 동작되는 명령어가 바로 CIL 명령어에 해당된다. 물론 CIL 명령어들을 이해하는 것이 가장 중요하다. CIL 명령어들의 종류와 하는 일들에 대해서 알고 싶다면 http://en.csharp-online.net/CIL_Instruction_Set 블로그를 방문해 보도록 하자. 여기에서는 다양한 CIL 명령어들을 정의해놓고 설명해주고 있다.


크래킹의 기초

그럼 간단한 프로그램을 작성하여 크래킹을 시도해보도록 하겠다. 먼저, 간단한 콘솔프로젝트를 생성해보자. 그리고 다음과 같은 간단한 코드를 작성하여 컴파일 해보도록 하자.

Console.WriteLine("Input CD Key Please");
string CdKey = Console.ReadLine();
if (CdKey == "HOONS")
{
    Console.WriteLine("Success");
}
else
{
   Console.WriteLine("Fail");
}



컴파일 된 exe 파일을 IL 디스어셈블리로 열어보자. 그리고 메뉴의 덤프를 이용하면 모든 CIL를 생성하여 il파일로 저장할 수 있다. 
 

[화면2] CIL 코드 내보내기

그럼 다음과 같은 코드들이 출력되는 것을 볼 수 있을 것이다.
 


[화면3] 생성된 CIL 코드

여기서 특정 부분을 수정해서 ilasm.exe을 이용해서 컴파일 해보도록 하겠다. 필자는 모두 Success가 나오게 간단히 문자열만 수정한 후에 저장하였다. 그리고 비주얼 스튜디오 명령 프롬포트를 이용해서 이것을 다시 컴파일 해보도록 하겠다. 

ilasm /exe hoons.il /output=HOONS.exe



이렇게 실행하면 hoons.exe파일이 생성되고 HOONS가 아니어도 무조건 success가 출력되는 것을 볼 수 있을 것이다.

 


[화면5]