본문 바로가기

학습컨텐츠

Assembly Language & Rev. Engineering






Assembly Language & Rev. Engineering


- 1st An Introduce to Computer Architecture -



Contents

Computer Architecture

들어가면서

디지털 컴퓨터와 구성요소

논리 게이트

데이터의 종류

레지스터

어셈블리 언어와 디버거

Assembly Language vs. C Language

Reverse Engineering





들어가면서

굳이 프로그램을 만드는 프로그래머의 위치에 있지 않더라도 프로그램을 사용하다가 문득 '이 프로그램은 어떤 원리로 실행 이 되지?' 라는 의문이 들 때가 있다. 예를 들면 Photoshop 에서 필터를 먹일 때나 혹은 Internet Explorer 에서 Flash Media 를 재생 할 때, 어떠한 원리로 이런 것 들이 가능할까에 대한 것들 말이다.

프로그램 원리를 파악한다는 작업. 흔히 일반적으로 생각할 수 있는 프로그램의 원리를 파악하는 방법이 라고 하면 소스 얻어서 그 코드를 분석하는 것이 제일 빠른 방법이라고 생각 할 수 있다. (물론 다이어그램 등 많은 방법이 있지만 여기서는 생략하도록 한다) 우리는 프로그램을 만들 때나 분석하고 수정 할 때 아래와 비슷한 형태의 소스코드를 얻어서 그 프로그램의 원리를 알아 내곤 한다.

#include <stdio.h>
int main()
{
        Printf("Hell world!!");
        Return 0;
}

위의 코드는 화면에 'Hell World!!" 를 찍어내는 C 코드이다. 굳이 다른 설명이 필요 없다고 할 수 있을 만큼 간단한데다가 솔직히 이해를 필요하지도 않는 코드이다. (아마 C 언어를 알지 못하더라도 이정도 설명이면 충분할 것이다) 하지만 여기서 한가지 의문점이 생긴다. 과연 저것을 컴퓨터는 어떻게 알아들을까? 컴퓨터는 저 코드를 어떻게 분석해서 어떤 원리로 화면에 저 문자열을 내어 놓을 수 있을까?

여기서 필자는 '컴퓨터 시스템 구조'에 대해서 화두를 던지고자 한다. 컴퓨터는 사람이 컴퓨터에 명령과 데이터를 입력하는 입력장치에서부터 사람이 그 결과를 볼 수 있는 출력 장치까지, 여러 가지의 복잡하게 연결 되어 있는 부품들에서 일어나는 데이터 전달과 연산 과정을 거쳐서 그 결과를 출력하는 장치라고 할 수 있다. 우리가 Windows 운영체제를 통해서 Internet 을 즐기는 순간부터 C 언어를 통해 소스코드를 입력하고 결과를 얻는 것까지 모두 이러한 과정을 통해서 우리가 원하는 것들을 얻을 수 있는 것이다.

하지만, 그런 것들의 모든 구조와 원리에 대해서 배우기에는 많은 시간이 걸릴 뿐더러 복잡한 이론 (컴퓨터 구조 이론, 오토마타, 컴파일러 이론, 최적화 이론 … ) 들이 많이 필요하며 그 이론들을 다 배우기에는 각 이론들이 가지는 난이도 역시 상당한 수준이다. 여기서는 컴퓨터의 기본 구성 원리를 간단히 배운 다음 컴퓨터 하드웨어와 가장 밀접한 '어셈블리' 언어를 배우고 간단한 '역 공학' 에 대해서 배우도록 한다.

역 공학은 영어를 번역하여 한글 뜻으로 나타낸 용어인데(필자는 역 공학이라는 단어는 좋아하지 않는다. 되도록이면 번역을 하기 보다는 재 창조를 하는 쪽이었으면 하는 바람이다.) 영어로는 'Reverse Engineering'. 읽으면 '리버스 엔지니어링' 이라고 하며 흔히 이렇게들 부르니 앞으로 교재에서도 역 공학 이라는 단어보다는 리버스 엔지니어링 이라는 단어 혹은 영어를 더 많이 쓰도록 하겠다.

