Search


정대근 보안기술팀장 (A.K.A 1ndr4)

indra@a3security.com


보통 개발자들이 프로그램을 만들때 유지보수의 편의성이나 다양한 이유들을 근거로 모듈화 작업을 합니다. 소스코드 트리 구조부터 비슷한 작업을 하는 코드들을 파일 단위로 모듈화 하기도 하고 배포를 위한 응용프로그램도 사실 모듈화를 한 결과물입니다. Windows OS를 쓰고 계신 분이라면, 프로그램 폴더 안에 실행파일인 *.exe 파일외에 *.dll 파일과 같은 다른 확장자의 파일을 보실 수 있을겁니다. *.dll 파일들은 Dynamic-link library라고 하여 내부에 구현된 함수들을 응용프로그램에서 필요 시 동적으로 불러 실행하도록 되어 있는데요. 이러한 동적 링크 라이브러리는 다른 플랫폼 환경에서도 볼 수 있습니다. 개념은 같지만 일컫는 용어가 다를뿐이지요.


이 글에서는 Linux 상에서 동작하는 동적 링크 라이브러리에 대해 설명 드려볼까 합니다.

우선 Linux에서는 동적 링크 라이브러리 개념을 가진 파일을 Shared Objects 혹은 Shared Libraries(공유 라이브러리)라는 명칭으로 부르고 있습니다. 


우선 다음의 예제 소스코드를 참조하시면 좋을 것 같습니다.


/*

* libso_exam.so.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example c code for shared library

*/

#include <stdio.h>

#include <unistd.h>

#include <string.h>


int teamcrak(const char *msg)

{

        fprintf(stdout, "%s", msg);

        return 0;

}


보통 프로그램 실행 시 기준이 되는 main() 함수는 존재하지 않고 teamcrak() 이라는 함수만을 정의했는데, 해당 함수는 인자로 받은 데이터를 fprintf()로 출력하는 함수입니다. 위 소스코드 파일은 컴파일 과정을 거쳐 공유 라이브러리의 역할을 할 것입니다.


* 공유 라이브러리 컴파일 명령행

$ gcc \

    -Wall \                                  # 모든 경고 출력 옵션

    -shared \                              # 공유 라이브러리로 컴파일 하겠다는 옵션

    -Wl,-soname,libso_exam.so \ # 링크 옵션으로 soname(Shared Object NAME)을 지정하는 옵션

    -o libso_exam.so \                 # output 옵션. 컴파일 결과 파일: libso_exam.so

    libso_exam.so.c                     # 컴파일 할 소스코드



다음 예제 소스코드는 공유 라이브러리에 존재하는 함수를 실행 내용의 소스코드입니다.


/*

* so_loader.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example c code for shared library loader

*/

#include <stdio.h>

#include <stdlib.h>


int main(void)

{

        teamcrak("Call by shared library\n");

        return 0;

}


위 so_loader.c 소스코드에는 teamcrak()이라는 함수의 정의나 구현부분이 존재하지 않지만, 컴파일 및 링크시에 사용되는 옵션으로 teamcrak() 함수가 정의된 라이브러리를 참조하여 실행 할 수 있습니다.


* 공유 라이브러리를 참조하는 코드 컴파일 명령행

$ gcc \

    -Wall \                # 모든 경고 출력 옵션

    -o so_loader \     # output 옵션. 컴파일 결과 파일: so_loader

    so_loader.c \       # 컴파일 할 소스코드

    -L. \                    # 라이브러리 링크 경로 지정 옵션. 라이브러리 경로: . (현재 디렉터리)

    -lso_exam           # 링크 할 라이브러리 지정 옵션. 라이브러리 이름: libso_exam.so


gcc의 -l 옵션을 사용하여 특정 라이브러리를 링크하도록 지정할 때 파일 이름에 존재하는 lib(접두사).so(접미사)는 제외해야 합니다.

아래는 예제코드들을 실제 컴파일 과정을 거쳐 실행 해 본 화면입니다.


