Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

D1N0's hacking blog

C언어 함수 호출과정 - x64 본문

Pwnable

C언어 함수 호출과정 - x64

D1N0 2021. 2. 9. 18:08

이 글에서는 64bit 환경에서의 함수 호출 과정을 알아보겠다

나는 함수 호출 규약을 64bit에선 fastcall만 쓰는 줄 알았는데,

찾아보니 vectorcall을 사용한다, 기본 64bit 호출 규약이 있다,...

말이 다 달라서 용어는 모르겠지만 나는 그냥 gcc 기본옵션으로 컴파일된 바이너리를 살펴보겠다

어차피 일반적으로 그냥 컴파일 된 바이너리를 제일 자주 볼 테니 말이다

물론 내가 기본옵션으로 컴파일 한 파일이 일반적인 방식으로 컴파일된 파일이 아닐 수도 있다

 

아무튼 나중에 자세히 알게된다면 이 부분에 대해선 다시 정리하겠다

 

이 글은 전 글과 연결되어 있으니 아직 보지 않았다면 이전 글을 보고 오는 것을 추천한다

 

C언어 함수 호출과정 - cdecl

포너블에서 너무 중요하고 기초적인 내용이지만 아직도 헷갈려서 정리한다 이 글은 독자가 기초적인 C언어와 어셈블리어를 알고 있는 상태라고 가정한다 모든 코드는 WSL Ubuntu 20.04 LTS에서 -m32 -n

d1n0.tistory.com

#include <stdio.h>
int foo(int a, int b, int c) {
    return a+b+c;
}

int main() {
    int a = foo(1, 2, 3);
    return 0;
}

이전 글과 같은 코드를 x64 환경에서 컴파일했다

main은 이렇게

 

foo는 이렇게 생겼다

 

32bit cdecl과 다른점은 함수를 호출하기 전 인자를 넘길 때 push로 스택에 저장하는 것이 아니라

mov edx, 0x3

mov esi, 0x2

mov edi, 0x1 처럼 레지스터에 넣는다는 것이다

 

foo함수에서는

push rbp

mov rbp, rsp로 64bit 레지스터에 해당하는 SFP를 푸시한다

또, 함수를 호출할 때 레지스터에 넣었던 인자를 함수 내부에서 스택에 넣는 것을 확인할 수 있다

 

그러면 이제 직접 실행하며 스택과 레지스터의 변화를 살펴보자

call foo 직전의 레지스터의 모습이다

인자로 넘길 1, 2, 3이 rdi, rsi, rdx에 들어가 있다

또한 스택은 sub rsp, 0x10으로 할당되어있는 상황이다

함수 호출 전 스택은 이러하다

 

함수에 들어가서

push rbp

mov rbp, rsp를 한 모습이다

32bit와 마찬가지로 RET가 푸시되어있고, SFP가 그 위에 푸시되어있다

32bit와 다르게 8byte씩 사용되는 것을 잊지 말자

 

레지스터로 넘긴 인자를 스택에 저장하고 나면 이렇게 SFP 위에 들어가게 된다

 

그 뒤로는 함수의 기능을 하고, pop rbp로 SFP를 rbp에 넣어 원래 rbp를 복구하고, ret을 통해 main으로 돌아간다

 

여기까지 알아봤다면 몇가지 궁금증이 생긴다

먼저, 인자가 엄청 많아서 레지스터에 다 집어넣을 수 없다면 어떻게 될지, 그리고 그 기준은 어떤지이다

이걸 알아보기 위해 다음과 같은 코드를 컴파일해보겠다

#include <stdio.h>
int foo(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) {
    return a+b+c+d+e+f+g+h+i+j;
}

int main() {
    int a = foo(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    return 0;
}

main은 이렇게 컴파일된다

첫 번째부터 여섯 번째까지의 인자는 순서대로 rdi, rsi, rdx, rcx, r8, r9에 들어가고,

그다음부턴 스택에 쌓는 것을 확인할 수 있다

 

foo는 이렇게 생겼는데, 레지스터에 들어있는 인자는 스택에 넣어주고, 원래 스택에 넣었던 7번째 이후의 인자는 별다른 처리 없이 바로 참조한다

 

6번째까지의 인자와 그 후의 인자가 분리되기 때문에 스택 모습을 보면 되게 묘한데,

7번째부터의 인자가 아래에 깔려있고, 그위에 RET와 SFP이, 그 위에 1~6번째 인자가 올라와있다

 

이 점 말고는 전과 크게 다르지 않다

 

그다음 궁금한 점은 x86과 마찬가지로 함수 내에서 지역변수가 사용되면 어떻게 될지이다

이번에도 전과 같은 코드를 준비했다

#include <stdio.h>
int foo(int a, int b, int c) {
    int d=4;
    int e=5;
    int f=6;
    return a+b+c+d+e+f;
}

int main(){
    int a = foo(1, 2, 3);

    return 0;
}

main은 전과 같고, foo는 위의 형태로 컴파일되었다

한 가지 이상한 점은 sub rsp부분이 없고, 때문에 leave부분도 없다는 것이다

이 부분에 대해선 잘 모르겠는데, 다른 글에서는 애초에 rbp가 아니라 rsp를 기준으로 스택이 지정된다고도 하고,

스택 공간이 애초에 할당되지도 않은걸 보면 앞서 말했듯 기본 컴파일 옵션이 뭔가 최적화를 했다거나 해서 변화가 일어난 게 아닐까 추측해본다

이 부분 외에는 역시 특별한 건 없다

 

이렇게 64비트 환경에서의 기본적인 함수 호출 과정을 알아보았다

애초에 그냥 간단히 흐름만 정리한다는 걸 너무 자세히 정리한 것 같다

다시 한번 말하지만, 이 글은 그냥 내 환경에서 간단히 테스트해본 것이므로 일반적인 상황과 다를 수 있다

인터넷에 다른 좋은 글도 많으니 내 글과 안 맞다고 이상하게 생각하지 말고 그때그때 상황에 맞는 정보를 찾아보도록 하자

 

이 글에서 사용한 실행파일은 모두 이곳에 올려놓았다

drive.google.com/file/d/1f9cWjZKnVYuAGaaJmM4sPI8XemB3Gqhr/view?usp=sharing

 

64bit.tar.gz

 

drive.google.com

 

'Pwnable' 카테고리의 다른 글

C언어 함수 호출과정 - x86  (0) 2021.02.04
Comments