소프트웨어 분야에서는 사람들이 이해하기 어려운 컴파일 된 실행 가능한 코드를 하나하나 해석하면서 사람들이 이해할 수 있는 형태로 바꾸는 작업을 하기도 하는데 이런 작업들을 리버스 엔지니어링이라고 한다. 이와 같은 기술은 주로 크래킹, 타 회사의 핵심 기술과 같은 '옳지 않은' 작업에 사용된다고 오해하기도 쉽다. 하지만 실제로는 디버깅에 제일 많이 사용되며 (필자는 디버깅에 효율적으로 사용하고 있다) OS Kernel 분석 및 디바이스 드라이버 작업 등 시스템 소프트웨어 제작에도 많이 사용되고 있다.

특히 컴퓨터 바이러스 등의 Malware 분석에는 이와 같은 기술이 필수적이다. 컴퓨터 바이러스를 치료하기 위해서는 분석 과정을 거쳐야 하는데 ㅡ 컴퓨터 바이러스가 어떤 경로로 파일을 감염시키며 어떤 특정한 피해를 줄 수 있는지에 대한 분석은 컴퓨터 바이러스 분석의 가장 기초적인 분석이며 그것을 하나하나 실행하면서 알아보기에는 시간이 너무 많이 걸린다 ㅡ 이와 같은 과정에서 리버스 엔지니어링은 큰 도움을 준다.

리버스 엔지니어링은 컴파일러가 만들어낸 Byte Code 분석을 기초로 한다. 그 Byte Code 는 16진수 구조의 Instruction 을 바탕으로 되어 있으며 이것들의 기초적인 조합들을 이용하여 프로그램을 수행하고 그 결과를 예측한다. 하나의 Instruction 이 할 수 있는 작업은 매우 제한적이므로 그것들을 하나하나 분석하고 예측하면서 실행결과를 예측해야 하는데 그를 위해서는 우선 아래와 같은 지식들이 필요하며 이번 장에서는 우선 필요한 기초 지식들을 주로 다룰 예정이다.

컴퓨터 구조와 컴퓨터의 실행 원리

어셈블리 언어와 실행파일의 구조

[생각문제 1] – 어셈블리어로 컴퓨터 바이러스를 많이 만들었는데 왜 그럴까?

[생각문제 2] – 앞에서 있는 C 코드가 어셈블리 코드로는 약 몇 줄 정도가 될까?

컴퓨터 구조와 원리

컴퓨터는 여러 가지 계산을 수행하는 디지털 시스템이다. '디지털'이라는 용어는 컴퓨터 내부의 정보가 제한된 수의 불 연속적인 값으로 표신 된다는 것을 의미한다. 예를 들어 십진수는 10개의 불연속적인 값을 나타낸다.

초기의 컴퓨터들은 십진수를 사용하여 산술 계산을 수행하였다고 한다. 여기서부터 디지털 컴퓨터라는 용어가 생겨나기도 했다. 그러나 전자 부품의 물리적인 제약과 인간 논리가 이진적이라는 이유 때문에 디지털 시스템들은 오직 두 개의 값만 가지도록 더욱 제한 되었고, 이것을 이진수 라고 불렀다.

디지털 컴퓨터는 0과 1의 두 개의 숫자만을 사용하는 이진수 시스템이다. 하나의 이진 숫자를 비트라고 부르며, 디지털 컴퓨터에서의 정보는 비트들의 그룹으로 표현된다. 다양한 기법들을 통해 오늘날 비트 그룹은 이진수 뿐만 아니라 십진수나 문자까지 표현하며, 또한 컴퓨터에서의 완전한 명령어 집합 즉, Instruction 들 도 나타낼 수 있다. 이진수는 2를 밑으로 하는 수 체계라고 할 수 있기 때문에 십진수를 2의 거듭제곱의 나열을 통해 이진수로 변환할 수 있다. 예를 들어서 이진수 1001011 은 다음과 같이 십진수로 나타낼 수 있다.

1 x 2^6 + 0 x 2^5 + 0 x 2^4 + …. = 75

즉, 일곱 비트의 이진수 10010011은 십진수로 75 와 같다. 그러나 같은 비트 그룹 일지라도 여러 가지 다른 의미로 사용될 수 있는데, 위의 일곱 비트 그룹도 때에 따라서는 명령어 또는 문자열이 될 수 있는 것이다. (이것은 추후에 ASCII 와 Instruction 섹션에서 다를 예정이다)

