본문 바로가기

공부/보안

[dreamhack] System Exploitation 6강 - Return Address Overwrite

반응형

6강 Linux Exploitation & Mitigation Part 1

6.2 Return Address Overwrite

스택 버퍼 오버플로우 취약점에서 주로 스택의 리턴 주소를 덮는 공격을 한다.

-> 스택에 저장된 리턴 주소를 다른 값으로 바꾼다.

 

리턴 주소 : 함수가 끝나고 돌아갈 이전 함수의 주소

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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;
}
cs

main 함수에서 argv[1]vuln 함수의 인자로 전달한다.

vuln 함수에서는 srcbufstrcpy 함수를 이용해 복사한다.

 

strcpy 함수는 src에 대한 길이 검증이 없기 때문에, 프로그램의 첫 번째 인자에 buf 배열의 크기보다 긴 문자열을 넣으면 스택 버퍼 오버플로우가 발생한다.

 

이때 vuln 함수의 메모리 구조는 왼쪽 그림과 같다.

x86 아키텍처 호출 규약에 의해 vuln 함수가 호출되면 vuln 함수의 인자인 src 문자열 포인터가 스택에 먼저 쌓이고 vuln 함수의 리턴 주소가 쌓인다.

그리고 함수의 프롤로그로 ebp 레지스터를 저장한 다음 지역 변수의 공간을 할당한다.


취약점이 존재하는 vuln 함수에 브레이크포인트를 설정한 후 첫번째 인자와 함께 바이너리를 실행해본다.

스택 메모리를 보면, 첫 4 바이트는 vuln 함수의 리턴 주소, 다음 4 바이트는 vuln 함수의 인자인 argv[1]의 주소임을 알 수 있다.

strcpy 함수가 실행되기 직전 브레이크 포인트를 설정해 스택을 보면 첫 번째 인자인 buf 주소와 두 번째 인자인 argv[1]의 주소가 저장되어 있는 것을 볼 수 있다.

strcpy 함수를 실행시켜 첫번째 인자인 buf(0xffffd5fc)argv[1]의 문자열이 복사된 것을 볼 수 있다.

argv[1]에 buf의 길이인 32 바이트보다 긴 문자열을 주었기에 vuln의 리턴 주소가 저장된 0xffffd520 너머까지 argv[1] 문자열이 복사된 것을 확인할 수 있다.

 

이를 스택 버퍼 오버플로우 취약점을 통해 eip 레지스터를 임의의 값으로 바꿔 원하는 주소의 코드를 실행할 수 있다.

 

(Linux Exploitation는 로컬 환경의 타겟을 대상으로 하여 익스플로잇 최종 목표는 프로그램의 실행 흐름을 조작하여 /bin/sh 혹은 셸 바이너리를 실행한다. → 권한 상승, 본래 프로그램이 의도치 않은 행위를 위함)

 

그렇다면 eip 레지스터 값을 바꾸어 원하는 주소의 기계어 코드(/bin/sh이나 셸 바이너리)를 실행해 셸에서 제공하는 여러 명령어들을 실행할 수 있다.

 

위 예제 바이너리에서 /bin/sh 바이너리를 실행시켜(이를 위해 execve 시스템 콜 사용) 기계어 코드를 만들어 볼 수 있다.

 

 


1
2
#include <unistd.h> 
int execve(const char *filename, char *const argv[], char *const envp[]);
cs

첫번째 인자(filename) : 실행시킬 바이너리의 경로

두번째 인자(argv) : 프로그램의 인자 포인터 배열

세번째 인자(envp) : 프로그램의 환경변수 포인터 배열

실행가능한 파일인 filename 의 실행코드를 현재 프로세스에 적재하여, 기존 실행코드와 교체하는 새로운 기능으로 실행한다. (현재 실행되는 프로그램의 기능은 없애고 filename 프로그램을 메모리에 loading해 처음부터 실행한다.)

지금은 /bin/sh 바이너리를 실행시키는 것이 목적이기에 최종적으로 sys_execve("/bin/sh" 주소, NULL, NULL)을 호출하면 된다.

  1. sys_execve("/bin/sh" 주소, NULL, NULL) 을 실행하는 어셈블리 코드 생성
  2. 어셈블리 코드를 기예어 코드로 바꾼다.

→ 이 기계어 코드는 x86 리눅스 아키텍처의 어느 바이너리에서 실행시켜도 항상 /bin/sh 바이너리를 실행시키는 기능을 하고, 이를 셸코드라 부른다.

 

어셈블리 코드

section .text
global _start
_start
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f // /bin/sh 문자열을 스택에 저장한다.
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x
$objdump --dump-section .text=shellcode.bin shellcode.o
$xxd shellcode.bin 
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"

 

이렇게 만들어진 셸 코드를 예제 프로그램의 인자로 전달하면 셸코드가 스택 메모리에 저장된다.


 

vuln 함수의 리턴 주소를 스택에 저장된 셸코드의 주소로 바꿔볼 수 있다.

디버깅 결과를 보면 eip 레지스터 값이 바뀌었고, argv[1]의 36번째 오프셋을 알 수 있다.

리턴 주소가 저장된 스택 메모리를 덮기까지 남은 36바이트의 위치(오프셋)에 셸코드를 위치시키고, 리턴 주소를 저장된 셸코드 주소로 바꾸는 공격 코드를 만들었다.

 

셸코드의 주소를 확인하기 위해 바이너리를 실행하고 strcpy 함수를 호출하는 주소에 브레이크포인트를 설정해 디버깅을 해보겠다.

이렇게 하면 gdb 상에서 쉘을 사용해 Return Address Overwrite 를 배울 수 있다!

 

 

 

 

 

[+]


[+] 만들어진 공격 코드를 gdb가 아닌 셸 환경에서 실행해보면 셸을 제대로 획득하지 못 하고 프로그램이 비정상 종료

-> 위 익스플로잇 코드는 리턴 주소를 스택에 있는 셸코드의 주소로 덮지만, 스택의 셸코드의 주소는 바뀐다.

본 예시의 gdb와 셸에서의 지역 변수 주소가 다른 이유는 argv[0] 문자열, 즉 실행 파일의 경로가 각각 절대 경로와 상대 경로로 다르기 때문이다.

 

gdb는 프로그램을 실행할 때 실행 파일의 절대경로를 argv[0]에 저장하지만, 셸에서 프로그램을 실행할 땐 사용자가 입력한 경로가 argv[0]에 저장된다.

 

반응형