Security Tech

스택 구조 그리기

작성자 - dhkstn

1. 목표


주어진 두 개의 문제 파일을 컴파일하여 실행 파일을 만든 후, 각각의 스택 구조를 그려보고 `you win!` 문자열을 출력하기!

 

2. 문제 1번


  • 문제 1번 prob1.c
    • gcc -o prob1 prob1.c -fno-stack-protector
  • -fno-stack-protector 옵션
    • GCC 컴파일 옵션으로 스택 보호(Stack Smashing Protector) 기능을 비활성화함
// gcc -o prob1 prob1.c -fno-stack-protector

#include <stdio.h> 

int main(void) { 
    int a; 
    int b; 
    int c[10]; 

    printf("Idx: "); 
    scanf("%d", &b);

    b = (b ^ 1);
    b = (b << 1) + 1;

    printf("Data: "); 
    scanf("%d", &c[b]); 

    if(a == 31337) { 
        printf("you win!\n"); 
    }
    else{
        printf("wrong!\n");
    }
    
    return 0;
}

 

2.1. 코드 분석

  • 프로그램 실행 시 동작 과정

프로그램 실행

  • 코드 흐름
    • 사용자로부터 Idx 값을 입력받아 변수 b에 저장함
    • 이후 b는 다음과 같이 연산됨
      • `b = (b ^ 1);` -> 1과 XOR 연산을 통해 최하위 비트를 반전시킴
      • `b = (b << 1) + 1;` -> left shift 연산으로 `b`를 2배로 만든 뒤, 1을 더함(항상 홀수 값이 됨)
    • 이렇게 만들어진 `b`를 인덱스로 사용해 배열 `c[b]`에 값을 입력받음
    • 마지막으로, 변수 `a`의 값이 `31337`이면 `you win!`을 출력하고, 그렇지 않으면 `wrong`을 출력함

 

2.2 풀이 과정

2.2.1 접근 방법

1. 분석 대상 파일의 아키텍처 및 메모리 보호 기법 확인함

2. `scanf("%d",&c[b]);`

    a. 배열의 범위를 검사하지 않으므로 스택 오버플로우 취약점이 발생할 수 있음

3. 배열 `c[b]`를 통해 스택에 있는 변수 `a`의 값을 `31337`로 덮어쓸 수 있다면, 조건문 `if(a == 31337)`을 만족시켜 `you win!`을 출력할 수 있음

 

2.2.2 풀이

1. `checksec` 명령어를 통해 분석 대상 파일의 아키텍처 및 적용된 메모리 보호 기법을 분석함

    a. 분석 대상 파일은 64 bit 환경의 ELF 파일이며, 스택 보호 기능이 비활성화되어 있음

checksec 결과

 

2. 이후 pwndbg를 이용하여 `main()` 함수를 분석함

    a.  `main()` 함수에 `breakpoint` 설정 후, 디스어셈블을 수행함

    b. 디스어셈된 코드를 보고 변수 및 배열의 위치를 파악함

        ⅰ 변수 `a`: rbp-0x4

        ⅱ 변수 `b`: rbp-0x8

        ⅲ 배열 `c[0] ~ c[9]`: rbp-0x30 ~ rbp-0xc

main 함수 디스어셈

 

3. 따라서, 스택 구조를 그려보면 아래와 같음

    a. 스택은 높은 주소에서 낮은 주소로 쌓임

    b. 변수 `a`, `b`, 배열 `c[b]`는 `int` 형이며, 각각 4 byte 크기로 스택에 연속적으로 배치됨

    c. `sfp`(stack frame pointer)는 함수 호출 시 호출자의 `rbp` 값을 백업해두는 공간으로, 64bit 환경에서 8 byte를 차지함

    d. 함수 종료 시, `sfp`를 통해 이전 함수의 `rbp` 값을 복원하고, 그 후 `ret` 명령어로 원래 실행 흐름으로 복귀함

    e. `ret` 주소는 호출한 함수의 복귀 주소를 저장하는 공간으로, 64bit 환경에서 8 byte를 차지함

