OS/Linux

objdump을 통한 해부

gukbap 2012. 1. 18. 23:28
반응형
바이너리 파일 해킹 시 사용되는 툴

리눅스는 아래의 a.c에서 만든 main()을 어떻게 실행시키는가.

요약

1. GCC는 crtbegin.o, crtend.o, gcrt1.o를 첨가형 프로그램을 컴파일한다. 이 때 다른 기본 라이브러리들도 동적으로 링크된다. 프로그램의 시작 주소는 _start의 주소로 설정된다. 

2. 커널은 실행 파일을 읽어들여 /text/data/bss/stack을 생성한다. 이 때 매개변수와 환경변수를 위한 페이지를 할당하고 필요한 정보는 스택에 push 한다.

3. _start가 실행된다. _start는 스택에서 커널이 집어넣은 정보를 얻고 __libc_start_main을 위한 매개변수 스택을 만든 후 _start를 부른다.

4. __libc_start_main은 malloc과 같이 필요한 것들을 초기화하고 사용자가 만든 main을 호출한다.

5. __libc_start_main에서 main의 함수형은 main(int, char**, char**)이다.
********


/* a.c */
main(){

return0;

}


$ gcc -o a a.c
$ objdump -f a 

a:     file format elf32-i386

architecture: i386, flags 0x00000112:

EXEC_P, HAS_SYMS, D_PAGED

start address 0x080482e0

 

ELF(Executable and linking Format)는 유닉스 시스템에서 사용되는 여러 오브젝트파일/실행파일 형식 중 하나이다.
모든 ELF 실행파일은 다음과 같은 ELF 헤더를 가진다.

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;
 
여기서 e_entry 필드는 실행파일의 시작주소이다.
a의 시작주소는 "0x080482e0"이 된다.

시작주소는 무엇인가

우선 a를 역어셈블하자.

$ objdump --disassemble simple

...중략...

 080482e0 <_start>:

 80482e0: 31 ed                 xor    %ebp,%ebp

 80482e2: 5e                   pop    %esi

 80482e3: 89 e1                 mov    %esp,%ecx

 80482e5: 83 e4 f0             and    $0xfffffff0,%esp

 80482e8: 50                   push   %eax

 80482e9: 54                   push   %esp

 80482ea: 52                   push   %edx

 80482eb: 68 00 84 04 08       push   $0x8048400

 80482f0: 68 a0 83 04 08       push   $0x80483a0

 80482f5: 51                   push   %ecx

 80482f6: 56                   push   %esi

 80482f7: 68 94 83 04 08       push   $0x8048394

 80482fc: e8 c3 ff ff ff       call   80482c4 <__libc_start_main@plt>

 8048301: f4                   hlt    

 8048302: 90                   nop

 8048303: 90                   nop

 8048304: 90                   nop

 8048305: 90                   nop

 8048306: 90                   nop

 8048307: 90                   nop

 8048308: 90                   nop

 8048309: 90                   nop

 804830a: 90                   nop

 804830b: 90                   nop

 804830c: 90                   nop

 804830d: 90                   nop

 804830e: 90                   nop

 804830f: 90                   nop

...중략... 

 
어셈블리 코드를 보게 되면 스택에는 다음과 같이 쌓일 것이라 예상 가능하다.

Stack Top --------
0x8048394
---------------- 
esi
----------------  
ecx
----------------  
0x80483a0
----------------  
0x8048400
---------------- 
edx
---------------- 
esp
----------------  
eax
----------------  


분석
1> 16진수 값
0x08048394 : main() 함수의 주소

0x80483a0 : _init 함수의 주소

0x8048400 : _fini 함수.
// _init과 _fini는 GCC가 제공하는 초기화(initialization)/종료(finalization) 함수이다.

이 16진수 값들은 함수포인터 값들이다.

2> start()에서 호출하는 080482c4는 무슨 의미

080482c4 <__libc_start_main@plt>:

 80482c4: ff 25 04 a0 04 08     jmp    *0x804a004

 80482ca: 68 08 00 00 00       push   $0x8

 80482cf: e9 d0 ff ff ff       jmp    80482a4 <_init+0x30>


단순히 주소 0x804a004에 저장된 주소로 건너뛴다.
포인터 연산(*)이 있기에 0x804a004로 가는 것이 아니라 그곳에 저장된 주소로 가는 것.


ELF와 동적링크

ELF를 사용하면 라이브러리에 동적으로 링크되는 실행파일의 생성이 가능하다. 동적링크란 링크 과정이 실행 시 발생한다는 것이다. 그렇지 않다면 호출하는 모든 라이브러리를 포함한 크기가 큰 실행파일을(이것이 "정적으로 링크된" 실행파일) 만들어야 한다.

$ ldd a

linux-gate.so.1 =>  (0x00b40000)

libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0x0078e000)

/lib/ld-linux.so.2 (0x00d2d000)


a와 동적링크된 모든 라이브러리를 볼 수 있다. 동적으로 링크된 자료와 함수는 모두 "동적 재배치 항목(dynamic relocation entry)"을 가진다.



1. 링크시 동적 심볼의 실제 주소는 모른다. 실행되고 나서야 심볼의 실제 주소를 알게 된다.
2. 동적 심볼의 실제 주소를 위해 메모리 공간을 남겨놓는다. 로더(loader)가 실행시 이곳에 심볼의 실제 주소를 쓴다.
3. 프로그램은 전의 start() 함수에서 부른 080482c4로 가서 포인터연산을 통하여 0x804a004에 저장된 주소로 가게 된다. 

