stack-based buffer overflow의 기본 아이디어
[buffer] [sfp] [ret]
버퍼공간에는 내가 원하는 코드(쉘코드)를 넣어놓고, 버퍼 아래쪽에 위치한 ret을 내가 원하는 코드가 위치한 주소로
바꿔놓는다.
1. c코드 작성
2. 어셈블리 분석
shellcode.c의 어셈블리 코드를 분석하기 위해 static 옵션으로 컴파일한 후 gdb로 실행한다
0x08048e47 <+3>: and $0xfffffff0,%esp
이부분은 esp 레지스터의 값을 16byte 단위로 align 하는 부분이다. 16byte 단위로 alignment 해주는 이유는 SIME
instruction 사용 시 메모리 내 값들이 16byte 단위로 정렬되어 있어야 하기 때문이다.
참고 : http://stackoverflow.com/questions/4175281/what-does-it-mean-to-align-the-stack
위의 main함수에서 execve함수 호출 바로 전까지의 스택 변화는 아래 그림과 같다
execve함수 호출 시 인자로 name[0], name, NULL을 전달하는데, 스택에 들어갈 때에는 NULL, name, name[0]
순으로 들어감을 확인할 수 있다. 이 상태에서 call execve 명령어를 실행할 경우 돌아올 return address가 스택에
push되고, instruction pointer가 execve함수의 시작위치로 변할 것이다.
execve 함수를 디스어셈블해보면 다음과 같다.
0x0806c482 <+18>: call *0x80ea9f0
indirect call을 수행하는 부분인데, 0x80ea9f0 메모리 주소 내에는 0x0806f040이 들어있다. 따라서 위의 call 명령어는
call 0x0806f040과 같을 것이고, 이 주소로 disas 명령어를 수행해보면 이 주소는 _dl_sysinfo_int80 함수의 주소이고
이 함수에서는 int $0x80 명령어를 수행하여 system call을 호출하는 것을 알 수 있다. int $0x80을 바로 호출하지 않고
이렇게 중간에 _dl_sysinfo_int80을 거치는 이유는 아래 페이지 참조.
http://iswwwup.com/t/9f5cf4801ace/c-whats-the-purpose-of-dl-sysinfo-int80.html
요약하면 system call을 호출할 때 사용하는 명령어 중에 int 0x80이 가장 오래된 방법이고, 그 이후로 인텔의
sysenter, amd의 syscall 등이 생겨났는데 이 때문에 _dl_sysinfo_int80이라는 함수를 중간에 둔다는 것 같다.
아무튼 call *0x80ea9f0이 system call을 호출하는 부분이고, 그 전까지가 system call을 호출하면서 전달할 인자들을
eax, ebx, ecx, edx 레지스터들에게 전달하는 부분인데, 이 부분에 대한 스택 변화를 분석하면 아래와 같다.
리눅스 커널에서 제공하는 system call들은 각각의 고유한 번호를 가지고 있는데 execve 함수의 경우 0xb이며, 위의
그림에서 eax에 0xb가 들어감을 알 수 있다. 그리고 ebx부터 차례로 execve의 인자가 들어가는데 ebx에는
"/bin/sh" 의 주소가 들어가고, ecx에는 이 문자열 주소가 저장된 스택 공간의 주소, 그리고 edx에는 0이 들어간다.
3. 쉘코드 작성
이로부터 쉘코드를 만들기 위해 필요한 절차는 아래와 같다.
1. "/bin/sh" 문자열을 메모리 어딘가에 저장 (문자열이 저장된 메모리 공간의 주소를 주소1이라 하자)
2. 주소1을 메모리 어딘가에 저장 (주소1이 저장된 메모리 공간의 주소를 주소2라 하자)
3. eax 레지스터에 0xb 저장
4. ebx 레지스터에 주소1 저장
5. ecx 레지스터에 주소2 저장
6. edx 레지스터에 0 저장
7. int $0x80 명령어 수행
이를 어셈블리 코드로 작성하면 아래와 같다
위 코드를 컴파일 한 후 gdb로 분석해보면 아래와 같다.
0x080483ed <+0>: push %ebp
0x080483ee <+1>: mov %esp,%ebp
위 두 명령어는 procedure prelude에 해당하는 부분이다.
0x080483f0 <+3>: jmp 0x804840f <main+34>
0x804840f로 jmp하는데, 0x804840f에는 call 명령어가 위치한다.
0x0804840f <+34>: call 0x80483f2 <main+5>
0x80483f2로 다시 이동. 0x80483f2에는 pop 명령어가 위치한다. jmp 명령어와 call 명령어를 사용한 이유는 "/bin/sh"
문자열의 주소를 얻기 위해서인데, call 명령어 밑의 0x08048414 주소에는 "/bin/sh"문자열이 들어 있다. call 명령어
수행 시 return address에 해당하는 0x08048414 주소를 스택에 push하는데, 이를 이용해 "/bin/sh"문자열이 들어
있는 메모리 공간의 주소를 알 수 있다.
0x080483f2 <+5>: pop %esi
스택에 저장된 "/bin/sh" 문자열의 주소 0x08048414를 esi 레지스터에 저장한다. (esi : 주소1)
0x080483f3 <+6>: mov %esi,0x8(%esi)
esi레지스터에 저장된 주소1을 (주소1+8)에 해당하는 공간에 저장한다. 따라서 이 명령어를 수행하고 나면
"/bin/sh"문자열 뒤에는 0x08048414가 저장된다. 이렇게 하는 이유는 주소2를 얻기 위해서이다.
0x080483f6 <+9>: movb $0x0,0x7(%esi)
0x080483fa <+13>: movb $0x0,0xc(%esi)
주소1+7 위치와 주소1+12 위치에 각각 0을 저장한다. 따라서 "/bin/sh" 문자열 바로 뒤에 0을 넣고, 또 주소1이 저장된
공간의 마지막에 0을 넣는다. 따라서 0x08048414 번지부터 메모리 값은 '/', 'b', 'i', 'n', '/', 's', 'h', 0x0, 0x08, 0x04,
0x84, 0x14, 0x0 이 된다.
0x080483fe <+17>: mov $0xb,%eax
eax에 execve 함수의 system call number인 0xb를 저장한다. (eax : 0xb)
0x08048403 <+22>: mov %esi,%ebx
esi레지스터의 값(주소1)을 ebx레지스터에 저장한다. (ebx : 주소1)
0x08048405 <+24>: lea 0x8(%esi),%ecx
esi레지스터의 값+8 위치에 있는 content의 주소를 ecx 레지스터에 저장한다. 즉, 주소1이 저장된 메모리 공간의 주
소를 ecx 레지스터에 넣게 된다. (ecx : 주소2)
0x08048408 <+27>: mov $0x0,%edx
edx레지스터에 0값을 저장한다. (edx : 0)
0x0804840d <+32>: int $0x80
system call을 호출한다.
이제 objdump -d 명령어를 이용해 이 어셈블리 코드의 기계어 코드를 얻는다.
procedure prelude 부분은 건너뛰고 0x80483f0 주소부터 0x804841a 주소 까지의 값을 복사하면 된다.
컴파일 및 실행 결과는 아래와 같다.
참고로 실행환경이 최신버전이라 gcc 컴파일 시 -fno-stack-protector 옵션과 -z execstack 옵션을 주었다.
-fno-stack-protector 옵션은 gcc가 버퍼 오버플로우 체크를 위한 별도의 코드를 삽입하지 않게 한다.
-z execstack 옵션은 스택을 executable하도록 설정한다. (스택의 데이터를 실행가능하도록 설정)
4. 널바이트 제거
일단은 쉘을 실행시키는데 성공했으나 버퍼 오버플로우 공격을 하기 위해서는 만든 쉘코드의 널바이트를 제거해야
한다. 버퍼 오버플로우 공격의 경우 strcpy와 같은 문자열 복사 함수를 사용해 버퍼에 쉘코드를 복사해야 하는데, 만든
쉘코드에 널바이트가 들어있으면 문자열 복사 함수에서 널바이트까지만 복사하고 뒷부분을 복사하지 않기 때문이다.
널바이트 제거를 위해서 어셈블리 코드를 아래와 같이 수정한다.
기존 코드중 변경해야 하는 부분1 :
0x080483f6 <+9>: movb $0x0,0x7(%esi)
0x080483fa <+13>: movb $0x0,0xc(%esi)
0x080483fe <+17>: mov $0xb,%eax
변경된 코드1 :
0x080483f6 <+9>: xor %eax,%eax
0x080483f8 <+11>: mov %al,0x7(%esi)
0x080483fb <+14>: mov %al,0xc(%esi)
0x080483fe <+17>: mov $0xb,%al
0x0을 esi 레지스터에 대입하는 대신 eax레지스터를 xor 명령어를 이용해 0으로 초기화한뒤, al 레지스터의 값을 esi
레지스터에 대입하는 방식으로 명령어의 널바이트를 제거했다. 또 0xb를 eax에 대입하는대신 al에 대입하게 해서
널바이트를 제거했다.
기존 코드중 변경해야 하는 부분2 :
0x08048408 <+27>: mov $0x0,%edx
변경된 코드2 :
0x08048405 <+24>: xor %edx,%edx
edx레지스터에 0을 대입하는 대신 edx 레지스터를 xor 연산하여 0으로 초기화한다. 이렇게 어셈블리 코드를 수정한
뒤 objdump -d 명령어를 사용해보면 이전과는 달리 명령어에 널바이트가 존재하지 않음을 알 수 있다.
이제 아까와 같은 방법으로 기계어 코드를 복사한다.
맨 마지막에 \xff를 추가로 6번 덧붙였는데, 쉘코드가 실행될 때 "/bin/sh" 뒤에 0, 주소1, 0 을 집어넣기 때문에 미리
이자리에 6바이트 공간을 덧붙여놨다.
이제 아까와 같은 방법으로 컴파일 후 실행해보면 결과는 아래와 같다.
'Security > 공개글' 카테고리의 다른 글
fail open과 fail closed에 대해서 (1) | 2016.03.17 |
---|---|
암호화의 궁극적인 목적(내 생각) (0) | 2015.06.28 |
access control 관점에서 본 fine grained vs coarse grained (0) | 2015.03.02 |
ECB 모드가 취약한 이유? (0) | 2014.06.28 |
diffusion (0) | 2014.06.28 |