Post

LD\_PRELOAD를 이용한 so injection과 hooking. + wrapping function

LD\_PRELOAD는 prefix로 LD\_가 붙은,ld.so 에 속하는 환경변수로, windows의 AppInit\_Dlls 레지스트리와 비슷한 역할을 한다.

LD\_PRELOAD 에 설정된 shared object는 libc를 비롯한 다른 모든 shared object보다 먼저 로딩 된다.

LD\_PRELOAD에 설정된 shared library의 함수 중에 이후 로딩된 libc의 함수 이름과 동일한 함수가 있다면 먼저 로딩된( = LD\_PRELOAD에 설정된) shared library의 함수를 호출하게 된다. 그래서 결과적으로 보면 자동으로 후킹을 수행하는 것과 같다.

  • 타인 소유 파일에도 동작한다.
  • secure-execution mode 로 실행되면 제약이 걸린다.
  • overloading을 따지지 않는다. 즉 파라미터, 리턴 타입 상관 없이 이름만 일치하면 후킹한다.
  • main은 후킹이 안된다
  • LD\_PRELOAD에 등록할 library에 대해 읽기 권한이 있어야 한다.
  • LD\_PRELOAD에 등록한 library가 위치한 path가 LD\_LIBRARY\_PATH/etc/ld.so.conf에 등록되어 있을 필요는 없다.
  • LD\_LIBRARY\_PATH/etc/ld.so.conf에 등록된 디렉토리에 위치한 library일 경우 library 파일 이름만 적어도 인식한다.

