Search

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 (구조론적 고찰) 편을 기대해주세요.


감사합니다.