BUFFRER OVERFLOWS ON REDHAT 6.2

1. Abstract

버퍼오버플로우 공격은 1988년 그 취약점이 발견된 이후 가장 심각하고 오래 지속되고 있는 공격 중 하나이다. 공격성공을 최소화 시키기 위해 몇 가지 기법이 개발되어 왔지만 30년이 지난 지금도 문제가 곳곳에서 발생하고 공격자가 시스템에 침입하여 임의 코드를 실행할 수 있도록 계속 허용하고 있다.

이 문서는 레드햇 6.2환경에서의 SUID가 설정된 쉘코드를 이용해서 취약한 프로그램의 버퍼오버플로우를 이용한 사용자 코드 실행 기법, 운영체제의 다양한 방어 기법에 대해 다룬다.

setreuid()와 setuid()가 포함된 쉘코드를 생성하고 NOP Sled와 환경변수를 활용한 버퍼오버플로우 공격에 대해 다룬다.

개선된 운영체제(redhat6.2 이후)에서 추가된 메모리 공격 보호 기법에 대해서 분석한다.

2. Attack Scenario

버퍼오버플로우 공격에 취약한 함수 strcpy()를 사용한 프로그램 strcpy2이 있다. 이 프로그램은 소유자가 root이며 SUID 비트가 설정되어 있어서 프로세스가 실행될 때 상위(root) 권한으로 동작한다.

공격자는 시스템 내에 일반사용자 권한을 가지고 있으며 공격을 위해 SUID 권한을 가진 파일들 중 취약한 함수를 사용하는 프로그램 strcpy2을 발견한다. 입력 버퍼의 값을 넘치게 입력하는 버퍼오버플로우를 발생시킨 후 복귀주소에 쉘코드를 입력하여 root 권한을 획득하여 시스템을 장악한다.

권한을 획득한 후에는 추후 손쉬운 시스템 접근을 위해 백도어를 설치한다. 백도어 또한 suid 비트가 설정되어 있으며 cron 데몬을 사용하여 파일이 삭제되더라도 일정한 주기로 재생성하도록 하여 권한을 계속해서 가져올 수 있다.

2.1 Shellcode

[exploit.c]

gcc -static -o exploit exploit.c

컴파일 하기 전 표준 라이브러리(libc)를 사용하는 execve를 쉘코드 내에 포함시키기 위해 정적으로 바이너리 파일 내로 포함될 수 있도록 “-static” 옵션을 사용한다.

“/bin/sh\0”은 Word 단위로 저장되는 스택의 특성 상 “/bin”과 “/sh\0”으로 나누어서 16진수 리틀엔디언 형식으로 변환되어 저장된다. 따라서 “/bin”은 “0xE69622F”, “/sh\0”은 “0x0068732F”의 형태로 저장된다. 6번째 줄의 “push $0x0”은 스택에 NULL을 의미하는 0을 저장한다.

다음 코드 부터는 스택에 execve 함수 호출을 위한 인자들을 준비한 후 호출하는 과정이다. 함수 호출 직전까지의 스택의 모습은 다음과 같다.

[execve 함수 호출 이전 스택 구조]
[__execve 함수]

main 함수와 마찬가지로 첫 부분과 마지막 부분은 함수의 프롤로그와 에필로그로 구성되어 있으며 세번째 코드부터 call 명령어 전까지 인터럽트 호출을 위해서 레지스터에 인자를 할당한다. execve 함수 프롤로그 이후의 EBP를 기준으로 한 스택의 구조는 다음과 같다.

[execve 호출 이후 스택 구조]
[__execve 함수 어셈블리 코드]

__execve 함수의 내부 동작을 보면 int $0x80으로 인터럽트를 발생시키고 있다. 바로 위의 코드를 보면 “mov $0xb,%eax”로 eax레지스터에 0xb(11)을 넣어주고 있다. 이는 시스템 로 호출할 인터럽트의 종류를 지정한다. 이 숫자를 시스템 호출 번호(System Call Number)라고 하며, “/usr/include/asm”의 “unistd.h”에 명시되어 있다.

그리고 ebx에 첫번째 인자, ecx에 두 번째 인자, edx에 세번째 인자를 넣어주고 있는 모습이다. <__execve>호출하는 과정을 어셈블리언어로 나타내 보면 다음과 같다.