모든 동적 링크 항목 표시

$ objdump -R a

DYNAMIC RELOCATION RECORDS

OFFSET   TYPE              VALUE 

08049ff0 R_386_GLOB_DAT    __gmon_start__

0804a000 R_386_JUMP_SLOT   __gmon_start__

0804a004 R_386_JUMP_SLOT   __libc_start_main


실제로 0x804a004를 통하여 __libc_start_main을 호출하게 된다.



__libc_start_main에 대하여>

__libc_start_main은 libc.so.6에 담겨 있는 함수이다. 함수형은 다음과 같다.

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));

_start는 매개변수(argument) 스택을 만들고 __libc_start_main을 부른다.
__libc_start_main은 몇몇 자료구조와 환경변수를 생성, 초기화 후 main()을 부른다.

위의 __libc_start_main의 함수형을 참조하면 기존의 start 아래의 스택이 가진 의미는 다음과 같다.

Stack Top --------
0x8048394               main
---------------- 
esi                        argc
----------------  
ecx                       argv
----------------  
0x80483a0              _init
----------------  
0x8048400              _fini
---------------- 
edx                      -rtlf_fini
---------------- 
esp                      stack_end
----------------  
eax                      0
----------------   

하지만 위의 스택프래임에 따르면 __libc_start_main이 실행되기 전에 스택에 적다한 값들이 채워져있어야한다. _start는 이 레지스터들을 설정하지 않는다. 이 값들을 설정해 주는것은 커널이다.

커널이 하는 일> 

쉘에 명령어를 입력하여 프로그램 실행 시

1. 쉘은 argc/argv를  가지고 커널 시스템호출 execve를 호출한다.
2. 커널 시스템호출 핸들러가 제어를 맡아 시스템 호출을 처리하기 시작한다. 이 때, 커널에서 핸들러는 sys_execve이다. x86 아케텍처에서는 사용자모드 프로그램은 아래 레지스터를 통해 필요한 파라미터를 커널에 넘긴다.

ebx : 프로그램명 문자열의 포인터
ecx : argv 배열 포인터
edx : 환경변수 배열 포인터

3. 일반적인 커널 시스템 호출 핸들러 do_execve()가 호출된다. 이는 자료구조를 만들고 사용자 영역에서 커널 영역으로 자료 복사한 후, search_binary_handler()를 부른다. 리눅스는 다양한 형태의 실행파일 형식을 지원하기 위해 가각의 바이너리 형식을 읽어들일 수 있는 함수의 포인터가 담긴 struct linux_binfmt 자료구조가 있다.
search_binary_handler는 적당한 핸들러를 찾아서 호출하는데, 이 경우에서는 load_elf_binary()가 된다. *******
load_elf_binary()는 먼저 파일 작업을 위해 커널 자료구조를 만들고 ELF 실행파일을 읽어들이고 코드 크기, 자료 세그먼트 시작(data segment start), 스택 세그먼트 시작(stack segment start) 등 커널 자료구조를 생성한다. 그리고 이 프로세스에 대한 사용자모드 페이지를 할당하고 argv와 같은 환경변수를 할당된 페이지 주소로 복사한다.
마지막으로 create_elf_tables()를 사용하여 argc, argv 포인터, 환경변수 배열 포인터를 사용자모드 스택에 push 후, start_thread()로 프로세스를 실행한다.


_start 명령어 실행시 

Stack Top
--------------------
argc
-------------------- 
argv pointer
-------------------- 
env pointer
-------------------- 

앞서봤듯이

 80482e2: 5e                    pop    %esi

 80482e3: 89 e1                 mov    %esp,%ecx
이것이다.

pop %esi 실행 시 argv를 얻게 되고
move %esp, %ecx 실행 시 argv를 얻게 된다.

argv 주소는 지금의 스택 포인터와 같다.

다른 레지스터들의 설정>
esp는 스택의 끝을 가리키게 된다. _start는 필요한 정보를 pop한 후 esp 레지스터에서 하위 4비트를 끈다.
edx는 프로그램의 파괴자(destructor)인 rtld_fini에 사용된다. 커널은 다음의 메크로를 사용해서 edx를 0으로 만든다.
레지스터에서의 0은 그 기능을 사용하지 않는다는 것이다.
#define ELF_PLAT_INIT(_r) do{ \
_r->ebx =0 ; _r->ecx =0; _r->edx = 0; \
_r->esi = 0;  _r->edi = 0;  _r->ebp = 0; \
_r->eax = 0; \
}while(0)

이들 코드들은 GCC 코드의 일부이다. 이 코드들에 대한 오브젝트파일들은
/usr/bin/gcc-lib/i386-redhat-linux/abcdefg        (abcdefg는 gcc 버전)
/usr/lib
에서 찾을 수 있고 파일명은
crtbegin.o
crtend.o
gcrt1.o
이다. 


굽신
http://smilk.egloos.com/486882 
이거 스크랩한 블로그 많네요...
전 처음부터 끝까지 타이핑으로 끄적끄적





 
반응형

'OS > Linux' 카테고리의 다른 글

ubuntu에서 grub2 복구 후 windows7 부트로더 복구  (0) 2013.11.08
export  (0) 2012.01.19
euid, uid  (0) 2012.01.18
gcc에서 -static 오류  (0) 2011.09.02
Fedora에서 VMWare 사용시 vmware kernel Module Updater  (0) 2011.09.01