[Ghidra] Reversing Basic knowledge - 2

코코·2023년 4월 16일
1

Ghidra

목록 보기
4/4
post-thumbnail

이번에는 두 번째로 프롤로그 및 에필로그와 함수 호출 규약에 대해서 알아보고자 한다.


우선, 간단하게 함수 호출 규약부터 살펴보자. 64bit기준으로 살펴볼 것이다!


또한 들어가기에 앞서, x64dbg를 통해 분석할 예정이니, x64dbg 설치 사이트 링크도 아래에 달아두겠다.
👉 https://x64dbg.com/


✔ 함수 호출 규약(Calling Convention) - 64bit

함수 호출 규약(Calling Convention)이란 호출하고자 하는 함수 or 서브루틴에게 매개 변수를 전달하는 방법과 결과를 전달받는 방법에 대한 약속이다.
🌟 64bit 윈도우 환경에서 함수 호출 규약은 Fastcall 방식이다.

MS의 fastcall(64bit)은 첫 번째와 두 번째 인자, 세 번째 인자, 네 번째 인자들을 각 rcx, rdx, r8, r9 레지스터에 저장한다.
그 후 다섯 번째 인자부터는 역순으로 Stack에 저장하여 전달한다. 또한 해당 함수가 호출되고 반환할 때, 반환값이 있다면 rax 레지스터를 통해 전달한다.


우리가 자주 사용하는 printf 함수를 예시로 살펴보자.

int ret_num;
ret_num = printf("%d + %d = %d", 3, 5, 3+5)

printf 함수에는 총 4개의 인자가 전달되고 있다.
첫 번째 인자("%d + %d = %d")는 rcx레지스터에 저장될 것이고, 두 번째 인자(3)은 rdx 레지스터에 저장될 것이다. 그리고 이후 세 번째 인자는 r8 레지스터, 네 번째 인자(3+5)는 r9 레지스터에 저장될 것이다.

이후 ret_num에는 printf가 출력한 문자열의 개수를 rax 레지스터에 담고, 해당 값을 ret_num에 넣어줄 것이다.


즉, 정리해보면 rcx -> rdx -> r8 -> r9 순이고, 함수의 리턴 값은 rax 레지스터를 통해 전달한다.


🤚 잠깐, Pwnable에서 자주 사용되는 리눅스 ELF 파일의 함수 호출 규약도 간단하게 살펴보고 넘어가자.
Linux의 함수 호출 규약은 SYSTEM V(SYSV)로, rdi -> rsi -> rdx -> rcx -> r8 -> r9 순으로 인자를 전달하고, 마찬가지로 rax 레지스터를 통해 반환값을 전달한다.

✔ 함수 프롤로그(prolog) & 에필로그(epilog)

프롤로그(prolog) : 함수가 실행될 때, 스택 프레임(Frame)을 생성하기 위한 과정
에필로그(epilog) : 함수가 실행을 마치고 원래의 코드로 돌아올 때, 스택 프레임(Stack Frame)을 호출한 시점으로 복원시켜주기 위한 과정

프롤로그(prolog)

함수의 프롤로그는 아래와 같이 구성되어있다.

push rbp
mov rbp, rsp
sub rsp, 40

간단한 c코드를 작성하여 디버거(x64dbg)로 함수 프롤로그 부분을 살펴보자.

  • study.c
#include <stdio.h>

int study(char *ptr, int num1, int num2, int num3, int num4, int num5, int num6);

int main(int argc, char *argv[]){
    
    printf("[*] Main function!\n");
    study("arg1", 1, 2, 3, 4, 5, 6);

    return 0;
}

int study(char *ptr, int num1, int num2, int num3, int num4, int num5, int num6){
    printf("[*] study function!\n");
    printf("String : %s", ptr);

    return num1 + num2 + num3 + num4 + num5 + num6;
}

코드는 간단하다. main 함수에서 study 함수를 호출하고, study 함수에서는 첫 번째로 받은 문자열을 출력하고, 리턴 값으로는 두 번째 인자부터 마지막 인자까지의 합을 리턴한다.


자 이제 x64dbg로 호출부분부터 살펴보자! 👊
우선 main 함수부터 찾아가야한다. main 함수 or Entrypoint를 찾는 방법은 다양한데, 이번에는 문자열을 통해 찾아가보도록 할 것이다.(다음에 main or Entrypoint를 찾는 방법도 정리해봐야겠다)

