본문 바로가기

컴퓨터와 보안/리버싱

프로세스 메모리 구조와 스택 프레임 구조

프로세스 메모리 구조

프로세스의 메모리 구조는 Text, Data, Heap, Stack 영역으로 구분되어 있다.

프로세스 메모리 구조

  • Text 영역 프로그램 코드와 상수가 정의되어 있고, 읽기만 가능한 메모리 영역이기 때문에 데이터를 저장하려고 하면 분할 충돌을 일으켜 프로세스가 중지된다.
  • Data 영역 : 전역 변수(Global variable)와 정적 변수(Static variable)가 저장되어 있는 영역이다.
  • Heap 영역 : 프로그래머의 필요에 따라 동적 메모리 호출에 의해 할당되는 메모리 영역이다. c언어의 기준으로 malloc() 함수나 calloc() 함수에 의해 생성된 변수들이 이 곳에 할당된다.
  • Stack 영역 : 함수 인자 값, 함수 내의 지역 변수, 함수의 반환 주소 등이 저장되는 영역으로 함수 호출의 전반적인 처리와 리턴 값을 가지고 있다. 상위 메모리 주소에서 하위 메모리 주소로 데이터가 저장된다.

힙이나 스택 영역은 프로그램이 실행하는 동안 크기가 결정되는 동적 할당 영역이다. 입력 값에 의한 함수의 호출 횟수나 동적으로 생성되는 변수들의 크기는 런타임 시에 결정되기 때문이다.

 

반면에 프로그램 코드 자체나 전역 변수의 경우 실행되기 전에도 크기가 이미 정해져 있다. 또한 바뀔 일도, 바뀌어서도  안된다. 따라서 컴파일 시에 위치와 크기가 결정되는 정적 할당 영역이다.

 

스택 프레임 구조

 

스택 프레임의 구조

출처 : 알기사 - 정보보안기사 & 산업기사 필기

스택 프레임 (Stack Frame)

스택 프레임이란 함수가 호출(system call)되었을 때 그 함수가 가지는 공간 구조이다. 함수가 동작을 종료하고 복귀 주소로 돌아갈 때 스택 프레임은 소멸된다.

 

ESP 레지스터는 스택 포인터 역할을 하고, EBP 레지스터는 베이스 포인터 역할을 한다.(Base pointer = frame pointer)

ESP 레지스터의 값은 프로그램 안에서 수시로 변경되기 때문에 스택에 저장된 변수, 파라미터에 접근하고자 할 때 ESP 값을 기준으로 하면 프로그램을 만들기 어려워진다.

따라서 어떤 기준 시점(함수 시작)의 ESP 값을 EBP에 저장하고 이를 함수 내에서 유지해주면, ESP 값이 아무리 바뀌어도 EBP를 기준으로 안전하게 해당 함수의 변수, 파라미터, 복귀 주소에 접근할 수 있게 된다. (EBP는 스택프레임이 소멸하기 전까지 고정값이다. 고정값을 기준으로 삼아 상대적 주소, 옵셋(Offset) 값을 사용할 수 있다.)

 

스택 프레임의 구조

PUSH EBP              : 함수 시작 (EBP 사용하기 전에 초기 값을 스택에 저장)

 

MOV EBP, ESP        : 현재의 ESP를 EBP에 저장

 

...                         : 새로운 함수의 내용

 

 

MOV ESP, EBP         : ESP를 함수 시작했을 때의 값으로 복원

 

POP EBP                : 리턴되기 전에 저장해놨던 원래 EBP 값으로 복원

 

RETN                    : 함수 종료

 

 

스택 프레임의 동작 방식

int main(void)

{

    func1();  // func1() 호출

    return 0;

}

 

void func1()

{

    func2();  // func2() 호출

}

 

void func2()

{

}

 

다음 그림은 코드에서 함수 호출에 의한 스택 프레임의 변화를 보여준다.

 

함수 실행
함수 종료

출처 - TCPSCHOOL.com

 

Step 1. 프로그램이 실행되면, 가장 먼저 main() 함수가 호출되어 main() 함수의 스택 프레임이 스택에 저장된다.

Step 2. func1() 함수를 호출하면 해당 함수의 매개변수, 반환 주소값, 지역 변수 등의 스택 프레임이 스택에 저장된다.

Step 3. func2() 함수를 호출하면 해당 함수의 스택 프레임이 추가로 스택에 저장된다.

Step 4. func2() 함수의 모든 작업이 완료되어 반환되면, func2() 함수의 스택 프레임만이 스택에서 제거된다.

Step 5. func1() 함수의 호출이 종료되면, func1() 함수의 스택 프레임이 스택에서 제거된다.

Step 6. main() 함수의 모든 작업이 완료되면, main() 함수의 스택 프레임이 스택에서 제거되면서 프로그램이 종료된다.

 

함수 호출(System call)

