Security Tech

스택 구조 그리기

작성자 - dhkstn

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

 


  • 문제 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; }

 

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

프로그램 실행

  • 코드 흐름
    • 사용자로부터 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을 출력함

 

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

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

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

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

 

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. 결과 확인

문제 해결

 


  • 문제 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; }

 

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

프로그램 실행

 

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

 

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

2. 코드 흐름을 분석해 보면, 변수 bb % 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!을 출력할 수 있음

 

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 크기임

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

           - c[20]ab 순서로 스택에 쌓임

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

           - 참고(chatgpt)

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

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

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

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

스택 구조

 

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

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

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

        -> 변수 a와 동일한 위치

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

 

5. 결과 확인

 


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

https://she11.tistory.com/130

 

 

 

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