[어셈블리어가 있는 코드부분에서 마우스 우클릭] - [다음을 찾기(S)] - [모든 유저 모듈] - [문자열 참조] 순으로 클릭한다.
그 후 main 함수에 존재하는 문자열을 더블클릭하면 해당 어셈블리어부분으로 이동하게 된다. 더블클릭! 타닥!

위와 같이 main 함수안에 있는 printf 함수의 인자로 들어가는 문자열("[*] Main function!")을 확인할 수 있다. 그러면 바로 main함수로 올 수 있다.
조금만 올려서 함수의 프롤로그부분을 찾으면, 해당 부분이 main 함수의 시작부분이다.

알기 쉽게 해당 부분에 주석을 작성하고, 바로 찾아올 수 있도록 F2(Breakpoint)키를 눌러, 브레이크포인트를 설정한다.

자 이제, 프롤로그의 첫 부분을 살펴보자. 맨 처음 rbp에 0xA014C0의 값이 들어있는 것을 확인할 수 있다. push rbp를 하게되면, 바로 스택에 해당 값이 들어갈 것이다. 다음으로 넘어가서 살펴보자.

해당 스택 값안에 이전에 rbp 값이 들어간 것을 확인할 수 있다.

크게 살펴보면, 0x61FE20(RSP가 가리키고 있는...)에 0xA014C0가 들어간 것을 확인할 수 있다.

다음에 실행할 부분은 mov rbp, rsp이다. 그러면, rsp에 있는 값을 rbp 레지스터에 넣게 될 것이다.
즉, 0x61FE20 값을 rbp 레지스터에 넣을 것이다.

역시, RSP에 있던 값이 RBP(0x61FE20)에도 똑같이 저장되었다.

그 다음 실행할 명령어는 sub rsp, 40이다!
sub rsp, 0x40은 rsp - 0x40을 다시 rsp 레지스터에 넣는 것을 의미한다. 해당 명령어를 실행하여 스택의 공간을 확보하는 작업을 한다.

RSP에 0x61FDE0(0x61FE20 - 0x40) 값이 들어간 것을 확인할 수 있다.


즉, 위의 순서대로 해당 함수에서 사용할 스택 공간을 확보한다.


에필로그(epilog)

다음으로는 에필로그 코드를 살펴보자.

add rsp, 0x40
pop rbp
ret

우선 rsp, 0x40을 수행하여, rsp = rsp + 0x40의 값(0x61FE20)을 넣어준다.

0x61FE20에는 이전의 rbp 값(0xA014C0)을 가지고 있다.

그 후 수행할 pop rbp인데, pop은 2개의 어셈블리어로 이뤄져있다.
rbp 레지스터에 현재 스택에 있는 값을 가져오고, rsp 레지스터에 +0x8을 수행한다.

수행한 결과이다. 0x61FE20에서 0x61FE28로 이동한 것을 확인할 수 있다.

또한 레지스터 창을 확인해보면, rbp 레지스터에 0x61FE20에 저장되어있던 0xA014C0 값이 저장된 것을 확인할 수 있다.



참고로 책에는 함수의 프롤로그 & 에필로그를 아래의 어셈블리어로 표현하고 있는데, 결국 같은 의미이다.

또한 지역변수는 ss : [ptr]로 표현하고, 전역변수는 ds : [주소]로 표현한다고 한다.



✔ 함수 호출 규약

마지막으로 함수 호출 규약에 대해 x64dbg로 살펴보자.

윈도우의 함수 호출 규약은 MS의 fastcall로 함수에 인자를 rcx -> rdx -> r8 -> r9 -> Stack 순으로 전달한다.

Ctrl + F2키를 눌러 프로그램을 재실행해주자.

그 후 [중단점] -> [중단점 따라가기] 를 클릭하여, 중단점으로 곧바로 이동해주면 된다! 아니면 F9(단축키)를 눌러 이동해주면 된다.


분석할 코드는 위에서 봤던 코드를 사용할 것이다. 다만 이번에는 study 함수를 호출하는 부분을 집중적으로 살펴볼 예정이다❕

#include <stdio.h>

int study(char *ptr, int num1, int num2, int num3, int num4, int num5, int num6);

