how program start

로더, libc.. 정말 많은 정보들이 많다. 그럼 이런 질문을 받을 수 있다.

프로그램이 리눅스에서 정확히 어떤 방식으로 실행되는 거지?

아마 그럼 이렇게 답할 것이다.

_start → _libc_start_main → main 으로 시작된다.

당연히 맞는 소리이다. 하지만, 좀더 자세히 말하고 싶지 않나?

이번에는 그 질문에 대한 답을 자세히 알아가보자

사실 이 글이 쓰여진 원인은 심볼에 대해 공부하다가 시작되었다.

우리가 gdb를 쓸때 심볼이 stripe 되어있으면, main 심볼이 있지 않아서 gdb가 main에서 시작하지 않고 함수를 볼 수 없어서 불편하다. 그런데 프로그램은 어떻게 심볼이 없는데 main함수 위치를 알 수 있지? 라는 생각에서 시작되었다.

처음부터 차근차근 가보자면, ELF 파일에는 entry point라는 개념이 있다.

C
pwndbg> info files
Symbols from "/home/amemoyoi/1day_2pwn/test".
Local exec file:
	`/home/amemoyoi/1day_2pwn/test', file type elf64-x86-64.
	Entry point: 0x1060
	0x0000000000000318 - 0x0000000000000334 is .interp
	0x0000000000000338 - 0x0000000000000368 is .note.gnu.property
	0x0000000000000368 - 0x000000000000038c is .note.gnu.build-id
	0x000000000000038c - 0x00000000000003ac is .note.ABI-tag
	0x00000000000003b0 - 0x00000000000003d4 is .gnu.hash
	0x00000000000003d8 - 0x0000000000000480 is .dynsym
	0x0000000000000480 - 0x000000000000050d is .dynstr
	0x000000000000050e - 0x000000000000051c is .gnu.version
	0x0000000000000520 - 0x0000000000000550 is .gnu.version_r
	0x0000000000000550 - 0x0000000000000610 is .rela.dyn
	0x0000000000000610 - 0x0000000000000628 is .rela.plt
	0x0000000000001000 - 0x000000000000101b is .init
	0x0000000000001020 - 0x0000000000001040 is .plt
	0x0000000000001040 - 0x0000000000001050 is .plt.got
	0x0000000000001050 - 0x0000000000001060 is .plt.sec
	0x0000000000001060 - 0x0000000000001167 is .text
	0x0000000000001168 - 0x0000000000001175 is .fini
	0x0000000000002000 - 0x0000000000002011 is .rodata
	0x0000000000002014 - 0x0000000000002048 is .eh_frame_hdr
	0x0000000000002048 - 0x00000000000020f4 is .eh_frame
	0x0000000000003db8 - 0x0000000000003dc0 is .init_array
	0x0000000000003dc0 - 0x0000000000003dc8 is .fini_array
	0x0000000000003dc8 - 0x0000000000003fb8 is .dynamic
	0x0000000000003fb8 - 0x0000000000004000 is .got
	0x0000000000004000 - 0x0000000000004010 is .data
	0x0000000000004010 - 0x0000000000004018 is .bss
pwndbg> 

gdb로 프로그램을 시작하기 전에 Info files 명령어로 한번보면

Entry point: 0x1060 라는 값이 있다. 이값은 프로그램이 시작하면

C
pwndbg> info files
Symbols from "/home/amemoyoi/1day_2pwn/test".
Native process:
	Using the running image of child process 2250082.
	While running this, GDB does not access memory from...
Local exec file:
	`/home/amemoyoi/1day_2pwn/test', file type elf64-x86-64.
	Entry point: 0x555555555060
	0x0000555555554318 - 0x0000555555554334 is .interp
	0x0000555555554338 - 0x0000555555554368 is .note.gnu.property
	0x0000555555554368 - 0x000055555555438c is .note.gnu.build-id
	0x000055555555438c - 0x00005555555543ac is .note.ABI-tag
	0x00005555555543b0 - 0x00005555555543d4 is .gnu.hash
	0x00005555555543d8 - 0x0000555555554480 is .dynsym
	0x0000555555554480 - 0x000055555555450d is .dynstr
	0x000055555555450e - 0x000055555555451c is .gnu.version

0x555555555060 다음의 주소로 바뀌는데 이 주소는