[그림 1] LD_LIBRARY_PATH 환경변수로 라이브러리 경로 지정 후 정상 실행 확인


컴파일 과정에서 teamcrak() 함수의 원형이 so_loader.c 파일에 정의되지 않아 warning이 발생된 것 빼고는 큰 장애없이 컴파일이 완료되었습니다. 그러나 실행 시 에러가 발생했네요. 해당 에러는 공유 라이브러리의 경로를 지정해 주지 않아 발생한 에러였습니다. 라이브러리 관련 환경변수인 LD_LIBRARY_PATH에 . (현재 디렉터리)를 명시한 이후에 정상 실행이 된 것을 확인 할 수 있습니다.

프로그램 실행 시 이러한 공유라이브러리의 의존성을 확인할 수 있도록 ldd라는 명령어를 활용할 수 있습니다.


[그림 2] ldd 명령어로 대상 프로그램의 의존성이 존재하는 라이브러리 확인


기본적으로 공유라이브러리는 위와 같이 특정 경로에 위치되도록 하여 실행 프로그램에서 해당 라이브러리의 기능을 사용할 수 있도록 되어 있습니다. 그러나 컴파일, 링크 시에 공유라이브러리를 참조하도록 하는 방법 외에 다른 방법은 없을까요? 코드 레벨에서 이와 같은 과정에 관여 할 수는 없을까요? 이를 위해 우리는 DL 라이브러리를 사용할 수 있습니다. 다음 소스코드는 DL 라이브러리에서 제공하는 dlopen()dlsym(), dlclose()를 통해 외부 라이브러리에 존재하는 함수를 참조하여 실행할 수 있도록 구현된 소스코드입니다.


/*

* so_loader_by_dlopen.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example c code for dlopen()

*/

#include <stdio.h>

#include <unistd.h>

#include <dlfcn.h>


int main(int argc, char **argv)

{

        int ret;

        void *dl = NULL;

        // XXX: Function prototype of teamcrak() in a shared object

        int (*func)(const char *msg) = NULL;


        if(argc != 4) {

                fprintf(stdout, "Usage: %s <SO-PATH> <FUNCTION-NAME> <MESSAGE>\n", argv[0]);

                return 0;

        }

        // XXX: Load a library

        if((dl = dlopen(argv[1], RTLD_LAZY)) == NULL) {

                fprintf(stderr,

                        "[%s] Can not load library: %s\n", __FILE__, argv[1]);

                goto failed;

        }


        // XXX: Map the function by loaded library

        if((func = (int (*)(const char *))dlsym(dl, argv[2])) == NULL) {

                fprintf(stderr,

                        "[%s] No such %s() function from the library.\n",

                        __FILE__, argv[2]);

                goto failed;

        }

        // XXX: Function-call using function pointer

        ret = func(argv[3]);


failed:

        if(dl != NULL)

                dlclose(dl); // XXX: Resource free

        return 0;

}



위 소스코드는 먼저 특정 라이브러리에서 참조하고자 하는 함수의 원형을 정의하는데 이를 함수포인터 형태로 정의하도록 합니다. 그 후 특정 경로에 있는 공유 라이브러리를 open(dlopen)하고 라이브러리 안에 정의된 함수를 찾아 맵핑(dlsym)합니다. 함수 맵핑 후 반환받은 주소는 해당 함수가 존재하는 주소이므로 함수포인터 형태로 이를 실행 할 수 있습니다. 위 소스코드를 컴파일 하고 실행해보았습니다. 


[그림 3] dlopen과 dlsym으로 특정 공유라이브러리에 구현된 함수 실행


컴파일 시 dlopen()을 사용할 수 있도록 DL 라이브러리를 참조하도록 하고, 프로그램 실행 시 라이브러리 경로와 라이브러리에 구현된 함수 이름, 함수 실행 시 전달할 인자 정보를 포함하도록 했습니다.

프로그램은 정상적으로 실행되었고, ldd로 컴파일 된 파일을 분석 한 결과 DL 라이브러리가 기본 참조되도록 만들어져 있는 것을 알 수 있습니다.

