Dork's port

[Linux] LD_PRELOAD의 이해와 LD_PRELOAD를 이용한 함수 후킹 구현하기 본문

Linux

[Linux] LD_PRELOAD의 이해와 LD_PRELOAD를 이용한 함수 후킹 구현하기

Dork94 2018. 11. 1. 01:30

안녕하세요.


오늘은 LD_PRELOAD를 이용해 함수 후킹을 구현해보려고 합니다.


최종적으로 SSL통신을 후킹하기 위해 SSL_write와 SSL_read를 후킹하여 프로그램에서 사용하는 통신을 출력하는 것이 목적입니다.


아직 공부 단계이므로 차근차근 정리하여 추후에 포스팅 하도록 하겠습니다.



  #LD_PRELOAD


우선 처음으로 사용될 개념인 LD_PRELOAD에 대해 알아 보도록하겠습니다.


저도 이번에 SSL을 후킹할 방법에 대해 찾아보다가 luke1337님을 통해 알게된 개념인데 개념은 아래와 같이 정리할 수 있습니다.


환경 변수 중 하나로 변수에 설정된 라이브러리를 기존 라이브러리가 로딩 되기 전에 로딩 시킨다. 이때 라이브러리에 중복된 이름의 함수가 있을 경우 LD_PRELOAD에 설정된 라이브러리가 먼저 로딩 되므로 LD_PRELOAD의 라이브러리에서 함수를 호출한다.


※SetUID가 걸린 파일에 대해서 LD_PRELOAD는 보안상의 이유로 무시 됩니다.


저 친구 덕분에 우리는 원하는 함수에 대해 후킹하는 효과를 얻을 수 있으며 최종적으로 SSL의 통신을 볼 수 있도록 할것입니다.

그럼 바로 본론으로 들어가 보도록 하죠! 


  #LD_PRELOAD를 이용한 함수 후킹



환경은 아래와 같이 kali linux에서 진행하였습니다.


root@kali:~# uname -a

 Linux kali 4.18.0-kali1-amd64 #1 SMP Debian 4.18.6-1kali1 (2018-09-10) x86_64 GNU/Linux



저희는 strcpy라는 함수를 해킹하는 것을 목표로 진행 해보도록 하겠습니다.

이유는 함수의 return 값과 인자값이 있어 SSL함수 후킹과 비슷할 것 같아 타깃으로 지정하였습니다.

저는 C++을 좋아하므로 C++을 이용하여 후킹을 시도해 보도록 하겠습니다.

우선 간단한 프로그램을 하나 작성해 보도록 하죠! 


//main.cpp
#include <iostream>
#include <cstring>

using namespace std;

int main(int argc, char* argv[])
{
    cout<<"String copy src : "<<argv[1]<<endl;
    char tmp[100];
    cout<<"Trying to string copy src to tmp buf..."<<endl;
    strcpy(tmp,argv[1]);
    
    cout<<"tmp buffer : "<<tmp<<endl;

    return 0;

}

위의 프로그램은 프로그램 실행시 인자를 받은 후 strcpy를 이용해  tmp라는 변수에 문자열을 저장한 후 출력하고 종료하는 간단한 프로그램입니다.


프로그램의 실행 결과를 보도록 할게요!

Compile은 아래의 명령어로 할 수 있습니다.

$ g++ -o main main.cpp




아래와 같이 실행이 잘 되네요! 





그렇다면 이제 strcpy를 후킹 할 라이브러리를 만들어 보도록 할게요! 


//hook_test.cpp

//#define _GNU_SOURCE
#include <dlfcn.h>
#include <iostream>
#include <cstring> //This header also contain _GNU_SOURCE
                                     //_GNU_SOURCE needed by use RTLD Flags
using namespace std;


char* (*origin_strcpy)(char* dest, const char *src);


char* strcpy(char* dest, const char *src)
{
    cout<<"strcpy hooked by hook_test.so!!!"<<endl;
    cout<<"Hooked src Contents : " <<src<<endl;
        
    origin_strcpy = (char * (*)(char*,const char*))dlsym(RTLD_NEXT, "strcpy");
    return  (*origin_strcpy)(dest,src);

}



하나하나 차근차근 알아 보도록하죠! 


우선 dlfcn.h 헤더는 dlsym을 call하기 위해 사용되는 헤더 입니다.


dlsym이란 ?

라이브러리에서 symbol을 찾는 즉, 함수의 이름을 이용해 주소를 return해 주는 함수



void *dlsym(void *handle, const char *symbol);

 
이때 handle은 RTLD와 같은 flag가 들어가며, symbol은 함수의 이름



그 후 cpp이니 iostream과 strcpy를 사용하기 위해 cstring을 인클루드 하였습니다.


이때 RTLD와 관련된 Flag를 사용하기 위해 _GNU_SOURCE를 define해야 하지만 cstring안에 포함된 내용으로 따로 추가하지 않았습니다.


그리고 전역 변수로 원래의 strcpy를 호출할 포인터 변수를 선언합니다.


포인터 변수의 형태는 원형(리턴값 및 파라미터)과 같아야 하며 원형은 다음과 같이 man page를 참조하여 알 수 있습니다.





그리고 우리가 작성한 라이브러리의 호출을 확인하기 위해 디버깅 메세지를 cout을 통해 출력합니다.


그리고 다음 코드를 주목해주세요! 


origin_strcpy = (char * (*)(char*,const char*))dlsym(RTLD_NEXT, "strcpy");
    return  (*origin_strcpy)(dest,src);


dlsy을 이용해 원래의 strcpy의 주소를 얻어 와 보도록 하겠습니다.