<__execve>를 호출하는 과정을 소스코드로 인라인 어셈블리 한 결과이다. ‘__volatile__’ 옵션은 C, C++언어의 ANSI 표준 키워드로써 프로그래머가 입력한 그대로 남겨두게 된다. 최적화나 변수의 위치를 옮기는 일을 하지 않는다. 쉘코드에서 최적화가 발생하면 원하는 코드가 실행되지 않을 수 있기 때문에 옵션을 사용하여 코딩한다.

어셈블리 언어를 objdump를 통해 기계어로 열어본 결과이다. 함수의 프롤로그와 에필로그를 제외하면 그대로 컴파일된 모습이다. 기계어 중 “00”은 실행 중 널(NULL)문자로 판단하여 프로그램을 종료하기 때문에 쉘코드로 작성할 때에는 모두 제외시켜 주어야 한다. eax가 자기 자신을 xor연산해서 ‘0’이 되게 한 후 eax를 인자로 사용하는 방법으로 “00”문제를 해결할 수 있다. 7바이트인 “/bin/sh”대신 같은 기능을 하는 “/bin//sh”를 사용해서 8바이트로 만들면 “push 0x0068732f” 코드의 “\x00”도 제외시킬 수 있다.

위의 과정을 유의하여 OP Code를 추출하면 다음과 같다.

획득한 쉘코드는 다음과 같다.

\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

25 바이트의 쉘코드가 정상적으로 동작하는 모습이다.

 

 

하지만 쉘코드가 실행중인 프로세스의 권한을 받으려면 “unistd.h”에 정의된 setreuid 함수와 setuid 함수를 사용해야 한다. 이는 프로그램을 실행시켰을 때 사용자는 프로그램의 uid로 ruid(real uid)가 바뀌게 된다. 이는 그 계정 권한 내의 effective id를 획득할 수 있다는 것이다. setuid가 설정된 파일을 실행시켰을 때 사용자는 프로그램의 uid 권한을 가지게 된다. setuid() 함수가 호출되지 않는 프로그램을 버퍼오버플로우 공격을 수행하려면 setuid() 함수를 실행시킨 다음에 쉘을 실행시켜야 한다.

  [shellcode 작성 과정]

앞선 설명과 마찬가지로 함수 수행 과정을 분석하고 기계어로 쉘코드를 작성한다. 

\x31\xc0\xb0\x31\xcd\x80\x89\xc3\x89\xc1\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

 

[setuid이용하여 root권한 획득]

공격자는 root 권한을 가져올 있는 작은 프로그램을 쉘코드로 준비했다. 로컬(Local) 쉘코드 하나를 제작한 것이다. 로컬 쉘코드는 공격자가 대상 시스템에 대한 제한적인 접근 권한을 가지고 있을 , 취약점이 있는 높은 권한을 가진 프로세스를 공격하여 프로세스와 같은 권한을 가질 있다. 공격자는 목적에 따라 다양한 리버스텔넷 쉘코드, 바인드 쉘코드 등을 제작할 있다.

2.2 Bufferoverflow

[취약 프로그램strcpy2 코드]

strcpy2프로그램은 stdin 입력으로 받는 strcpy()함수를 사용하여 버퍼오버플로우 공격에 취약한 프로그램이다. strcpy 함수는 EOF new line 문자가 때까지 입력을 받으며,  해당 비트가 입력될 경우 NULL byte 바꾼다. 그리고 소유자가 root이고 setuid 설정되어 있다고 가정한다.

Redhat 6.2운영체제는 bash 버전이 낮아서 상에서 “\xff” 입력했을 , “\x00”으로 치환해버리는 문제가 발생한다. 이러한 문제가 발생하지 않도록 유저들의 default shell 상위 버전인 bash2 설정한 진행해야 한다.

NOP No Operation 약자로 아무 동작도 수행하지 않는다는 어셈블리 명령어이다. NOP 반복해서 여러행 동안 나타난다면 이는 아무 동작도 수행하지 않는 명령행이 Skip 되면서 실행된다. 이를 sled(썰매) 연상한다고 하여 NOP sled 공격이라고 한다.

