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();
}
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);
}
매크로로 정의된 거에 따라서 진행이 된다. 자세한 거는 모르지만, 디버깅 해본 결과 _init() 함수가 호출되고 그 아래 로직이 수행되는것을 확인했다.
for문을 보면 init_array
섹션의 크기가 size 변수에 들어간다. 그 후에 해당 섹션의 사이즈를 만큼 반복문을 돌면서 .init_array
에 저장된 함수 포인터들 호출한다. 요부분을 현재 challenge 바이너리를 대상으로 확인해보자.
요 부분에 저장된 함수포인터들이 for문을 돌면서 호출된다. 현 바이너리에는 _frame_dummy 머시기 하나만 호출되는 것 같다. 이 부분을 디버깅으로 확인해보자
저 초록색 라인이 바로 .init_array
에 저장된 함수포인터가 호출되는 라인이다. __libc_csu_init() 함수에서 아래 라인을 말하는 것이다.
(*__preinit_array_start [i]) (argc, argv, envp);
해당 값은 아이다에서 확인한 바와 같이 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. 정리
이 모든 과정을 간략히 표현하면 다음과 같다
출처 : https://www.slideshare.net/AngelBoy1/linux-binary-exploitation-basic-knowledge
6. 참고자료
'보안 > Linux' 카테고리의 다른 글
fuzzing 이란 (3) | 2020.08.26 |
---|---|
seccomp 간단 정리 (0) | 2020.07.08 |
fclose 분석 (0) | 2020.05.01 |
House of Orange 분석 (0) | 2020.04.29 |
House of Force 분석 (0) | 2020.04.20 |