스택 구조 그리기
작성자 - dhkstn1. 목표
주어진 두 개의 문제 파일을 컴파일하여 실행 파일을 만든 후, 각각의 스택 구조를 그려보고 `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 파일이며, 스택 보호 기능이 비활성화되어 있음
2. 이후 pwndbg를 이용하여 `main()` 함수를 분석함
a. `main()` 함수에 `breakpoint` 설정 후, 디스어셈블을 수행함
b. 디스어셈된 코드를 보고 변수 및 배열의 위치를 파악함
ⅰ 변수 `a`: rbp-0x4
ⅱ 변수 `b`: rbp-0x8
ⅲ 배열 `c[0] ~ c[9]`: rbp-0x30 ~ rbp-0xc
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를 차지함
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 파일이며, 스택 보호 기능이 활성화되어 있음
2. 이후 pwndbg를 이용하여 `main()` 함수를 분석함
a. `main()` 함수에 `breakpoint` 설정 후, 디스어셈블링
b. 디스어셈된 코드를 보고 변수 및 배열의 위치를 파악
ⅰ 스택 `카나리`: rbp-0x8
ⅱ 변수 `a`: rbp-0x64
ⅲ 변수 `b`: rbp-0x68
ⅳ 배열 `c[0] ~ c[19]`: rbp-0x60 ~ rbp-0x10
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를 활성화할 수 있음
- 아무런 옵션을 지정하지 않아도, 스택 보호 기능은 자동으로 활성화됨
- 스택 오버플로우 취약점을 방지하기 위한 보호 메커니즘으로, 버퍼와 SFP(Stack Frame Pointer) 사이에 `스택 카나리(Canary)`라는 무작위 값을 삽입함
- Stack Smashing Protector(SSP)
'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 |