======================================================================================= Title: gcc-4.1.1/gcc/crtstuff.c 코드 분석을 통한 전역 생성자, 소멸자 분석 Author : 유동훈 (Xpl017Elz) in INetCop E-mail : szoahc@hotmail.com Home: http://x82.inetcop.org ======================================================================================= - 전역 생성자, 전역 소멸자란 무엇인가? C의 전역 생성자, 전역 소멸자는 C++에서 제공하는 클래스 생성자, 소멸자와 유사한 개념이다. main 함수 이전과 이후로 전역 개체를 핸들링하기 위해 gcc에서는 두 가지 함수 속성 타입을 제공한다. 이는 main 함수 이전에 개체를 초기화하는 전역 생성자와, main 함수 종료 후 개체를 소멸하는 전역 소멸자로 나뉜다. -- 전역 생성자 함수 속성 타입: __attribute__ ((constructor)); 전역 소멸자 함수 속성 타입: __attribute__ ((destructor)); -- 전역 생성자와 전역 소멸자가 언제 어떻게 호출되는지 알아보도록 하겠다. - 함수 호출 순서 분석 함수 호출 순서를 분석을 통해 우리는 전역 생성자와 전역 소멸자가 언제 호출되는지 정확히 알 수 있다. 또한, 어떤 함수가 이들을 핸들링하는지도 정확히 분석할 수 있다. gcc-4.1.1, glibc-2.5 버전을 사용하는 Fedora core 6 시스템에서 분석한 결과이다. -- _start 함수 (사용자 프로그램) | +- __libc_start_main() 호출 (libc.so) | |<__libc_start_main+127>: call *%edi +- __libc_csu_init() 호출 (사용자 프로그램) | | | +- _init() 호출 (사용자 프로그램) | | | +- __do_global_ctors_aux() 호출 (사용자 프로그램) - 전역 생성자 | | |<__libc_start_main+217>: call *0x8(%ebp) +- main() 함수 호출 (사용자 프로그램) | | | +- libc.so 라이브러리 함수 재배치 수행 (※ 재배치 문서 내용 참고할 것) | | |<__libc_start_main+223>: call 0x81a850 <exit> +- exit() 함수 호출 (libc.so) | | |<exit+231>: call *%eax +- _dl_fini() 함수 호출 (ld-linux.so) | | |<_dl_fini+480>: call *%eax +- _fini() 함수 호출 (사용자 프로그램) | +- __do_global_dtors_aux() 호출 (사용자 프로그램) - 전역 소멸자 -- 참고로, 수행에 중요한 부분만 간추린 것이며, 전역 생성자와 소멸자를 핸들링하는 함수를 찾을 수 있게 되었다. 전역 생성자는 __do_global_ctors_aux() 함수에 의해서 다뤄지고, 전역 소멸자는 __do_global_dtors_aux() 함수에 의해서 다루어진다. init() 호출과 exit()->fini() 호출에 의해 수행된다는 점을 기억하자. - 전역 생성자, 소멸자를 사용하는 프로그램 예제와 디버깅 결과 그럼, 실제로 전역 생성자와 소멸자를 다루는 간단한 프로그램을 소개하도록 하겠다. -- [root@localhost tmp]# cat test.c #include <stdio.h> void __attribute__((constructor)) ctors_1() { printf("ctors_1();\n"); } void __attribute__((constructor)) ctors_2() { printf("ctors_2();\n"); } int main() { printf("main();\n"); } void __attribute__((destructor)) dtors_1() { printf("dtors_1();\n"); } void __attribute__((destructor)) dtors_2() { printf("dtors_2();\n"); } [root@localhost tmp]# gcc -o test test.c [root@localhost tmp]# ./test ctors_2(); ctors_1(); main(); dtors_1(); dtors_2(); [root@localhost tmp]# -- 결과를 살펴보면, constructor 속성 함수는 main() 함수 이전에 호출되었고, destructor 속성 함수는 main() 함수 이후에 호출된 것을 볼 수 있다. 여기서 재미있는 점은 전역 생성자 함수의 경우, 맨 나중에 선언된 전역 생성자 함수가 제일 처음 호출된다는 점이다. 어떤 내용을 참고하여 각 호출이 이루어지는지 다음 디버깅 결과를 살펴보면 한 눈에 확인할 수 있다. -- [root@localhost tmp]# objdump -h test | grep -e ctors -e dtors 15 .ctors 00000010 080494e8 080494e8 000004e8 2**2 16 .dtors 00000010 080494f8 080494f8 000004f8 2**2 [root@localhost tmp]# gdb -q test (no debugging symbols found) Using host libthread_db library "/lib/libthread_db.so.1". (gdb) x &__CTOR_LIST__ 0x80494e8 <__CTOR_LIST__>: 0xffffffff (gdb) 0x80494ec <__CTOR_LIST__+4>: 0x0804837c ; ctors_1() 함수 (gdb) 0x80494f0 <__CTOR_LIST__+8>: 0x08048394 ; ctors_2() 함수 (gdb) 0x80494f4 <__CTOR_END__>: 0x00000000 (gdb) x/i 0x0804837c 0x804837c <ctors_1>: push %ebp (gdb) x/i 0x08048394 0x8048394 <ctors_2>: push %ebp (gdb) x/x &__DTOR_LIST__ 0x80494f8 <__DTOR_LIST__>: 0xffffffff (gdb) 0x80494fc <__DTOR_LIST__+4>: 0x080483da ; dtors_1() 함수 (gdb) 0x8049500 <__DTOR_LIST__+8>: 0x080483f2 ; dtors_2() 함수 (gdb) 0x8049504 <__DTOR_END__>: 0x00000000 (gdb) x/i 0x080483da 0x80483da <dtors_1>: push %ebp (gdb) x/i 0x080483f2 0x80483f2 <dtors_2>: push %ebp (gdb) -- 디버깅 결과를 통해 우리가 선언한 함수들이 __CTOR_LIST__ 섹션과 __DTOR_LIST__ 섹션에 포인터로 적재되어 있는 것을 볼 수 있다. 그럼, 왜 이런 구조를 갖는지 gcc-4.1.1/gcc/crtstuff.c 코드 분석을 통해 답을 찾아보도록 하겠다. - gcc-4.1.1/gcc/crtstuff.c 코드 분석 우리는 crtstuff.c 코드 전체를 분석할 필요가 없다. 이들 중 __CTOR_LIST__ 섹션과 __DTOR_LIST__ 섹션을 참조하여 수행되는 실속있는 코드들만 분석해보면 쉽게 답을 얻을 수 있다. 먼저, 전역 생성자를 핸들링하는 __do_global_ctors_aux() 함수이다. -- __do_global_ctors_aux() 함수 -- ... 145 /* Declare a pointer to void function type. */ 146 typedef void (*func_ptr) (void); ... 513 static void __attribute__((used)) 514 __do_global_ctors_aux (void) 515 { 516 func_ptr *p; 517 for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--) 518 (*p) (); 519 } -- 참고로, func_ptr은 void 형을 뜻한다. 잘 살펴보면, __CTOR_END__-1(워드 단위) 위치를 기준으로 p의 값이 -1(0xffffffff)이 나올 때까지 검사하며, p(__CTOR_LIST__ 섹션)에 위치한 함수들을 하나씩 호출하는 것을 볼 수 있다. 결국, __CTOR_LIST__ 섹션 시작 부의 0xffffffff 값을 만나면 전역 생성자 함수 호출을 멈추게 된다. 이러한 이유로 constructor에 예약된 전역 생성자 함수 중 맨 나중에 선언된 함수가 제일 처음으로 수행된 것이다. 다음은 해당 코드를 disassemble 한 결과이다. -- __do_global_ctors_aux() 함수 -- <__do_global_ctors_aux+0>: push %ebp <__do_global_ctors_aux+1>: mov %esp,%ebp <__do_global_ctors_aux+3>: push %ebx <__do_global_ctors_aux+4>: push %edx <__do_global_ctors_aux+5>: mov 0x80494f0,%eax // (1) __CTOR_LIST__ 내용(값) 복사 <__do_global_ctors_aux+10>: cmp $0xffffffff,%eax // (2) -1인지 검사 <__do_global_ctors_aux+13>: je 0x8048485 <__do_global_ctors_aux+33> <__do_global_ctors_aux+15>: mov $0x80494f0,%ebx // (3) 주소 위치 복사 <__do_global_ctors_aux+20>: call *%eax // (4) 호출 <__do_global_ctors_aux+22>: mov 0xfffffffc(%ebx),%eax // (5) -4 위치 내용(값) 복사 <__do_global_ctors_aux+25>: sub $0x4,%ebx // (6) -4 만큼 감소 <__do_global_ctors_aux+28>: cmp $0xffffffff,%eax // (7) -1인지 검사 <__do_global_ctors_aux+31>: jne 0x8048478 <__do_global_ctors_aux+20> <__do_global_ctors_aux+33>: pop %eax <__do_global_ctors_aux+34>: pop %ebx <__do_global_ctors_aux+35>: leave <__do_global_ctors_aux+36>: ret <__do_global_ctors_aux+37>: nop <__do_global_ctors_aux+38>: nop <__do_global_ctors_aux+39>: nop -- 0x80494f0 주소는 __CTOR_END__ -1(워드 단위) 위치이며, 이 주소가 가진 값을 %eax 레지스터에 넣고 call *%eax 코드로 해당 위치에 있는 함수를 호출한다. %ebx 레지스터는 __CTOR_END__-1 위치부터 4byte씩 자신을 감소시키며, 자신이 가지고 있는 주소 값을 %eax 레지스터에 복사하는 역할을 한다. 결과적으로, %eax 레지스터에 -1(0xffffffff) 값이 나올 때까지 함수를 반복 호출하게 된다. 그럼, 이번에는 전역 소멸자를 핸들링하는 __do_global_dtors_aux() 함수를 살펴보자. -- __do_global_dtors_aux() 함수 -- ... 145 /* Declare a pointer to void function type. */ 146 typedef void (*func_ptr) (void); ... 260 static void __attribute__((used)) 261 __do_global_dtors_aux (void) 262 { 263 #ifndef FINI_ARRAY_SECTION_ASM_OP 264 static func_ptr *p = __DTOR_LIST__ + 1; 265 func_ptr f; 266 #endif /* !defined(FINI_ARRAY_SECTION_ASM_OP) */ 267 static _Bool completed; 268 269 if (__builtin_expect (completed, 0)) // 0인지 검사 270 return; ... 281 while ((f = *p)) 282 { 283 p++; 284 f (); // 해당 위치 값이 null이 아닐 경우 함수 호출 285 } ... 300 completed = 1; // 함수 내용이 두 번 이상 호출되지 않도록 301 } -- __DTOR_LIST__ +1(워드 단위) 위치부터 함수 호출을 시작하며, completed 섹션의 값이 0인지 검사 후, 함수 호출을 시작한다. 1일 경우, 이미 __DTOR_LIST__ 섹션에 있는 함수들이 수행된 것으로 간주하고 함수 수행을 중단한다. 함수 호출은, p 섹션 위치를 4byte씩 증가시켜 반복적으로 호출하며, null일 경우 함수 호출을 중단하게 된다. 앞서, constructor와는 달리, destructor는 예약된 함수를 순서대로 호출하는 것을 볼 수 있다. 다음은 해당 코드를 disassemble 한 결과이다. -- __do_global_dtors_aux() 함수 -- <__do_global_dtors_aux+0>: push %ebp <__do_global_dtors_aux+1>: mov %esp,%ebp <__do_global_dtors_aux+3>: sub $0x8,%esp <__do_global_dtors_aux+6>: cmpb $0x0,0x80495fc // (1) completed 섹션 값이 0인지 검사 <__do_global_dtors_aux+13>: je 0x804833e <__do_global_dtors_aux+30> <__do_global_dtors_aux+15>: jmp 0x8048350 <__do_global_dtors_aux+48> <__do_global_dtors_aux+17>: lea 0x0(%esi),%esi <__do_global_dtors_aux+20>: add $0x4,%eax // (5) %eax 4byte 증가 <__do_global_dtors_aux+23>: mov %eax,0x80495f8 // (6) p 섹션에 증가한 주소를 복사 <__do_global_dtors_aux+28>: call *%edx // (7) 함수 호출 <__do_global_dtors_aux+30>: mov 0x80495f8,%eax // (2) p 섹션 값을 %eax에 복사 <__do_global_dtors_aux+35>: mov (%eax),%edx // (3) %eax가 가진 값을 %edx에 복사 <__do_global_dtors_aux+37>: test %edx,%edx // (4) 해당 위치에 값이 있는지 검사 <__do_global_dtors_aux+39>: jne 0x8048334 <__do_global_dtors_aux+20> <__do_global_dtors_aux+41>: movb $0x1,0x80495fc <__do_global_dtors_aux+48>: leave <__do_global_dtors_aux+49>: ret -- 0x080495fc는 completed 섹션이며, 이 값이 0인지 검사하여 수행을 여부를 결정 짓는다. 0x080495f8는 p 섹션이며, __DTOR_END__+1(워드 단위) 위치 주소 값을 가지고 있다. 이 주소를 %eax에 넣고, 해당 위치 값을 %edx에 넣어 null인지 검사한다. 호출할 예약 함수가 있는 경우, 4byte만큼 증가시킨 %eax 레지스터를 p 섹션에 복사하므로써, 다음 위치에 있는 함수가 반복적으로 호출되도록 구성한다. - 정리 지금까지 전역 생성자 constructor와 전역 소멸자 destructor에 대해 알아보았다. 전역 생성자와 소멸자, 재배치 부분은 사용자 프로그램 내에서 매우 중요한 역할을 하고 있다. 부디 이 글을 통해 한층 더 재미있고 즐거운 exploit 작성에 도움이 되었길 빈다. - 레퍼런스 최호진 - main 함수 이전에 일어나는 일에 대한 간단한 추적 Guido Bakker - Overwriting ELF .dtors section to modify program execution
2018년 5월 11일 금요일
[펌] 전역 생성자, 전역 소멸자에 대해
피드 구독하기:
댓글 (Atom)
댓글 없음:
댓글 쓰기