티스토리 뷰

최종 수정: 2014-06-27


안녕하세요. Hackability 입니다.


이번에 포스팅할 내용은 지난번 만든 쉘 코드를 좀 더 다이어트 시켜 보려고 합니다.


쉘 코드를 다이어트 시키는 이유는 작은 버퍼에도 우리가 만든 쉘 코드를 넣을 수 있는 장점이 있고 쉘 코드의 특성상 널 바이트 (0x00)을 제거 시켜줘야 하기 때문입니다.


우리가 진행하고자 하는 동작을 처음부터 좀 더 자세하게 보도록 하겠습니다. 일반적으로 공격의 순서는 (1) 프로그램 취약점 발견, (2) 취약점을 이용한 익스플로잇 코드를 통해 실행 흐름 (EIP)를 조작 (3) 이후, 메모리에 해커가 원하는 동작을 하는 쉘 코드를 메모리에 저장 (4) 취약한 프로그램 실행 후, 익스플로잇 코드를 실행하면 프로그램 흐름이 쉘 코드의 위치로 변경되고 쉘 코드가 실행되면서 공격자가 원하는 코드를 실행


먼저, 아래와 같은 취약한 프로그램이 있다고 가정해봅니다.


1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
 
int main (int argc, char **argv)
{
    char buffer[16];
    strcpy(buffer, argv[1]);
 
    printf("%s", buffer);
    return 0;
}


위 프로그램은 사용자가 프로그램에 입력한 값을 buffer로 복사한 뒤, 출력해주는 프로그램입니다. 이 프로그램이 실행 했을 때, 간단히 메모리 구조가 다음과 같이 이루어 집니다.



우리가 main 이라는 함수를 사용하기 때문에 메모리에는 main에서 사용하는 변수들을 저장하기 위한 공간과 main 함수가 끝낫을 때 돌아가기 위한 주소인 리턴 주소가 순차적으로 저장되어 있습니다. (자세한 메모리 구조는 조금씩 늘려 나가도록 하겠습니다.)


char buffer[16]의 의미는 최대 15개의 문자와 1개의 NULL 값을 기대하는 버퍼 입니다. 사용자가 15개의 입력을 할 경우 알아서 버퍼의 마지막에 NULL을 추가하여 총 16개의 문자열이 이루어지게 됩니다. 그림으로 표현하면 다음과 같습니다.



(* 현재는 설명을 위해 간단하게 메모리 구조를 그렷고, 필요에 따라서 점점 더 자세하게 설명 드리도록 하겠습니다.)


하지만 여기서 우리가 좀더 많은 A를 입력하면 어떻게 될까요? 예를들어 100개의 A를 입력한다고 가정해봅시다. 그러면 아래와 같이 될 것 입니다.


만약 우리가 100개를 입력해도 프로그램에서 알아서 짤라서 15개를 쓰는게 아니라 실제 메모리에는 100개가 입력되어 중요한 값이 저장되어 있는 리턴 주소가 저장되어 있는 위치까지 AAAA로 덮어 써져 버렸습니다. 만약 이렇게 프로그램을 실행 시킨다면 strcpy 가 실행된 후, 메모리가 저렇게 변경이 되고 main 함수가 종료되어 리턴주소 값이 저장되어 있는 내용을 읽어 그 값을 주소로 가려고 할 때 에러가 발생할 것입니다. A는 16진수로 0x41 이기 때문에 리턴 주소가 0x41414141 (AAAA)이 되어 이 주소를 실행 할 수 없기 때문입니다. (일반적이라면 코드 영역의 주소를 가리키고 있어야 하는데 0x41414141은 코드 영역의 주소가 아닙니다.)


위의 예제를 통해, 버퍼 오버플로우의 조건과 메모리에서 어떤식으로 동작하는지 간단히 살펴 보았습니다. 이제 우리가 이전에 만든 exit(0); 하는 쉘 코드를 위의 취약한 프로그램에 대한 내용과 연결해보도록 하겠습니다.


(지난번에 만들었던 exit(0);의 쉘 코드)

\xb8 \x01 \x00 \x00 \x00    mov eax, 0x1

\xbb \x00 \x00 \x00 \x00    mov ebx, 0x0