int main(int argc, char *argv[]){
    
    printf("[*] Main function!\n");
    study("arg1", 1, 2, 3, 4, 5, 6);

    return 0;
}

int study(char *ptr, int num1, int num2, int num3, int num4, int num5, int num6){
    printf("[*] study function!\n");
    printf("String : %s", ptr);

    return num1 + num2 + num3 + num4 + num5 + num6;
}

다시 한 번보면, study 함수에 인자로 "arg1", 1, 2, 3, 4, 5, 6을 순서대로 전달하고 있다.
그러면, "arg1" ~ 3까지는 rcx -> rdx -> r8 -> r9 순으로 전달할 것이고 이후 인자들은 stack을 통해서 전달될 것이다.


자 그럼 지금부터 x64dbg로 살펴보자! GoGo

위의 과정(Ctrl + F2 -> F9)에서 main 함수로 이동했었다.

이번에 살펴볼 부분은 study 함수를 호출하기 직전 함수에게 인자를 전달하는 부분이다!
해당 0x4015A0가 study 함수를 호출하는 부분이다.

그리고 실제 값을 전달하는 부분은 위의 부분이다.
예상했듯이 4번째 인자까지는 레지스터를 통해 전달하고, 나머지 값(4, 5, 6)들은 스택에 값을 저장하고 있다.

mov dword ptr ss:[rsp+30], 6

어셈 한 부분만 간단하게 짚고 넘어가보자.
우선 앞의 dword ptr ss:[rsp+30] 부터 살펴보자. 우선 ss는 Stack Segment를 의미한다. 또한 어셈블리어에서 "[주소값]" (괄호)는 해당 주소가 가리키는 값을 뜻한다. 마지막으로 dword는 4byte를 의미한다.

즉, 값 "6"을 rsp+30에 있는 주소에 넣겠다는 뜻이다.

mov dword ptr ss:[rsp+30], 6이 실행된 직후이다.
이 때 레지스터 값과 스택창을 살펴보자.

현재 rsp 레지스터의 값은 0x61FDE0이다.
rsp + 0x30의 주소 값을 넣을 것이다. rsp + 0x30 = 0x61FE10이다. 바로 스택창을 확인해보자.

현재 rsp 레지스터는 검은색으로 칠해진 0x61FDE0를 가르키고 있다.
그리고 6의 값은 예상했던 대로, 0x61FE10에 저장되어있다!


다음 인자들 4, 5도 마찬가지로 5 -> 4 순서로 스택에 쌓일 것이다.
예상했던 대로 6 -> 5 -> 4 순으로 Stack에 저장된 것을 확인할 수 있다.

다음은 함수 호출직전으로 이동하여, 레지스터의 값을 살펴보자.
잘 안보이니 확대해서 살펴보자...😧

1. 첫 번째 인자 arg1 ➜ rcx 레지스터
2. 두 번째 인자 1 ➜ rdx 레지스터
3. 세 번째 인자 2 ➜ r8 레지스터
4. 네 번째 인자 3 ➜ r9 레지스터

순으로 차례대로 들어간 것을 확인할 수 있다❗️


마지막으로 함수의 리턴 값은 어떻게 전달되는지 확인해보자.
study 함수를 호출하는 부분에서 F7(함수 내부로 진입)을 누르자.

study 함수의 내부이다. 마찬가지로 함수의 프롤로그를 진행한다.
이제 리턴하는 부분으로 이동해보자.


return num1 + num2 + num3 + num4 + num5 + num6;

Study 함수의 리턴 값은 전달받은 숫자들을 모두 더한 값을 리턴한다.

빨간 박스부분을 보면, 스택 세그먼트의 rbp+0x18 주소에 있는 값을 edx 레지스터로 가져오고, rbp+0x20에 있는 값을 eax 레지스터로 가져와서 더해주는 것을 확인할 수 있다.
이후부터는 쭉 eax 레지스터에 값을 가져와서 edx와 add를 수행한다.

그리고 마지막에 add eax, edx를 통해 eax 레지스터에 저장한다.

이만... 👋




※ 참고
👉 https://myreversing.tistory.com/84
👉 https://en.wikipedia.org/wiki/Calling_convention - 함수 호출 규약에 대한 설명
👉 https://fascination-euna.tistory.com/76

profile
화이팅!

1개의 댓글