컴퓨터는 주로 하드웨어와 소프트웨어의 두 부분으로 나뉘어 진다. 하드웨어는 컴퓨터의 모든 전자 부품과 주변 장치를 구성하는 전자기적인 부품들을 이야기하며, 소프트웨어는 컴퓨터의 다양한 정보 처리 작업을 위해 전자기적 부품들과 주변장치를 운용하는 명령어와 데이터들도 구성 된다. 컴퓨터에서 연속된 명령어들을 우리는 '프로그램' 이라고 부르며, 프로그램에 의해 조작되는 자료들을 '데이터' 라고 한다. 이 데이터들은 하나의 집합을 이루어 '데이터베이스'를 형성하기도 한다.

시스템적인 입장에서 컴퓨터는 하드웨어와 시스템 소프트웨어로 구성 되는데, 시스템 소프트웨어는 '운영체제'를 포함하는 용어로 컴퓨터 시스템을 효율적으로 사용하기 위한 목적을 가진 프로그램들의 집합을 말한다. 시스템 소프트웨어는 특정 목적을 위하여 작성된 응용 프로그램과는 구별이 된다. 예를 들어서 어떤 데이터 처리 작업을 위해서 작성된 포토샵과 프로그램과 같은 것들은 응용 프로그램에 속하지만, 어떤 소스코드를 기계어로 바꾸어 주는 컴파일러는 시스템 프로그램이다. 시스템 소프트웨어는 전체 컴퓨터 시스템에 있어서 없어서는 안될 부분으로 사용자 요구와 컴퓨터 하드웨어 기능간에 생기는 공백을 보상해 주는 역할을 한다.

컴퓨터 하드웨어는 간단히 세가지 부분으로 나누는 경우가 많다. 우리가 흔히 CPU 라고 부르는 중앙 처리 장치와 데이터를 조작하는 부분, 데이터를 저장하는 부분 이다. 특히 이중 컴퓨터의 기억장치는 명령어와 데이터를 저장하고 있는데 (이 부분에서 여유가 있으신 분은 '폰 노이만 구조' 에 대해서 인터넷에서 찾아 보면 많은 것을 얻을 수 있다) CPU 가 바이너리 정보를 입력하거나 가져올 때 언제나 일정한 시간이 소요되기 때문에 이것을 Random Access Memory 즉, RAM 이라고 부른다. 그리고 데이터를 입력하고 출력하는 부분인 입출력 프로세서라고 불리는 부분이 있는데 이것은 컴퓨터와 외부와의 통신을 담당하는 부분이다. 컴퓨터의 입출력 장치에는 키보드, 모니터, 프린터, 마우스 … 하다못해 마이크 까지 여러 가지가 있다.

논리 게이트

디지털 컴퓨터에서 바이너리 데이터는 물리양인 전압 신호를 이용해 0 과 1로 표현된다. 예를 들어서 어떤 컴퓨터에서는 3v 의 신호를 1로, 0.5v 의 신호를 0 으로 나타낸다. (많은 컴퓨터에서 5v 를 이용하는 경우가 많은데 왜 그럴까?)

바이너리 데이터의 처리는 Gate 라고 불리는 논리 회로에서 행해진다. Gate 는 입력 논리의 필요조건을 만족 할 때, 즉 쉬운 말로 입력이 대충 잘 됐을 때 1 또는 0의 신호를 만드는 하드웨어의 블록이라고 할 수 있다. 현대 컴퓨터에는 여러 가지의 Gate 가 있다. 각 각 Gate 는 기호가 다르고 그것의 동작은 Algebra Function 방법으로 표시 된다. 입출력 관계는 Truth table 즉 진리표 라는 일정한 표의 형식으로 표시 된다.

Gate 는 일반적으로 8 개의 큰 기초적인 것들로 나눌 수 있다. AND, OR, NOT, BUFFER, NAND, NOR, XOR, NOR (NXOR) 이 그것들이다. 이것들에 대해서는 차차 또 다루도록 하자.

데이터의 종류