NOP Sled 장점은 NOP 개수만큼 주소 공간을 확보할 있고 bash 쉘코드의 시작 주소를 잘못 입력했다 하더라도 NOP 개수 오차 범위 내에 있는 주소로 리턴주소를 덮어쓸 수만 있다면 공격이 성공한다는 점이다.

[환경변수 ISHELL 선언]

환경변수를 선언한 후에는 등록한 쉘코드가 저장된 환경변수의 주소를 구해야 한다. 이는 stdlib 구현되어 있는 getenv()함수를 이용해 환경변수의 주소를 가져올 수있다. ‘getenv2’ 프로그램은 환경변수와 실행할 프로그램을 매개변수로 받아서 해당하는 환경변수의 주소를 출력해 주도록 공격자가 작성한 것이다.

[버퍼오버플로우 공격 전/후 스택 상태]

RET영역을 덮어쓰게 하여 EIP 변조시키는 공격이다. 함수가 종료되면 EIP RET영역의 주소로 JMP 수행하게 되는데 영역에 작성한 쉘코드 주소를 입력하여 실행되게 한다.

버퍼사이즈와 쉘코드가 입력된 환경변수의 주소를 알기 때문에 공격을 수행할 있게 된다. 페이로드는 다음과 같이 구성된다.

[버퍼 256 Byte] + [SFP 4 Byte] + [환경변수의 주소값(리틀엔디언)]

#./strcpy2 $(python -c ‘print(“\x90”*260+”\xa4\xfe\xff\xbf”)’)

[버퍼오버플로우 공격 수행]

euid ‘0’(root) 변경되어 권한을 획득한 것을 있다.

[shell 획득 후 cat /etc/passwd]

2.3 Backdoor

 공격자는 시스템에 일반 사용자로 접속하여 버퍼오버플로우 공격을 시도하여 루트 권한을 얻었다. 하지만 매번 공격을 수행하면서 접속하기에는 복잡한 과정을 거쳐야 하기 때문에 쉽게 재침입할 있도록 백도어파일을 생성해 둔다.

백도어 파일을 생성하는 과정은 다음과 같다.

[backdoor.c]
[backdoor 파일 setuid 권한 지정]
[backdoor 권한 획득 확인]

시스템 관리자가 백도어 파일을 찾기 위해서 주기적으로 검사를 것이라고 가정한다. 공격자는 탐지가 어려운 백도어파일을 만들기 위해서 파일을 컴파일하고 setuid 설정된 다른 명령어 파일(usernetctl) 교체하는 스크립트(setlog.sh) 만든다.

[/etc/cron.d/setlog.sh]
[/etc/crontab]

cron 데몬을 사용하여 분마다 스크립트를 실행하여 백도어 파일이 삭제되더라도 계속해서 반복해서 생성하도록 구성한다.

공격자는 백도어파일을 생성함으로써 사용자 인증 절차를 거치지 않고 상위 권한(root) 쉘에 빠른 시간 내에 접근할 있게 된다.

3.  Exploit Analysis

Linux/Unix 환경에서 Shellcode 사용자의 명령어 쉘을 실행시키는 것이 아니라, 시스템에 대한 제한적인 접근 권한을 가지고 있는 공격자가 상위 권한(root) 획득하기 위해서 사용한다. 리눅스 시스템에서는 의외로 합법적인 권한 상승이 많은데, SetUID 사용한 것이 하나이다. 파일 소유자가 root 파일이 실행되는 프로세스는 실행 시간 동안 파일의 소유자인 root권한으로 실행한다.

메모리 보호는 운영체제 내에서 실행중인 프로세스가 허가 받지 않은 다른 영역의 메모리에 의해 접근되는 것을 막는 것이 주된 목적이다.

Linux 운영체제가 업그레이드 되면서 새로운 운영체제 보안 기법 또한 추가되었다.

다음은 Linux 운영체제의 업그레이드에 따른 메모리 보호 기법이다.

l  ASLR(Address Space Layout Randmoization)

메모리 상의 공격을 방어하기 위해서 주소 공간배치를 랜덤(난수화)하게 저장하는 기법이다. 스택, , 라이브러리 등의 데이터 영역 주소 등을 난수화한다.

l  DEP/EX

메모리 상의 보호를 위해 스택과 힙에서 코드가 실행되는 것을 막는 기법이다. 프로그램의 실행 권한은 code 영역에서만 가지게 하여 DEP 적용된 상태에서는 실행권한이 없으므로 프로그램에 대한 예외처리 , 종료가 된다.

