5. Buffer overflow의 이해
버퍼(buffer)
- 시스템이 연산 작업을 하는데 있어 필요한 데이터를 일시적으로 저장하는 메모리 상의 저장 공간
- 대부분의 프로그램에서는 버퍼를 스택에 생성함.
- 스택은 함수 내에서 선언한 지역 변수가 저장되고 함수가 끝나면 반환됨.
buffer overflow
- 미리 준비된 버퍼에 버퍼의 크기보다 큰 데이터를 쓸 때 발생함.
- 공격자가 메모리 상의 임의의 위치에다 원하는 코드를 저장시켜 놓고 return address가 저장되어 있는 지점에 그 코드의 주소를 집어 넣음으로써 EIP에 공격자의 코드가 있는 곳의 주소가 들어가게 해 공격을 하는 방법.
void function(int a, int b, int c){
char buffer1[15];
char buffer2[10];
}
void main(){
function(1, 2, 3);
}
<simple.c>
- function()함수 내에서 정의한 buffer1[15]와 buffer2[10]의 버퍼가 있고 여기에는 40바이트의 버퍼가 할당되어 있음.
strcpy(buffer2, receive_from_client);
- 이 코드는 client로부터 수신한 데이터를 buffer2와 buffer1에 복사함.
- strcpy함수는 길이 체크가 불가하므로 receive_from_client 안에 들어있는 데이터에서 NULL(\0)를 만날 때까지 복사를 함.
- <그림 14>와 같은 스택 구조에서 45~48바이트 위치에 있는 return address도 조작해줘야 하고 공격 코드도 넣어줘야 함.
- 클라이언트인 공격자가 전송하는 데이터는 receive_from_client에 저장되어 버퍼에 복사됨. (그 데이터가 <그림 15>처럼 구성하여 전송한다고 가정)
- strcpy가 호출되어 receive_from_client라 buffer2에 복사가 될 것을 예상하면 <그림 16>과 같이 매칭됨.
- strcpy가 호출되고 나면 스택 안의 데이터는 <그림 17>처럼 됨.
- <그림 17>은 receive_from_client의 데이터를 버퍼에 복사한 후의 모습.
- <그림 16>에서 만들어낸 데이터와 순서에 있어 약간의 차이가 있음.
Byte order 방식
- 바이트 정렬 방식 때문에 데이터가 저장되는 순서가 바뀜.
- 현존하는 시스템들이 가지는 바이트 순서(byte order)에는 big endian 방식과 little endian 방식이 있음.
big endian 방식
- 바이트 순서가 낮은 메모리 주소에서 높은 메모리 주소로 되어 있음.
- IBM 370 컴퓨터, RISC 기반의 컴퓨터, 모토로라의 마이크로프로세서 등
- ex. 74E3FF59 → 74E3FF59
- 한 바이트 내에서 bit의 순서
little endian 방식
- 바이트 순서가 높은 메모리 주소에서 낮은 메모리 주소로 되어 있음.
- 일반적인 IBM 호환 시스템, 알파 칩의 시스템 등
- ex. 74E3FF59 → 59FFE374
(더하거나 빼는 셈을 할 때 낮은 메모리 주소 영역의 변화는 수의 크기 변화에서 더 적기 때문에 저장 순서를 뒤집음)
- return address 값을 넣을 때 바이트 순서를 뒤집어서 넣어야 함.
- <그림 17>처럼 return address가 변경되었고 실제 명령이 들어 있는 코드는 그 위에 있음. (이 시점까지는 에러 발생 x)
- 함수 실행이 끝나고 ret instruction을 만나면 return address가 있는 위치의 값을 EIP에 넣고 EIP가 가리키는 곳의 명령을 수행하려 함. 이 때 이 주소에 명령어가 들어 있지 않으면 오류 발생.
- <그림 18>의 공격 코드는 exeve("/bin/sh",···). 즉 쉘을 띄우는 것.
- 쉘 코드의 시작 지점은 스택 상의 0xbffffa60
- 함수가 리턴될 때 return address는 EIP에 들어가게 될 것이고 EIP는 0xbffffa60에 있는 exeve("/bin/sh",···)를 수행.
만날 수 있는 문제점 한 가지
- <그림 18>에서의 공격 코드는 총 24바이트 공간 안에 있음.
- return address 위의 버퍼 공간이 쉘 코드를 넣을 만큼 충분하지 않다면 다른 공간을 찾아보는 수 밖에 없음.
- 위의 예에서 function()함수가 사용한 40바이트 + main()함수의 base pointer가 저장되어 있는 4바이트까지 총 44바이트가 낭비되고 있음.
- return address가 EIP에 들어간 다음 40바이트의 스택 공간의 명령을 수행할 수 있도록 해야 함.
- <그림 19>에서는 쉘 코드가 return address 아래(40바이트가 남아 있던 공간)에 있음.
- BOF 정리 (1)의 Step7 과정 실행
쉘 코드 만들기
- 쉘 코드: 쉘(shell)을 실행시키는 바이너리 형태의 기계어 코드(혹은 opcode).
- 쉘: 명령 해석기, 일종의 유저 인터페이스. 사용자의 키보드 입력을 받아 실행 파일을 실행시키거나 커널에 어떤 명령을 내릴 수 있는 대화통로.
- 먼저 쉘을 실행시키는 프로그램을 작성 후 어셈블리 코드를 얻어내고 일부 수정 후 바이너리 형태의 데이터를 만드는 과정을 거침.
쉘 실행 프로그램
- 쉘 상에서 쉘을 실행시키려면 '/bin/sh '라는 명령 사용.
- execve(): 바이너리 형태의 실행 파일이나 스크립트 파일을 실행시키는 함수
- execve()함수로 인해 이 프로그램은 컴파일 되면서 Linux libc와 링크되므로 static library 옵션을 주어 컴파일 해야 함.
Dynamic Link Library & Static Link Library
- Dynamic Link Library: 동적 링크 라이브러리. 응용 프로그램이 공통적으로 사용하는 함수들의 기계어 코드가 운영체제에 라이브러리 형태로 존재함. 리눅스에서는 libc라는 라이브러리에 들어있고 .so 혹은 .a 파일로 존재. 윈도우에서는 DLL 파일로 존재. 운영체제의 버전과 libc의 버전에 따라 호출 형태나 링크 형태가 달라질 수 있음.
- Static Link Library: 자주 쓰는 함수의 기계어 코드를 실행 파일이 직접 가지고 있게 하는 방법.
- sh.c를 static link library로 컴파일하여 sh라는 실행파일 생성.
- objdump를 이용하여 기계어 코드 출력, grep으로 execve()부분 32줄 출력
- 덤프된 코드는 세 개의 column으로 출력되는데 맨 왼쪽은 address, 가운데는 기계어 코드, 맨 오른쪽은 기계어 코드에 대응하는 어셈블리 코드를 나타냄.
- execve()함수 내에서 함수 프롤로그를 하고 함수 호출 이전에 스택에 쌓인 인자값들을 검사함. 이상이 없으면 인터럽트를 발생시켜 시스템 콜(system call, 운영체제와 약속된 행동을 해 달라고 요청)을 함.
- execve()함수는 인터럽트를 발생시키기 전에 범용 레지스터에 각 인자들을 집어넣어줘야 함.
- 인터럽트 0x80은 'Maskable Interrupts'로써 External interrupt 영역에 있음.
- main()함수에서는 execve()를 호출하기 위해서 세 번의 push를 함.
- 제일 처음 '/bin/sh'라는 문자열이 들어있는 곳의 주소를 ebp 레지스터가 가리키는 곳의 -8바이트 지점에 넣음.
- ebp - 4바이트 지점에는 0을 넣음.
- push $0x0
- lea 0xffffffff8(%ebp); %eax
- push %eax
- pushl 0xffffffff8(%ebp)
- call 804c75c <__execve>
쉘을 띄우기 위한 과정 정리
1. 스택에 execve()를 실행하기 위한 인자들을 제대로 배치
2. NULL과 인자값의 포인터를 스택에 넣음
3. 범용 레지스터에 이 값들의 위치 지정
4. interrupt 0x80을 호출하여 system call 12를 호출하게 함
- 위의 코드에서는 'bin/sh'가 data segment에 저장되어 있으므로 data segment의 주소를 이용할 수 있지만 buffer overflow 공격 시점에서는 'bin/sh'가 어느 지점에 저장되어 있다는 것을 기대하기 어려움. 따라서 직접 넣어주어야 함.
NULL의 제거
- 쉘 코드를 실행시키기 위해 보통 아래와 같은 프로그램을 작성함.
또 다른 방법 - 쉘 코드를 int형 배열로 실행
- int형 배열을 사용할 때에는 objdump를 이용하여 얻은 기계어 코드를 little endian 방식으로 재정렬해야 하는 귀찮음이 따르고 대부분의 buffer overflow 공격 방법이 문자열형 데이터 처리의 실수를 이용하는 것이므로 char 형으로 생성하는 것이 편함.
setreuid(0,0)와 exit(0)가 추가된 쉘 코드
- setuid 비트가 set되어 있는 프로그램을 이용하면 root권한을 얻을 수 있음.
- setuid 비트가 set되어 있는 프로그램을 오버플로우시켜 쉘 코드를 실행시키고 루트의 쉘을 얻어낼 방법이 필요.
- 만약 프로그램이 정상 종료되지 않으면 에러 메시지가 로그 파일 혹은 관리자에게 그대로 전달될 수 있으므로 깔끔한 마무리를 위해 exit(0)가 필요함.
- exit(0)에 대한 기계어 코드: "x31\xc0\xb0\x01\xcd\x80"
Buffer Overflow 공격
고전적인 방법
- 쉘 코드가 있는 곳의 address를 추측하는 것.
- 몇 번의 시행착오를 거치면서 쉘이 떨어질 때까지 계속 공격을 시도
- 쉘 코드가 실행되는 확률을 좀 더 높이기 위해, buffer를 채우기 위해 NOP를 사용하는데 보통 NOP는 0x90 값을 많이 씀.
NOP
- No Operation.
- 기계어 코드가 다른 코드와 섞이지 않게 함.
- CPU는 NOP를 만나면 아무런 수행을 하지 않고 유효한 instruction을 만날 때까지 다음 instruction을 찾기 위해 한 바이트씩 EIP를 이동시킴.
- return address에는 NOP로 채워져 있는 0xbffffa30~0xbfffffa4c 사이의 값을 넣어주면 EIP는 return address가 가리키는 지점으로 가지만 NOP가 있기 때문에 한 바이트씩 증가하여 쉘 코드를 만나는 0xbffffa4c에까지 자동으로 이동함.
환경변수를 이용하는 방법
- 공격자는 환경 변수를 하나 만들고 이 환경 변수에다 쉘 코드를 넣은 다음에 취약한 프로그램에 환경변수의 address를 return address에 넣어줌으로써 쉘 코드를 실행할 수 있음.
- overflow 되는 버퍼의 크기가 쉘 코드가 들어갈 만큼 넉넉하지 못할 경우에 매우 유용함.
- eggshell.c는 putenv()함수를 통해 egg 배열에 들어있는 데이터를 EGG라는 환경 변수로 등록함.
- EGG라는 환경 변수가 새로 생성됨으로써 스택 세그먼트의 상단에 등록된 환경 변수들의 크기가 늘어나고 main 함수의 base pointer는 그만큼 낮은 곳에 자리잡음.
- eggshell 실행 후 보여주는 stack pointer 값은 EGG라는 환경 변수가 위치한 범위 내의 어딘가를 가리키고 있음.
- EGG안에는 많은 NOP들이 들어있고, 이 때문에 stack pointer가 쉘 코드의 정확한 시작점을 가리키지 않더라고 instruction pointer가 흘러서 쉘 코드 시작점까지 도달할 수 있게 됨. (return address의 대체값으로 활용)
-vul.c 소스코드에서는 1024바이트의 배열을 만들었지만 gcc가 8바이트 더미를 추가해 총 1032바이트만큼 스택이 확장됨.
- 따라서 이전 함수의 base pointer가 저장되는 4바이트를 고려하면 1036바이트를 채우고 그 이후 4바이트에 return address가 있을 것.
[dalgona@redhat8 bof]$ ./eggshell
esp : 0xbffffa58
sh-2.05b$ ls -al vul
-rwsr-xr-x 1 root root 9588 Jul 27 11:00 vul
sh-2.05b$ id
uid=500(dalgona) gid=500(dalgona) groups=500(dalgona),1001(staff),1002(sysadmin) sh-2.05b$ ./vul `perl -e 'print "A"x1036,"\x58\xfa\xff\xbf"'`
sh-2.05b# id
uid=0(root) gid=500(dalgona) groups=500(dalgona),1001(staff),1002(sysadmin)
sh-2.05b# exit
exit
<buffer overflow 공격 과정>
Return into libc 기법
- 스택 영역의 코드를 실행하지 못하게 하는 non-executable stack 보호 기법이나 일부 IDS(intrusion detection system)에서 네트워크를 통해 쉘 코드가 유입되는 것을 차단하는 보호 기법을 뚫기 위한 방법으로 제안됨.
※ non-executable stack 기법: EIP 레지스터에 stack segment 영역의 주소가 들어가게 되면 에러 메시지를 출력하고 실행을 종료시켜버리거나 에러 메시지 없이 실행을 멈춰버리는 것.
- Return-into-libc 기법은 overflow 공격에 기반함.
- 버퍼를 overflow시켜 return address를 조작하여 실행의 흐름을 libc 영역으로 돌려서 원하는 libc 함수를 수행하게 하는 것.
int main()
{
system();
}
- system()함수의 시작점은 0xb7e42db0
- main()함수가 수행을 마치고 return 할 때 return 지점이 system()함수의 시작 지점이 됨.
- system()함수를 보면 함수 프롤로그가 끝나고 난 다음에 mov 0x8(%ebp), %eax를 수행함. (argument 처리 과정)
- argument가 있는 곳의 address는 ebp+8바이트 지점에 있고 이것을 eax 레지스터에 넣은 후 do_system을 호출함.
-따라서 "/bin/sh"가 있는 곳의 주소는 이 시점에서의 ebp+8이 되어야 함.
- main()함수가 수행을 마치고 return address를 따라 system()함수의 시작점으로 가게 되고 argument를 얻어서 do_system 호출.
- main()함수의 메모리 구조에 system()함수가 필요로하는 데이터를 채워주면 됨.
- <그림 49>와 <그림 50>을 함께 고려해서 볼 때 vul.c를 overflow 시킬 데이터의 구조는 <그림 51>과 같음.
beist's execl 방법
- buffer overflow 취약점을 가진 프로그램(vul.c)를 Return-into-libc 기법으로 overflow 시켜 공격함.
- main()함수 return시 return할 libc 함수는 execl.
- execl은 세 개의 argument를 가짐.
int execl(const char *path, const char *arg, ...);
- shell.c는 setreuid()와 setregid()를 이용하여 소유자의 권한을 얻어오는 역할.
- <그림 55> 구조의 버퍼에 <그림 56>과 같은 형식의 공격 코드를 집어 넣음.
[dalgona@redhat8 rtl]$ gdb -q execl
(gdb) disass main
Dump of assembler code for function main:
0x80481d0 <main>: push %ebp
0x80481d1 <main+1>: mov %esp,%ebp
0x80481d3 <main+3>: sub $0x8,%esp
0x80481d6 <main+6>: and $0xfffffff0,%esp
0x80481d9 <main+9>: mov $0x0,%eax
0x80481de <main+14>: sub %eax,%esp
0x80481e0 <main+16>: call 0x804c740 <execl>
0x80481e5 <main+21>: leave
0x80481e6 <main+22>: ret
0x80481e7 <main+23>: nop
End of assembler dump.
(gdb) disass execl
Dump of assembler code for function execl:
0x804c740 <execl>: push %ebp
0x804c741 <execl+1>: mov %esp,%ebp
0x804c743 <execl+3>: push %edi
0x804c744 <execl+4>: push %esi
0x804c745 <execl+5>: push %ebx
0x804c746 <execl+6>: sub $0x101c,%esp
0x804c74c <execl+12>: mov 0xc(%ebp),%eax
0x804c74f <execl+15>: lea 0x10(%ebp),%ecx
0x804c752 <execl+18>: test %eax,%eax
0x804c754 <execl+20>: movl $0x400,0xfffffff0(%ebp) 0x804c75b <execl+27>: mov %esp,%esi
0x804c75d <execl+29>: mov %eax,(%esp,1)
0x804c760 <execl+32>: mov %ecx,0xffffffec(%ebp) 0x804c763 <execl+35>: mov $0x1,%edx
0x804c768 <execl+40>: je 0x804c79a <execl+90> 0x804c76a <execl+42>: mov $0x4,%ebx
0x804c76f <execl+47>: movl $0x17,0xffffffe4(%ebp) 0x804c776 <execl+54>: mov %esi,%esi
0x804c778 <execl+56>: cmp 0xfffffff0(%ebp),%edx
0x804c77b <execl+59>: je 0x804c7b4 <execl+116>
0x804c77d <execl+61>: mov 0xffffffec(%ebp),%eax 0x804c780 <execl+64>: mov (%eax),%eax
0x804c782 <execl+66>: mov %eax,(%esi,%edx,4) 0x804c785 <execl+69>: mov %edx,%ecx
0x804c787 <execl+71>: mov (%esi,%ecx,4),%edi 0x804c78a <execl+74>: addl $0x4,0xffffffec(%ebp) 0x804c78e <execl+78>: add $0x4,%ebx
0x804c791 <execl+81>: addl $0x8,0xffffffe4(%ebp) 0x804c795 <execl+85>: inc %edx
0x804c796 <execl+86>: test %edi,%edi
0x804c798 <execl+88>: jne 0x804c778 <execl+56> 0x804c79a <execl+90>: push %eax
0x804c79b <execl+91>: pushl 0x809d2f4
0x804c7a1 <execl+97>: push %esi
0x804c7a2 <execl+98>: pushl 0x8(%ebp)
0x804c7a5 <execl+101>: call 0x804fa5c <execve> 0x804c7aa <execl+106>: lea 0xfffffff4(%ebp),%esp 0x804c7ad <execl+109>: pop %ebx
0x804c7ae <execl+110>: pop %esi
0x804c7af <execl+111>: pop %edi
0x804c7b0 <execl+112>: leave
---Type <return> to continue, or q <return> to quit---q Quit
(gdb) quit
[dalgona@redhat8 rtl]$
execl()의 dissassemble
- execl()함수를 쭉 따라가보면 execve()를 호출하기 직전에 pushl 0x8(%ebp)를 볼 수 있음.
- 즉 ebp 레지스터가 가리키는 곳의 8바이트 뒤의 값을 스택에 넣고 execve() 호출.
- argv[2]의 -8에 있는 주소를 넣어두었으므로 +8을 하면 argv[2]의 주소를 가리키게 되고 여기에는 "./shell"이라는 쉘 프로그램 실행 명령이 들어가있으므로 shell을 실행.
'SYSTEM > 개념 정리' 카테고리의 다른 글
[Dream hack] Linux Exploitation & Mitigation Part 1 中 Return Address Overwrite & NOP Sled (0) | 2021.03.04 |
---|---|
[Dream hack] Memory Corruption - C (I) 中 스택 버퍼 오버플로우 (0) | 2021.03.04 |
BOF 정리 (1) (0) | 2021.02.21 |
리눅스 기초 명령어(2) (0) | 2021.02.19 |
리눅스 기초 명령어 (0) | 2021.02.15 |