Search

main 함수가 호출, 종료되는 과정

Tag
linux
Create time
2019/05/16
elf 파일을 보통 디버깅 할때 main문 부터 확인했었다. 하지만 문제를 풀다가 main함수가 호출되기 까지, 종료된 후 의 과정을 알아야 할 필요가 생겨서 간단하게 정리하고자 한다.

1. ELF 헤더 확인

우선 readelf -h 명령어로 현재 challenge 라는 바이너리를 확인한 결과이다. 엔트리 포인트 주소를 보면 0x4006c0을 가리키고 있는데 해당 주소가 무엇인지 아이다로 확인해보자.
해당 주소는 _start 함수이다. 따라서 _start 함수가 처음에 호출된다

2. _start 함수

_start() 함수
void __usercall __noreturn start(__int64 a1@<rax>, void (*a2)(void)@<rdx>) { int v2; // esi int v3; // [rsp-8h] [rbp-8h] __int64 _0; // [rsp+0h] [rbp+0h] v2 = v3; *(_QWORD *)&v3 = a1; _libc_start_main( (int (__fastcall *)(int, char **, char **))main, v2, (char **)&_0, (void (*)(void))_libc_csu_init, _libc_csu_fini, a2, &v3); __halt(); }
C
복사
start 함수를 살펴보면, 바이너리 실행 과정에 필요한 여러 요소들을 초기화하기 위해_libc_start_main 함수를 호출한다. 해당 함수는 libc안에 존재하는 함수이므로, 일반적인 함수들과 같이 plt를 뒤져 got table에 먼져 등록을 한다음 호출이 된다. 인자로 _libc_csu_init, 요 함수포인터가 들어가는데, 이 함수안에서 초기화가 이뤄진다.

3. __libc_start_main() 함수

__libc_start_main 을 디버깅하고 있는 화면이다. 쭉 가다보면 +125 위치에 call rbp 부분이 있다. 현재 rbp에는 아까 인자로 넣었던, __libc_csu_init 함수이다. 해당 함수안에서 초기화가 이루어진다.

3.1. __libc_csu_init 함수

__libc_csu_init (int argc, char **argv, char **envp) { /* For dynamically linked executables the preinit array is executed by the dynamic linker (before initializing any shared object). */ #ifndef LIBC_NONSHARED /* For static executables, preinit happens right before init. */ { const size_t size = __preinit_array_end - __preinit_array_start; size_t i; for (i = 0; i < size; i++) (*__preinit_array_start [i]) (argc, argv, envp); } #endif #ifndef NO_INITFINI _init (); #endif const size_t size = __init_array_end - __init_array_start; for (size_t i = 0; i < size; i++) (*__init_array_start [i]) (argc, argv, envp); }
C
복사
매크로로 정의된 거에 따라서 진행이 된다. 자세한 거는 모르지만, 디버깅 해본 결과 _init() 함수가 호출되고 그 아래 로직이 수행되는것을 확인했다.
for문을 보면 init_array 섹션의 크기가 size 변수에 들어간다. 그 후에 해당 섹션의 사이즈를 만큼 반복문을 돌면서 .init_array 에 저장된 함수 포인터들 호출한다. 요부분을 현재 challenge 바이너리를 대상으로 확인해보자.
요 부분에 저장된 함수포인터들이 for문을 돌면서 호출된다. 현 바이너리에는 _frame_dummy 머시기 하나만 호출되는 것 같다. 이 부분을 디버깅으로 확인해보자
저 초록색 라인이 바로 .init_array 에 저장된 함수포인터가 호출되는 라인이다. __libc_csu_init() 함수에서 아래 라인을 말하는 것이다.
(*__preinit_array_start [i]) (argc, argv, envp);
C
복사
해당 값은 아이다에서 확인한 바와 같이 frame_dummy 머시기가 들어가있고 이것이 호출되는 것이다. 현재 바이너리는 저 하나만 호출하고 끝나게 된다. 어쨋든 __libc_csu_init() 함수의 호출이 끝나고 다시 __libc_start_main 로 돌아가서 쭉 진행을 하다보면
' call rdx ' 를 하게된다. 이는 우리가 잘 아는 바로 main 이다!. call rax가 끝나고 그 아래 라인은 main 함수에서 ret시에 실행되는 라인이다. 이때부터는 아래에서 분석할 것이다.
이렇게 main 함수가 호출되기 전까지의 과정을 간략히 알아보았다. 이제 main함수가 끝나고 ret이 될때의 과정을 살펴보자

4. fini_array 섹션

init_array 섹션이 main 함수 호출전 초기화를 위한 영역이라면 fini_array를 main함수가 종료된후 정상적인 종료를 위해 참조되는 섹션이다. 직접 디버깅을 하면서 확인해보자
main 함수에서 ret을 하게되면 libc_start_main+240 으로 오게된다. 아까 위에서 말했던 부분이다. 그다음 call __GI_exit 를 하게 된다. 이 함수에서 결국 최종 종료가 된다고 보면 된다. 해당 함수로 들어가보자.
__GI_exit 함수를 따라가다 보면 __run_exit_handlers를 호출하게 된다. 해당 함수는 exit_function 구조체 멤버 변수인 flavor 값에 따라서 함수를 호출하는데, 기본적으로는 로더 라이브러리 내부에 존재하는 _dl_fini 함수를 호출한다고 한다. - 참조
__run_exit_handlers 를 쭉 따라가다 보면 ' call rdx ' 가 나온다. 이 부분이 방금 말한 _dl_fini 함수이다. 해당 함수로 들어가보자
_dl_fini 함수를 또 쭉 따라가다보면 __libc_csu_init 와 비슷한 로직이 있다. r12에는 현재 0x600bc0이 담겨져 있고 해당 주소는 0x400770을 가리키고 있는데 결국 0x400770이 호출된다.
0x400770__do_global_dtors_aux 함수로, fini_array에 담겨져 있는 함수포인터이다. main 함수를 종료하는 소멸자 정도로 생각하면 될 것같다. 이렇게 _dl_fini 함수가 쭉쭉 실행되고 다시 __run_exit_handlers 로 돌아온다.
다시 __run_exit_handlers 를 쭉쭉 실행하다가 보면 __GI__exit 함수가 호출된다. 헷갈리면 안되는게, 아까 위에서 호출한거를 또 호출하지? 라고 생각하면 안된다. 자세히 보면 exit 앞에 ' _ ' 가 2개 이다. 아까는 한개였다.ㅋ 확인해보니 해당 함수에서 syscall로 실제로 종료가 되었다.
해당 함수로 들어와서 쭉쭉 디버깅을 하다보면 rax = 0xe7 을 인자로하여 syscall이 호출된다. 이는 sys_exit_group syscall 테이블 번호이다. 여기서 실제로 종료가 되버린다.

5. 정리

이 모든 과정을 간략히 표현하면 다음과 같다

6. 참고자료