컴퓨터는 간단하게 말하면 '일정 데이터를 특정한 연산에 의해 불변하는 값을 얻는' 장치라고 할 수 있다. 즉 어떠한 데이터를 입력하고 그것을 미리 프로그램 된 연산을 통해서 그 결과를 (여러 번 수행하더라도 동일한) 제공 받을 수 있는 장치를 의미한다. 1 + 1 이 자기 마음에 따라 2가 나왔다가 3 이 나왔다가 하는 것은 컴퓨터가 아니라는 것이다.

위와 같은 구조로 예측 해보면 컴퓨터의 데이터 종류도 세가지라고 말할 수 있다. 자세히 들여다 보면 다음과 같은 큰 구조로 컴퓨터의 데이터를 나눌 수 있다.

  1. 연산에 쓰이는 숫자
  2. 데이터 처리에 쓰이는 명령어
  3. 데이터 출력 및 입력에 쓰이는 문자

컴퓨터에서는 이 데이터들이 이진수의 형태로 컴퓨터 기억장소 (레지스터, 메모리) 에 들어갈 수 있어야 한다. 이진법은 앞에서도 말했듯이 컴퓨터에서 쓸 수 있는 가장 자연스러운 진법 형태이다. 그러나 사람들은 이진법이 낯설기만 하다. 그렇기 때문에 이것들을 변환해야 하는 과정을 거처야 비로소 사람이 알아보기 쉬운 십진법의 형태로 나타낼 수 있는 것이다.

만약 '0x56' 이라는 16진수의 데이터가 있다고 가정하자. 이와 같은 데이터는 컴퓨터에 1byte 의 용량으로 저장되어 있을 것이다. 이 수를 10진수로 변환하면 86 를 의미한다. 이것은 컴퓨터에서 여러 가지 용도로 사용될 수 있다. 만약 연산에 쓰이는 숫자이면 이것은 그대로 109 를 의미할 것이다. 하지만 이것이 데이터 처리에 쓰이는 명령어라면 6D 에 해당하는 명령어 (IA-32 에서는 56 의 해당하는 명령어가 'PUSH'이다) 로 사용될 것이다. 하지만 이 값이 데이터 출력 및 입력에 쓰이는 문자일 경우 이것은 ASCII 코드에 의해 대문자 'V' 로 인식 될 것이다.

ASCII 코드는 American Standard Code for Information Interchange 의 약어로 미국 정보 교환 표준 부호라고 해석 되며 영문 알파벳을 사용하는 대표적인 문자 인코딩이다. 아스키는 컴퓨터와 통신 장비를 비롯한 문자를 사용하는 많은 장치에서 사용되며, 대부분의 문자 인코딩이 아스키에 기반한다.

아스키는 7비트 인코딩으로, 33개의 출력 불가능한 제어 문자들과 공백을 비롯한 95개의 출력 가능한 문자들로 이루어진다. 밑에 첨부된 아스키 코드에는 볼 수 없지만 ACK, SYN 등 제어 문자들은 현재는 거의 쓸모 없으나 역사적인 이유로 남아 있다. 예를 들어 SYN 과 같은 경우에는 전자 통신에 있어서 연결을 요청하는 정도로 해석 되며 ACK 는 그에 대한 응답으로 해석 할 수 있지만 현재와 같은 컴퓨터 통신에는 사용되지 않는다. 아스키코드에서 출력 가능한 문자들은 52개의 영문 알파벳 대소문자와, 10개의 숫자, 32개의 특수 문자, 그리고 하나의 공백 문자로 이루어진다.

아스키는 컴퓨터에서의 문자 표현을 위해서 사용되지만 넓게는 거의 모든 디지털 장치에 사용된다. 하지만 ASCII 코드로 표현할 수 있는 것은 영문자뿐이기에 이들을 확장 시켜서 확장 아스키 코드 등을 만들어 그 한계를 극복 하려는 노력이 있었다.

레지스터

모든 컴퓨터는 '레지스터(Register)' 라는 프로세서에 있는 아주 작은 공간을 통하여 데이터들을 가공하거나 연산 한다. 레지스터는 마이크로프로세서의 일부분으로서 아주 적은양의 데이터를 잠시(단위는 클럭 정도) 저장할 수 있는 공간이다. 또 하나의 명령어에서 다른 명령어 또는 운영체계가 제어권을 넘긴 다른 프로그램으로 데이터를 전달하기 위한 장소를 제공한다.