prob1.c 스택 구조

 

4. 배열 `c[b]`를 통해 스택에 있는 변수 `a`의 값을 `31337`로 덮어쓰기

    a. 배열 `c[9]` 다음으로 스택에 배치된 변수는 `b`(4 byte), 그다음이 `a`(4 byte)임

    b. 따라서 `c[10]`은 변수 `b`, `c[11]`은 변수 `a`를 덮게 됨

    c. 변수 `b`는 다음과 같은 연산을 통해 최종 인덱스를 결정함

b = (b ^ 1);
b = (b << 1) + 1;
printf("data: ");
scanf("%d", &c[b]);

 

    d. 위 연산을 역으로 계산하면 `b`가 4일 때 값이 11이 되는 것을 알 수 있음

b = 11
-> b = (b - 1) >> 1 = 5
-> Idx = (5 ^ 1) = 4

 

    e. 따라서 사용자 입력으로 `Idx: 4`, `data: 31337`을 넣으면, `c[11] == 31337`이 되어 변수 `a`를 덮을 수 있고, 이후 `if (a == 31337)` 조건이 참이 되어 `you win!`이 출력됨

 

5. 결과 확인

문제 해결

 

3. 문제 2번


  • 문제 2번 prob2.c
    • gcc -o prob2 prob2.c
  • GCC 옵션 지정 X
    • 스택 보호(Stack Smashing Protector) 기능이 자동으로 활성화됨
// gcc -o prob2 prob2.c

#include <stdio.h>

int main(void) {

    int b; 
    int c[20]; 
    int a;

    printf("idx : ");
    scanf("%d", &b); 

    b = b % 10; 

    printf("data: "); 
    scanf("%d", &c[b]); 
    

    if(a == 31337) { 
        printf("you win!\n"); 
    }

    else{
        printf("wrong!\n");
    }
    
    return 0;
}

 

3.1 코드 분석

  • 프로그램 실행 시 동작 과정

프로그램 실행

 

  • 코드 흐름
    • 사용자로부터 `Idx` 값을 입력받아 변수 `b`에 저장함
    • 이후 `b`는 나머지 연산(`b % 10`)을 수행함
    • 연산된 `b`를 인덱스로 사용해 배열 `c[b]`에 값을 입력받음
    • 마지막으로, 변수 `a`의 값이 `31337`이면 `you win!`을 출력하고, 그렇지 않으면 `wrong`을 출력함

 

3.2 풀이 과정

3.2.1 접근 방법

1. 분석 대상 파일의 아키텍처 및 메모리 보호 기법 확인함

2. 코드 흐름을 분석해 보면, 변수 `b`는 `b % 10` 연산을 통해 배열 인덱스로 사용되며, 그 범위가 제한됨

    a. 예를 들어 `b = 9`일 경우, `c[b]`는 `c[9]`에 접근하게 됨

    b. 하지만 `b = -9`일 경우, `c[b]`는 `c[-9]`에 접근하게 되어, 배열의 앞쪽 메모리(스택 상단)를 덮을 수 있을 것으로 보임

3. 문제 1과 비슷한 형태로, 배열 `c[b]`를 통해 스택에 있는 변수 `a`의 값을 `31337`로 덮어쓸 수 있다면, 조건문 `if(a == 31337)`을 만족시켜 `you win!`을 출력할 수 있음

 

3.2.2 풀이

1. `checksec` 명령어를 통해 분석 대상 파일의 아키텍처 및 적용된 메모리 보호 기법을 분석함

    a. 분석 대상 파일은 64 bit 환경의 ELF 파일이며, 스택 보호 기능이 활성화되어 있음

checksec 결과

 