l  ASCII-Armor

Libc 영역을 보호하기 위한 기법으로 RTL(Return To Library)공격에 대응한다. 메모리 상위 주소를 \x00으로 시작하게 만들어서 라이브러리를 호출하는 BOF 공격 , NULL 바이트가 삽입된 주소로 접근할 없다.

l  Canary

Windows Stack cookie 같은 개념으로 Buffer RET사이에서 스택의 BOF 모니터링 한다. 컴파일러가 프로그램의 함수를 호출(프롤로그) 때마다 RET앞에 Canary값을 주입하고 종료(에필로그) Canary값을 변조했는지 여부를 확인하여 버퍼 오버플로우 공격을 탐지한다.

 하지만 이러한 보호기법들은 모두 ROP(Return Oriented Programming) 우회가 가능하다. 취약한 프로그램 내부에 있는 기계어 코드 섹션들(Gadget) 이용하여 버퍼오버플로우 공격을 수행하면 특정 명령을 수행하게 하는 것이다. 보통 Gadget 함수 끝에 기술되어 있는 ret명령어를 포함해서 상위 가지 명령어들의 집합이며 이를 이용하여 쉽게 공격할 있다.

4.  Conclusion

Linux/Unix 환경에서 Setuid비트가 설정된 파일들은 모두 해킹 대상이 있다. 파일들은 백도어 버퍼오버플로우 여러 공격에 이용된다. 파일을 목록화하여 불필요한 파일이 생성되었는지 비교하는 작업도 필요할 것이다.

버퍼오버플로우 공격의 기본 개념은 지정된 버퍼보다 사용자에게 많은 데이터를 입력 받아 프로그램의 비정상적인 동작을 야기하는 것이다. 무엇보다 가장 근본적인 예방법은 프로그래머가 공격에 취약한 함수를 사용하지 않고 입력 값을 검증하는 루틴을 추가하는 것이다. 프로그래머와 컴파일러가 입력될 데이터의 크기와 저장된 공간의 크기를 검사하지 않아서 공격이 가능해진다. 사용자는 데이터를 버퍼의 배열 공간보다 넘치게 입력한다. 스택에 있는 배열 영역의 주소를 넘어가게 되고 결과적으로 다른 데이터를 덮어쓰게 되는 것이다.

버퍼오버플로우의 수많은 취약점에 대해 해결책을 찾는 사용자들에게 운영체제나 컴파일러에서 제공하는 많은 옵션을 이용할 있게 되었다. 소프트웨어의 목적에 따라서 이용 가능한 보호 메커니즘을 선택할 있다는 것을 알게 되었다. 커널과 컴파일러 기반 보호 소프트웨어 조합은 현재 최고의 방어 수단으로 사용되고 있다. 컴파일러의 보호 기법은 생성된 바이너리 자체의 구조를 수정하고 런타임 검사를 구현할 있는 반면, 커널은 프로그램이 실행되는 환경을 보호하고 하드코딩 주소가 필요한 익스플로잇 취약점들의 공격 가능성을 줄일 있다.

 


References

1.    김종의(Jong Ewi Kim),이성욱(Seong Uck Lee),홍만표(Man Pyo Hong). 2002. 정보보안 : 버퍼오버플로우 공격 방지를 위한 컴파일러 기법. 정보처리학회논문지C, 9(4) : 453-458

2.    TEAMCRA@K, 메모리 보호기법 우회 – 1 – Windows/Linux 환경에서의 Stack Overflow 보호 기법, https://teamcrak.tistory.com/330?category=168705

3.    임베디드 시스템 엔지니어를 위한 리눅스 커널 분석, C.1 인라인 어셈블리 기초. https://wiki.kldp.org/KoreanDoc/html/EmbeddedKernel-KLDP/app3.basic.html

4.    Library of Ezbeat. Shell 코드 만들어 보기. https://ezbeat.tistory.com/150?category=160275

5.    세모노트. LOB 다운로드&초기 설정(bash 문제). https://ssaemo.tistory.com/2

6.    Newbie Hacker. Shellcdoe 만들기 -4(쉘코드 동작 원리 이해 제작). https://bob3rdnewbie.tistory.com/124

반응형