컴퓨터에서 메모리는 용량은 크지만 그리 빠르지가 않다. 따라서 조금 더 빠르고 그 데이터를 하나하나씩 가공할 수 있는 공간이 필요한데 이것을 '레지스터'라는 부분이 담당하고 있는 것이다. 일반적으로 레지스터는 현재 계산을 수행중인 값을 저장하는데 사용된다. 대부분의 현대 프로세서는 메인 메모리에서 레지스터로 데이터를 옮겨와 데이터를 처리한 후 그 내용을 다시 레지스터에서 메인 메모리로 저장하는 Load-Store 설계를 사용하고 있다.

컴퓨터를 가만히 들여다 보면 모든 연산이 '레지스터'를 통해야지 이루어진다는 것을 알 수 있다. 연산이라고 하면 더하고 빼는 것을 생각하기 쉬운데 것 뿐만 아니라 데이터를 입력하고 출력하고 옮기는 것 까지 모두 컴퓨터에서는 하나의 연산이라고 보고 있으며 따라서 당연히 레지스터를 통해서 이루어 지는 것이니 그 빠르기는 상상을 초월 한다.

하나의 레지스터는 하나의 명령어를 저장하기에 충분히 커야 하는데, 예를 들어 32 비트 명령어 컴퓨터에 사용되는 레지스터의 길이는 32 비트 이상이어야 한다. 그러나 많은 종류의 컴퓨터에서 길이가 짧은 명령어를 위해, 하프 레지스터라고 불리는 크기가 더 작은 레지스터를 쓰기도 한다.

프로세서 설계나 언어규칙에 따라 차이가 있지만, 레지스터에는 대개 번호가 붙어있거나 또는 나름대로의 이름을 가지고 있는데 역할로 분류한 레지스터의 종류는 아래와 같다.

[데이터 레지스터]

정수 값을 저장할 수 있는 레지스터.

[주소 레지스터]

메모리 주소를 저장하여 메모리 접근에 사용되는 레지스터. 어떤 프로세서에서는, 주소를 저장하는 것이 아니라 조작하기 위한 목적으로 색인 레지스터를 사용하기도 한다.

[범용 레지스터]

데이터와 주소를 모두 저장할 수 있는 레지스터.

[부동 소수점 레지스터]

움직이지 않는다는 뜻의 '부동'이 아님, 떠다닌다는 의미의 '부동'

많은 시스템에서 부동 소수점 값을 저장하기 위해 사용된다.

[상수 레지스터]

0이나 1 등 고정된 값을 저장하고 있는 레지스터.

[특수 레지스터]

프로그램의 상태를 저장한다. 프로그램 카운터, 스택 포인터, 상태 레지스터 등이 있다.

[명령 레지스터]

현재 실행중인 명령어를 저장한다.

[색인 레지스터]

실행중에 피연산자의 주소를 계산하는 데 사용된다.

우리가 앞으로 다룰 Register 에 관련한 것들은 Intel 에서 배포한 32 Intel Architecture 즉, IA32 문서에 정의 된 것을 기준으로 다루도록 한다.

어셈블리어와 디버거

앞에서 리버스 엔지니어링을 배우기 위해서는 우선 어셈블리 언어를 먼저 배워야 한다고 하였다. 왜 굳이 어셈블리언어에 대해서 배워야 하는 것일까? 어셈블리 언어는 컴퓨터의 동작에 있어서 어떤 위치에 있으며 그 언어의 역할이 어떤 것일까?

그것에 대해서 설명하기 전 우선 한가지 예를 통해서 그 형태를 간단히 설명해 보고자 한다. 윈도우를 쓰는 사용자라면 지금 즉시 [Windows 키] + [R] 을 눌러 실행 창을 띄운 뒤 'command' 를 쳐 보도록 하자. 그러면 도스 창이 뜨고 프롬프트가 나올 것이다. 여기에 'debug' 라고 치고 [엔터]를 입력해 보도록 하자. 그럼 아래와 같은 창이 나올 것이다.

