출처 – http://club.paran.com/multiloader 저자 – deVbug
옮긴이의 실력부족으로 그림은 옮기지 못하였습니다. ( 그대로 복사하니 안되더군요 )
deVbug의 멀티로더 만들기
1장 2번째 시간
사전지식
이번 시간엔 리버스 엔지니어링과 어셈블리어, 컴퓨터 구조에 대해서 공부해볼 것이다.
리버스 엔지니어링
여러분들은 크랙에 대해서 아는가?
크랙crack.
부수다 라고 해석할 수 있다. 부수다?
소프트웨어의 크랙은 본래의 내용을 부숴서 자기 마음대로 수정하는 것이라 할 수
있겠다. 그리고 크래킹의 세계에서 패치는 결국 크랙과 같은 의미로 쓰이고 있다.
크랙을 하기위해선 결국 궁극적으로 리버싱을 해야한다. 리버싱?
리버스reverse
뒤집힌 것이겠지. 정확한 뜻은 사전을 찾아보라.
리버싱은 리버스 엔지니어링을 하는 것을 일컫는다.
리버스 엔지니어링reverse engineering
역공학이라 해석한다. 완성되어진 실행파일 등을 분석해서 내부가 어떤식으로
수행되는지 분석하는 것을 역공학, 리버스 엔지니어링이라 할 수 있다.
그렇게 분석하여 우리가 원하는 대로 수정하는 작업이 크랙이자 패치가 되는 것이다.
여러분들은 No-CD 크랙(패치)이나 키젠(키제너레이터key generator) 등을
들어보았는가? 본인은 많이도 들어봤다.
이런 프로그램들이 바로 이 리버스 엔지니어링을 거쳐 완성된 것들이다.
내부를 분석해야 우리가 원하는 데로 하도록 할 수 있을 것 아닌가.
2진수, 10진수, 16진수 그리고 기계어와 어셈블리어
파일은 두가지로 나눌 수 있다.
Text 파일과 Binary 파일. 텍스트 파일은 텍스트 에디터로 열 때 제대로 볼 수 있는
파일이다. 바이너리 파일은 텍스트 에디터로 열어보면 완전히 깨져서 나오게 된다.
그림 파일을 하나 골라 열어보라. 볼 만할 것이다.
사실 텍스트 파일도 바이너리 파일이라 할 수 있다.
파일의 내용은 결국 숫자의 조합이거든. 파일은 sequence of bytes이다.
bytes는 byte들이고 byte는 8bit이다. 8bit는 뭘까?
01000001과 같은 것이다. 그리고 저 값은 A이다.
bit는 이것 아니면 저것을 의미하는 컴퓨터에서 쓰이는 단위이다.
bit는 컴퓨터에서 쓰이는 최소 단위이다. 0아니면 1, yes or no!
메모장에서 A만 입력한 뒤 저장해보자. 이를 헥사 에디터로 열어보자.
41
이라고 적혀있을 것이다. 아래와 같이.
41은 16진수이다. 이것을 2진수로 바꾸면, 01000001이다. 그리고 이것은 A이다.
텍스트 파일도, 바이너리 파일도 결국 내부에는 2진수로 저장되는 것이다.
단지 이 2진수가 특정 서식에 맞춰져 있는 파일이면 그 규칙에 따라 실행이
되고, 텍스트 파일은 그대로 읽어올 뿐이다.
아스키ASCII에 대해 들어보았는가?
ASCII(American Standard Code for information interchange)
해석하자면 정보 교환을 위한 미국 표준 코드이다.
아스키코드는 기본적으로 7바이트로 이뤄져있다.
A는 아스키코드에 1000001으로 지정되어있다. 이는 규칙이다.
10진수로 바꾸면 65가 되고 16진수로 바꾸면 41이 된다.
B는 당연히 1000010이겠지?
이렇게 제어코드부터 알파벳, 숫자에 몇몇 특수문자까지 미리 지정해두었다.
무슨 소릴하려고 이런 쓸데없는 소릴 한참이나 하고 있을까?
2진수의 집합인 파일이 저런 규약으로 정해진 문자들을 의미하는 것일 수도 있고
명령어를 의미하는 것일 수도 있다는 것이다.
16진수 90은 어셈블리어 명령어 nop이다.
16진수 EB는 어셈블리어 명령어 JMP이다.
이 둘은 나중에 매우 많이 쓰일테니 꼭 외워두라.
이런 명령어가 실은 CPU에 내장되어있는 명령어 세트의 한 명령을 의미한다.
어셈블리어는 결국 CPU에 내장되어있는 명령어와 1대 1 대응되는 것이다.
이는 CPU가 다르면 어셈블리어도 다르다는 의미이다.
이렇게 CPU에 내장되어 있는 명령어가 바로 기계어이고, 보통 2진수나 16진수로
표현된다. 그리고 이것을 사람이 좀 더 보기 좋게 해석하는 데에 쓰이는 언어가
어셈블리어이다.
초창기 프로그래밍 언어가 전무한 시절, 사람들은 기계어로 프로그램을 짰다.
지난 시간에 텍스트 에디터나 헥사 에디터로 열어본 사람은 알 것이다.
기계어는 결코 할만한게 아니라는 것을.
도저히 머리 빠질 지경이 되자 사람들은 좀 더 편하게 하기 위해 고민하게 됐다.
이렇게 등장한 것이 어셈블리어이고, 덕분에 어셈블리어는 기계와 매우 가깝다.
즉 기계에 종속적이다. 기계가 바뀌면 언어도 바뀐다.
이런 언어가 저급언어이다.
아무튼 여러분들은 어셈블리어를 알아야한다. 그것도 Intel계열로.
그러면 길이 열린다.
모든 파일은 숫자의 조합이라 했다. 그리고 그 숫자는 특정한 의미를 갖고.
그리고 특정한 서식에 의해 만들어진 파일(실행 파일)을 디스어셈블러에 넣고
돌리면 어셈블리어로 된 소스가 나온다고 지난 시간에 이야기했다.
이게 있어야 우리 맘대로 주무를 수 있다고도 이야기 했다.
여러분들은 또한 2진수, 8진수, 10진수, 16진수를 알아두라.
이정도의 연산(서로 바꾸는 등의)을 약간 할 줄은 알아야한다.
특히 2진수와 16진수, 알아보기 편하게 10진수도 함께 알아두면 좋다.
컴퓨터는 2진수를 쓴다고 했다. 왜 2진수를 쓰는지는 알고 있으리라 믿고 싶지만
대충 적어보자면, 기계는 전기로 작동한다. 전기는 전류가 흐른다, 흐르지 않는
다 이렇게 두가지 상태 밖에 표현을 못 한다.
즉 yes or no, 이것 혹은 저것, 0 아니면 1, 이렇게 흑백 두가지 밖에 표현하지
못 하는 것이다. 이렇기 때문에 2진수가 쓰이고 binary라고 불리고 digital이라고
불린다.
그런데 헥사 에디터를 보면, 0과 1로 이뤄진게 아니라 0부터 F까지 쓰이는 16진수
를 쓰고 있다. 이상한가?
사실 2진수로 표현하도록 할 수도 있지만 알아보기가 쉽지 않다. 거기다가 분량도
만만치 않게 나온다.
2진수 : 010001000011101011010000
10진수: 4471504
16진수: 443AD0
같은 내용이라도 분량이 줄어들어 있는게 보이는가?
게다가 2진수에서 16진수로는 쉽게 바꾸지만 10진수로는 바꾸기가 어렵다.
보기에도 2진수보다도 좋고 쓰기에도 좋으며 효율성도 좋다면 당연히 16진수를
쓰겠지?
아래 파트에서는 내용을 자세히 할지, 멀티로더 만드는 데만 필요한 것을
적을지 많이 고민을 했다.
준회원용 공개 강좌에서는 훑어보기만 하기로 했다.
부족하다 느껴지면 다른 책자나 인터넷 강좌, 다른 분들의 리버스 엔지니어링
강좌 등을 살펴 보기 바란다.
컴퓨터의 구조
컴퓨터는 입력장치, 출력장치, 연산장치 등으로 구성되어있다.
이중에 우리가 알아야할 것은 연산장치이다. 그 중에서도 중심에 있는 CPU에
대해서 살펴보자.
중앙처리장치CPU
Central Processing Unit
중앙처리장치는 컴퓨터의 두뇌에 해당하는 곳으로 입력장치로부터 받은 정보를
가공해서 출력장치로 보내주는 역할을 한다.
정확하게는 모든 연산과 제어를 하는 곳이다.
우리는 이 CPU에 명령을 보내고 CPU는 그걸 받아서 명령에 맞는 작업을 수행한다.
명령을 그럼 어떻게 보낼까?
그 때 쓰이는 것이 바로 프로그램이다.
프로그램은 CPU로 보낼 명령들의 집합체이다.
이런 명령은 내부에서 덧셈 연산기 하나만으로 뺄셈, 곱셈, 나눗셈 모두를 수행
한다. 신기한가? 나도 신기하다.
그리고 저 4칙 연산만으로 모든 작업을 수행한다.
그리고 모두들 산수는 배웠을 것이다. 덕분에 아래의 글을 이해할 것이다.
무언가를 더하는데, 당연히 더할 그 무언가가 필요하다는 것은 다들 알 것이다.
+ 만으로는 아무것도 못 한다.
2 + 3이어야 덧셈이고, 결과는 5라는 것을 알 수 있다.
이런 +와 같은 것을 연산을 의미하는 기호라는 뜻으로 연산자라 하고, 나머지
양쪽의 2와 3 같은 것은 연산을 당하는 쪽이라 하여 피연산자(operand)라고 부른다.
즉 모든 연산에는 피연산자가 하나 이상은 존재해야하는 것이다.
문제는 CPU 역시 연산을 하려면 피연산자를 입력받아야 하는 것인데, 우리들은
머리에 그 값들을 저장하지만 CPU는 어디에 저장을 할까?
컴퓨터에대해 약간 아는 사람들은 이렇게 답할지도 모른다.
램, 메모리.. 심지어 하드디스크.
하드디스크라고 생각한 사람은 반성하라.
CPU는 빛의 속도로 움직인다고 생각하자.
하드디스크는 거북이이다. 램은 토끼 정도?
당신이 옆에서 기어가는 거북이를 보면 무슨 생각이 드는가?
당연히 답답할 것이다.
그럼 우리보다도 빠른, 매우 빠른 속도로 연산을 하는 CPU가 하드디스크나 램을
보면 어떤 생각이 들까? 오만상 쥐어 패고 싶을 것이다.
값을 달라고 아무리 떠들어도 떠뜸떠뜸 이야기를 해대면 미친다.
즉 CPU는 그런 장치 말고 자신만의 초고속 저장장치가 필요하다.
그 장치가 바로 레지스터이다.
(캐시라고 생각했는가? 캐시는 아니다.)
레지스터는 연산에 필요한 값들이나 결과값, 플래그 값이나 오버플로 값 등을 잠깐
저장해두고 쓰는 곳이다.
필요한 값을 즉시 즉시 저장해서 쓰고 날리고 쓰고 날리고 하는 곳이다.
게다가 수학적인 연산도 가능한 곳이다.
그리고 용도에 따라서 여러개로 분류해서 CPU에 내장해두었다.
여러분들은 이것에 대해서 알아야한다.
하지만 여러분들은 멀티로더만 만들면 될 것 아닌가? 자세한 것은 넘어가고 종류만
보도록 하자.
범용 레지스터 : EAX, EBX, ECX, EDX
스택 레지스터 : ESP, EBP
포인터 레지스터 : ESI, EDI, EIP
플래그 레지스터
세그먼트 레지스터: CS, DS, SS, ES…
범용 레지스터가 많이 쓰이고 중요한 역할을 가지고 있다.
범용 레지스터
General Purpose Registers
16bits 시절에는 범용 레지스터가 2바이트였으나 32bits로 넘어오면서 4바이트가
됐고 이름도 AX, BX, CX, DX에서 Extended가 붙었다.
그 구조는 아래와 같다.
참고로 네모 한 칸에 4bits 이다. 즉 두 칸에 1byte이다.
* EAX 레지스터(Accumulator)
주로 곱셈, 나눗셈 자료의 입출력에 쓰이는 레지스터다. 다른 레지스터에 비해
자료의 입출력 속도가 빠르다.
* EBX 레지스터(Base)
주로 포인터의 역할을 지정한다. 포인터란 어떤 장소를 가리키는 것이다. 유식
하게 하자면 “간접 주소 지정”이라 하겠다.
자료의 입출력이 가능하다.
* ECX 레지스터(Count)
주로 반복문에서 카운터로 많이 사용되는 레지스터이다.
역시 자료의 입출력이 가능하다.
* EDX 레지스터(Data)
주로 곱셈, 나눗셈과 자료 입력에 많이 쓰이는 레지스터이다.
ABCD가 그냥 알파벳순으로 쓰고자 붙였다고 생각했는 사람 있는가?
모르면 다 그런다.
스택 레지스터
Stack Registers
* ESP 레지스터(Stack Pointer)
스택에 쌓아둔 자료의 마지막 위치를 기억하는 레지스터로 이 값은 PUSH와
POP 명령으로 바뀔 수 있다.
자료구조(특히 스택)에 대해서 공부한 사람은 쉽게 이해할 것이다.
* EBP 레지스터(Base Pointer)
간접 주소를 지정할 때 사용하는 레지스터로 보통은 다른 레지스터와 함께
사용된다.
포인터 레지스터
Pointer Registers
특정 주소를 가리킬 때 사용하는 레지스터로 많이 사용되지는 않는다.
* ESI 레지스터(Source Index)
주로 간접 주소지정에 사용되며 자료를 읽어올 때 오프셋 주소를 기리키게
된다.
* EDI 레지스터(Destination Index)
마찬가지로 주로 간접 주소지정에 사용되며 자료를 저장할 때 오프셋 주소를
가리키게 된다.
* EIP 레지스터(Instruction Pointer)
이 레지스터는 어떤 명령을 수행하고 다음으로 수행할 명령어가 있는 위치를
가리키는 레지스터이다.
플래그 레지스터
Flag Registers
플래그 레지스터는 명령어 실행 중 결과를 저장하는 레지스터로 임의로 변경
할 수 없다.
명령의 결과와 분기명령의 조건, 자리 올림의 표시 등을 가지고 있기에 플래그
레지스터는 상당히 중요한 레지스터라 할 수 있다.
* 플래그 레지스터의 모습
0 – Carry : 연산결과 자리 올림, 내림에 사용하는 플래그
1 – 1
2 – Parity : 연산결과 하위 8비트에서 1로 되어 있는 비트의 수가 짝수
이면 세트
3 – 0
4 – Auxiliary carry, 보조 캐리 플래그
5 – 0
6 – Zero : 연산결과 0이면 세트
7 – Sign : 부호 있는 연산에서 결과가 음수일 때 세트
8 – Trap : 하나의 명령이 실행될 때마다 INT 01h를 발생할 지 결정
9 – Interrupt : 인터럽트를 걸리게 할 것인지 결정. 세트되면 인터럽트 허용
A – Direction : 문자열 명령어에 사용되는 플래그, 세트되면 SI, DI 레지
스터 값이 증가. 문자열 비교나 이동할 때 왼쪽이나 오른
쪽으로의 방향을 결정
B – Overflow : 연산결과 값이 레지스터에 표현할 수 없을 만큼 클 경우 세트
CD – IOPL : 입출력 조작과 관련
E – NT : 386에서 추가된 Nested Task Flag
F – 0
10 – Resume : 386에서 추가된 Resume Flag
11 – VM : 386에서 추가된 Virtual Mode Flag
세그먼트 레지스터
Segment Registers
세그먼트를 관리하는 레지스터로 데이터 세그먼트, 스택 세그먼트 등을 쉽게 관리
하게 해주는 레지스터이다.
* CS 레지스터(Code Segment)
현재 프로그램의 명령어가 시작되는 곳을 가리키는 레지스터
* DS 레지스터(Data Segment)
데이터 세그먼트를 가리키는 레지스터. 즉, 자료가 저장되어 있는 곳 중 시작 위치
를 가리킨다.
* SS 레지스터(Stack Segment)
스택 세그먼트의 위치를 가리키는 레지스터이다.
* ES 레지스터(Extra Segment)
세그먼트의 시작 위치를 가리키며 문자열 명령어에 사용되기도 한다.
쓰다보니 세그먼트가 등장했다. 세그먼트와 오프셋에 대해서는 별로 중요치 않으니
그냥 넘어가자.
어셈블리어
기본 명령어
본 강좌에서는 2 address 방식의 명령어를 중심으로 설명한다.
어셈블리어의 명령어는 다음과 같은 구조를 이루고 있다.
[opcode] [destination operand], [source operand]
[명령어] [대상 오퍼랜드], [소스 오퍼랜드]
* PUSH: SP 레지스터를 조작하는 명령어 중의 하나로 스택에 데이터를 저장한다.
push
push eax ; eax 레지스터의 값을 스택에 집어 넣는다.
push 20 ; 20을 스택에 집어 넣는다.
push 40203F ; 메모리 주소 40203F의 값을 스택에 집어 넣는다.
* POP: 역시 SP 레지스터를 조작하는 명령어로 스택의 데이터를 꺼낸다.
pop
pop eax ; 맨 마지막에 스택에 집어넣은 값을 꺼내 eax 레지스터에 저장.
* MOV: 메모리나 레지스터 등의 값을 옮길 때 쓰인다.
move
mov eax,ebx ; ebx의 값을 eax로 옮긴다.
mov eax,20 ; 20을 eax에 옮긴다.
mov eax,dword ptr [40203F] ;메모리 주소 40203F의 값을 eax에 옮긴다.
* LEA: 대상 오퍼랜드의 값을 소스 오퍼랜드의 값으로 만든다.
load effective address
lea eax,ebx ; eax의 값을 ebx의 값으로 만든다.
* INC: 레지스터의 값을 1 증가 시킨다.
increase
inc eax ; eax의 값을 1 증가 시킨다.
* DEC: 레지스터의 값을 1 감소 시킨다.
decrease
dec eax ; eax의 값을 1 감소 시킨다.
* ADD: 레지스터의 값이나 메모리의 값을 더할 때 쓰인다.
add
add eax,ebx ; eax = eax + ebx
add eax,20 ; eax = eax + 20
add eax,dword ptr [40203F] ; eax = eax + 40203F의 값
* SUB: 레지스터의 값이나 메모리의 값을 뺄 때 쓰인다.
subtract
sub eax,ebx ; eax = eax – ebx
sub eax,20 ; eax = eax – 20
sub eax,dword ptr [40203F] ; eax = eax – 40203F의 값
* NOP: 아무 것도 하지 않는다.
no operation
* CALL: 프로시져(procedure)를 호출할 때 쓰인다.
call
call dword ptr [40203F] ; 메모리 주소 40203F을 호출한다.
* RET, RETN: 호출한 지점으로 돌아간다.
return
* CMP: 레지스터 끼리 혹은 레지스터와 값을 비교한다.
compare
cmp eax,ebx ; eax와 ebx를 비교한다.
cmp eax,20 ; eax와 20을 비교한다.
cmp eax,dword ptr [40203F] ; eax와 메모리 40203F의 값을 비교한다.
* JMP: 특정 위치로 무조건 점프한다.
unconditional jump
jmp dword ptr [40203F] ; 메모리 주소 40203F로 점프한다.
조건부 점프: CMP나 TEST와 같은 연산 결과에 따라 점프를 결정
* JE: CMP나 TEST 의 결과가 같다면 점프
jump if equal
* JNE: CMP나 TEST 의 결과가 같지 않다면 점프
jump if not equal
* JZ: 왼쪽 인자의 값이 0 이라면 점프
jump if zero
* JNZ: 왼쪽 인자의 값이 0 이 아니라면 점프
jump if not zero
* JL: 왼쪽 인자의 값이 오른쪽 인자의 값보다 작으면 점프
jump if less; signed
* JNL: 왼쪽 인자의 값이 오른쪽 인자의 값보다 작지 않으면 (크거나 같으면) 점프
jump if not less; signed
* JB: 왼쪽 인자의 값이 오른쪽 인자의 값보다 작으면 점프
jump if below; unsigned
* JNB: 왼쪽 인자의 값이 오른쪽 인자의 값보다 작지 않으면 (크거나 같으면) 점프
jump if not below; unsigned
* JG: 왼쪽 인자의 값이 오른쪽 인자의 값보다 크면 점프
jump if greater
* JNG: 왼쪽 인자의 값이 오른쪽 인자의 값보다 크지 않으면 (작거나 같으면) 점프
jump if not greater
* JLE: 왼쪽 인자의 값이 오른쪽 인자의 값보다 작거나 같으면 점프
jump if less or equal; signed
* JGE: 왼쪽 인자의 값이 오른쪽 인자의 값보다 크거나 같으면 점프
jump if greater or equal
논리 연산
논리 연산에도 여러가지가 있지만, 자주 쓰이는 and, or, not, xor, text만
다루도록 하겠다.
* AND: 대응되는 각 비트가 모두 1이면 1, 나머지는 모두 0
다음 표를 알아두라.
A B &
—–
0 0 0
0 1 0
1 0 0
1 1 1
mov eax,10
and eax,8
eax의 값은,
1010 & 1000 = 1000
8이다.
* OR: 대응되는 각 비트 중 하나라도 1이면 1, 모두 0이면 0
A B |
—–
0 0 0
0 1 1
1 0 1
1 1 1
* XOR: 대응되는 각 비트가 서로 다르면 1, 서로 같으면 0
A B ^
—–
0 0 0
0 1 1
1 0 1
1 1 0
* NOT: 각 비트를 반전시킨다.
A !
—
0 1
1 0
mov eax,8
not eax
eax의 값은,
1000을 반전시킨 0111이므로 7이다.
* TEST: 오퍼랜드에는 영향을 끼치지 않고 플래그만 세트시킨다.
여러분들은 어떤 멀티로더를 만들고 싶은가?
다음 시간엔 프로그래밍과 디버깅에 대해서 할 것이다.
역시 중요하다.. OTL
참고 사이트
dual5651님의 홈페이지
참고 서적
CRACK, 파워북 2000, Sky Hacker, Debugging Shock 공저(절판)
Computer Organization & Architecture : designing for performance – 6th ed, Pearson Education Inc 2003, William Stallings 저
글 옮긴이의 하고 싶은말..
조금 읽으면서.. –
참고 사이트가 듀얼님.. ^^ 너무 유명해 ..ㅋ