| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- 프로그래머스
- 리버싱
- python
- TryHackMe
- 네트워크
- AWS 침해사고 사례 분석
- 파일 시스템
- 운영체제
- terraform
- 드림핵
- sql
- dreamhack
- operating system
- reversing.kr
- 악성코드 분석
- C
- crackme
- network
- programmers
- AWS 3 Tier Architecture
- AWS 침해 사고 사례 분석
- AWS 보안 아키텍처 분석
- Database
- 악성코드분석
- AWS
- AWS 사고 사례 분석
- 버퍼 오버플로우
- reversing
- CrackMe 문제
- Amazon S3
- Today
- Total
lhywk 님의 블로그
[악성코드 분석] 메모리 공격 기법 본문
버퍼 오버플로우
정의: 메모리 공간에 정해진 크기보다 더 큰 값을 넘치게 입력하여, 인접한 메모리의 특정 값을 변경하려 시도하는 공격이다.
발생 영역: 데이터(값)를 쓸 수 있는 어느 메모리 영역에서나 발생할 수 있다.
- 스택(Stack)
- 힙(Heap)
- 데이터 세그먼트(Data Segment)
다음 코드를 보자.
#include <stdio.h>
void check_password() {
int flag = 0;
char buffer[20];
printf("password: ");
gets(buffer); // 입력 길이 제한 없음
if (flag != 0)
printf("***** ACCESS ALLOWED *****\n");
else
printf("***** ACCESS DENIED *****\n");
}
buffer[20]과 flag는 스택에 연속적으로 할당되고, flag 변수가 buffer의 바로 다음 메모리 주소(더 높은 주소)에 위치한다.
오류가 발생한 게 아니라 권한이 허용된 것으로 바뀐 이유는 스택의 구조 때문이다. gets()로 20바이트를 초과하는 데이터를 입력하면, 초과된 데이터(21번째 문자)가 buffer의 경계를 넘어 바로 뒤에 있는 flag 변수의 메모리 공간을 덮어쓰게 된다. 이로 인해 flag의 값이 0에서 0이 아닌 다른 값(21번째 문자의 ASCII 값)으로 변경되어, if (flag != 0) 조건이 참이 되어버린다.
bof.c
#include <stdio.h>
void foo(int x, int y) {
int check = 0;
char buffer[20];
printf ("before fgets(): buffer=[%s], check=%d\n", buffer, check);
fgets(buffer, 48, stdin); // ? 중단점 설정
printf ("after fgets(): buffer=[%s], check=%d\n", buffer, check);
}
int main (int argc, char *argv[], char *env[]) {
int a = 3;
int b = 4;
//
foo (a, b);
printf ("End of Program");
}




3, 4를 파라미터로 스택에 넣고 함수를 호출한다.

1234567890이 들어간 곳이 buffer의 지역변수 스택 공간이다.
0x61FE6C이 0인데 이 곳이 check 지역 변수 공간이다.
| buffer[20] | 지역 변수가 저장되는 공간 |
| SFP ($ebp) | Saved Frame Pointer. 이전 함수의 베이스 포인터 주소가 저장됨 |
| Return address ($eip) | 함수 실행이 끝난 후 돌아가야 할 주소. 정상적인 경우 foo(a, b) 호출 다음 명령어 위치를 가리킴 |
취약점이 발생하는 이유
foo 함수 내부에서 char buffer[20]으로 20바이트 크기의 버퍼를 할당했다. 하지만 fgets(buffer, 48, stdin) 함수를 사용하여 최대 48바이트까지 사용자 입력을 받는다. 할당된 버퍼 크기(20바이트)보다 입력받는 데이터의 크기(48바이트)가 더 크기 때문에, 초과된 데이터가 인접한 메모리 영역을 침범하는 오버플로우가 발생한다.
버퍼 오버플로우에 안전한 코딩 (시큐어 코딩)
1) 경계 검사를 포함하는 함수 사용
| 위험한 함수 | 안전한 대체 함수 |
| strcpy() (크기 제한 없음) | strncpy() (최대 길이 지정) |
| strcat() (크기 제한 없음) | strncat() (최대 길이 지정) |
| gets() (크기 제한 없음) | fgets() (크기 지정 가능, 단 지정한 크기를 버퍼 크기와 일치시켜야 함) |
2) 입력값 크기 점검
프로그램이 사용자로부터 값을 입력받을 때, 그 값이 할당된 버퍼 크기를 초과하지 않는지 직접 점검한다. 잘못된 입력이 들어오면 작업을 수행하지 않고 오류 메시지를 출력하도록 처리한다.
스택 오버플로우

