기술문서 LD_PRELOAD 와공유라이브러리를사용한 libc 함수후킹 정지훈 binoopang@is119.jnu.ac.kr
Abstract libc에서제공하는 API를후킹해본다. 물론이방법을사용하면다른라이브러리에서제공하는 API들도후킹할수있다. 여기서제시하는방법은리눅스후킹에서가장기본적인방법이될것이기때문에후킹의워밍업이라고생각하고읽어보자 :D
Content 1. 목적 1 1.1. 아이디어 1 2. Hooker 제작 2 2.1. 실제 libc 함수의포인터획득 2 2.2. Hooker 공유라이브러리생성 6 3. Hooker 테스트 10 3.1. 테스트환경 10 3.2. LD_PRELOAD 를사용한 Hooker 로드 10 4. 마치며 12 참고문헌 13
1. 목적 이문서에서는공유라이브러리와 LD_PRELOAD 를사용하여 API 후킹을시도할것이다. 기본적인 아이디어와기술이간단해서쉽게사용할수있는기술일것이다. 1.1. 아이디어 리눅스에서프로세스가로드될때여러가지라이브러리들을로드한다. 그중 'libc' 는 C언어에서제공되는 API들이들어있는라이브러리이다. 여기서 LD_PRELOAD를사용해서강제로라이브러리를프로세스에먼저로드시키고만약그라이브러리안에 'libc' 에들어있는함수와동일한이름의함수가존재할경우프로세스는 'libc' 가아닌 LD_PRELOAD를통해로드된라이브러리의함수를호출하게된다. 이것은라이브러리의로드순서의문제이다. 따라서우리는 LD_PRELOAD 를사용해서 'libc' 함수와동일한형식의후킹함수를로드시켜서 그함수를먼저실행한다음원래 'libc' 의함수로흐름을넘겨줄것이다. - 1 -
2. Hooker 제작 2.1. 실제 'libc' 함수의포인터획득 후킹의기본은함수포인터이다. 먼저함수포인터를사용해서실제 'libc' 의함수주소를가지고있다가우리의함수가일을마치거나필요시에실제 'libc' 함수를호출해야한다. 그렇다면어떻게실제 'libc' 함수주소를얻어낼수있을까? gdb를사용하여 getuid 주소를찾아보자. [ 그림 1] gdb 를사용한 getuid 함수주소출력 gdb 를사용해서 'getuid' 함수의주소를출력해보았다. [ 그림 1] 에서총 3 번에걸쳐서실행 하여각각주소를출력하였지만모두결과가다른것을볼수있다. 이것은실행할때마다라 이브러리의로드되는주소가약간씩달라지기때문에함수의주소도조금씩다른것이다. 2.1.1. 메모리스캐너 그렇다면매번실행할때마다 'getuid' 함수를찾아야할것같다. 여기서는메모리를스캔하 는방법을사용해보았다. - 2 -
1 main() 2 { 3 unsigned int offset; 4 char pattern[] = "\x55\x89\xe5\xb8"; 5 for(offset = 0xb7e00000 ; offset < 0xb7efffff ; offset++) 6 { 7 if(!strncmp((char *)offset, pattern, strlen(pattern))) 8 { 9 printf("found!! 0x%x\n", offset); 10 } 11 } 12 } [ 코드 1] 메모리스캔코드 [ 그림 1] 에서 getuid 의주소가 0xb7e00000 부근에서나타나는것을착안하여그부근을검 색하는것이다. 이때 pattern 에등록되어있는 '\x55\x89\xe5\xb8' 는 gdb 를사용해 서찾은 getuid 함수의기계어이다. [ 그림 2] getuid() 주소와기계어코드 [ 그림 2] 와같은방법으로찾을수있으며기계어코드의의미는다음과같다. 0xb7f01a50 <getuid+0>: push 0xb7f01a51 <getuid+1>: mov 0xb7f01a53 <getuid+3>: mov %ebp %esp,%ebp $0xc7,%eax [ 코드 2] 기계어코드해석 [ 코드 2] 에서보여지는어셈블리코드가 [ 그림 2] 에서찾은 'getuid' 의기계어이다. 조금 더정확도를높이고싶다면긴패턴을사용하면될것이다. - 3 -
[ 그림 3] 스캐너실행 [ 그림 3] 에서처럼스캐너를실행하면총 4개의함수주소가발견된다. 4개의주소는순서대로 getuid, geteuid, getgid, getegid 이다. 모두비슷한패턴을쓰다보니한꺼번에검색된것이다. [ 그림 3] 에서한가지문제점이있는데하단분에 'Segmentation fault' 가발생하였다. 보통 'SIGSEGV' 시그널은잘못된메모리에접근하였을때발생한다. 여기서는아마도 'libc' 를벗어난다른메모리지역에들어갔을경우이렇게 'SIGSEGV' 시그널이발생하는것같다. 2.1.2. libc 가로드된주소검색 [ 그림 3] 의문제점을해결하기위해서는실제 'libc' 가로드된주소를정확히알필요가있다. 많은검색을통해서 'dl_iterate_phdr()' 를알게되었고아래와같은코드를작성할수있었다. 1 #define _GNU_SOURCE 2 #include <stdio.h> 3 #include <link.h> 4 5 static int print_callback(struct dl_phdr_info *info, 6 size_t size, 7 void *data) 8 { 9 if((strstr(info->dlpi_name, "libc"))) 10 printf("%08x %s\n", info->dlpi_addr, info->dlpi_name); 11 return 0; 12 } 13 14 int main() 15 { 16 dl_iterate_phdr(print_callback, NULL); 17 return 0; 18 } [ 코드 3] 'libc' 라이브러리시작주소검색루틴 - 4 -
[ 코드 3] 에서 'dl_iterate_phdr' 은콜백함수를사용하며콜백함수는로드되는라이브러리횟수만큼호출되어서라이브러리정보를구조체에담는다여기서우리가필요한항목은라이브러리이름과시작주소이다. [ 코드 3] 의 9번줄에서 'strstr()' 을사용하여라이브러리이름에 'libc' 가들어가있으면해당라이브러리의시작주소를가져오는형식이다. [ 그림 4] 로드된 libc 라이브러리주소획득 참고로여기서사용된함수는 'GNU 확장함수 ' 1) 이기때문에 '#define _GNU_SOURCE' 를정의해주어야한다. 여기까지해서메모리스캔하는데필요한작업이완료되었다. 실제완성된스캔코드는아래와같다. 1 void search_getuid_offset(void) 2 { 3 printf("[hooker] Searching getuid offset\n"); 4 char pattern[] = "\x55\x89\xe5\xb8"; 5 for(offset = libc_start ; offset < 0xb7ffffff ; offset++) 6 { 7 if(!strncmp((char *)offset, pattern, strlen(pattern))) 8 { 9 printf("[hooker] Found offset : 0x%x\n", offset); 10 break; 11 } 12 } 13 if(offset == (unsigned long)null) 14 { 15 printf("[hooker] Can not found offset\n"); 16 exit(0); 17 } 18} [ 코드 4] 메모리스캔코드 1) GNU 확장함수는표준함수가아니기때문에꼭 _GNU_SOURCE 를정의해주어야한다. 그렇지않으면컴파일시에에러가발생한다. - 5 -
[ 코드 4] 에서 5 번줄의 'libc_start' 변수는 'dl_inerate_phdr' 의콜백함수에의해얻어낸 'libc' 가로드된주소이다. 우리는검색되는 4 개의함수중첫번째함수만필요하므로첫번 째함수가호출되면 'break' 를사용하여 for 문을빠져나온다. 2.2. Hooker 공유라이브러리생성 스캔코드를기반으로이제공유라이브러리를생성해야한다. 여기서는 'getuid' 함수를후킹할것이기때문에공유라이브러리에서메인함수는 'getuid()' 이름으로해야한다. 그래야실제어플리케이션에서 'getuid()' 를호출하면우리의라이브러리에존재하는 'getuid()' 를호출한다. 1 // getuid 후킹함수 2 int getuid() 3 { 4 int ret; 5 if(signal(sigsegv, sig_usr) == SIG_ERR) 6 fprintf(stderr, "Can not catch signal\n"); 7 8 dl_iterate_phdr(scan_callback, NULL); 9 printf("[hooker] libc start address : 0x%x\n", libc_start); 10 search_getuid_offset(); 11 orig_getuid = offset; 12 printf("[hooker] Call Orig getuid!!\n"); 13 ret = orig_getuid(); 14 printf("[hooker] Return value of getuid : %d\n", ret); 15 return ret+1; 16 } [ 코드 5] getuid() 후킹함수 [ 코드 5] 는 'getuid()' 후킹함수이다. 이함수가흔히우리가프로그래밍할때사용하는 main() 의역할을하게된다. 8번줄에서가장먼저 'libc' 의로드주소를찾은다음 10번줄에서실제 'libc' 의 getuid함수주소를찾는다. 찾아낸주소는 11번줄에서함수포인터에넣어지고 13번줄에서호출되어변수에저장된다. 가장마지막줄에 'return ret+1' 이있는데이렇게되면실제 uid에 1을더한 uid가리턴될것이다. - 6 -
1 /* -- API Hooking by bin00pang -- */ 2 3 #define _GNU_SOURCE 4 #include <stdio.h> 5 #include <link.h> 6 #include <stdlib.h> 7 #include <string.h> 8 #include <signal.h> 9 10 // 실제 getuid의주소를담을변수 11 unsigned long offset = (unsigned long)null; 12 13 unsigned long libc_start = (unsigned long)null; 14 15 // 실제 getuid() 의진입점을가리킬함수포인터 16 int (*orig_getuid)(void) = NULL; 17 18 void search_getuid_offset(void); 19 20 // SIGSEGV 시그널핸들러 21 static void sig_usr() 22 { 23 printf("[hooker] Received SIGSEGV signal.. quit.. \n"); 24 exit(0); 25 } 26 27 // libc가로드된주소를찾기위한루틴 28 static int scan_callback(struct dl_phdr_info *info, 29 size_t size, 30 void *data) 31 { 32 if(strstr(info->dlpi_name, "libc")) 33 libc_start = (unsigned int)info->dlpi_addr; 34 return 0; 35 } 36 37 38 // getuid 후킹함수 39 int getuid() - 7 -
40 { 41 int ret; 42 if(signal(sigsegv, sig_usr) == SIG_ERR) 43 fprintf(stderr, "Can not catch signal\n"); 44 45 dl_iterate_phdr(scan_callback, NULL); 46 printf("[hooker] libc start address : 0x%x\n", libc_start); 47 search_getuid_offset(); 48 orig_getuid = offset; 49 printf("[hooker] Call Orig getuid!!\n"); 50 ret = orig_getuid(); 51 printf("[hooker] Return value of getuid : %d\n", ret); 52 return ret+1; 53 } 54 55 // 실제 getuid의진입점찾는루틴 56 void search_getuid_offset(void) 57 { 58 printf("[hooker] Searching getuid offset\n"); 59 char pattern[] = "\x55\x89\xe5\xb8"; 60 for(offset = libc_start ; offset < 0xb7ffffff ; offset++) 61 { 62 if(!strncmp((char *)offset, pattern, strlen(pattern))) 63 { 64 printf("[hooker] Found offset : 0x%x\n", offset); 65 break; 66 } 67 } 68 69 if(offset == (unsigned long)null) 70 { 71 printf("[hooker] Can not found offset\n"); 72 exit(0); 73 } 74 } [ 코드 6] Hooker 전체코드 [ 코드 6] 은전체적인 Hooker 의코드이다. 생각보다길지않은것을볼수있다.( 물론더짧 게만들수도있다.:D ) - 8 -
이제실제 [ 코드 6] 을공유라이브러리로만들어보자. 공유라이브러리는아래와같은과정 으로만들어진다. [ 그림 5] 공유라이브러리생성 생성되는대략적인순서는다음과같다. 먼저오브젝트파일로컴파일한다. $ gcc -fpic -c -o hooker hooker.c 공유라이브러리로컴파일한다. $ gcc -shared -W1,-soname,libhooker.so.0 -o libhooker.so hooker 이와같은과정을거치면 [ 그림 5] 와같이 'libhooker.so' 라는공유라이브러리가생성된다. 이제모든후킹의준비가완료되었다. - 9 -
3. Hooker 테스트 3.1. 테스트환경 Hooker 의테스트환경은다음과같다. - 커널버전 : 2.6.24-19-generic - gcc 버전 : 4.2.3 3.2. LD_PRELOAD 를사용한 Hooker 로드 우리가만든 Hooker 를사용하기위해서는일단라이브러리가프로세스에로드되어야한다. 이작업은환경변수인 LD_PRELOAD 가쉽게해준다. [ 그림 6] LD_PRELOAD 익스포트 [ 그림 6] 과같이 LD_PRELOAD 의값을 libhooker.so 의절대경로로설정한다음 export 시키면 이제준비가끝난것이다. [ 그림 7] 디버그모드로실행 [ 그림 7] 은 'id' 명령어를디버그모드로실행한것이다. 보면가장먼저로드하는라이브러리 - 10 -
가바로방금우리가만든 'libhooker.so' 라는것을확인할수있다. 먼저정상적인 'id' 의결 과를확인해보자. [ 그림 8] 정상적인 id 의결과 'libhooker.so' 를로드하기전에는정상적인 id 결과가출력된다. 실제 'binoopang' 의 uid 는 1000 이다. 이제후킹을한다음의결과를보자. [ 그림 9] 후킹후의 id 결과 [ 그림 9] 에서정상적으로 getuid 가후킹되었고원래 1000 이었던 uid 가 1001 로바뀌어출력된 것을확인할수있다. - 11 -
3. 마치며 이문서에서제시한후킹방법은매우간단하다.( 사실걸어가다생각나서해본것이다.) 이방법에는후킹으로써는제한되는부분이많이있다. 특히라이브러리를사용하는방법이기때문에제거하는것이너무쉽다. 다음에는다른방법으로후킹하는방법에대해서써봐야겠다. - 12 -
참고문헌 [1] Linux Manpage, "ld_iterate_phdr" http://linux.die.net/man/3/dl_iterate_phdr - 13 -