본문 바로가기

3. Pwnable (포너블)/2) 개념 정리

[2020.11.21] 4너블 4ever - 달고나 문서 70p ~ 84p(끝!)

Return into libc 기법

 

스텍 영역의  코드를 실행하지 못하게 하는 non-executable stack보호 기법이나 일부 IDS에서 네트워크를 통해 쉘 코드가 유입되는 것을 차단하는 보호 기법을 뚫기 위한 방법으로 제안.

non-executable stack 기법

-스텍 영역에 있는 코드를 실행하지 못하게 하는 것. 

-CPU 레벨, 운영체제 영역 에서의 보호 기법.

-위의 사진처럼 butter overflow 취약점이 있는 버퍼를 공격.

-버퍼를 overflow시켜 butter위에 있는 return address영역에 실행시키고자 하는 libc 함수의 주소를 넣어줌.

-return address가 libc함수로 바뀌어서 함수가 리턴되면서 libc함수가 실행.

-system()함수의 시작지점은 0x4005ca4c

-system()함수의 호출지점은 0x8048258 이지만 실행 후 system()함수의 시작점을 찾아야 함.

-system() 함수가 실행되는 시점에 유의!

-call instruction은 다음 수행할 instruction의 address 즉 return address를 스텍에 PUSH 한 다음에 해당 함수의 시작점으로 이동한다.

but 여기서는 main()함수가 수행을 마치고 return할 때 return 시점이 system()함수의 시작 지점이 된다는 것을 조심.

- system() 함수를 보면 함수 프롤로그가 끝난 다음에 mov 0x8(%ebp),%eax 를 수행하는데, 이것이 system() 함수의 argument 처리 과정임.

- argument가 있는 곳의 address는 ebp+8 byte 지점에 있고 이를 eax 레지스터에 넣은 후 do_system을 호출함. 따라서 “/bin/sh”가 있는 곳의 주소는 이 시점에서의 ebp+8 지점이 되어야 함.

 

  • 따라서 0x80485e6에 있는 명령을 수행할 시점의 스택 구조는 위 사진처럼 되어 있어야 함.

 

  • 그림 50을 보면 main() 함수가 수행을 마치고 return address를 따라 system() 함수의 시작점으로 가게 될 것이고, argument를 얻어서 do_system을 호출하게 될 것임.

  • 그러므로 main()함수의 메모리 구조에 system() 함수가 필요로 하는 데이터를 채워주면 됨.

 

  • vul.c를 overflow 시킬 데이터의 구조는 그림 51과 같음.

 

  • 다음으로 “/bin/sh”를 메모리상에 올리고 그 주소를 알아와야 함. → 환경변수 이용

 

  • MYSHELL 환경변수

  • env.c를 이용해 환경변수가 위치한 메모리의 address를 알아냄.

 

  • 공격에 사용된 데이터는 “ABCD”를 7번 반복하여 총 28byte의 dummy 값과 system() 함수의 address, 4 byte dummy(“KKKK”), “/bin/sh”가 환경변수로 등록되어 있는 곳의 address를 연결하여 생성함.

  • system() 함수 내에서의 return address가 “KKKK”로 조작되어 있기 때문에 exit를 해서 쉘 문을 빠져나오면 Segmentation fault가 뜨는 것을 볼 수 있음.

 

 

beist’s execl 방법

  • 현재 컴파일된 vul이 setuid 비트가 set 되어 있지만 root의 쉘이 떨어지지 않았음.

  • 이는 system() 함수가 단순히 호출만 하는 역할을 하기 때문

  • 따라서 root의 권한을 얻어오기 위해서는 execl() 함수를 사용할 수 있음

 

  • 공격의 기본 개념

    • buffer overflow 취약점을 가진 프로그램(vul.c)를 Return-into-libc 기법으로 overflow 시켜 공격함.

    • main() 함수 return 시에 return할 libc 함수는 execl.

    • execl은 man 페이지에서 볼 수 있듯이 세 개의 argument를 가짐

 

  • 첫 번째 인자는 실행할 프로그램의 full path와 실행파일 이름으로 구성된 문자열의 address, 실행 파일 실행시에 주어질 argument들, NULL을 넣어주면 됨. → 여기서 쉘을 띄우는 프로그램은 shell.c

  • shell.c는 setreuid()와 setregid()를 이용하여 소유자의 권한을 얻어오는 역할을 해줌.

 

  • 먼저 공격에 사용될 데이터의 구조를 살펴보면 그림 55와 같음.

 

  • 이 구조의 버퍼에 그림 56과 같은 형식의 공격 코드를 집어 넣음.

 

  • 다음은 execl() 함수가 있는 곳의 address를 찾고 argv[2]의 주소를 찾으면 됨.(argv[2]: 그림 54에서 본 쉘을 띄우는 프로그램의 실행명령)


argv[2]의 주소를 알기 위해 vul을 실행시켜 보자.

argument를 쉘을 띄울 프로그램 실행 명령을 주었는데 “.shell”앞에 한 칸을 띄어야 argument 가 분리된다.

  •  main 함수의 base pointer, ebp를 기점으로 argv[0]의 주소(0xbffffab4) 를 찾을 수 있는데 이를 따라가 보면 각 각 포인터들이 각각이 argument를 가르키고 있다는 것을 알 수 있다. 
  • argv[0]의 주소 뒤 8byte 지점(0xbffffabc)에 있음을 알 수 있다.

따라서 argv[2]의 주소 -8은 argv[0]의 주소와 같은 값(0xbffffab4) 이다. 



이제는 공격을 시도해 볼 차례이다. 

 

execl 함수를 disassemble하면 다음과 같다.

굵은 글씨를 보았을 때 execl +3 을 하는 이유는 execl 함수가 시작되고 함수 프롤로그 작업을 하지 않게 하기 위해서다. 따라서 이 지점의 주소를 return address로 지정해 주었다.

다음으로 pushl 0x89%ebp 

 

즉 ebp 레지스터가 가르키는 곳의 8byte 뒤의 값을 스텍에 집어 넣고 exeve()를 호출한다. 우리는 argv[2]의 주소-8에 있는 주소를 넣어놨음으로 +8을 하게 되면  (0xbfffabc)를 가르키게 되고 여기에는 “./shell” 이라는 쉘 프로그램 실행 명령이 들어있음을 shell을 실행하게 된다.