본문 바로가기

정보보안

[어셈블리어 분석 도전] Hello World 프로그램 분석해보기

<범용 레지스터의 종류>

EBP(RBP) : 스택의 가장 아랫 부분을 가리키는 포인터

ESP(RSP) : 스택의 가장 윗부분을 가리키는 포인터

EAX(RAX) : 연산결과 & 함수 반환값 등을 저장

ECX(RCX) : 반복 작업 등을 할 때 카운트를 저장

 

 

수학을 이론만 알고 실제 문제에 적용해보지 않으면 실력 향상에 크게 도움이 안되듯 어셈블리어 해석에 익숙해지기 위해 기본적인 프로그램들을 이용해서 실제로 적용해보기로 하였다.

 

첫 번째 프로그램은 C언어를 처음 시작할때 짜봤던 그 유명한 Hello World으로 정했다.

변수의 저장방식을 알아보기 위해 Hello World와 함께 변수에 저장된 값 하나를 출력하는 기능도 더해보았다.

 

<C언어>

#include <stdio.h>

int main(){
	int a = 5;
	printf("Hello World! %d\n", a);
	return 0;
}

 

<출력 결과>

Hello World! 5

 

 

위의 코드에서 나온 실행파일을 어셈블리어로 변환하면 대충 이렇게 생겼다.

 

 

이중에서 해석할 부분은 main 함수 부분

 

처음인만큼 한줄한줄씩 해석해 보았다.

 

데이터들은 메모리에 스택 형태로 저장되는데, 말그대로 '자료를 쌓으면서 저장'하는 방식이다.

A4용지 크기에 딱 맞는 상자에 용지들을 담는다고 가정해보면, 종이를 꺼내고 넣는 공간은 상자 위쪽에 뚫려있는 공간밖에 없다.

 

즉, 위쪽 방향에서만 자료를 꺼내고 넣을 수 있으며, 아래에 있는 자료를 꺼내기 위해서는 반드시 그 위에 쌓여 있는 자료들을 모두 꺼내야 하므로 가장 먼저 들어간 자료는 가장 마지막에 나오는(선입후출) 특징을 가지고 있다.

 

main함수가 실행되기 직전 메모리를 그림으로 표현하면 이렇게 되어있다.

rsp는 이전 함수의 밑바닥을, rbp는 이전 함수의 가장 윗부분을 가리키고 있을 것이다. 이게 왜 그러냐하면, 함수들도 각자 변수들을 갖고 있을텐데, 각자 함수들이 사용하는 데이터들이 스택에서 위 그림과같이 분리되어있기 때문이다. 

 

이 상태에서 main 함수를 호출하면 main 함수를 통해 전달되는 매게변수와(지금 상황에서는 전달된 매게변수는 없다) 이전 어셈블리 코드 주소가 스택에 저장된다. 하지만, 아직 스택을 완전하게 사용할 수 있는 상태는 아니다.

데이터가 채워지고 있으니 rsp는 최근에 호출된 main함수의 최상단을 계속 가리키게 된다. 하지만 rbp의 위치는 어떨까?

rbp는 아직 이전 함수가 차지하는 공간의 밑바닥 부분을 가리키고 있다 (정확히 맨 밑바닥은 아니다. 매개변수와 return address 때문에). main 함수의 호출 직후 실행되는 동안에 이전 함수에 있는 공간들은 빠져나갈 일이 없으므로 rbp는 변할 일이 없다. main 함수에서 스택을 제대로 활용하기 위해서는 저 rbp를 이전 함수가 아닌 main 함수쪽으로 이동시켜야 한다. 이 작업이 맨 처음 두 줄에서 이루어진다.

 

 

스택 사용 준비 완료!

push rbp를 통해서 기존 rbp에 들어있던 주소를 저장한다.

함수 실행이 끝나면 이전 함수로 rbp를 되돌려놔야하기 때문이다.

그리고 mov rbp, rsp를 통해 rbp를 main함수쪽으로 이동시켰다.

이로써 Stack Frame이 완성되었다

참고로 mov는 첫번째 인자에 두번째 인자 값을 대입하는 기능을 한다.

 

이제 본격적으로 main 함수의 사용이 시작되었다.

 

 

 

스택의 최상단 주소를 30칸만큼 뺐다. 스택에서는 주소가 높아질수록 스택의 낮은 지점을 가리키게 된다. 따라서 30칸을 뺐다는 것은 스택에서 30칸만큼 높은 주소를 가리키게 된것이다.

(여기에서 스택의 1칸은 4바이트만큼의 공간을 가지고 있다. -> 0x00000000 ~ 0xFFFFFFFF)

 

