How main() is executed on Linux
ELF에 대해 공부를 하다가 아래 문서를 발견하고 기록 겸 블로그에 포스팅 합니다. : - )
http://www.tldp.org/LDP/LGNET/issue84/hawk.html - How main() is executed on Linux - By Hyouck "Hawk" Kim
Starting
리눅스 환경에서 main() 함수의 동작 원리를 알아 보기 위해 아래 코드를 작성 후 빌드 한다.
1 2 3 4 5 6 7 | [root@localhost ~]# vi main.c main() { return (0); } |
Build
1 | [root@localhost ~]# gcc -o main main.c |
What's in the executable?
objdump 를 이용하여 컴파일된 파일의 정보를 확인할 수 있다. -f 옵션은 해당 파일의 fileheader를 출력해주는 옵션이다.
상기 과정을 통해 생성했던 main 파일의 정보를 확인할 수 있는데 우리가 눈여겨 봐야할 부분은 file format, start address의 정보이다.
첫번쨰로 ELF는 무엇인가? 두번째로 start address는 왜 0x080482a0을 가리키고 있을까?
1 2 3 4 5 6 7 | [root@localhost ~]# objdump -f main main: file format elf32-i386 architecture: i386, flags 0x00000112: EXEC_P, HAS_SYMS, D_PAGED start address 0x080482a0 |
What's ELF?
ELF는 Executable and Linking Format의 줄임말이며, 유닉스 시스템에서 사용되는 여러 오브젝트파일/실행파일 형식중 하나이다.
모든 ELF 실행파일은 다음과 같은 ELF 헤더를 가진다. 구조체에서 "e_entry" 필드는 실행파일의 시작주소이다.
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
What's at address "0x080482a0", that is, starting address?
main 파일을 디스어셈블 해보도록 한다. 디스어셈블할 수 있는 도구는 많지만 여기에선 objdump를 이용하여 디스어셈블을 진행한다. 옵션은 -d를 사용한다. 출력 결과가 길지만 여기에선 0x080482a0 부분만 보도록 하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | [root@localhost ~]# objdump -d main main: file format elf32-i386 Disassembly of section .init: 08048250 <_init>: 8048250: 55 push %ebp 8048251: 89 e5 mov %esp,%ebp 8048253: 83 ec 08 sub $0x8,%esp 8048256: e8 69 00 00 00 call 80482c4 <call_gmon_start> 804825b: e8 f0 00 00 00 call 8048350 <frame_dummy> 8048260: e8 9b 01 00 00 call 8048400 <__do_global_ctors_aux> 8048265: c9 leave 8048266: c3 ret Disassembly of section .plt: 08048268 <__gmon_start__@plt-0x10>: 8048268: ff 35 a8 95 04 08 pushl 0x80495a8 804826e: ff 25 ac 95 04 08 jmp *0x80495ac 8048274: 00 00 add %al,(%eax) ... 08048278 <__gmon_start__@plt>: 8048278: ff 25 b0 95 04 08 jmp *0x80495b0 804827e: 68 00 00 00 00 push $0x0 8048283: e9 e0 ff ff ff jmp 8048268 <_init+0x18> 08048288 <__libc_start_main@plt>: 8048288: ff 25 b4 95 04 08 jmp *0x80495b4 804828e: 68 08 00 00 00 push $0x8 8048293: e9 d0 ff ff ff jmp 8048268 <_init+0x18> Disassembly of section .text: 080482a0 <_start>: 80482a0: 31 ed xor %ebp,%ebp 80482a2: 5e pop %esi 80482a3: 89 e1 mov %esp,%ecx 80482a5: 83 e4 f0 and $0xfffffff0,%esp 80482a8: 50 push %eax 80482a9: 54 push %esp 80482aa: 52 push %edx 80482ab: 68 80 83 04 08 push $0x8048380 80482b0: 68 90 83 04 08 push $0x8048390 80482b5: 51 push %ecx 80482b6: 56 push %esi 80482b7: 68 74 83 04 08 push $0x8048374 80482bc: e8 c7 ff ff ff call 8048288 <__libc_start_main@plt> 80482c1: f4 hlt 80482c2: 90 nop 80482c3: 90 nop . . . | cs |
"_start"함수의 스택 프레임을 정리하면 아래와 같은 모양의 스택프레임이 생성될 것이다.
Stack Top -------------------
0x8048374
-------------------
esi
-------------------
ecx
-------------------
0x8048390
-------------------
0x8048380
-------------------
edx
-------------------
esp
-------------------
eax
-------------------
이제 이 스택프레임에 대한 궁금증이 더 생겼다.
1. 이 16진수 값들은 무엇인가?
2. _start가 호출하는 주소 8048288에는 무엇이 있는가?
3. 어셈블리 명령어는 레지스터를 의미있는 값으로 초기화하지 않는 것 같다. 그러면 누가 레지스터를 초기화하나?
Question 1> The hexa values.
0x8048374 : 이는 우리가 만든 main() 함수의 주소이다.
0x8048390 : _init 함수.
0x8048380 : _fini 함수. _init과 _fini는 GCC가 제공하는 초기화(initialization)/종료(finalization) 함수이다.
기본적으로 이 16진수 값들은 함수포인터다.
Question 2> What's at address 08048288?
디스어셈블된 코드에서 80482bc를 확인 시 jmp *0x80495b4 명령을 통해 0x80495b4에 저장된 주소로 건너뛰는 것을 확인할 수 있다.
1 2 3 4 | 08048288 <__libc_start_main@plt>: 8048288: ff 25 b4 95 04 08 jmp *0x80495b4 804828e: 68 08 00 00 00 push $0x8 8048293: e9 d0 ff ff ff jmp 8048268 <_init+0x18> | cs |
More about ELF and dynamic linking
ELF를 사용하여 라이브러리에 동적으로 링크되는 실행파일을 만들 수 있다.
여기서 "동적으로 링크된다는" 말은 링크 과정이 실행 시 발생함을 의미한다. 그렇지않으면 호출하는 모든 라이브러리를 포함하는 큰 실행파일을 ("정적으로 링크된" 실행파일) 만들어야 한다.
1 2 3 4 | [root@localhost ~]# ldd main linux-gate.so.1 => (0x00a07000) libc.so.6 => /lib/libc.so.6 (0x0042a000) /lib/ld-linux.so.2 (0x0040b000) | cs |
ldd 명령어를 이용해 생성한 main 파일의 동적으로 링크되어있는 모든 라이브러리를 확인할 수 있으며, 동적으로 링크되는 자료와 함수는 모두 "동적 재배치 항목"을 가지게 된다.
대략적인 개념은 다음과 같다.
1. 우리는 링크 시 동적 심볼(Dynamic Symbol)의 실제 주소를 모른다. 실행 시 심볼의 실제 주소를 알게 된다.
2. 동적 심볼의 실제 주소를 위해 메모리 공간을 남겨둔다. 로더(loader)가 실행 시 남겨둔 메모리 공간에 심볼의 실제 주소를 쓴다.
3.
objdump의 -R 옵션을 이용하여 동적 링크된 항목을 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 | [root@localhost ~]# objdump -R main main: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 080495a0 R_386_GLOB_DAT __gmon_start__ 080495b0 R_386_JUMP_SLOT __gmon_start__ 080495b4 R_386_JUMP_SLOT __libc_start_main | cs |
What's __libc_start_main?
__libc_start_main은 libc.so.6에 잇는 함수다. glib 소스 코드에서 __libc_start_main 함수를 찾아보면 함수형은 아래와 같다.
d
extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
int argc,
char *__unbounded *__unbounded ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void *__unbounded stack_end)
__attribute__ ((noreturn));
아래의 스택 프레임을 보면 __libc_start_main 함수가 실행되기 전에 esi, ecs, edx, esp, eax 레지스터에 적당한 값이 채워져아 한다. 그러나 _start 명령어는 이 레지스터를 설정하지 않는다. 그러면 누가 이 레지스터를 설정할까? 한 가지 추측할 수 있는 부분은 커널이다.
세번째 질문으로 돌아가보자.
Stack Top -------------------
0x8048374 main
-------------------
esi argc
-------------------
ecx argv
-------------------
0x8048390 _init
-------------------
0x8048380 _fini
-------------------
edx _rtlf_fini
-------------------
esp stack_end
-------------------
eax this is 0
-------------------
Question 3> What does the kernel do?
우리가 쉘(Shell)에 명령어를 입력 하여 프로그램을 실행시킬 때 리눅스의 경우 다음과 같은 현상이 일어난다.
1. 쉘은 argc/argv를 이용해 커널 시스템 콜인 "execve"를 호출 한다.
2. 커널 시스템 콜 핸들러는 제어와 시스템 콜을 처리하기 시작한다. 커널 코드에서 핸들러는 "sys_execve" 이며 x86 시스템에서 사용자 모드 어플리케이션은 아래 레지스터를 통해 필요한 파라미터를 커널에 넘긴다.
○ ebx : 프로그램 문자열의 포인터
○ ecx : argv 배열 포인터
○ ebx : 환경변수 배열 포인터
'Hacking > Binary' 카테고리의 다른 글
Binary@library# 공유 라이브러리 (0) | 2018.02.25 |
---|---|
Binary@library# 정적 라이브러리 (0) | 2018.02.25 |
Binary@ELF# ldd - 공유 라이브러리 의존관계 확인 (0) | 2018.02.25 |
Binary@ELF# nm - 오브젝트 파일에 포함된 심볼 확인 (0) | 2018.02.17 |
Binary@ELF# ELF Header (0) | 2018.02.15 |