<실행하려는 파일이 타인 소유 파일이고, setUID가 걸려있는 경우(secure-execution mode) library를 찾을 수 없을 때의 오류메시지와 동일한 오류가 나온다.> #### Usage ##### 특정 바이너리 실행시만 환경변수로 지정해 바로 사용할 경우 : `LD\_PRELOAD=pathname ./binary` ##### 쉘에 등록한 다음 사용할 경우 ( 어떤 바이너리든 실행시 마다 LD\_PRELOAD 동작 ) : `export LD\_PRELOAD=pathname` ##### /etc/ld.so.preload에 삽입하는 경우 없으면 만들어주면 된다. 계정 상관 없이 시스템 전역 적으로 후킹된다. ##### 명령어에도 적용된다. 이를 이용해 실행할 바이너리가 `LD\_PRELOAD`로 넘긴 library를 로딩하는지 확인할 수 있다. `LD\_PRELOAD=pathname ldd ./binary` \* 명령어도 `/bin`같은 command path에 위치한 파일이기 때문에, `ldd`에도 `LD\_PRELOAD`가 적용되어 library를 로딩하게 된다. `constructor` 등이 있는 경우 `ldd`에서 `constructor`가 실행된다. binary에 setUID가 걸려있어도 명령어에 적용된 `constructor`는 실행되므로 이를 보고 setUID 걸려있어도 `LD\_PRELOAD`가 적용된다고 생각해서는 안된다. \* binary를 `fork` 등으로 실행하게 되는 명령어라면 명령어 자체와 `fork`로 실행된 자식프로세스 모두가 library를 로딩한다. #### hooking `LD\_PRELOAD`로 프로세스 실행 시 내가 원하는 library를 로딩하도록 할 수는 있는데, library를 로딩하는 것과 library 내의 함수를 호출하는 것은 별개다. 프로세스가 내가 원하는 library 내의 함수를 호출하도록 하려면, GCC attribute로 library를 로딩하면서 자동으로 호출하는 함수를 만들거나, 프로세스가 원래 호출하는 함수를 hooking해야한다. ##### GCC attribute [2017/01/13 - [System/LINUX & UNIX] - 생성자, 소멸자 / GCC \_\_attribute\_\_((constructor / destructor))](https://umbum.dev/146) hooking은 다음과 같이 진행한다. 1. `nm -D`로 심볼을 출력해 해당 프로그램이 사용하는 함수 이름을 얻는다. 2. user\_library에 hooking 타겟 함수 이름과 동일한 함수를 만든다. 3. `LD\_PRELOAD`로 대상 프로세스가 library를 로딩하도록 하면, 대상 프로세스가 hooking 타겟 함수를 호출할 때 마다 user\_library의 함수가 대신 호출된다. 4. 추가) user\_library의 함수(wrapper function)가 실행된 다음 원래 함수(hooking 타겟 함수)가 호출되어야 하는 경우, `dlsym(RTLD\_NEXT, ...)`를 사용한다. \* 프로그램 실행 시 기본적으로 사용되는 libc를 hooking 할 경우 4번 과정을 수행하지 않으면 제대로 동작하지 않는 경우가 대부분이다. \* hooking 함수에서 `dlsym(RTLD\_NEXT,... )`을 이용해 원래 함수 포인터를 얻은 뒤 `return orig\_func` 하면 된다. ( `RTLD\_NEXT`를 주면 symbol과 일치하는 다음 함수의 포인터를 반환한다. ) \* `dlsym()`을 이용하기 위해 `#include`해야 하며 library를 컴파일할 때 `-ldl` 옵션을 줘야 한다. \* `RTLD\_NEXT`는 `dlfcn.h`에 있는게 아니라 `#define \_GNU\_SOURCE` 해야 정의된다. ##### e.g. `getchar()`를 hooking해서 wrapping function이 동작하도록 했다. wrapping function은 `"hooked getchar!"` 문자열을 출력한 다음 original function을 호출한다. ##### libhook.so : ```c #include #define \_GNU\_SOURCE typedef int (\*orig\_getchar\_ftype)(void); int getchar(){ orig\_getchar\_ftype orig\_getchar; orig\_getchar = (orig\_getchar\_ftype)dlsym(RTLD\_NEXT, "getchar"); puts("hooked getchar!"); return orig\_getchar(); } ``` ##### tes : ```c #include int main(){ getchar(); return 0; } ``` ##### LD\_PRELOAD를 이용해 자식 프로세스를 생성하는 생성자, 소멸자를 포함하는 .so를 로딩하는 경우? 일반적으로 사용할 경우 `constructor` 안에 `system`이나 `fork`가 있어도 전혀 문제가 되지 않는다. 자식 프로세스를 만들면서 현재 프로세스의 상태를 복사하기 때문에 `constructor` 내부의 `system`이나 `fork`가 다시 실행되지 않기 때문. 그러나 환경변수는 parent의 환경변수를 child가 물려 받으면서 재적용 되는 것 같다. `LD\_PRELOAD`가 자식 프로세스에서도 동작한다. 그래서 `LD\_PRELOAD`를 사용해서 library를 로딩할 때,**constructor 또는 destructor로 지정한 함수 내에 system 혹은 fork 등이 있을 경우** 자식으로 생성된 프로세스도 `LD\_PRELOAD`가 적용되어 이 library를 로딩하게 되므로 연쇄적으로 `system` 혹은 `fork`를 호출하게 된다. `exec` 계열의 경우는 동작하지 않는 것 같다. 위 경우는 로딩할 library 내의 `constructor`가 `system, fork` 하게 되는 경우로,**실행할 프로세스가 system, fork하는 경우는 상관없다.** 이 경우 당연히 연쇄적인 child 생성은 일어나지 않지만, 자식 프로세스도 `LD\_PRELOAD`가 적용되기 때문에, 이 library를 로딩하게 되어 `constructor` 또는 `destructor`를 호출하게 된다는 점은 유의. ##### 참고 <http://optumsoft.com/dangers-of-using-dlsym-with-rtld_next/> \* `malloc(), free()`를 wapping하면서 `dlsym(RTLD\_NEXT, ...)`를 사용할 때 주의해야 할 점. `RTLD\_NEXT`는 순서대로 NEXT 함수를 반환하므로 wapper function이 있는 `.so`보다 `libc.so`가 뒤에 있어야 `RTLD\_NEXT`로 `libc.so`의 함수를 반환받을 수 있다.(`ldd`로 확인 가능) 만약 wapper function의 `.so`보다 `libc.so`가 앞에 있다면, `libc.so`의 원래 `malloc()`과 `func()`를 건너 뛰고 `ld-linux...so`의 `malloc()`과 `free()`를 반환하게 된다. 이 경우 `ld-linux...so` 자신이 또 다른 `malloc(), free()`를 가지고 있기 때문에 memory leak이 발생할 수 있다.
This post is licensed under CC BY 4.0 by the author.