다음으로는 __main 함수를 호출하는데 초기화 함수의 일종이므로 자세히 알아보지는 않았다.(출처 : c - Understanding empty main()'s translation into assembly - Stack Overflow)

 

다음은 스택에 변수를 넣는 기능을 한다.

mov dword ptr [rbp-4], 0x5랑 똑같은 말인데, 메모리 주소를 직접 지정해서 접근하는 경우에는 dword ptr 등을 통해 사용할 크기를 지정해줘야 한다. 위의 경우에는 rbp에서 4바이트만큼 위에있는 지점에 5를 대입했다.

 

byte ptr : 1바이트

word ptr : 2바이트

dword ptr : 4바이트

qword ptr : 8바이트

 

int형 변수라 4바이트 크기만큼을 할당하고 변수에 5를 할당했으니, 스택에 5가 저장되는것이다.

 

변수 저장 후

 

 

rbp-4를 eax에 넣고, eax의 값을 다시 edx에 넣는다.

그리고 rcx에 Format의 주소를 넣는다

lea는 두번째 인자의 값을 첫 번째 인자에 대입하는 mov와 달리 두번째 인자의 "주소값"을 첫번째 인자에 넣는다.

 

int a = 5;
int b = a; //b에는 a의 값이 저장된다.
int *c = &a; //c에는 a의 주소가 저장된다.
int d = *c; //d에는 c에 있는 주소를 통해 a의 위치를 찾아간다음 그 안에 있는 값을 읽는다.
// 따라서 b와 c는 같지 않지만, b와 d는 같다.

Format에는 무엇이 들어있나 봤더니..

 

Format에 들어있는 문자열

 printf문 안에 있던 문자열이 담겨있다.

0Ah는 개행 문자 '\n'을 뜻하고, 마지막에 붙어 있는 0은 C언어에서 문자열의 끝을 나타내는 널문자를 뜻한다.

저 문자들을 다 이어붙이면 'Hello world! %d\n'이 된다.

그리고 c언어에서 문자열은 문자 주소를 통해서 읽고 쓰여지기 때문에(문자가 모여서 만들어진 배열이기 때문) 레지스터에 저장할때도 주소 형태로 저장되는것 같다.

드디어 printf가 호출되었다. 함수가 호출될때 인자를 가져갈때는 스택의 맨 위에있는 순서대로 가져간다. 하지만, 여기서 의문이 하나 생긴다. hello world가 들어있는 문자열의 주소는 rcx에 저장되어있지 스택에 저장되어있지는 않는다. printf 함수는 어떻게 알고 hello world 문자열을 가져가는걸까?

 

구글링을 해본 결과

windows - Why "mov rcx, rax" is required when calling printf in x64 assembler? - Stack Overflow

 

첫번째 인자에 해당되는 문자열을 rcx에서도 가져온다고 나와있었다.(단, 알맞은 값이 저장되어 있어야 한다는게 조건) 이걸 이해하느라 상당히 오랜 시간이 걸렸다.

 

결국 printf는 5와 rcx에 들어있는 문자열 주소를 인자로 가져가서 화면에 출력하는 역할을 해준다. 이로써 main 함수에서의 모든 기능 실행이 완료되었다.

 

이제는 main함수를 종료하기 위해 스택을 정리하고, rbp rsp 포인터를 원래대로 되돌려놔야 한다.

 

반환값으로 0이 설정되어 있기 때문에 eax에 0을 넣고, 30칸 위로 이동시켰던 rsp 포인터도 원래대로 돌려놓는다.

rsp 원상복구

 

pop을 통해 기존 rbp 주소를 rbp에 넣고(이 값이 들어가는 순간 rbp는 이전 함수의 밑부분을 가리키게 됨)

- pop 명령어는 스택 가장 위에있는 값을 삭제하면서 이 값을 첫번째 인자로 넘겨준다 -

pop rbp 직후

retn이 되면서 main함수가 완전히 종료되고, 스택은 main함수 실행 직전의 상태로 돌아가며(main 함수의 return address와 전달받은 인자가 삭제되기 때문), 저장되어있던 Return Address를 통해 호출 이전 어셈블리어 코드 위치로 돌아간다.

 

이로써 간단한 출력 프로그램의 분석이 모두 끝났다.

획실히 직접 짠 프로그램을 분석해보면서 알아가는 방식이 도움이 많이 되는것같다.

조건문, 반복문 등도 이런 식으로 분석해보면 좋지 않을까 싶다.

 

그리고 블로그가 처음인지라 쓰는데 2시간 반 넘게 잡아먹었다. 여러번 쓰다 보면 나아지지 않을까