dlopen()이나 dlsym()과 같은 함수 사용법은 Windows OS의 API를 다뤄보셨던분들에게 LoadLibrary() 나 GetProcAddress() API의 인터페이스와 비슷하여 친숙함을 느끼실 수도 있을 것 같습니다. 실제 Windows OS에서도 특정 DLL파일을 열고 내부에 구현되어 있는 API를 실행 할 때에 위와 같은 인터페이스의 API를 사용합니다. 플랫폼 자체가 다르기에 내부 구현 자체는 다르게 되어 있을지라도 인터페이스가 비슷하다는 것을 비교해 볼 수 있도록 아래의 소스코드를 참조 하실 수 있습니다.


/*

* loader.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - Compile & Execute

*  <+> Linux  : gcc -Wall -o loader loader.c -ldl && LD_LIBRARY_PATH=. ./loader

*  <+> Windows: cl loader.c /D"WIN32" && loader.exe

*/

#include <stdio.h>

#ifdef WIN32

#  include <windows.h>

#  define EXT   "dll"

# else

#  define EXT   "so"

#  include <unistd.h>

#  include <dlfcn.h>

#  define LoadLibrary(soname) dlopen(soname, RTLD_LAZY)

#  define GetProcAddress dlsym

#  define FreeLibrary dlclose

#endif


#define SONAME  "teamcrak." EXT

#define FUNCNAME "library_call"


int main(void)

{

        int ret;

        void *dl = NULL;

        int (*func)(void) = NULL;


        if((dl = LoadLibrary(SONAME)) == NULL) {

                fprintf(stderr,

                        "[%s] Can not load library: %s\n", __FILE__, SONAME);

                goto failed;

        }

        fprintf(stdout, "[%s] Loaded a library successfully: %s\n",

                __FILE__, SONAME);


        if((func = (int (*)(void))GetProcAddress(dl, FUNCNAME)) == NULL) {

                fprintf(stderr,

                        "[%s] Not found %s() function from the library.\n",

                        __FILE__, FUNCNAME);


                goto failed;

        }

        fprintf(stdout,

                "[%s] Loaded %s() function from '%s'\n"

                "[%s] %s() address: %p\n" ,

                __FILE__, FUNCNAME, SONAME, __FILE__, FUNCNAME, func);


        fprintf(stdout, "[%s] - FUNCTION CALL START\n", __FILE__);

        ret = func();

        fprintf(stdout, "[%s] - FUNCTION CALL END\n", __FILE__);


        fprintf(stdout, "[%s] Return value: %d\n", __FILE__, ret);

failed:

        if(dl != NULL)

                FreeLibrary(dl);

        return 0;

}



/*

* teamcrak.so.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - Compile

*  <+> Linux  : gcc -Wall -shared -Wl,-soname,teamcrak.so -fPIC -o teamcrak.so teamcrak.so.c

*  <+> Windows: cl teamcrak.so.c /D"WIN32" /TC /link /dll /out:teamcrak.dll

*/

#include <stdio.h>

#include <string.h>

#ifdef WIN32

#  include <io.h>

#  define write _write

#  define TeamCRAK __declspec(dllexport)

#  define COMMENT "Dynamic Link Library"

# else

#  include <unistd.h>

#  define TeamCRAK

#  define COMMENT "Shared Object"

#endif


TeamCRAK int library_call(void)

{

        char *msg = "[*] Welcome to " COMMENT "'s world!\n";

        write(1, msg, strlen(msg));

        return 1337;

}



위 소스코드는 같은 인터페이스를 가진 DL 라이브러리의 함수들과 Windows API를 전처리하여 Windows/Linux 양쪽 플랫폼에서 컴파일과 실행을 할 수 있도록 구현된 소스코드입니다. loader.c는 teamcrak.so / teamcrak.dll 파일을 로드하고 해당 라이브러리에 존재하는 library_call이라는 함수를 실행하도록 합니다. teamcrak.so.c는 컴파일 되어 *.so 형태나 *.dll 파일로 만들어지고 내부에 구현되어 있는 library_call() 함수를 실행하면 플랫폼 환경에 맞도록 특정 문자열을 출력하도록 되어 있습니다.