C
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size Offset File
    0x555555554000     0x555555555000 r--p     1000      0 /home/amemoyoi/1day_2pwn/test
    0x555555555000     0x555555556000 r-xp     1000   1000 /home/amemoyoi/1day_2pwn/test
    0x555555556000     0x555555557000 r--p     1000   2000 /home/amemoyoi/1day_2pwn/test
    0x555555557000     0x555555558000 r--p     1000   2000 /home/amemoyoi/1day_2pwn/test
    0x555555558000     0x555555559000 rw-p     1000   3000 /home/amemoyoi/1day_2pwn/test

프로그램의 base 주소 + 원래 entry point 값을 합친값이 된다.

그리고 이 위치는

C
pwndbg> disass 0x555555555060
Dump of assembler code for function _start:
   0x0000555555555060 <+0>:	endbr64
   0x0000555555555064 <+4>:	xor    %ebp,%ebp
   0x0000555555555066 <+6>:	mov    %rdx,%r9
   0x0000555555555069 <+9>:	pop    %rsi
   0x000055555555506a <+10>:	mov    %rsp,%rdx
   0x000055555555506d <+13>:	and    $0xfffffffffffffff0,%rsp
   0x0000555555555071 <+17>:	push   %rax
   0x0000555555555072 <+18>:	push   %rsp
   0x0000555555555073 <+19>:	xor    %r8d,%r8d
   0x0000555555555076 <+22>:	xor    %ecx,%ecx
   0x0000555555555078 <+24>:	lea    0xca(%rip),%rdi        # 0x555555555149 <main>
   0x000055555555507f <+31>:	call   *0x2f53(%rip)        # 0x555555557fd8
   0x0000555555555085 <+37>:	hlt
End of assembler dump.

_start 함수의 위치이다. 즉, ELF 파일에는 헤더의 entry point 값이 존재하며 이 값은 _start 함수의 offset이다.

ida로도 헤더 부분에 Entry point 값이 있는 것을 알 수 있다.

그럼, 이제 _start 함수를 보자면

C
pwndbg> disass 0x555555555060
Dump of assembler code for function _start:
   0x0000555555555060 <+0>:	endbr64
   0x0000555555555064 <+4>:	xor    %ebp,%ebp
   0x0000555555555066 <+6>:	mov    %rdx,%r9
   0x0000555555555069 <+9>:	pop    %rsi
   0x000055555555506a <+10>:	mov    %rsp,%rdx
   0x000055555555506d <+13>:	and    $0xfffffffffffffff0,%rsp
   0x0000555555555071 <+17>:	push   %rax
   0x0000555555555072 <+18>:	push   %rsp
   0x0000555555555073 <+19>:	xor    %r8d,%r8d
   0x0000555555555076 <+22>:	xor    %ecx,%ecx
   0x0000555555555078 <+24>:	lea    0xca(%rip),%rdi        # 0x555555555149 <main>
   0x000055555555507f <+31>:	call   *0x2f53(%rip)        # 0x555555557fd8
   0x0000555555555085 <+37>:	hlt
End of assembler dump.

다음과 같다.

이거의 ida코드는

C
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void))
{
  __int64 v3; // rax
  int v4; // esi
  __int64 v5; // [rsp-8h] [rbp-8h] BYREF
  char *retaddr; // [rsp+0h] [rbp+0h] BYREF

  v4 = v5;
  v5 = v3;
  _libc_start_main((int (*)(int, char **, char **))main, v4, &retaddr, 0LL, 0LL, a3, &v5);
  __halt();
}

다음과 같다. 일단 우리는 위에 부분은 다른 곳에서 다루고 아래에 있는

_libc_start_main 에 집중해 보자

_libc_start_main 함수의 첫번째 인자로 main 함수의 위치가 들어간다.

그렇다면 $rdi로 들어간다는 것인데

C
0x0000555555555078 <+24>:	lea    0xca(%rip),%rdi   

다음과 같이 이미 컴파일러 입장에서는 _start 함수내에 있을 현재 rip와 offset을 통해 main 함수를 넣는다.

.init_array에 대해서도 정리 해야할듯?

C
_start
  └──→ __libc_start_main(main, ...)
         ├──→ 실행 환경 초기화 (libc 내부 처리)
         ├──→ .init_array에 등록된 함수들 호출 👈
         └──→ main() 호출