2017. 6. 2. 18:59ㆍSystemHacking/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의 코드작성 시 |
< 최종 쉘 생성 코드 >
|
setreuid() 부분은 원하는 계정의 UID에 따라서 바꿔 넣어주어야 해당 계정의 권한으로 쉘코드가 떨어집니다
이 쉘 실행코드를 가지고 있는 실행파일의 소유권과 setreuid설정 그리고 실행파일의 내부에서 setreuid()함수가 실행된 후
쉘 생성코드가 실행되어야 원하는 계정의 권한으로 쉘이 떨어집니다 ( 세가지 조건이 만족되야 소유계정의 프로그램 권한을 그대로 상속받을 수 있다 )
ㅡ 해당 부분은 FTZ문제풀이( 버퍼오버플로우부분 ) 를 통해서 더 알아보도록 합시다
이제 버퍼 오버플로우의 취약점을 가진 프로그램에 이 쉘 생성 코드를 집어 넣어서 실행 시키는 방법을 알아보자 ㅡ [3]장으로
'SystemHacking > System' 카테고리의 다른 글
[6] Format String Bug [FSB] (0) | 2017.06.08 |
---|---|
[5] Heap Buffer Overflow ( Heap기반의 버퍼오버플로우 ) (0) | 2017.06.07 |
[4] 환경변수를 이용한 버퍼 오버플로우 공격 ( 에그쉘) (1) | 2017.06.05 |
[3] Buffer Overflow 공격 (0) | 2017.06.03 |
[1] BufferOverflow Attack의 기초 ( 스택기반의 버퍼오버플로우 ) (0) | 2017.06.01 |