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.html - How 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이 되겠습니다.
감사합니다.