\xcd \x80                   int 0x80 


위의 취약한 프로그램과 위의 쉘 코드를 이용하여 익스플로잇 코드 + 쉘 코드를 만든다고 할 때 다음과 같이 진행이 됩니다. 입력을 다음과 같이 준다고 가정합니다.


./test_program AAAAAAAAAAAAAAAA + shellcode 주소 + shellcode



처음의 A 16개에 의해 지역 변수인 Buffer의 내용이 메모리에서 꽉 차게 되고 그 다음에 shellcode 의 주소가 리턴 주소를 저장하는 부분을 덮어 쓰게 되며 더 위쪽에다가 쉘 코드를 쓰게 됩니다. strcpy에서 문자열 길이를 체크 하지 않기 때문에 발생하는 버퍼 오버플로우로 strcpy 가 끝난 뒤에는 메모리가 위와 같이 변경되어 있고, main 함수가 끝날 때 프로그램은 리턴 주소 (쉘 코드 주소로 변경됨)를 읽고 해당 주소를 실행 시키는데 해당 주소로 가면 우리의 쉘 코드가 실행이 되게 됩니다.


꽤 장황하게 설명했는데, 이 부분이 쉘 코드 다이어트랑 무슨 상관이 있을까요? 첫 번째로는 우리의 쉘 코드를 Buffer 안에 넣어야 할 수도 있습니다. Buffer가 엄청 크면 큰 쉘코드도 상관 없지만 우리가 사용할 수 있는 Buffer 가 작다면 동일한 역할을 하는 작은 쉘 코드를 만드는 것이 중요할 수 있습니다. 두 번째로는 우리가 쉘 코드를 메모리에 담기 위해 사용하는 변수가 문자 배열 이기 때문에 strcpy 할 때, NULL Byte (0x00)을 만나면 뒤의 내용이 모두 끊겨 버리게 되어 우리가 원하는 내용이 메모리에 안써질 수 있습니다. 따라서 쉘 코드 안에 있는 0x00을 제거해 주어야 합니다.


요약하면, 쉘 코드를 다이어트 해야 하는 이유는 우리가 쉘 코드를 넣어야 할 장소가 작을 수도 있기 때문이고, NULL Byte와 같은 문자를 지워주기 위함입니다.

(NULL Byte와 같은 문자라고 표현한 이유는 추후에 프로그램 마다 짤리는 문자들이 있을 수 있기 때문입니다. 본 포스팅에서는 간단히 어셈 명령 변경을 통한 NULL Byte 제거를 하고 추후에 좀더 tricky 한 실행 중 NULL Byte 삽입을 통한 해결 방법을 소개 드리겠습니다.)


여기 까지 서론을 마치고 이제 실습을 통해 다이어트를 실행 해보도록 하겠습니다. 위에서 언급했듯이 여기서는 어셈 명령어 변경을 통해 NULL Byte 우회 및 쉘 코드 크기를 줄여 보도록 하겠습니다.


먼저 다시 한번 기존의 쉘 코드를 확인해보겠습니다.


(지난번에 만들었던 exit(0);의 쉘 코드)

\xb8 \x01 \x00 \x00 \x00    mov eax, 0x1

\xbb \x00 \x00 \x00 \x00    mov ebx, 0x0

\xcd \x80                   int 0x80 


eax와 ebx는 모두 32 bit (4바이트) register 입니다. 우리가 저장해야 하는 값은 단순히 0 또는 1인데 너무 큰 레지스터를 사용하는 것 같습니다. 16 bit 레지스터를 이용하여 변경해보도록 하겠습니다. eax는 16 레지스터로도 접근이 가능합니다. al (eax low)와 ah (eax high)이며 ebx도 마찬가지로 bl (ebx low)와 bh (ebx high)로 접근이 가능합니다. 따라서, 기존의 02_001.asm 코드를 16 bit 레지스터를 이용하여 다시 제작하면 다음과 같습니다.


03_001.asm
1
2
3
4
5
6
[SECTION .text]
BITS 32
 
mov al, 1
mov bl, 0
int 0x80


(잠자고 있는 리눅스를 깨워주세요~)