'-' 표시는 프롬프트를 의미하며 입력이 준비 되었다는 것을 의미한다. 여러분은 지금 윈도우 에서 기본적으로 제공하는 디버거 프로그램에 들어 와 있다. 이 프로그램 프로그램의 버그를 찾아내는데 주로 쓰이지만 기초적인 리버스 엔지니어링에도 많이 쓰인다. 좋은 디버거가 많지만 그것은 차후에 배우기로 하고 일단은 모두 가지고 있는 구시대의 디버거로 화면만 간단히 나타내보도록 하자. 우선 여기서 'd' 라고 쳐 보자

D 명령어는 'dump' 명령어로 그 쓰임새는 나중에 배우게 될 것이다. D 명령어를 쳐보면 나오는 것은 세가지 단으로 된 것을 볼 수 있다.

보는 방법은 왼쪽부터 오른쪽으로 '주소', '주소 속에 담긴 16진수 값', '문자열 값' 으로 생각하면 된다. 이 화면을 해석하면 240A:0100 부터 240A:01FF' 까지의 주소를 가지는 메모리에 있는 값이 두 번째에 16진수 형태로 담겨 있다고 보면 된다. 그 값은 한 바이트 단위로 담겨 있다. 그리고 세 번째에 있는 것은 그 코드를 ASCII 코드로 대응한 것이다. 혹시 담고 있는 내용이 문자열이면 아래와 같이 변환된다.

위의 화면을 보면 특정한 Hex 값이 'MS-DOS' 와 'LOADHIGH' 등의 문자로 변환되어 있음을 볼 수 있다 (하지만 이것의 쓰임새는 분석해 보지 않으면 알 수 없다) 이것들이 문자열인지 숫자인지는 특정한 포맷에 의해서 결정된 것이기 때문에 당장의 Hex 값들의 조합으로는 알 수 없다. 이것에 대해서는 아래에서 좀 더 알아보기로 하자. 그럼 다음으로 'u' 를 한번 쳐 보자

무언가 알기 힘든 것들이 나오는 것을 알 수 있다. U 명령어는 '역어셈블' 명령어로 위의 명령어로 본 내용을 어셈블리 코드로 해석한 것이다. 자세히 뜯어보면 무언가 공통점을 알 수 있다. 앞의 화면에서 D 를 해서 본 값과 U 로 해서 본 값이 같다는 것을 알 수 있다. 240A:0100 값이 0xB8 (16진수는 앞에 '0x' 를 붙여서 구분한다) 인 것은 같으나 U 로 해서 본 값은 단순한 Hex 코드로 되어있던 것들을 뜻이 있는 명령어의 집합으로 바꾸어 놓은 것이다. 결국 위에서 본 값인 0xB8, 0x3E, 0x2B 의 조합은 'MOV' 라는 Op-code 와 AX 와 2B3E 라는 Operand 로 이루어 지는 것이다. 좀더 정확히 해석하면 0xB8 은 MOV AX 이며 그 값으로 0x3E, 0x2B 를 취한다는 의미로 해석 할 수도 있다.

하지만 모든 Hex Code 들이 명령어와 주소의 형태로만 있는 것은 아니다 앞에서 말한 데이터의 세 종류에 따라 일반적인 정수 값 일수도 있고 문자열일수도 있다. 문자열도 ASCII 코드의 연속으로 '0x65, 0x66, 0x54, 0x6D' 정도로 저장되어 있을 수 있다. 이런 경우에는 어떤 것이 명령어이며 어떤 것이 문자열인지 알아야 하는데 그것 역시 나중에 '실행파일구조' 부분에서 배우게 되며 위에 있는 디버거에서는 그것을 잘 구별해 주지는 못하므로 저 해석이 맞는지 보장할 수는 없다.

그러므로 앞에 화면에서 본 'MSDOS' 라는 문자열이 담겨 있을 것이라고 직관적으로 알 수 있는 Hex 코드가 담긴 주소인 240A:230A 영역 역시 그것이 문자열이 아닌 명령어인 것처럼 번역되어 있을 것이다. 한번 그 주소만 다시 역어셈블을 해보도록 하겠다.

역시 명령어와 데이터의 조합으로 되어있는 것처럼 분석해낸다. 즉 MSDOS 라는 문자열을 문자열로 보고 있지 않다는 것이다. 이것은 디버거가 얼마나 똑똑하나에 따라서도 달라지기 때문에 우선은 '어떤 것이 문자열이고 어떤 것이 명령어 코드인지 알기는 참 힘들구나' 라고 생각하고 넘어가도록 하자.