2. 이후 pwndbg를 이용하여 `main()` 함수를 분석함

    a. `main()` 함수에 `breakpoint` 설정 후, 디스어셈블링

    b. 디스어셈된 코드를 보고 변수 및 배열의 위치를 파악

        ⅰ 스택 `카나리`: rbp-0x8

        ⅱ 변수 `a`: rbp-0x64

        ⅲ 변수 `b`: rbp-0x68

        ⅳ 배열 `c[0] ~ c[19]`: rbp-0x60 ~ rbp-0x10

main 함수 디스어셈

 

3. 따라서, 스택 구조를 그려보면 아래와 같음

    a. 스택은 높은 주소에서 낮은 주소로 쌓임

    b. 변수 `a`, `b`, 배열 `c[b]`는 `int`형이며, 각각 4 byte 크기임

       -> 선언 순서는 `b` → `c[20]` → `a`이지만, 실제 메모리에서는 해당 순서가 보장되지 않았음

           - `c[20]` → `a` → `b` 순서로 스택에 쌓임

           - 컴파일러가 자동으로 최적화한 것으로 보임

           - 참고(chatgpt)

              -> 컴파일러가 alignment, padding, 변수 크기 등을 고려하여 최적의 구조로 재배치함

    c. 함수 호출 시, `스택 카나리`는 `sfp` 바로 아래에 저장되며, 8 byte를 차지함

    d. 함수 종료 시, `sfp`를 통해 이전 함수의 `rbp` 값을 복원하고, 그 후 `ret` 명령어로 원래 실행 흐름으로 복귀함

    e. `ret` 주소는 호출한 함수의 복귀 주소를 저장하는 공간으로, 64bit 환경에서 8 byte를 차지함

스택 구조

 

4. 배열 `c[b]`를 통해 스택에 있는 변수 `a`의 값을 `31337`로 덮어쓰기

    a. 변수 `a`는 `rbp-0x64`에 저장되어 있고, 배열 `c[0]`는 `rbp-0x60`부터 시작함

    b. 배열 `c`는 `int`형으로 연속된 공간에 저장되므로, `c[-1]`은 `rbp-0x60-0x4 = rbp-0x64`를 가리키게 됨

        -> 변수 `a`와 동일한 위치

    c. 따라서 사용자 입력으로 `idx: -1`, `data: 31337`을 넣으면, `c[-1] = 31337`이 되어 변수 `a`를 덮을 수 있고, 이후 `if (a == 31337)` 조건이 참이 되어 `you win!`이 출력됨

 

5. 결과 확인

 

4. 참고 자료


  • Stack Smashing Protector(SSP)란
    • 스택 오버플로우 취약점을 방지하기 위한 보호 메커니즘으로, 버퍼와 SFP(Stack Frame Pointer) 사이에 `스택 카나리(Canary)`라는 무작위 값을 삽입함
      • 함수 종료 시, 리턴 주소의 무결성을 확인하기 위한 보호 장치 역할을 수행함
    • 함수 리턴 직전에 카나리 값이 변조되었는지 검사하여, 스택 오버플로우 발생 여부를 감지하고 프로그램을 강제 종료함
    • GCC에서는 `-fstack-protector` 옵션으로 SSP를 활성화할 수 있음
      • 아무런 옵션을 지정하지 않아도, 스택 보호 기능은 자동으로 활성화됨

https://she11.tistory.com/130

 

 

 

'Security Tech' 카테고리의 다른 글

[Mobile] QuarkEngine 툴 소개 및 사용법  (0) 2025.05.31
함수 프롤로그/에필로그 분석  (0) 2025.05.31
OWASP - MASTG UnCrackable-LEVEL 3 (1)  (0) 2025.04.27
V2X (Vehicle-to-Everything) 통신  (0) 2025.02.28
API 취약점 진단  (0) 2025.02.07
Contents

이 글이 도움이 되었다면, 응원의 댓글 부탁드립니다.