아래는 위 소스코드를 각각 Linux와 Windows 플랫폼에서 컴파일 하고 실행해 본 화면입니다.


[그림 4] Linux 플랫폼에서 실행한 Shared Library 로더


[그림 5] Windows 플랫폼에서 실행한 DLL 로더


위 테스트 화면을 보면 Linux 플랫폼에서는 "Shared Object"로, Windows 플랫폼에서는 "Dynamic Link Library"로 인식되어 문자열이 출력됩니다. 실제 라이브러리 내부에 구현되어 있는 library_call() 함수에서 반환하는 고정적인 "1337" 정수 값도 온전히 양쪽 플랫폼에서 반환 값으로 인식되는 것을 볼 수 있습니다.


사실 위와 같은 라이브러리의 실행 형태는 악성코드 분석을 포함해 컴파일 된 응용프로그램의 분석 시 매우 유용하게 사용됩니다. 특정 라이브러리 파일에 데이터 인코딩이나 디코딩 로직이 존재할 때 해당 로직만 따로 떼어서 테스트 해 볼 수도 있고 더 많은 분석 과정에서 사용되기도 합니다. 물론 함수 원형에 대한 정보가 정확하지 않아 여러 애로사항이 있는 분석 기법이지만 TeamCR@K에서 수행한 모의해킹 프로젝트 중 이러한 분석 과정을 통한 여러 실 예가 있고, 추후 해당 내용에 대해 정리하여 새 글로 공유하는 기회를 갖도록 하겠습니다.


정대근 보안기술팀장 (A.K.A 1ndr4)

indra@a3security.com


여러분은 "선적재 라이브러리"라는 말을 들어보신적 있으신가요? 선적재(Pre-loaded)된 라이브러리는 다른 로드 된 공유라이브러리보다 우선순위를 가지고 있다는 특징이 있습니다. 그로 인해 특정 함수에 대한 Hooking에 사용되기도 하고 개발자의 디버깅에도 유용하게 사용되고 있습니다. 우선 다음의 페이지에서 선적재 라이브러리에 대해 간략한 설명을 보실 수 있습니다.


Secure Programming for Linux and Unix HOWTO - 3.7. 동적 링크 라이브러리

https://wiki.kldp.org/HOWTO/html/Secure-Programs-HOWTO/dlls.html


페이지 글 중간에 전반적으로 Linux 시스템에서 동작하는 라이브러리의 구조와 함께 ld.so.preload 파일의 특징과 활용법, 그리고 같은 맥락으로 동작하는 LD_PRELOAD 환경변수에 대해 설명하고 있습니다.

(2018년 2월 1일 기준 위 페이지에서 설명하는 LD_RELOAD라는 환경변수는 LD_PRELOAD의 오타로 확인되고 있습니다.)

선 적재 라이브러리가 활용되는 흔적은 strace(system call tracer)에서도 확인할 수 있습니다.


[그림 1] strace로 확인한 선 적재 라이브러리 활용의 흔적


/bin/ls 프로그램 실행 시작 직후 시스템 내부에서는 /etc/ld.so.preload 파일의 존재 여부를 확인하고 있습니다. /etc/ld.so.preload라는 파일은 어떠한 파일일까요? ld.so의 man 페이지에서는 다음과 같이 설명하고 있습니다.


LD.SO(8)                   Linux Programmer’s Manual                  LD.SO(8)


NAME

       ld.so, ld-linux.so* - dynamic linker/loader


DESCRIPTION

       The  programs ld.so and ld-linux.so* find and load the shared libraries

       needed by a program, prepare the program to run, and then run it.

>> snip <<

ENVIRONMENT

       There are four important environment variables.