1단계: 쉘 코드 주입 (Shell Code Injection)
공격자는 먼저 스택 버퍼에 할당된 크기보다 더 큰 데이터를 입력하여 오버플로우를 발생시킨다. 이 과정에서 스택 메모리 영역에 공격자가 의도한 악성 코드인 쉘 코드(shell code)를 복제해 넣는다.
2단계: 복귀 주소 조작 (Return Address Overwrite)
함수가 종료된 후 돌아가야 할 주소를 담고 있는 복귀 주소(return address) 영역을 덮어쓴다. 이때 정상적인 복귀 주소 대신, 1단계에서 스택에 복제해 둔 쉘 코드의 시작 주소를 기록한다.
3단계: EIP 레지스터 갱신 (Load to EIP)
현재 실행 중인 함수가 종료되면(ret 명령 실행 시), CPU는 스택에 저장되어 있던 복귀 주소를 읽어 EIP(Extended Instruction Pointer) 레지스터로 가져온다. 공격에 의해 이미 복귀 주소 위치에는 쉘 코드의 주소가 저장되어 있으므로, EIP에는 쉘 코드의 시작 주소가 로드된다.
4단계: 악성 코드 실행 (Execution)
EIP 레지스터는 CPU가 다음에 실행할 명령어의 주소를 가리킨다. EIP가 쉘 코드의 시작 주소를 가리키게 되었으므로, 결과적으로 프로그램의 정상적인 흐름이 아닌 공격자가 주입한 쉘 코드가 실행된다.
방어 기법
방어 1: 주소 예측 방해 (1·2단계 대응) - ASLR
ASLR (Address Space Layout Randomization): 프로그램이 실행될 때마다 스택, 힙, 라이브러리 등 데이터 영역의 메모리 주소를 무작위로 배치하는 기법이다.
공격자가 쉘 코드를 스택에 복제하더라도(1단계), 그 시작 주소가 매번 달라지므로 복귀 주소에 덮어쓸 정확한 주소값(2단계)을 예측하기 어렵게 만든다.

ASLR 적용 시 공격이 실패하는 과정:
- 공격자의 시도 (주소 예측 불가): 공격자는 스택 오버플로우를 일으켜 복귀 주소를 자신이 주입한 셸 코드의 주소로 덮어쓰려 한다. 이때 공격 코드를 작성하는 시점에 셸 코드가 위치할 메모리 주소를 예측하여 하드코딩(고정된 값)해야 한다.
- 주소 불일치 발생: ASLR이 적용되어 있으면 프로그램이 실행될 때마다 스택 영역의 시작 주소가 무작위로 변경된다. 즉, 공격자가 "이 위치에 셸 코드가 있을 것"이라고 예측하고 적어 넣은 주소와, 실제 실행 시 셸 코드가 로드된 주소가 서로 달라지게 된다.
- 결과 (엉뚱한 주소 참조): 함수가 종료되어 변조된 복귀 주소로 점프하더라도 그곳에는 셸 코드가 존재하지 않는다. CPU는 엉뚱한 메모리 주소를 참조하게 되며, 유효하지 않은 명령어를 실행하거나 접근 권한이 없는 영역에 닿게 되어 프로그램은 실행되지 않고 종료(Crash)된다.
방어 2: 오버플로우 탐지 (3단계 대응) - Canary
Canary(또는 Stack Guard): 버퍼와 복귀 주소(Return Address) 사이에 무결성 검증을 위한 특정 값(카나리아)을 삽입하는 기법이다.
함수가 종료되어 복귀 주소를 가져오기 전(3단계), 이 카나리 값이 변조되었는지 검사한다. 값이 변경되었다면 스택 오버플로우가 발생한 것으로 간주하고 프로그램을 강제로 종료시켜 공격을 차단한다.