flag에는 RTLD_NEXT를 이용하며 함수의 이름에는 우리가 찾는 함수인 strcpy를 전달해 주게 되면 함수의 주소를 리턴하게 됩니다. 


이때 RTLD_NEXT는 무엇을 의미 할까요??


RTLD_NEXT는 처음에 찾는 함수의 주소가 아닌 2번째 함수의 주소를 찾으라는 설정입니다. 왜 그래야 하는지 한번 알아보도록 할게요!


LD_PRELOAD를 이용한 프로그램의 실행 당시 현재 우리의 메모리에는 strcpy라는 이름의 함수가 2개가 로딩되어 있을 것 입니다


하나는 우리가 만든 hook_test.cpp를 컴파일하여 만든 라이브러리에서 만든 strcpy와 원래 라이브러리에서 제공하는 strcpy가 있겠죠!


LD_PRELOAD를 이용하여 우리가 만든 strcpy를 우선 로드하도록 하였으므로 처음에 찾는 strcpy의 주소는 우리가 만든 strcpy일 것 입니다! 


즉! 만약 RTLD_NEXT를 해주지 않게되면 우리가 만든 라이브러리의 strcpy주소가 return 되어 결과적으론 재귀 함수가 되는 것이죠!


따라서 우리는 NEXT를 이용해 다음 strcpy의 주소를 참조하므로써 원래의 strcpy주소를 얻을 수 있습니다.


그리고 마지막엔 함수포인터를 실행하고 우리가 만든 함수의 인자(dest, src)를 함수포인터에게 전달하면 정상적으로 함수가 실행되며 return 값 또한 전달 될 것입니다.


조금 설명이 복잡할 수 도있습니다. 추가적인 문의는 댓글로 해주시면 상세하게 답변드릴 수 있도록 할게요!


그럼 이제 만들었으니 컴파일을 해보도록 할까요!?


$ g++ -fPIC -shared -o hook_test.so hook_test.cpp -ldl


위와 같이 컴파일 할 수 있습니다.


-fPIC은 object file을 만들때, 그 안의 symbol (function, variable)들이 어떤 위치에 있어도 동작을 하는 구조로 compile 하라는 것 이며, -fPIC을 하지 않아도 공유 라이브러리를 만들 수 있으나 재배치 시간이 소요되고 다른 프로세스와 코드 블록을 공유할 수 없다는 단점이 있다고 한다. 또한, 실제로 -fPIC을 빼고 컴파일을 시도해 보았다.


root@kali:~/hook_test# g++ -shared -o hook_test.so hook_test.cpp -ldl 

 /usr/bin/ld: /tmp/ccWUZGDV.o: relocation R_X86_64_PC32 against symbol `_ZSt4cout@@GLIBCXX_3.4' can not be used when making a shared object; recompile with -fPIC /usr/bin/ld: final link failed: bad value collect2: error: ld returned 1 exit status


위와 같이 오류가 발생하는데 아마 cpp의 namemangle규칙 때문일 것이라고 예측할 수 있다. 컴파일러에서 친절하게 -fPIC을 사용하라고 알려주니 쓰도록 하자!


-shared는 shared library를 만든다는 옵션이며 우리는 공유 라이브러리를 만들고 LD_PRELOAD를 통해 먼저 로드를 시켜줄 것이므로 컴파일 옵션에 추가해준다.


-ldl은 -l옵션을 이용해 libdl라이브러리를 로드하라는 의미이다. -l뒤에 나오는 라이브러리를 로드하여 컴파일 하라는 의미이며 예를들어 -lpcap은 libpcap을 로드하라는 말과 같다. dlsym을 이용하였는데 dlsym은 libdl 라이브러리에 존재하므로 컴파일 타임에서 libdl과 함께 컴파일 해줄 수 있도록 한다.



그럼 마지막으로 대망의 실행을 해보도록 하죠! 


실행은 아래와 같이 먼저 로드될 라이브러리와 LD_PRELOAD를 이용해 실행할 파일을 뒤에 인자로 전달하면 LD_PRELOAD를 이용해 프로그램을 실행할 수 있습니다.



$ LD_PRELOAD=./hook_test ./main


그럼 결과를 확인해 볼까요?



정상적으로 우리가 작성한 라이브러리가 불려 디버깅 코드가 출력되는 것을 볼 수 있습니다.


이 경우 우리가 main에서 tmp를 출력하는 코드를 작성하여서 strcpy를 후킹한 것에 대한 큰 의미는 없지만 만약 main에서 strcpy를 이용하는 인자를 출력해주는 코드가 없다면 충분히 의미있는 후킹이 될 수 있겠죠!?


또한,  마지막에 원래의 strcpy가 정상적으로 호출되어 값이 tmp로 복사돼 main의 마지막에 출력한 것 처럼 tmp값에 인자가 복사된 것을 볼 수 있습니다.


이 개념을 ssl을 이용하는 ssl_write와 ssl_read에 적용하면 로컬에서 동작하는 프로그램에 대해 내용을 출력하거나 log file로 저장할 수 있을 것 입니다. 즉 ssl을 후킹하는 것과 같은 효과를 내는 것이지요! 다른 방법으로는 ssl strip도 있습니다.


다음엔 위의 내용으로 찾아올 수 있도록 하겠습니다.


code 및 make file은 깃허브(https://github.com/janghanbin/hook_test)에서 확인할 수 있습니다.



질문은 댓글로 받을게요!






thanks to : gilgil, luke1337 , umbum




참조 : http://i5on9i.blogspot.com/2013/05/hooking-wrapper-function.html

http://umbum.tistory.com/128

https://kldp.org/node/1107

http://devanix.tistory.com/198



Comments