>> snip <<
       LD_PRELOAD
              A whitespace-separated list of additional,  user-specified,  ELF
              shared  libraries  to  be loaded before all others.  This can be
              used  to  selectively  override  functions   in   other   shared
              libraries.   For  set-user-ID/set-group-ID  ELF  binaries,  only
              libraries in the standard search directories that are also  set-
              user-ID will be loaded.
>>snip <<
FILES
>> snip <<
       /etc/ld.so.preload
              File  containing  a  whitespace  separated  list  of  ELF shared
              libraries to be loaded before the program.
       lib*.so*
              shared libraries
...


Secure Programming for Linux and Unix HOWTO에서도 설명하는 것 처럼 ld.soman 페이지에 따르면 선적재 라이브러리를 활용할 수 있는 방법에 대해 2가지로 설명되고 있습니다. 첫번째는 환경변수인 LD_PRELOAD 값의 설정을 통해 특정 라이브러리를 선적재 할 수 있고, 두번째로는 /etc/ld.so.preload 파일을 이용해 선적재 라이브러리를 지정할 수 있다고 합니다.


LD_PRELOAD 환경변수를 이용한 선적재 라이브러리 활용은 2014년에 저희 TeamCR@K 블로그에 올린 zygote 프로세스에 LD_PRELOAD 환경변수 삽입하기 편에도 일부 언급되어 있어 본 글에서는 ld.so.preload를 중점으로 설명하고자 합니다. Linux에서 파일실행에 의해 프로세스 화 된 시점의 기본적인 플랫폼 환경은 GLIBC가 근간이 되고 있습니다. GLIBC는 실행된 프로그램과 Kernel 중간에 위치하면서 프로그램 실행에 여러가지 관여를 합니다. GLIBC 소스코드를 다운로드 받아 분석해보면 ld.so.preload 의 동작 구성도 엿 볼 수 있습니다.


아래의 소스코드는 GLIBC 소스코드 트리에서 elf/rtld.c 파일의 일부 내용입니다.


[그림 2] GLIBC 2.9 버전에서 ld.so.preload 를 이용하여 선적재 라이브러리를 구성하는 로직


GLIBC 소스코드 트리 중 elf/rtld.c 소스코드를 참조하면 do_preload() 에 의해 라이브러리의 선적재하는 과정을 알 수 있습니다. 위에 언급되어 있는 것 처럼 선적재 라이브러리는 다른 라이브러리에 우선한다고 했습니다. 그 말은 같은 함수가 로드되는 다른 라이브러리에 구현이 되어 있더라도 선적재 라이브러리에 구현된 함수가 우선순위를 가지고 있는 것을 말하며, 해당 특성을 이용하여 Wrapping Function의 구현과 같은 방법으로 특정 함수를 Hooking할 수 있습니다.


음의 코드는 setuid() 함수를 Hooking하는 코드입니다.


