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