1주차 정리 - Yerim-Lee/Reversing GitHub Wiki
목차
- Register
- Structure
- Stack
- Stack Frame
- PE File
Register
레지스터란 CPU 내부에 존재하는 다목적 공간이다. 레지스터는 CPU와 근접해있기 때문에 빠른 속도로 데이터를 처리할 수 있다. 대표적으로 IA-32 레지스터가 있다. IA-32 레지스터는 지원하는 기능이 많기 때문에 레지스터의 수도 많다.
가장 많이 알고 있는 Basic Program execution registers 는 4개의 그룹으로 나누어진다.
- General Purpose Registers (32비트 8개)
- Segment Registers (16비트 6개)
- Program Status and Control Register (32비트 1개)
- Instruction Pointer (32비트 1개)
범용 레지스터(General Purpose Registers)는 여러 분야나 용도로 널리 쓰이는 레지스터이다. 32비트 모두 사용하고 싶을 때는 EAX, 16비트만 사용하고 싶을 때는 AX, 8비트만 사용하고 싶을 때는 AH 혹은 AL을 사용할 수 있다. 또한 앞에 붙어있는 E는 Extended의 약자로 16비트 시절 레지스터들을 확장시키면서 생겨난 것으로 32비트를 뜻한다. (추가로 64비트에서는 R이 붙는다고 한다.)
레지스터의 종류를 보게 되면 크게 2가지로 나눌 수 있다. 첫 번째로는 산술연산 명령어에서 값의 저장 용도로 사용할 수 있는 레지스터들과 두 번째로는 메모리 주소를 저장하는 용도로 사용할 수 있는 레지스터들이다.
1)산술연산 명령어에서 값의 저장 용도로 사용
- EAX(Accumulator Register) : 함수의 리턴 값을 저장
- EBX(Base Register) : 메모리 주소를 저장
- ECX(Count Register) : 반복문에서의 카운트
- EDX(Data Register) : 큰 수의 복잡한 연산에 사용
2)메모리 주소를 저장하는 용도로 사용
- EBP(Base Pointer Register) : Stack의 시작 주소
- ESP(Stack Pointer Register) : Stack의 끝 주소, 함수가 실행될 때마다 값이 변경됨 (Stack에서 PUSH나 POP을 통해서 Stack값이 바뀔 때 ESP도 값이 바뀌는 것 같다.)
- ESI(Source Index Register) : 데이터를 복사할 때 데이터의 주소를 저장
- EDI(Destination Index Register) : 데이터를 복사할 때 목적지의 주소를 저장
아직 정확하게 이해하려면 좀 더 봐야할 것 같다. 범용 레지스터를 정확하게 어느 정도 이해하게 되면 다른 레지스터도 다음에 알아봐야겠다!
Structure
프로세스 구조를 보면 HDD에서 PE파일 즉 exe실행 파일을 메모리에 로드하면 메모리에 PE파일이 등록이 된다. 그리고 이 PE파일은 CPU에 있는 Register에 복사시키게 되고, Control Unit과 ALU를 통해 컨트롤과 연산을 한다.
*프로세스의 메모리 구조
프로세스 메모리의 구조는 Code영역, Data영역, Heap영역, Stack영역 등으로 구성되어 있다. Code에는 프로그래밍 코드를 구성하는 메모리 영역이고, Data에는 전역 변수,정적 변수 등이 저장되며 프로그램이 실행될 때 생성되고, 종료 시에 반환된다. Heap은 동적 데이터 영역이고, Stack에는 함수 내의 지역변수 및 매개변수 등 잠시 사용되는 데이터의 영역이다.
Stack
스택 영역은 함수가 호출될 때 생기며 함수의 호출이 끝나면 사라진다. 스택은 LIFO(Last In First Out)구조로 선입후출 구조이다. PUSH(삽입), POP(삭제)가 스택의 끝부분 즉 ESP에서만 이루어진다. ESP가 스택의 끝, EBP가 스택의 시작이다. ESP는 스택의 가장 윗부분에 있는 데이터를 가리키는 포인터로 Intel CPU에서 스택이 거꾸로(리틀 인디언 방식)이므로 데이터가 하나 PUSH가 되면 ESP가 감소하게 된다. EBP는 스택의 가장 아랫부분에 있는 데이터를 가리키는 포인터로 실행중인 함수가 종료될 경우에 값이 바뀔 수 있다.
추가로 Heap은 스택과 반대로 FIFO(First In First Out)구조로 들어온 순서대로 나가게 되는 형태이다.
Stack Frame
위 예제는 스택 프레임의 동작 방식이다. 스택 프레임이란 스택 영역에 함수의 호출 정보가 차례대로 저장이 되는 것을 말한다. 스택 프레임이 있기 때문에 함수의 호출이 끝난 후 해당 함수가 호출되기 이전의 상태로 되돌아갈 수가 있다. return address는 특정한 함수가 끝난 후에 돌아갈 장소이다. 예를 들어 C언어 프로그램을 돌리는데 main 함수를 호출했을 때 main 함수가 끝나면 main 함수를 불러들였던 위치로 돌아가게 된다. 이 돌아가는 위치가 return address이다.
함수가 실행이 되면 main 함수가 실행이 되고 main 함수에 있는 func1() 함수를 불러오게 된다. 그 함수의 매개변수를 넘겨주고 새롭게 함수를 호출 했기 때문에 반환 주소값 즉 return address 가 들어가게 되고 그 위로 변수값이 들어가게 된다.
함수 호출 시 프로세스
- call 일 때 return address 값을 push
- ebp 를 push
- ebp 에 esp 데이터를 넣는다
- 함수의 변수를 스택에 넣는다
함수 반환 시 프로세스
- ebp 까지 저장된 변수들을 POP 한다.( esp == ebp 이면?? 맞나?? )
- 스택 최상단에 들어있는 이전 ebp 값을 다시 ebs 에 넣어줌 (pop)
- 스택 최상단에 있는 return address 값으로 되돌아감
(앞서 언급했던 EBP, ESP로 구성이 되어 있으며 EBP는 스택 프레임의 말 그대로 base 주소를 저장하는 포인터다. EBP가 레지스터 이름이고, 일반적으로는 Frame Pointer,FP 라고 부른다.)
스택 오버플로우(stack overflow)
함수의 재귀 호출이 무한하게 반복이 되면 재귀 호출로 인해 스택 프레임이 계속 쌓여만 갈 것이다. 이러한 현상(스택 영역의 메모리가 지정된 범위를 넘어감)을 방지하기 위해 스택 오버플로우를 통해 해당 프로그램을 종료시킬 수 있다.
PE File
PE파일은 PE Header(Info)와 Body(PE Code)로 구성되어 있다. DOS header부터 Section header까지가 PE Header이고, 아래의 Section들이 PE Body이다. 구조가 섹션 별로 나누어져서 저장이 되어있기 때문에 프로그램이 좀 더 안정적일 것 같다. PE헤더에는 실행 파일을 실행하기 위한 정보들로 구성되어 있고 메모리 적재 방법이나 실행 위치, 필요한 dll 의 목록들이 있다. 그래서 파일을 실행시키면 코드가 실행되기 전에 PE 정보부터 읽어서 데이터 설정 작업을 하게 된다.
간단하게 말해서 PE파일은 Portable Executable File Format의 약자로 만든 File이 이식 가능한 다른 곳에 옮겨져도(Portable) 실행이 가능하도록(Excecutable) 만들어 놓은 포맷(Format)이다. 예를 들어 필요한 프로그램이 있을 때 어느 컴퓨터든 exe 파일을 설치하면 실행할 수 있는 것이다.
PE파일의 종류
- 실행 파일 : exe, scr
- 라이브러리 : dll, ocx
- 드라이버 : sys
- 오브젝트 파일 : obj (실행x)
dll,sys 파일은 직접 실행할 수 없지만 디버거를 사용하여 실행이 가능한 파일들이다.
PE파일의 생성과정
- 소스 코드 작성
- 컴파일
- 필요한 라이브러리 연결(링킹)
- exe 파일로 빌드
링킹(Linking),링커(Linker)
링킹이란 여러 개의 코드와 데이터를 모아 연결하여 메모리에 로드될 수 있고 실행될 수 있는 한 개의 파일로 만드는 작업이다. 링커는 링킹을 담당하는 프로그램이다. 소프트웨어 개발에서 독립적인 컴파일을 가능하게 하는 역할인데 큰 규모의 응용프로그램을 한 개의 소스 파일로 구성하면 관리하기가 굉장히 어려울텐데 링커로 인해 작은 모듈들로 나눌 수 있고, 소스 파일들을 다 모듈화하여 개발을 진행할 수 있다. 그래서 컴파일할 때도 하나의 모듈을 수정한다고 하면 모든 파일을 재컴파일할 필요없이 수정한 파일만 간단하게 재컴파일을 해서 다시 링크를 하여 변경사항을 적용할 수 있다.
그래서 간단하게 링킹은 내가 프로그래밍을 할 때 다른 모듈, 라이브러리를 가져와서 링크(연결)하는 것을 말한다. 링킹에는 동적 링킹과 정적 링킹 두 가지 방식이 있다.
동적 링킹
동적 링킹은 목적 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하지 않고 해당 모듈의 주소를 가지고 있는 방식이다. 런타임 운영체제로 이루어진다.() 예를 들어 5개의 프로그램에서 A라는 외부 함수를 사용한다면 A의 주소를 가리키는 방식이기 때문에 A라는 함수의 정보는 하나만 있으면 된다. 그래서 정적 링킹 방식과 비교해 목적파일의 크기가 작고, 메모리가 디스크 공간을 더 효율적으로 사용할 수 있다는 장점이 있다.
정적 링킹
정적 링킹은 목적 파일을 만들 때 프로그램에서 사용하는 모든 라이브러리 모듈을 복사하는 방식을 말한다. 예를 들어 5개의 프로그램에서 A라는 외부 함수를 사용하는데 정적 링킹 방식을 사용하면 5개의 프로그램의 목적파일 각각에 A하는 외부 함수의 정보가 담기는 것으로 중복이 발생한다. 그래서 크기가 크고 메모리 효율이 좋지 않다는 단점이 있다.
IMAGE_SECTION_HEADER
이미지 섹션 헤더는 각 섹션들의 속성을 정의하는 섹션 헤더이다. 섹션 헤더는 각 섹션 별 IMAGE_SECTION_HEADER 구조체의 배열로 되어있다.
VirutalSize : 메모리에서 섹션이 차지하는 크기 VirutalAddress : 메모리에서 섹션의 시작 주소(RVA) SizeOfRawData : 파일에서 섹션이 차지하는 크기 PointerToRawData : 파일에서 섹션의 시작 위치
RVA to RAW
PE파일이 메모리에 로딩되었을 때 각 섹션에서 메모리의 주소(RVA)와 파일 오프셋을 잘 매핑해야하는데 이러한 매핑을 RVA to RAW 라고 부른다. RVA가 속해 있는 섹션을 찾아서 아래 있는 비례식을 통해 RAW 계산할 수 있다.
비례식
RAW - PointerToRawData = RVA - Virtual Address
RAW = RVA - VirtualAddress + PointerToRawData
RAW는 파일에서의 주소, RVA는 메모리에서의 주소, VirtualAddress는 메모리에서의 섹션 시작 위치, PointerToRawData는 파일에서의 섹션 시작 위치이다.
VA(Virtual Address)는 프로세스 가상 메모리의 절대 주소를 말한다.