/*

* libsetuid.so.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example code for wrapped function of setuid()

*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <dlfcn.h>


static int (*orig_setuid)(uid_t uid);


int setuid(uid_t uid)

{

        int ret = 0;

        void *dl = NULL;

        char exe[1024] = {0,};


        readlink("/proc/self/exe", exe, sizeof(exe));

        fprintf(stdout, "[DEBUG] Hooked setuid(%d) by '%s'\n", uid, exe);

        if((dl = dlopen("/lib/libc.so.6", RTLD_LAZY)) != NULL) {

                if((orig_setuid = dlsym(dl, "setuid")) != NULL) {

                        ret = orig_setuid(uid);

                }

                dlclose(dl);

        }

        return ret;

}



GLIBC 내에 구현된 setuid() 함수를 라이브러리 직접 참조를 통해 불러오고 실행하기 위해 DL 라이브러리를 사용합니다. 또한 이를 위해 setuid() 함수의 원형을 함수포인터 형태로 정의합니다. setuid()가 실행되면 readlink()를 통해 현재 해당 함수를 실행하도록 한 프로그램의 경로를 받아오고 이를 디버그 메시지를 통해 사용자에게 전달합니다. 이 후 dlopen()dlsym()을 통해 GLIBC에 구현된 setuid() 함수를 실행할 수 있도록 구현되어 있습니다.


위 소스코드를 컴파일 후 /bin/su를 타겟 대상으로 삼고 테스트를 진행하였습니다. 패스워드 인증을 통해 사용자 권한을 변경할 수 있도록 한 /bin/su는 setuid()를 사용할 것이며, 프로그램 실행 초기에 /etc/ld.so.preload가 존재한다면 파일 안에 정의된 경로의 라이브러리를 선적재 할 것입니다. 내부적으로 setuid() 수행 시 선적재 된 라이브러리의 영향을 받는다면 디버그 메시지를 통해 wrapped function이 실행되는 것을 확인 할 수 있을 것 입니다.


[그림 3] ld.so.preload를 이용하여 setuid() 함수 hooking 가능 확인


위 화면에서 우리는 중요한 포인트 하나를 알 수 있습니다.


/etc/ld.so.preload는 상위 권한의 setuid bit가 설정된 프로그램에도 정상 동작을 보장하나, 같은 목적을 가진 LD_PRELOAD 환경변수를 통한 라이브러리 선적재의 경우 상위 권한의 setuid bit가 설정된 프로그램과 같이 실행되면 정상 실행이 되지 않습니다. 이는 사용자 누구나가 변경이 가능한 환경변수의 경우 기본적으로 신뢰할 수 없는 값으로 판단하여 처리하도록 설계 한 보안의 가장 기본적인 1원칙이 그 이유가 아닐까요?


[그림 4] setuid bit가 설정된 파일 실행 시 무시되는 LD_PRELOAD 환경변수


지금까지 Shared Library 특성에 대해서 알아보았는데요. 이에 더불어 기존에 알아보았던 Constructor의 개념도 선적재 라이브러리와 함께 활용될수는 없을까요? 이를 알아보기 위해 한 가지 더 테스트를 해 보기로 했습니다.


/*

* libmypriv.so.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example code for constructor of shared library

*/

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <dlfcn.h>


int __attribute__((constructor)) init(void)

{

        fprintf(stdout, "[DEBUG] UID: %d / GID: %d / EUID: %d / EGID: %d\n",

                getuid(), getgid(), geteuid(), getegid());

        return 0;

}


Constructor 역할을 하기 위한 함수를 구현했는데, 이는 사용자의 User-ID/Group-IDEffective-User-ID/Effective-Group-ID를 출력하고 리턴하는 함수입니다. 일반적으로 프로그램 실행 시 권한 관리를 위해 User-ID 권한과 Effective-User-ID 권한을 따로 분리하는데, 해당 개념을 이해하고 있으면 향후 setuid bit가 설정된 프로그램 분석에 많은 도움을 줍니다.


위 코드를 공유 라이브러리 형태로 컴파일 하고 선적재 하도록 한 후 setuid bit가 설정된 프로그램을 실행하면 어떻게 될까요?

 

[그림 5] setuid bit가 설정된 파일 실행 시에도 유효한 Pre-loaded Library 및 Constructor 속성


/bin/su 실행 시 libmypriv.so 가 선적재되고, Constructor 속성으로 인해 패스워드를 입력 받기 전 init() 함수가 호출되어 해당 함수가 실행되는 것을 볼 수 있습니다. 


[그림 6] System Call Tracer로 확인한 Pre-loaded Library와 Library의 Constructor 속성의 정상 동작


System Call Tracer를 통해 확인한 경우 ptrace()의 영향으로 인해 파일의 setuid bit가 무시되어 getuid() 계열의 함수 반환 값이 일반 사용자 User-ID로 표현되어 있지만, 선적재 된 라이브러리의 함수인 init() 함수가 Constructor 속성에 의해 프로그램 시작 초기에 실행된다는 것을 알 수 있습니다.


지금까지 Linux 환경에서 가능한 ConstructorPre-loaded Libraries에 대해 알아보았는데요. 실제 이것이 어떻게 Exploit Techniques와 연결될 수 있는지 그 실 예를 다음 편에서 알아보도록 하겠습니다.