D1N0's hacking blog
C언어 함수 호출과정 - x86 본문
포너블에서 너무 중요하고 기초적인 내용이지만 아직도 헷갈려서 정리한다
이 글은 독자가 기초적인 C언어와 어셈블리어를 알고 있는 상태라고 가정한다
모든 코드는 WSL Ubuntu 20.04 LTS에서 -m32 -no-pie 옵션을 사용해서 gcc로 컴파일 되었고,
실행 파일들은 글 하단에 올려놓았다
cdecl은 수많은 32bit 프로그램에서 사용되는 방식이다
함수를 호출하고, 호출자가 스택을 정리하는 호출자 정리 방식의 일종이다
#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;
}
위와 같은 C 코드를 짜고 32bit 컴파일을 했다
gef gdb로 까 보면
main 함수는 이렇게,
foo함수는 이렇게 컴파일되었다
여기서 우리가 주목해야 할 것은 main에서의 push 0x3, push 0x2, push 0x1이다
여기서 우리는 cdecl 호출 방식은 함수에 전달되는 인자는 오른쪽부터 하나씩 스택에 푸시하고 함수를 실행하는 것을 알 수 있다
두 번째는 call foo를 하면 어떤 일이 일어나는가인데,
위와 같던 스택이 call foo 후에는
이렇게 0x080491b5이 푸시된 것을 확인할 수 있다
call은 push eip
jmp [주소] 같은 역할을 하고,
0x080491b5는 main에서 call foo 다음 명령의 주소, 즉 push eip를 통해 푸시된 값임을 알 수 있다
이것을 RET라고 부르는데, 함수 실행이 끝나고 돌아갈 주소를 이 RET을 참조한다
수많은 바이너리 해킹이 이 RET을 조작하여 이루어진다
그 후 push ebp와 mov ebp, esp가 실행되며 main의 스택과 구분되는 새로운 foo의 스택이 할당된다
이 과정을 함수 프롤로그라고 하고, 푸시된 ebp는 SFP(Stack Frame Pointer)라고 부른다
스택 모양을 보면 다음과 같다
0xffffd068이 main의 ebp이고, foo에서는 0xffffd044라는 새로운 ebp를 갖게 된다
함수는 이 ebp를 바탕으로 인자를 사용하는데,
mov edx,DWORD PTR [ebp+0x8]와 같이 ebp+숫자 와 같은 방식으로 참조하게 된다
함수의 역할이 모두 끝난 후에는 pop ebp, ret을 한다
현재 foo에서는 스택에 값을 푸시하지 않았으므로 여전히 esp는 SFP를 가리키고 있다
때문에 pop ebp를 하면 ebp에 함수 프롤로그에서 넣었던 SFP가 들어가고,
esp는 함수를 호출하기 전과 같은 값을 가진다
ret은 pop eip
jmp eip 같은 기능을 한다
call에서 푸시된 RET를 참조해 원래 main의 주소로 돌아가며 foo 함수가 끝난다
이렇게 함수가 끝나고 처음 상태로 스택을 복원하는 것을 함수 에필로그라고 한다
마지막으로 main에서는 add esp, 0xc를 통해 인자로 넘기기 위해 푸시한 것을 복원한다
글로만 써놓으니 복잡한데, 스택의 변화를 그림으로 보겠다
3, 2, 1 순서대로 인자를 스택에 푸시한 뒤,
call foo에서 다음 main의 주소 RET을 스택에 푸시하고,
push ebp, mov ebp, esp에서 SFP를 스택에 푸시한다
함수의 역할이 끝나면 pop ebp로 ebp를 복원하고,
ret을 통해 main으로 돌아온다
cdecl의 또 다른 상황을 보도록 하자
#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;
}
앞의 예제에 foo를 살짝 바꾼 코드이다
gdb로 까 보면 main은 다르지 않지만 foo부분이 조금 달라졌다
int d=4;
int e=5;
int f=6;
때문에 foo에서도 스택을 사용하는 것을 볼 수 있다
sub esp, 0x10으로 esp를 0x10만큼 낮은 주소로 이동하여 스택을 할당하고,
mov DWORD PTR [ebp-0xc], 0x4 같은 명령으로 할당한 스택을 사용하고 있다
스택의 모습을 보면 3, 2, 1이 들어가 있고,
RET(0x080491dc)가 들어가 있고,
SFP(0xffffd068)가 있고,
6, 5, 4 순서대로 변수가 들어있다(맨 앞의 0x00080000는 더미 값이다)
여기서 주목할 점은 인자를 넘길 때와 다르게 함수 내부에서 스택을 사용할 때는 push를 하며 낮은 주소로 움직이는 게 아니라 이미 할당되어 있는 스택에 대해 mov 명령을 사용해 대입한다는 점이다
이 때문에 포너블에서 RET을 덮을 수 있다
그리고, pop ebp 대신 leave라는 명령어가 등장했다
leave는 mov ebp, esp
pop ebp라고 생각하면 된다
이것이 SFP를 사용하는 이유인데, 함수에서 스택을 사용해서 esp가 저 위에 가있더라도 esp를 ebp에 옮기고 (정확히는 esp에 ebp의 값을 넣고), ebp가 가리키고 있던 SFP를 ebp에 넣음으로써 스택을 원상태로 복구할 수 있다
이렇게 cdecl에서의 함수 호출 과정에 대해 알아봤다
cdecl은 함수를 호출할 때 인자를 스택에 푸시하는 방식으로 전달하고,
함수가 끝나면 호출자에서 그 스택을 정리하는 것이 특징이라고 요약할 수 있겠다
이 글에서 사용한 실행파일은 여기에 올려놓았다
drive.google.com/file/d/1IHDe1UldlgFvFXWveBGowpcbiv-0XpSi/view?usp=sharing
'Pwnable' 카테고리의 다른 글
C언어 함수 호출과정 - x64 (0) | 2021.02.09 |
---|