728x90
시스템 구성과 프로그램 동작
프로그램과 코드 보안
- 보안 취약점은 하드웨어, 어셈블리어, 소스코드 중 소스코드에서 가장 쉽게 발생합니다.
- 주된 원인은 데이터의 형태와 길이에 대한 불명확한 정의입니다.
- 예시: 장보기 심부름에서 물품 종류와 가격을 불명확하게 알려주어 문제가 발생하는 상황.
시스템 메모리의 구조
- 프로그램 동작 시 메모리에 가상 공간이 생성되며, 이 공간은 스택(Stack) 영역, 힙(Heap) 영역, 데이터 섹션, 코드 섹션 등으로 구성됩니다.
- 로더가 프로그램을 메모리로 올립니다.
Stack 영역과 Heap 영역
| 특징 | Stack 영역 | Heap 영역 |
| 용도 | 프로그램 로직 인자, 프로세스 상태 저장 | 런타임에 필요한 데이터 임시 저장 |
| 사용 방식 | 복귀 주소 저장, 서브루틴 인자 전달 | 동적 메모리 할당 (ex: malloc()) |
| 메모리 방향 | 상위 주소에서 하위 주소 방향 | (방향 언급 없음, 일반적으로 스택과 반대 방향으로 증가) |
| 데이터 처리 | 후입선출 (LIFO) 원칙 | 포인터 변수를 통해 동적으로 할당 및 회수 |
| 예시 | 함수의 지역 변수 및 함수 실행 인자 값 | malloc() 등으로 할당받은 메모리 공간 |
| 문제 발생 시 | 해당 힙 영역이 없어지면 메모리 부족으로 비정상 종료 (ex: free() 누락) |
레지스터
- CPU의 임시 메모리로, CPU 연산과 어셈블리어 동작에 필수적입니다.
- 인텔 80x86 CPU가 제공하는 주요 레지스터 종류는 다음과 같습니다.
| 범주 | 레지스터 이름 | 비트 | 용도 |
| 범용 | EAX (accumulator) | 32 | 산술 연산에 사용 (함수 결과값 저장) |
| EBX (base register) | 32 | 특정 주소 저장 (주소 지정을 확대하기 위한 인덱스로 사용) | |
| ECX (count register) | 32 | 반복 명령에 사용 (루프 반복 횟수, 시프트 비트 수 기억) | |
| EDX (data register) | 32 | 일반 데이터 저장 (입출력 동작에 사용) | |
| 세그먼트 | CS (code segment) | 16 | 실행 기계 명령어가 저장된 메모리 주소 지정 |
| DS (data segment) | 16 | 프로그램에서 정의된 데이터, 상수, 작업 영역의 메모리 주소 지정 | |
| SS (stack segment) | 16 | 임시 저장 데이터, 피호출 서브루틴이 사용할 데이터와 주소 포함 | |
| ES, FS, GS (extra segment) | 16 | 문자 연산과 추가 메모리 지정에 사용되는 여분 레지스터 | |
| 포인터 | EBP (base pointer) | 32 | SS 레지스터와 함께 스택 내 변수값 읽기 사용 |
| ESP (stack pointer) | 32 | SS 레지스터와 함께 스택의 가장 끝 주소를 가리킴 | |
| EIP (instruction pointer) | 32 | 다음 명령의 오프셋 저장, CS와 합쳐져 다음 수행될 명령 주소 형성 | |
| 인덱스 | EDI (destination index) | 32 | 목적지 주소의 값 저장 |
| ESI (source index) | 32 | 출발지 주소의 값 저장 | |
| 플래그 | EFLAGS (flag register) | 32 | 연산 결과 및 시스템 상태와 관련된 여러 플래그 값 저장 |
프로그램 실행 구조 (스택과 레지스터의 실제 사용)
- main 함수와 덧셈 서브루틴 function이 있는 프로그램 예시를 통해 스택과 레지스터의 동작을 어셈블리어 수준에서 분석합니다.
- 함수 프롤로그(prolog): pushl %ebp, movl %esp, %ebp 명령을 통해 호출된 함수의 스택 프레임을 설정하고 이전 함수의 EBP 값을 스택에 저장합니다.
- 함수 에필로그(epilog): leave, ret 명령을 통해 함수를 종료하고 이전 스택 상태를 복구하며 호출자로 제어를 돌려줍니다.
- call 명령은 함수를 호출하고 복귀 주소를 스택에 저장합니다.
- 지역 변수 할당, 인자 전달, 결과값 저장 등의 과정에서 스택과 EBP, ESP, EAX 등의 레지스터가 어떻게 사용되는지 구체적으로 설명합니다.
int function(int a, int b) {
char buffer[10];
a = a + b;
return a;
}
void main() {
int c;
c = function(1, 2);
}
어셈블리어 코드 주요 동작:
- pushl %ebp: 현재 EBP 값을 스택에 저장.
- movl %esp,%ebp: ESP 값을 EBP에 복사하여 새 스택 프레임의 기준점 설정.
- subl $12, %esp: 지역 변수(buffer[10])를 위한 공간 12바이트 할당 (패딩 포함).
- movl 12(%ebp),%eax: function의 두 번째 인자 b (정수 2)를 EAX에 로드.
- addl %eax,8(%ebp): function의 첫 번째 인자 a (정수 1)에 EAX 값 (2)을 더함.
- movl 8(%ebp),%edx: a+b 결과값 (3)을 EDX에 로드.
- movl %edx,%eax: 결과값 (3)을 EAX에 복사 (함수 반환 값은 보통 EAX에 저장됨).
- leave: movl %ebp,%esp (스택 정리), popl %ebp (이전 EBP 복구).
- ret: 스택에서 복귀 주소를 팝하여 EIP에 저장하고 호출자(main)로 돌아감.
셸 (Shell)
- 운영체제를 둘러싸고 있으면서 명령어를 실행하는 해석기입니다. 조개껍데기(Shell)에 비유됩니다.
- 종류: Bourne Shell (본셸), C Shell (C 셸), Korn Shell (콘셸) 등 다양합니다. (가장 널리 사용되는 것은 Bash)
- 셸의 역할: 자체 내장 명령어 제공, 입출력 리다이렉션, 와일드카드, 파이프라인, 조건부/무조건부 명령열 작성, 서브셸 생성, 백그라운드 작업 처리, 셸 스크립트 작성 등.
- 버퍼 오버플로우나 포맷 스트링 공격의 최종 목적은 '관리자 권한의 셸' 획득입니다.
- 공격 시 /bin/sh를 기계어 코드로 변환하여 메모리에 올리는데, 이는 원하는 주소 공간에 올리기 위함입니다.
char shell[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00\x00"
"\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xb8\x01"
"\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff\xff"
"\x2f\x62\x69\x6e\x2f\x73\x68";
void main(){
int *ret;
ret =(int *)&ret+2;
(*ret)=(int)shell;
}
- 위 코드를 컴파일(gcc -o shell -g -ggdb shell.c) 후 실행하면 관리자 셸을 획득할 수 있습니다.
프로세스 권한과 SetUID
- SetUID: 유닉스 파일에 rwsr-xr-x와 같이 s 권한 비트가 설정된 경우를 말합니다.
- SetUID 파일은 누가 실행하든 상관없이 해당 파일이 실행될 때 파일 소유자 권한을 가집니다.
- 예시: /usr/bin/passwd 파일은 소유자가 root이며 SetUID가 설정되어 있어 일반 사용자가 실행해도 root 권한으로 암호를 변경할 수 있습니다.
- SetUID를 이용한 해킹: 공격자가 셸 파일에 SetUID 권한을 부여하고, 이 파일을 일반 사용자 권한으로 실행하면 root 권한의 셸을 획득할 수 있습니다. (SetUID는 chmod 4755 명령으로 설정)
버퍼 오버플로우 공격
버퍼 오버플로우 공격의 개념
- 데이터의 길이에 대한 불명확한 정의를 악용하여 발생하는 공격입니다.
- 할당된 버퍼의 경계선을 넘어서 다른 메모리 영역을 덮어쓰는 것을 의미합니다.
- 해커는 이를 통해 임의의 코드를 덮어쓰거나 프로그램의 제어 흐름을 변경할 수 있습니다.
- 특정 함수(strcpy, gets 등)가 버퍼 오버플로우에 취약하며, 이러한 함수 사용을 피하는 것이 중요합니다.
버퍼 오버플로우 공격 원리
bugfile.c 예시를 통해 버퍼 오버플로우 공격 원리를 상세히 설명합니다.
int main(int argc, char *argv[]) {
char buffer[10];
strcpy(buffer, argv[1]);
printf("%s\n", buffer);
}
- char buffer[10]: 10바이트 크기의 버퍼 할당.
- strcpy(buffer, argv[1]): argv[1] (첫 번째 인자)의 내용을 buffer에 복사. strcpy는 입력값의 길이를 검사하지 않으므로, 10바이트를 초과하는 입력이 들어오면 버퍼 오버플로우가 발생합니다.
- 스택 구조 변조: strcpy 함수가 10바이트를 초과하는 데이터를 buffer에 복사하면, buffer 뒤에 있는 EBP (이전 스택 프레임의 기준 포인터) 값과 Return Address (함수 종료 후 돌아갈 주소) 값이 덮어씌워집니다.
- 공격 목표: 해커는 Return Address를 자신이 원하는 **셸 코드(shellcode)**의 메모리 주소로 덮어씌웁니다. 셸 코드는 관리자 권한 셸을 획득하기 위한 기계어 코드입니다.
- 공격 실행: perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAA\x58\xfb\xff\xbf"'와 같이 A 문자로 버퍼를 채우고, 덮어씌울 셸 코드의 주소(예: 0xbfffb58)를 뒤에 붙여 bugfile을 실행합니다.
- 결과: 변조된 Return Address에 의해 프로그램은 셸 코드가 있는 주소로 점프하여 셸 코드를 실행하고, 결과적으로 관리자 권한의 셸을 획득하게 됩니다.
버퍼 오버플로우 공격의 대응책
- 버퍼 오버플로우에 취약한 함수 사용 금지:
- strcpy, strcat, getwd, gets, fscanf, scanf, realpath, sprintf 등.
- 대안: 길이를 제한하는 strncpy, strncat, fgets, snprintf 등 안전한 함수 사용. (ex: strncpy(dest, src, size);)
- 최신 운영체제 사용:
- Non-executable Stack: 스택 영역에서 코드 실행을 불가능하게 합니다. (NX bit, DEP)
- 스택 가드 (Stack Guard): 스택 프레임의 복귀 주소 앞에 카나리(Canary) 값을 삽입하여 오버플로우 감지 시 프로그램 종료.
- 스택 실드 (Stack Shield): 함수 프롤로그 시 복귀 주소를 안전한 별도의 스택에 저장하고, 함수 에필로그 시 두 복귀 주소를 비교하여 변조 여부 확인.
- ASLR (Address Space Layout Randomization): 메모리 주소 공간의 배치(스택, 힙, 라이브러리 등)를 매번 무작위로 변경하여 공격자가 셸 코드 주소를 예측하기 어렵게 만듭니
포맷 스트링 공격
포맷 스트링 공격의 개념
- 데이터의 형태에 대한 불명확한 정의 때문에 발생하는 취약점입니다.
- printf("%s\n", buffer);와 같이 포맷 스트링을 명확히 사용하면 문제가 없지만, printf(buffer);와 같이 사용자 입력값을 포맷 스트링으로 직접 사용하는 경우에 취약점이 발생합니다.
- %d, %x, %s와 같은 포맷 스트링 문자 외에 %n과 같은 특수 포맷 스트링 문자를 사용하여 메모리를 읽거나 변조할 수 있습니다.
- %n: 지금까지 출력된 총 바이트 수를 해당 주소에 저장
포맷 스트링 공격의 원리 (메모리 열람 및 변조)
- 메모리 열람:
- printf(buffer); 형태에서 buffer에 "%x %x %x %x"와 같은 포맷 스트링을 입력하면, 스택에 저장된 값들이 16진수로 화면에 출력됩니다. 이를 통해 해커는 스택의 내용을 열람할 수 있습니다.
- test1.c: char *buffer = "wishfree\n%x\n"; printf(buffer); 실행 시 메모리 값이 출력됩니다.
- 메모리 변조:
- %n 포맷 스트링은 지금까지 출력된 총 바이트 수를 특정 메모리 주소에 저장하는 기능을 합니다.
- test2.c
main(){
long i=0x00000064, j=1;
printf("i의주소: %x\n",&i);
printf("i의값: %x\n",i);
printf("%64d%n\n", j, &i); // 64만큼 공백을 채워 64바이트를 출력한 후, 그 64를 &i 주소에 저장
printf("변경된i의값: %x\n",i);
}
- 위 코드에서 printf("%64d%n\n", j, &i);는 j를 출력할 때 앞에 63개의 공백을 추가하여 총 64바이트를 출력합니다. 그리고 %n은 이 64라는 값을 변수 i의 주소(&i)에 저장합니다.
- 결과적으로 i의 초기값 0x00000064는 0x40 (10진수 64)으로 변경됩니다.
이를 악용하여 해커는 프로그램의 중요한 변수나 복귀 주소 등을 원하는 값으로 변조할 수 있습니다.
메모리 해킹
메모리 해킹의 개념
- 프로그램 동작에 관여하지 않고, 실행에 필요한 정보를 저장해둔 메모리를 직접 조작하는 공격입니다.
- 주로 게임 해킹(게임 머니, 아이템 조작)에 광범위하게 사용됩니다.
- 백도어 프로그램 설치를 통해 메모리의 패스워드를 빼내거나 계좌/금액 정보를 조작할 수 있습니다.
- 사용자가 인지하기 어렵고, 휘발성이 강한 메모리의 특성상 흔적 추적이 매우 어렵습니다.
- 대응책: 메모리 주소에 저장되는 값을 암호화해야 합니다.
728x90