방어 3: 실행 권한 제어 및 예외 처리 보호 (4단계 대응) — DEP/NX, Safe SEH
DEP (Data Execution Protection) / NX (No-eXecute): 스택이나 힙과 같은 데이터 영역에 저장된 코드가 실행되는 것을 하드웨어 또는 소프트웨어적으로 방지하는 기법이다.
공격자가 EIP를 쉘 코드 위치로 옮기는 데 성공하더라도(3단계까지는 성공), 해당 메모리 영역에 실행 권한이 없으므로 쉘 코드가 실행되지 않고 오류가 발생한다(4단계 차단).

Safe SEH (Safe Structured Exception Handling): 윈도우 환경에서 사용되는 기법으로, 스택에 저장된 예외 처리기(Exception Handler)의 주소가 덮어씌워지는 것을 방지한다. 공격자가 복귀 주소 대신 예외 처리 루틴을 조작하여 실행 흐름을 가로채려는 시도를 차단한다.
포맷 스트링 취약점
포맷 문자열(format string)은 C언어의 printf() 함수처럼 출력 형식을 지정하는 문자열이다.
printf() 같은 함수는 가변인자 함수라고 부른다. 함수에 전달되는 매개변수의 수가 고정되어 있지 않고 변할 수 있다는 의미다.
int printf(const char *format, ...);
printf() 함수는 첫 번째 매개변수인 format 문자열을 분석한다. format 문자열 안에서 % 문자로 시작하는 포맷 지정자의 개수를 파악하고, 이를 바탕으로 스택에 저장된 뒤따라오는 매개변수들의 개수와 타입을 예상하여 값을 가져온다.
문제가 되는 코드 패턴
char message2[100];
gets(message2); // 사용자가 자유롯게 입력
printf(message2); // message2를 그대로 포맷 문자열로 사용!
이 코드는 두 가지 문제 지점을 가지고 있다.
포맷 지정자가 있는데 인자가 없을 때
printf(message2); // message2 = "Format String %x %d"
printf 함수는 첫 번째 인자로 받은 문자열에서 %x와 %d라는 포맷 지정자를 발견한다. printf는 이 지정자들에 대응하는 추가 매개변수 두 개가 스택에 전달되었을 것이라 예상하고 스택에서 값을 읽으려 한다. 하지만 실제로는 아무 값도 전달되지 않았기 때문에, 스택의 엉뚱한 데이터를 읽어와 16진수(%x)와 10진수(%d) 형태로 출력하게 된다.
사용자 입력이 포맷 문자열 자체가 될 때
gets(message2); // 사용자가 직접 message2 내용을 입력
printf(message2); // 그 내용이 그대로 포맷 문자열로 쓰임
gets 함수를 통해 사용자가 message2 변수의 내용을 직접 입력할 수 있게 된다. 만약 사용자가 %x %x %x 같은 포맷 지정자를 포함한 문자열을 입력하면, 이 문자열이 printf 함수의 포맷 문자열로 그대로 사용된다. 이게 바로 포맷 문자열 공격(Format String Bug)이다.
실제로 일어나는 일 - 메모리 읽기 (Stack Dumping)
공격자가 %x를 여러 번 입력하면, 스택에 존재하는 값들(다른 변수의 값, 함수의 복귀 주소 등)이 순차적으로 노출된다.