어쨌거나 이와 같이 복잡한 것들을 보는 이유가 무엇일까? 앞에서 분석한 것과 같이 'MOV AX, 2B3E' 는 어셈블리 구조를 가진 코드이다. 컴파일러는 각각에 맞는 문법으로 작성된 코드를 다음과 같은 코드로 변환해주는 역할을 하고 있다. 즉, 여러분이 C 언어를 이용하거나 Python 을 사용하든 결과로 나오는 것은 CPU 와 OS 에 맞는 위와 같은 어셈블리 형태의 코드이다.

 


이것을 분석하는 것은 아주 복잡하다. 다른 원인도 많지만 레지스터의 크기가 매우 작기 때문이다. 무엇이든지 하나씩만 처리할 수 있는 (32bit 컴퓨터에서는 레지스터의 크기가 32bit 이며 16bit 컴퓨터에서는 레지스터의 크기가 64bit 이다) 레지스터에서 무언가를 계산하거나 저장해서 원하는 결과를 뽑기 위해서 해야 할 노력들을 생각해 보면 당연할 것이다.

예를 들어보자. 더하기와 미지수를 2개만 사용하여 구구단을 계산 한다고 하면 어떻게 계산 해야 할까? 대충 생각해본 결과로는 계속 여러 번 미지수를 옮겨가면서 계산을 해야 그 결과가 나올 것이라고 생각 할 수 있다. 이와 마찬가지로 한정적인 레지스터를 이용하여 연산을 하기 시작하면 메모리에 넣었다가 다시 뺐다가 레지스터에서 계산했다가 뺐다가 하는 일련의 과정을 지속적으로 거쳐야 한다. 그렇다면 속도가 느리지 않을까? 물론 느리다. 하지만 우리가 쓰는 컴퓨터의 속도는 그런 반복과정을 거치더라도 충분히 빠를 만큼 말 그대로 '빛의 속도' 이다. 그러므로 이런 일련의 말도 안 되는 작업들을 지속할 수 있는 것이다.

그럼 나중에 리버스 엔지니어링을 실제로 할 때에는 어떻게 해야 하는가? 이와 같은 작업을 사람이 해야 하는 것은 아닐지 걱정을 하고 있는 사람도 있을 것이라 생각한다. 하지만 애석하게도 이와 같은 작업을 사람이 해야 하는 것은 맞다. 컴퓨터가 실행하는 것을 하나하나씩 어떻게 될지 예측하면서 이 명령어가 가지는 의미와 이것이 어떤 논리적 구조를 나타내는지에 대해서 하나하나씩 분석해야만 전체 프로그램 구조를 파악할 수 있다는 것이다. 특히 구조가 복잡하여 분석하기 어려운 프로그램과 같은 경우 꽤 많은 시간을 요구하기도 한다. (예를 들어 90년대 초, 고래 바이러스 분석을 위해서 바이러스 분석가들이 7일 동안 프로그램을 분석했다는 이야기가 있다)

그러나 그렇게 걱정할 필요는 없을 것이다. 지금은 너무나도 좋은 디버거가 많기 때문에 사람의 수고를 많이 덜 수 있다. 이 코드가 어떤 의미를 가지는지 그리고 어떤 내용을 담을지 이것이 숫자인지 문자인지 등을 분석해주는 프로그램이 있기 때문에 분석하는데 걸리는 시간을 많이 단축할 수 있기 때문이다.

위의 디버거는 'OllyDbg' 라는 프로그램인데 3부에 있을 Reverse Engineering 에 주로 사용할 툴으로 많은 기능들을 제공하고 있다. 한눈에 봐도 앞장에서 사용했던 역사적인 디버거와는 그 수준이 다르다는 것을 알 수 있다. 이와 같은 도구를 사용하면 프로그램을 분석하거나 수정하는 데에 드는 시간을 많이 단축시킬 수 있다. 따라서 원리를 알고 편한 도구들을 잘 이용하는 것이 Reverse Engineering 을 위한 최선의 방법이라고 할 수 있다.