=======================================================================================
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)
댓글 없음:
댓글 쓰기