포맷만 넣어도 printf는 출력을 하기 때문에 스택 메모리 주소가 그대로 노출된다. 이 출력된 값들 중에 복귀 주소(RA, Return Address)가 섞여 있다면 매우 위험하다 공격자가 그 값을 보고 프로그램의 메모리 구조나 코드 위치를 추정할 수 있게 되기 때문이다.
리버싱



%x %x %x %x %x를 입력하면 차례로 EAX에 들어가는 것을 디버거에서 직접 확인할 수 있다. 각 %x가 호출될 때마다 스택의 그 다음 위치에서 값을 하나씩 꺼내오는 과정을 한 줄씩 추적하면, "포맷 문자열이 스택을 순회하며 값을 읽어오는" 과정이 눈에 보인다. -> 이중에 RA가 있으면 위험함
%n을 쓰면 더 위험해진다
포맷 문자열 공격은 메모리를 "읽는" 것에서 끝나지 않는다.
%n 지정자는 지금까지 출력된 바이트 수를 특정 주소에 쓰는 지정자다. 만약 공격자가 이 %n을 사용하면, 공격자가 원하는 메모리 주소의 값을 변경하여 프로그램의 실행 흐름을 조작할 수도 있다.
방어
포맷 스트링 취약점의 근본 원인은 단 하나다: 사용자 입력을 printf의 포맷 문자열 자리에 직접 넘긴 것.
// 취약한 코드
printf(user_input);
// 안전한 코드
printf("%s", user_input);
"%s"라는 고정된 포맷 문자열을 직접 작성하고, 사용자 입력은 그 인자 자리에만 넣으면 사용자가 포맷 지정자를 주입할 방법이 사라진다.
힙 오버플로우
힙(Heap)
힙의 정의: 힙은 사용자가 프로그램 실행 중에 malloc과 같은 함수를 사용하여 데이터 공간을 동적으로 할당하고 해제하며 자유롭게 사용하는 메모리 공간이다.
취약점이 발생하는 코드
char *heap_1 = malloc(20);
char *heap_2 = malloc(20);
gets(heap_1);
malloc으로 할당된 heap_1과 heap_2가 메모리상에 연속으로 위치할 수 있다. heap_1은 20바이트만 할당되었지만, gets() 함수를 사용하여 20바이트보다 더 큰 데이터를 입력받을 수 있다.
이때 heap_1에 20바이트를 초과하는 데이터가 입력되면, 그 데이터가 heap_1의 경계를 넘어 인접한 heap_2의 메모리 공간을 덮어쓰게 된다.
힙 오버플로우로 가능한 공격
힙 공간에 존재하는 다른 변수의 데이터를 임의의 값으로 변경할 수 있다.
1) 파일 경로 변경
만약 heap_2에 파일 경로가 저장되어 있었다면, 오버플로우를 통해 이 경로를 공격자가 원하는 다른 경로로 바꿀 수 있다.
예를 들어 프로그램이 heap_2에 적힌 경로의 파일을 나중에 열거나 실행한다면, 공격자는 이 경로를 자신이 준비한 악성 파일의 경로로 바꿔서 프로그램이 의도치 않은 파일을 처리하게 만들 수 있다.
2) 함수 주소 변경 (실행 흐름 조작)
만약 heap_2에 함수 포인터(실행할 함수의 주소)가 저장되어 있었다면, 공격자가 이 주소를 셸코드(공격 코드)가 있는 주소로 변경하여 프로그램의 실행 흐름을 바꿀 수 있다.
'Malware Analysis' 카테고리의 다른 글
| [악성코드 분석] Procmon 동적분석 (0) | 2026.07.08 |
|---|---|
| [악성코드 분석] 프로세스/코드 인젝션 (0) | 2026.06.24 |
| [악성코드 분석] 악성코드의 방어 기법 (0) | 2026.06.24 |
| [악성코드 분석] PE 파일 구조 (0) | 2026.06.24 |
| [악성코드 분석] 리버싱을 위한 기초 지식 (0) | 2026.06.24 |