시스템 호출(system call)은 운영 체제인 커널이 사용자에게 자신의 자원을 사용할 수 있도록 만든 인터페이스다.

즉 사용자 모드가 커널 영역의 기능을 사용 가능하게 해 준다.

 

시스템 호출을 하면 사용자 모드에서 커널 모드로 바뀐다. 커널에서 시스템 호출을 다 처리하면 커널 모드에서 사용자 모드로 돌아가 작업을 계속한다.

 

시스템 호출은 사용자가 응용 프로그램으로부터 컴퓨터의 자원을 직접적으로 접근하는 것을 막아 컴퓨터의 자원을 보호해준다.

 

system call의 구조

 

시스템 호출과 라이브러리 함수의 차이

 

응용 프로그램은 시스템 콜이나 라이브러리 함수를 통해 커널의 모듈을 사용해 특정 기능을 발휘할 수 있다.

여기서 바로 시스템 콜을 사용하느냐 라이브러리 함수를 사용하느냐 두가지로 나뉠 수 있는데 라이브러리 함수를 사용한다면 함수 내에 사용된 시스템 콜을 사용한다. 반대로 응용 프로그램 내에서 바로 시스템 콜을 사용한다면 라이브러리 함수를 거치지 않고 커널의 기능을 사용할 수 있다.

 

시스템 호출의 유형

  1. 프로세스 제어(process Control)
  2. 파일 조작(file manipulation)
  3. 장치 관리(Device Management)
  4. 정보 유지(Information maintenance)
  5. 통신(Communication)

시스템 호출의 종류

 

프로세스와 관련된 시스템 호출

   - 프로세스 제어용 : exec, fork, wait, pipe, signal, exit, getuid, setuit...

   - 표준 화일(장치)에 대한 입출력 시스템 호출 : open(), create(), close(), reade(), write(), lseek()...

   - 소켓 기반의 입출력 시스템 호출 : socket(), blind(), listen(), accept(), connect()

 

시스템 호출 함수 정리

 

exec() : 

기존의 프로세스를 새로운 프로세스로 전환하는 시스템 호출 함수다. 프로세스는 그대로 둔 채 내용만 바꾼다.

active process에 있는 파일을 실행하는데 쓰인다. 

이 시스템 호출을 사용하면 프로세스의 구조체를 재활용할 수 있다.

 

<프로세스의 변화>

-코드 영역에 있는 기존의 내용을 지우고 새로운 코드로 바꾼다.

-데이터 영역이 새로운 변수로 채워지고 스택 영역이 리셋된다.

-프로그램 카운터 레지스터 값을 비롯한 각종 레지스터와 사용한 파일 정보가 모두 리셋된다.

 

fork()

실행 중인 프로세스로부터 새로운 프로세스를 복사하는 시스템 호출 함수다. 새로운 프로그램을 실행하는 속도보다 훨씬 빠르다.

프로세스를 만드는데 쓰인다.

실행하던 프로세스는 부모 프로세스, 새로 생긴 프로세스는 자식 프로세스로 부모-자식 관계의 계층 구조가 형성된다.

 

<프로세스의 변화>

-프로세스 구분자(PID)가 변한다.

-자식 프로세스가 생성되는 메모리 위치가 다르므로 메모리 관련 정보가 바뀐다.

-부모 프로세스에는 CPID(Child PID), 자식 프로세스에는 PPID(Parents PID)가 바뀐다. 만약 자식 프로세스가 없으면 -1로 값이 설정된다.

 

wait()

자식 프로세스가 끝나기를 기다리는 시스템 호출이다. 주로 부모 프로세스와 자식 프로세스 간 동기화에 사용되는데, 이 이유는 자식 프로세스보다 부모 프로세스가 먼저 종료되는 경우를 막기 위함이다.

이럴 경우 자식 프로세스의 자원을 정리할 부모 프로세스가 없어지고, 돌아갈 곳도 없어지는 미아 프로세스가 발생하기 때문에 시스템의 자원이 낭비되고 성능이 저하된다.

 

exit()

작업의 종료를 알려주는 시스템 호출로, 자식 프로세스가 끝났음을 부모 프로세스에게 알려주어 부모 프로세스가 자원을 빨리 거둬갈 수 있게 만든다.

또한 인자 전달을 통해 자식 프로세스가 어떤 상태로 종료되었는지를 알려줄 수 있다.

exit() 호출을 하기 위한 C언어의 return() 문을 사용하는 이유도 프로세스의 처리 과정들을 보다 정확하고 확실히 함으로써 오류를 줄이고 프로그램의 성능을 더 좋게 만들 수 있다.

 

 

'컴퓨터와 보안 > 리버싱' 카테고리의 다른 글

abex' crackme #2 분석  (0) 2021.04.12
abex' crackme #1 분석  (0) 2021.04.06
어셈블리어와 레지스터  (0) 2021.04.04
PE File Format - pestudio  (0) 2021.04.02
PE File Format4  (0) 2021.04.01