기존의 12 BYTE의 쉘 코드가 6 바이트로 줄어 들었습니다. 하지만 아직도 NULL BYTE가 보이는 군요. ebx에 0을 넣는데 NULL BYTE를 쓰지 않고 ebx를 0을 만드는 방법은 xor 을 이용하면 됩니다. xor은 같은 비트끼리는 0 다른 비트 끼리는 1을 만드는 Exclusive OR 연산 입니다. 따라서 위 코드는 다음과 같이 변경할 수 있습니다.



오오.... NULL Byte (0x00)을 쓰지 않고 ebx에 0을 넣었습니다. 여기서 우리가 한 가지 더 신경써야 하는 부분은 eax 부분입니다. 우리가 eax의 하위 2바이트에는 1을 넣었기 때문에 별 문제가 없지만 상위 2바이트 (ah)에는 어떤 값이 들어가 있는지 알 수 없습니다. 따라서, 레지스터를 쓸 때는 항상 초기화를 하고 사용하는 것이 좀 더 신뢰성 있는 쉘코드를 만들 수 있습니다.


mov eax, 1 은 5바이트 이지만 xor eax, eax, 후 mov al, 0x01 은 4바이트 이기 때문에 1바이트를 줄이면서 NULL Byte도 제거할 수 있을것 같습니다.



후아..... 여기까지 쉘 코드 크기를 줄이면서 암덩어리인 널 바이트까지 제거 하였습니다. 8바이트로 뭔가 작업을 하는 (아직은 exit(0) 이지만...) 것도 충분해 보이지만 좀더 허리를 쫄라 보겠습니다.



대입 명령을 쓰지 않고 increment (1 증가 명령)을 통해 1 바이트를 더 줄일 수 있었습니다. 어떤 명령을 쓰든 결과가 동일하면 되기 때문에 퍼즐과 같은 재미가 있네요. 더욱 변태스러운 퍼즐을 원하는 분들은 Return Oriented Programming (without return)이나 Q: Exploit Hardening Made Easy 등을 참고하시면 좋을 것 같습니다 -_-;


자 이제 우리가 만든 쉘 코드를 통해 정상적으로 실행되는지 확인해보도록 하겠습니다. 2편에서 만들었던 shell_test.c 를 가져와서 shell_code 부분을 우리가 만든 부분으로 수정 한 뒤, 다시 컴파일 하여 실행을 해봅니다.


실행 하면 2에서와 동일한 결과로 그냥 다음 프롬프트만 껌뻑 껌뻑 거립니다. exit(0); 가 잘 실행 되었나 봅니다. 라고 말하면 그걸 어떻게 알아? 라고 말씀하실것 같아서 이번에는 정말 exit(0);가 실행되는지 확인해 보도록 하겠습니다. -_-;;


제가 예전에 리눅스 구조와 이해를 주제로 포스팅 하려다가 묵혀둔게 있는데 그 중 하나가 strace라는 녀석 입니다. strace는 리눅스에서 슈퍼 파워를 자랑하는 트레이스 도구로써, 프로세스와 리눅스 커널 사이의 시스템 호출들을 추적하는 훌륭한 도구 입니다. 워낙 강력한 툴이기 때문에 추후에 기회가 되면 이 부분도 같이 연재를 하면 좋을것 같습니다. (쉘 코드 작성 먼저...!!)


strace 를 이용하여 shell_test에서 실행하는 시스템 콜들을 추적해 봅니다.



실행 할때는 아무것도 실행이 안되는 것 같았는데 뭔가 많은 실행이 있어 보입니다. 위의 많은 코드들은 프로그램 실행 시 자동으로 실행되는 내용이고 실제 코드를 통해 실행되는 내용은 제일 마지막 줄의 _exit(0) 코드입니다. 따라서 우리의 의도 대로 _exit(0)가 실제로 호출됫음을 알 수 있습니다.


여기까지 "03. 쉘 코드 다이어트 시키기" 내용이였습니다. 


이번 내용을 통해 작성된 쉘 코드를 좀더 정교하게 만들고 생성된 쉘코드가 정상적으로 동작하는지 확인하였습니다.


다음에는 쉘 창에서 뭔가 좀 출력될 수 있는 내용으로 쉘 코드를 작성해 보도록 하겠습니다. :P

댓글
댓글쓰기 폼