You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 2 Next »

Excuse the ads! We need some help to keep our site up.

List

Shellcode

  • Shellcode라고 불리는 이유는 일반적으로 명령 shell을 실행하여 공격자가 해당 시스템을 제어하기 때문이라고 합니다.
    • Shellcode는 Machine code로 작성된 작은 크기의 프로그램입니다.
    • Shellcode는 일반적으로 어셈블리어로 작성 후 기계어로 변경합니다.
    • 셀코드는 세포에 침투하는 생물학적 바이러스처럼 실행중인 프로그램에 삽입된 코드를 뜻합니다.
    • 셀코드는 실제로 실행 가능한 프로그램은 아니므로 셀코드를 작성할 때 메모리상 배치라든지 메모리 세그먼트 등에 신경쓰지 않아도 됩니다.

The basics of shellcode(ubuntu-16.04)

C → ASM → Machine code

  • Shellcode를 개발하기 전에 다음과 같은 이해가 필요합니다.

    • 우리가 사용중인 많은 프로그램들은 C 언어와 같은 고수준의 언어를 컴파일 과정에 의해 Assembly, Machine code 같은 저수준 언어로 변경된 파일입니다.

      • C code는 해당 시스템에 맞는 Assembly code로 변환 → 변환된 Assembly code를 Machine code로 표현

  • 이렇게 Machine code 는 메모리에 로드되어 코드를 실행하게 됩니다.

    • 즉, 공격자는 취약성을 이용하여 Shell을 획득하기 위해서는 주수준 언어로 개발 된 코드가 필요합니다.

    • Shellcode는 Assembly, Machine code 같은 저수준 언어 개발 되어야 합니다.

C → ASM → Machine code
C code

Assembly code

Machine code

Helloworld.c
#include<stdio.h>
int main(){
    printf("Hello world!\n");
    return 1;
}
Helloworld.s
0000000000400526 <main>:
  400526:	push   %rbp
  400527:	mov    %rsp,%rbp
  40052a:	mov    $0x4005c4,%edi
  40052f:	callq  400400 <puts@plt>
  400534:	mov    $0x1,%eax
  400539:	pop    %rbp
  40053a:	retq   
Machine code
400526:	55
400527:	48 89 e5 
40052a:	bf c4 05 40 00
40052f:	e8 cc fe ff ff 
400534:	b8 01 00 00 00
400539:	5d
40053a:	c3

Assembly code

  • Shellcode를 개발하기 위해 아래와 같은 기본적인 Assembly 명령어에 대한 지식이 필요합니다.
    • 이외에도 다양한 Instructions이 존재합니다.
Intel syntax

Instructions

Meaning
mov destination, source목표 피연산자에 소스 피연산자를 복사합니다.
PUSH valuestack에 Value 값을 저장합니다.
POP registerstack 상위의 값을 레지스터에 저장합니다.
CALL function_name(address)리턴을 위해 CALL 명령어의 다음 명령주소를 스택에 저장한 후 함수의 위치로 점프를 합니다.
ret스택으로 부터 리턴 주소를 팝하고 그 곳으로 점프하여 함수에서 리턴 합니다.
inc destination목표 피연산자를 1증가 시킵니다.
dec destination목표 피연산자를 1 감소 시킵니다.
add destination, value목표 피연산자에 value 값을 더합니다.
sub destination, value목표 피연산자에 value 값을 뺍니다.
or destination, value비트 or 논리 연산을 한다. 최종 결과는 목표 피연산자에 저장됩니다.
and destination, value비트 and 논리 연산을 한다. 최종 결과는 목표 피연산자에 저장됩니다.
xor destination, value비트 xor 논리 연산을 한다. 최종 결과는 목표 피연산자에 저장됩니다.
lea destination, source목표 피연산자에 소스 피연산자의 유효 주소를 로드합니다.
  • Assembly code 에서 시스템 함수를 호출하기 위해 "int 0x80", "syscall" 명령어를 사용할 수 있습니다.
    • "int" 명령어의 피연산자 값으로 0x80을 전달하게 되면 EAX에 저장된 시스템 함수를 호출 합니다.
    • "syscall" 명령어를 호출하면 RAX에 저장된 시스템 함수를 호출 합니다.
INT 0x80 & SYSCALL
InstructionsMeaningArchitecture
INT <Operand 1>Call to interruptx86, x86_64
SYSCALLSYStem call

x86_64

Linux system call in assembly

  • Shellcode를 개발하기 위해 Assembly code에서 사용 가능한 system 함수들이 필요합니다.
  • C에서는 편의성과 호환성을 위해 표준 라이브러리가 제공됩니다.
    • 표준 라이브러리가 다양한 아키텍처에 맞는 시스템 콜을 알기 때문에 여러 시스템에서 컴파일이 가능합니다.
  • 어셈블리 언어는 어느 특정 프로세서 아키텍처용이라고 정해져 있습니다.
    • 호환성 있는 표준 라이브러리가 없으며, 커널 시스템 콜을 직접 호출 할 수 있습니다.
  • 다음과 같이 해당 파일에서 사용가능한 System 함수들을 확인 할 수 있습니다.
    • System call 정보는 운영체제, 프로세서의 아키텍처 마다 다릅니다.
    • 해당 파일에 사용한 가능한 리눅스 시스템 함수의 이름과 시스템 콜 번호가 나열돼 있습니다.
    • 어셈블리로 시스템 함수을 호출 할 때는 시스템 콜 번호를 이용합니다.
  • 아래와 같이 동일한 Ubuntu 시스템도 32bit, 64bit에서 사용되는 시스템 함수의 콜 번호가 다릅니다.
unistd_32.h
lazenca0x0@ubuntu:~/$ cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H 1

#define __NR_restart_syscall 0
#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
#define __NR_lchown 16
#define __NR_break 17
#define __NR_oldstat 18
#define __NR_lseek 19
#define __NR_getpid 20
...
unistd_64.h
lazenca0x0@ubuntu:~/$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
#define __NR_munmap 11
#define __NR_brk 12
#define __NR_rt_sigaction 13
#define __NR_rt_sigprocmask 14
#define __NR_rt_sigreturn 15
#define __NR_ioctl 16
#define __NR_pread64 17
#define __NR_pwrite64 18
#define __NR_readv 19
#define __NR_writev 20
...

Ubuntu 16.04

  • 32bit : /usr/include/x86_64-linux-gnu/asm/unistd_32.h
  • 64bit : /usr/include/x86_64-linux-gnu/asm/unistd_64.h

Save argument value in registers

  • 앞에서 확인한 시스템 함수의 인자 값은 다음과 같이 사용할 수 있습니다.
    • System call 번호는 EAX, RAX 에 저장합니다.
    • System 함수의 인자 값은 아래 표를 참고 하여 전달 하면 됩니다.
    • Kernel의 경우 기본적으로 cdecl 함수 호출 규약을 사용하지만, 동작의 유연성을 위해 "System V ABI" Calling Convention도 사용가능합니다.
    • Shellcode를 작성하는 경우 "System V ABI" Calling Convention을 사용합니다.
Argument
-Register(32bit)Register(64bit)
System callEAXRAX
Argument 1EBX

RDI

Argument 2ECX

RSI

Argument 3EDX

RDX

Argument 4ESI

R10

Argument 5EDI

R8

Argument 6

EBP

R9

Assembly Code Example("Hello, world!")

Build assembly code(32 bit)

  • 다음 어셈블리 코드를 보면 앞부분에 메모리 세그먼트가 선언돼 있습니다.
    • 데이터 세그먼트에 "Hello, world!" 문자열과 개행 문자(0x0a)가 있습니다.
  • 텍스트 세그먼트에 실제 어셈블리 명령이 있습니다.
    • ELF바이너리를 생성하려면 링커에게 어셈블리 명령이 어디서부터 시작하는지 알려주는 global _start줄이 필요합니다.
ASM32.asm
section .data							; 데이터 세그먼트
	msg	db	"Hello, world!",0x0a, 0x0d	; 문자열과 새 줄 문자, 개행 문자 바이트
 
section	.text							; 텍스트 세그먼트
	global	_start						; ELF 링킹을 위한 초기 엔트리 포인트
 
_start:
	; SYSCALL: write(1,msg,14)
	mov	eax, 4		; 쓰기 시스템 콜의 번호 '4' 를 eax 에 저장합니다. 
	mov ebx, 1		; 표준 출력를 나타내는 번호 '1'을 ebx에 저장합니다.
	mov ecx, msg	; 문자열 주소를 ecx에 저장니다.
	mov edx, 14		; 문자열의 길이 '14'를 edx에 저장합니다.
	int 0x80		; 시스템 콜을 합니다.
 
	; SYSCALL: exit(0)
	mov eax, 1		; exit 시스템 콜의 번호 '1'을 eax 에 저장합니다.
	mov ebx, 0 		; 정상 종료를 의미하는 '0'을 ebx에 저장 합니다.
	int 0x80		; 시스템 콜을 합니다.
  • -f elf 인자를 nasm 어셈블리를 사용해 helloworld.asm을 어셈블해서 ELF 바이너리로 링크할 수 있는 목적(object) 파일로 만들 것입니다.
  • 링커 프로그램 ld는 이 어셈블된 목적 파일에서 실행 가능한 a.out 바이너리를 만들어 냅니다.
Build
lazenca0x0@ubuntu:~/ASM$ nasm -f elf ASM32.asm 
lazenca0x0@ubuntu:~/ASM$ ld -m elf_i386 -o hello ASM32.o 
lazenca0x0@ubuntu:~/ASM$ ./hello 
Hello, world!
lazenca0x0@ubuntu:~/ASM$ file ./hello 
./hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped
lazenca0x0@ubuntu:~/ASM$ 


Install nasm

sudo apt-get install nasm

Build assembly code(64 bit)

ASM64.asm
section .data								; 데이터 세그먼트
	msg db      "hello, world!",0x0a, 0x0d	; 문자열과 새 줄 문자, 개행 문자 바이트

section .text								; 텍스트 세그먼트
    global _start							; ELF 링킹을 위한 초기 엔트리 포인트

_start:
	; SYSCALL: write(1,msg,14)
    mov     rax, 1		; 쓰기 시스템 콜의 번호 '1' 를 rax 에 저장합니다. 
    mov     rdi, 1		; 표준 출력를 나타내는 번호 '1'을 rdi에 저장합니다.
    mov     rsi, msg	; 문자열 주소를 rsi에 저장니다.
    mov     rdx, 14		; 문자열의 길이 '14'를 rdx에 저장합니다.
    syscall				; 시스템 콜을 합니다.

	; SYSCALL: exit(0)
    mov    rax, 60		; exit 시스템 콜의 번호 '60'을 eax 에 저장합니다.
    mov    rdi, 0		; 정상 종료를 의미하는 '0'을 ebx에 저장 합니다.
    syscall				; 시스템 콜을 합니다.
Build
lazenca0x0@ubuntu:~/ASM$ nasm -f elf64 ASM64.asm 
lazenca0x0@ubuntu:~/ASM$ ld -o hello64 ASM64.o 
lazenca0x0@ubuntu:~/ASM$ ./hello64 
hello, world!
lazenca0x0@ubuntu:~/ASM$ file ./hello64 
./hello64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
lazenca0x0@ubuntu:~/ASM$ 

Change to Shellcode format

Code

  • 앞에서 개발한 프로그램은 혼자서 동작되지 않으며 링킹 과정도 필요하지 때문에 Shellcode가 아닙니다. 
  • 다음과 같이 앞에서 개발한 코드를 독자적으로 동작 가능하게 변경 할 수 있습니다.
    • 텍스트, 데이터 세그먼트를 사용하지 않습니다.
    • 함수의 호출은 call 명령어를 사용하여 "helloworld" 함수를 호출합니다.
    • Write 시스템 함수에 의해 출력 될 문자열은 해당 명령어 뒤에 작성합니다.
  • 여기서 문제가 발생합니다.
    • Write 시스템 함수에 2번째 인자로 출력할 메시지가 저장된 주소를 전달 해야 합니다.
    • Assembly Code에서는 데이터 세그먼트를 사용하지 않기 때문에 mov 명령어를 이용해 값을 전달 할 수 없습니다.
    • 해당 문제는 call, ret 명령어를 사용해 해당 문제를 해결 할 수 있습니다.
      • call 명령어에 의해 함수가 호출될 때 call 명령어 다음 명령어의 주소(리턴주소)를 스택에 저장(push) 합니다.
      • 함수의 사용이 끝난 후에 ret 명령어를 이용해 스택에 저장된 주소(리턴주소)를 eip 레지스터에 저장합니다.
      • 이러한 구조에 의해 RET 명령어가 실행 되기전에 스택에 저장된 리턴 주소를 변경하면 프로그램의 실행 흐름을 변경 할 수 있습니다.
      • 즉, call 명령어 뒤에 출력할 메시지를 저장하면 pop 명령어를 이용해 ecx 레지스터에 주소값을 전달 할 수 있습니다.
  • 메모리 세그먼트를 사용하지 않고 완전히 위치 독립적인 방법으로 코드를 생성 했습니다.
ASM32.s
BITS 32    						; nasm에게 32비트 코드임을 알린다
 
call helloworld 				; 아래 mark_below의 명령을 call한다.
db "Hello, world!", 0x0a, 0x0d 	; 새 줄 바이트와 개행 문자 바이트
 
helloworld:
	; ssize_t write(int fd, const void *buf, size_t count);
	pop ecx			; 리턴 주소를 팝해서 exc에 저장합니다.
	mov eax, 4		; 시스템 콜 번호를 씁니다.
	mov ebx, 1		; STDOUT 파일 서술자
	mov edx, 15		; 문자열 길이
	int 0x80		; 시스템 콜: write(1,string, 14)
 
	; void _exit(int status);
	mov eax,1		;exit 시스템 콜 번호
	mov ebx,0		;Status = 0
	int 0x80		;시스템 콜: exit(0)
  • 다음과 같이 nasm으로 빌드하면 "Hello, world!" 메시지를 출력하는 shellocode가 생성됩니다.
Build & Disassemble
lazenca0x0@ubuntu:~/ASM$ nasm ASM32.s 
lazenca0x0@ubuntu:~/ASM$ ndisasm -b32 ASM32
00000000  E80F000000        call dword 0x14
00000005  48                dec eax
00000006  656C              gs insb
00000008  6C                insb
00000009  6F                outsd
0000000A  2C20              sub al,0x20
0000000C  776F              ja 0x7d
0000000E  726C              jc 0x7c
00000010  64210A            and [fs:edx],ecx
00000013  0D59B80400        or eax,0x4b859
00000018  0000              add [eax],al
0000001A  BB01000000        mov ebx,0x1
0000001F  BA0F000000        mov edx,0xf
00000024  CD80              int 0x80
00000026  B801000000        mov eax,0x1
0000002B  BB00000000        mov ebx,0x0
00000030  CD80              int 0x80
lazenca0x0@ubuntu:~/ASM$ hexdump -C ASM32
00000000  e8 0f 00 00 00 48 65 6c  6c 6f 2c 20 77 6f 72 6c  |.....Hello, worl|
00000010  64 21 0a 0d 59 b8 04 00  00 00 bb 01 00 00 00 ba  |d!..Y...........|
00000020  0f 00 00 00 cd 80 b8 01  00 00 00 bb 00 00 00 00  |................|
00000030  cd 80                                             |..|
00000032
lazenca0x0@ubuntu:~/ASM$

Test

  • 생성된 shellcode를 테스트 하기 위해 python을 이용해 shellcode 내용을 변환합니다.
Convert output format of shellcode
lazenca0x0@ubuntu:~/ASM$ python
Python 2.7.12 (default, Nov 20 2017, 18:23:56) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> f = open('ASM32','r')
>>> data = f.read()
>>> data
'\xe8\x0f\x00\x00\x00Hello, world!\n\rY\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xba\x0f\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80'
>>> 
  • 다음과 같은 코드를 이용하여 생성한 shellcode를 테스트 할 수 있습니다.
    • shellcode 에 생성한 shellcode를 저장합니다.
    • strlen() 함수에 의해 shellcode에 저장된 문자열의 길이를 출력합니다.
    • strcpy() 함수에 의해 shellcode의 내용이 code영역으로 복사됩니다.
    • code 변수의 형태를 함수 형태로 형 변화하여 "void (*function)()" 변수에 저장합니다.
    • function() 함수를 호출하면 data영역에 저장된 shellcode가 실행 됩니다.
shellcode.c
#include<stdio.h>
#include<string.h>

unsigned char shellcode [] = "\xe8\x0f\x00\x00\x00Hello, world!\n\rY\xb8\x04\x00\x00\x00\xbb\x01\x00\x00\x00\xba\x0f\x00\x00\x00\xcd\x80\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80";
unsigned char code[];

void main(){
	int len = strlen(shellcode);
	printf("Shellcode len : %d\n",len);
	strcpy(code,shellcode);
	(*(void(*)()) code)();
}
  • 빌드 후 실행하면 다음과 같이 에러를 확인 할 수 있습니다.
Build & Run
lazenca0x0@ubuntu:~/ASM$ gcc -o shell -fno-stack-protector -z execstack --no-pie -m32 test.c
test.c:5:15: warning: array 'code' assumed to have one element
 unsigned char code[];
               ^
lazenca0x0@ubuntu:~/ASM$ ./shell 
Shellcode len : 2
Segmentation fault (core dumped)
lazenca0x0@ubuntu:~/ASM$ 
  • 우분투 64bit 환경에서 32bit 프로그램을 컴파일 하기 위해 아래와 관련 라이브러리를 설치해야 합니다.

Ubuntu 64 bit

sudo apt-get install libx32gcc-5-dev libc6-dev-i386

Debugging

  • 디버깅을 통해 에러의 원인을 확인해 보겠습니다.
    • strcpy()함수가 호출되는 부분에 breakpoint를 설정하고 실행합니다.
    • strcpy()함수가 호출되기 전이기 때문에 code 변수 영역(0x804a074)에는 아무런 값이 저장되어 있지 않습니다.

    • shellcode 변수 영역에는 앞에서 작성한 값이 정상적으로 저장되어 있습니다.
debugging
lazenca0x0@ubuntu:~/ASM$ gdb -q ./shell
Reading symbols from ./shell...(no debugging symbols found)...done.
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x0804846b <+0>:	lea    ecx,[esp+0x4]
   0x0804846f <+4>:	and    esp,0xfffffff0
   0x08048472 <+7>:	push   DWORD PTR [ecx-0x4]
   0x08048475 <+10>:	push   ebp
   0x08048476 <+11>:	mov    ebp,esp
   0x08048478 <+13>:	push   ecx
   0x08048479 <+14>:	sub    esp,0x14
   0x0804847c <+17>:	sub    esp,0xc
   0x0804847f <+20>:	push   0x804a040
   0x08048484 <+25>:	call   0x8048340 <strlen@plt>
   0x08048489 <+30>:	add    esp,0x10
   0x0804848c <+33>:	mov    DWORD PTR [ebp-0xc],eax
   0x0804848f <+36>:	sub    esp,0x8
   0x08048492 <+39>:	push   DWORD PTR [ebp-0xc]
   0x08048495 <+42>:	push   0x8048550
   0x0804849a <+47>:	call   0x8048320 <printf@plt>
   0x0804849f <+52>:	add    esp,0x10
   0x080484a2 <+55>:	sub    esp,0x8
   0x080484a5 <+58>:	push   0x804a040
   0x080484aa <+63>:	push   0x804a074
   0x080484af <+68>:	call   0x8048330 <strcpy@plt>
   0x080484b4 <+73>:	add    esp,0x10
   0x080484b7 <+76>:	mov    DWORD PTR [ebp-0x10],0x804a074
   0x080484be <+83>:	mov    eax,DWORD PTR [ebp-0x10]
   0x080484c1 <+86>:	call   eax
   0x080484c3 <+88>:	nop
   0x080484c4 <+89>:	mov    ecx,DWORD PTR [ebp-0x4]
   0x080484c7 <+92>:	leave  
   0x080484c8 <+93>:	lea    esp,[ecx-0x4]
   0x080484cb <+96>:	ret    
End of assembler dump.
gdb-peda$ b *0x080484af
Breakpoint 1 at 0x80484af
gdb-peda$ r
Starting program: /home/lazenca0x0/ASM/shell 
Shellcode len : 2
Breakpoint 1, 0x080484af in main ()
gdb-peda$ x/64bx 0x804a074
0x804a074 <code>:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a07c:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a084:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a08c:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a094:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a09c:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a0a4:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a0ac:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
gdb-peda$ 
gdb-peda$ x/64bx 0x804a040
0x804a040 <shellcode>:	0xe8	0x0f	0x00	0x00	0x00	0x48	0x65	0x6c
0x804a048 <shellcode+8>:	0x6c	0x6f	0x2c	0x20	0x77	0x6f	0x72	0x6c
0x804a050 <shellcode+16>:	0x64	0x21	0x0a	0x0d	0x59	0xb8	0x04	0x00
0x804a058 <shellcode+24>:	0x00	0x00	0xbb	0x01	0x00	0x00	0x00	0xba
0x804a060 <shellcode+32>:	0x0f	0x00	0x00	0x00	0xcd	0x80	0xb8	0x01
0x804a068 <shellcode+40>:	0x00	0x00	0x00	0xbb	0x00	0x00	0x00	0x00
0x804a070 <shellcode+48>:	0xcd	0x80	0x00	0x00	0xe8	0x0f	0x00	0x00
0x804a078:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
gdb-peda$ 
  • 그렇다면 shellcode가 정상적으로 실행되지 않는 이유는 무엇일까?
    • 그 이유는 shellcode에 포함되어 있는 null byte(0x00) 때문입니다.
      • 문자열을 다루는 함수들은 전달된 문자열에 null byte(0x00) 값을 발견하면 문자열의 끝이라고 판단합니다.
      • strlen() 함수도 shellcode의 길이가 2 라고 출력하며, strcpy()함수 또한 null byte(0x00) 이전 까지의 값을 code영역에 복사합니다.
    • 즉, shellcode의 내용이 code 영역에 복사되지 않아 발생한 문제 입니다.
    • 해당 문제를 해결하기 위해 shellcode에 포함된 null byte를 제거해야합니다.
Error point
gdb-peda$ ni

0x080484b4 in main ()
gdb-peda$ x/32bx 0x804a074
0x804a074 <code>:	0xe8	0x0f	0x00	0x00	0x00	0x00	0x00	0x00
0x804a07c:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a084:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
0x804a08c:	0x00	0x00	0x00	0x00	0x00	0x00	0x00	0x00
gdb-peda$ 

Remove null byte

  • 다음 코드를 통해 제거해야 할 null byte를 확인 할 수 있습니다.
    • 0xE80F000000 : call dword 0x14

    • 0xB804000000 : mov eax, 4 
    • 0xBB01000000 : mov ebx, 1
    • 0xBA0F000000 : mov edx, 15
    • 0xB801000000 : mov eax,1
    • 0xBB00000000 : mov ebx,0
Null byte
lazenca0x0@ubuntu:~/ASM$ ndisasm -b32 ASM32
00000000  E80F000000        call dword 0x14
00000005  48                dec eax
00000006  656C              gs insb
00000008  6C                insb
00000009  6F                outsd
0000000A  2C20              sub al,0x20
0000000C  776F              ja 0x7d
0000000E  726C              jc 0x7c
00000010  64210A            and [fs:edx],ecx
00000013  0D59B80400        or eax,0x4b859
00000018  0000              add [eax],al
0000001A  BB01000000        mov ebx,0x1
0000001F  BA0F000000        mov edx,0xf
00000024  CD80              int 0x80
00000026  B801000000        mov eax,0x1
0000002B  BB00000000        mov ebx,0x0
00000030  CD80              int 0x80
lazenca0x0@ubuntu:~/ASM$

Call instruction

  • call 명령어에 첫번째 널바이트가 존재합니다.
    • call 명령어의 피연산자에서 사용 가능한 값의 크기는 dword(32bits) 입니다.
    • "0x14"와 같이 작은 값이 사용될 경우 남은 부분에 null byte가 포함되게 됩니다.
  • 다음과 같이 코드를 작성함으로써 해결 할 수 있습니다.
    • jmp 명령어를 사용해 helloworld 함수를 지나 last 함수로 이동합니다.
    • last 함수에서는 helloworld 함수를 호출하고, 출력할 메시지를 저장합니다.
ASM32-2.s
BITS 32			; nasm에게 32비트 코드임을 알린다

jmp short last	; 맨 끝으로 점프한다.
helloworld:
	; ssize_t write(int fd, const void *buf, size_t count);
	pop ecx 	; 리턴 주소를 팝해서 exc에 저장합니다.
	mov eax, 4	; 시스템 콜 번호를 씁니다.
	mov ebx, 1	; STDOUT 파일 서술자
	mov edx, 15	; 문자열 길지
	int 0x80	; 시스템 콜: write(1,string, 14)

	; void _exit(int status);
	mov eax,1	;exit 시스템 콜 번호
	mov ebx,0	;Status = 0
	int 0x80	;시스템 콜: exit(0)
 
last:
	call helloworld	; 널 바이트를 해결하기 위해 위로 돌아간다.
	db "Hello, world!", 0x0a, 0x0d	; 새 줄 바이트와 개행 문자 바이트
  • 다음과 같이 동일한 call 명령어를 사용했음에도 해당 코드에서 null byte가 제거된 이유는 음수 때문입니다.
    • jmp short 명령어에 의해 0x20 byte로 이동합니다.
      • jmp 명령어의 피연산에서 사용 가능한 값의 크기는 dword(32bits) 입니다.
      • jmp short 명령어의 피연산자에서 사용 가능한 값의 크기는 -128 ~ 127 입니다.
    • call 명령어는 helloworld 함수를 호출하기 위해 -0x22(0x2 - 0x24)로 이동 해야 합니다.
      • 여기서 -0x22를 표현하기 위해 2의 보수로 값(0xFFFFFFDD)을 표현하게 됩니다.
    • 즉, 'jmp short', 'call 음수' 명령어로 인해 null byte가 제거 되는 것입니다.
ndisasm -b32 ASM32-2
lazenca0x0@ubuntu:~/ASM$ nasm ASM32-2.s 
lazenca0x0@ubuntu:~/ASM$ ndisasm -b32 ASM32-2
00000000  EB1E              jmp short 0x20
00000002  59                pop ecx
00000003  B804000000        mov eax,0x4
00000008  BB01000000        mov ebx,0x1
0000000D  BA0F000000        mov edx,0xf
00000012  CD80              int 0x80
00000014  B801000000        mov eax,0x1
00000019  BB00000000        mov ebx,0x0
0000001E  CD80              int 0x80
00000020  E8DDFFFFFF        call dword 0x2
00000025  48                dec eax
00000026  656C              gs insb
00000028  6C                insb
00000029  6F                outsd
0000002A  2C20              sub al,0x20
0000002C  776F              ja 0x9d
0000002E  726C              jc 0x9c
00000030  64210A            and [fs:edx],ecx
00000033  0D                db 0x0d
lazenca0x0@ubuntu:~/ASM$ 

Register

  • call 명령어 다음으로 null byte가 포함되어 있는 명령어는 Register에 값을 저장하는 부분입니다.
  • Register의 크기를 이해하면 null byte가 포함된 이유를 알 수 있습니다.
    • 64 bit, 32bit, 16 bit 레지스터에 표현 가능한 값보다 작은 값을 저장하게 되면 나머지 공간은 null byte로 채워지게 됩니다.

Register
64 bitRAXRBXRCXRDXRSPRBPRSIRDI
32 bitEAXEBXECXEDXESPEBPESIEDI
16 bitAXBXCXDXSPBPSIDI
8 bitAH/ALBH/BLCH/CLDH/DL



  • L :하위 바이트(Low byte),H :상위 바이트(High byte)
A null byte generated according to the size of the register
Assessmbly codeMachine code
mov eax,0x4B8 04 00 00 00
mov ax,0x466 B8 04 00
mov al,0x4B0 04
  • shellcode에서 64 bit, 32비트 레지스터 영역을 사용하기 전에 레지스터의 모든 영역을 0으로 변경하는 것이 좋습니다.
    • shellcode는 shellcode가 실행되기 이전의 레지스터의 값들을 그대로 사용하기 때문입니다.
  • 다음과 같은 경우가 있을 수 있습니다.
    • write 시스템 콜의 경우 인자 값을 전달 받기 위해 ebx, ecx, edx 레지스터를 사용합니다.
    • 해당 사항을 고려하지 않고 null byte를 제거하기 위해 bl, cl, dl 레지스터에 값을 저장하면 안됩니다.
    • 그 이유는 아래 예제와 같이 shellcode가 실행되기 전에 해당 레지스터에 어떠한 값이 저장되어 있을 수 있기 때문입니다.
      • 하위 레지스터에 값을 저장한다고 해서 앞에 3 바이트가 null byte로 변경되는 것이 아닙니다.
Problem when register value is not initialized

CodeEBX
shellcode 호출 이전-0xdeaddead
shellcodemov bl,0x40xdeadde04
  • 레지스터 값의 초기화 하기 위한 방법은 다양하며, 그 중 다음과 같은 방법이 제일 효율적입니다.
  • sub 명령어를 이용한 초기화
    • sub 명령어를 이용해 자기 자신의 레지스터의 값을 뺍니다.
    • sub 명령어는 쉘 코드 앞부분에서 사용하면 잘 동작합니다.
    • sub 명령어는 연산 결과에 따라 OF, SF, ZF, AF, PF, CF flag의 값이 설정됩니다.
      • 이로 인해 코드가 예상과 다르게 동작할 수 있습니다.
  • xor 명령어를 이용해 초기화
    • xor 명령은 전달된 2개의 피연산자 값을 배타적 논리합(exclusive OR)을 수행합니다.
    • 배타적 논리합(exclusive OR)은 어떤 값이든 자신의 값을 연산하면 0이 됩니다.
    • OF, CF flag의 값이 지워지며, SF, ZF, PF flag는 결과에 따라 결정됩니다.
      • AF flag는 정의 되지 않습니다.
    • xor 명령어가 sub 명령어 보다 flag에 영향을 덜 주기 때문에 xor을 이용해 레지스터 값을 초기화 하는 것이 효율적입니다.
Initialize register value
Assessmbly codeMachine code
sub eax, eax29 C0
xor eax,eax31 C0
  • 앞에서 설명한 내용을 바탕으로 다음과 같이 코드를 작성 할 수 있습니다.
RemoveNullbyte.s
BITS 32			; nasm에게 32비트 코드임을 알린다

jmp short last	; 맨 끝으로 점프한다.
helloworld:
	; ssize_t write(int fd, const void *buf, size_t count);
	pop ecx 	; 리턴 주소를 팝해서 exc에 저장합니다.
	xor eax,eax	; eax 레지스터의 값을 0으로 초기화합니다.
	mov al, 4	; 시스템 콜 번호를 씁니다.
	xor ebx,ebx	; ebx 레지스터의 값을 0으로 초기화합니다.
	mov bl, 1	; STDOUT 파일 서술자
	xor edx,edx	; edx 레지스터의 값을 0으로 초기화합니다.
	mov dl, 15	; 문자열 길지
	int 0x80	; 시스템 콜: write(1,string, 14)

	; void _exit(int status);
	mov al,1	;exit 시스템 콜 번호
	xor ebx,ebx	;Status = 0
	int 0x80	;시스템 콜: exit(0)
 
last:
	call helloworld	; 널 바이트를 해결하기 위해 위로 돌아간다.
	db "Hello, world!", 0x0a, 0x0d	; 새 줄 바이트와 개행 문자 바이트
  • 다음과 같이 Null byte가 제거되었습니다.
Removed Null byte
lazenca0x0@ubuntu:~/ASM$ nasm RemoveNullbyte.s 
lazenca0x0@ubuntu:~/ASM$ ndisasm RemoveNullbyte
00000000  EB15              jmp short 0x17
00000002  59                pop cx
00000003  31C0              xor ax,ax
00000005  B004              mov al,0x4
00000007  31DB              xor bx,bx
00000009  B301              mov bl,0x1
0000000B  31D2              xor dx,dx
0000000D  B20F              mov dl,0xf
0000000F  CD80              int 0x80
00000011  B001              mov al,0x1
00000013  31DB              xor bx,bx
00000015  CD80              int 0x80
00000017  E8E6FF            call word 0x0
0000001A  FF                db 0xff
0000001B  FF4865            dec word [bx+si+0x65]
0000001E  6C                insb
0000001F  6C                insb
00000020  6F                outsw
00000021  2C20              sub al,0x20
00000023  776F              ja 0x94
00000025  726C              jc 0x93
00000027  64210A            and [fs:bp+si],cx
0000002A  0D                db 0x0d
lazenca0x0@ubuntu:~/ASM$ 
  • 앞에서 작성한 코드를 아래 코드를 이용해 테스트 할 수 있습니다.
test.c
#include<stdio.h>
#include<string.h>

unsigned char shellcode [] = "\xeb\x15\x59\x31\xc0\xb0\x04\x31\xdb\xb3\x01\x31\xd2\xb2\x0f\xcd\x80\xb0\x01\x31\xdb\xcd\x80\xe8\xe6\xff\xff\xffHello, world!\n\r";
unsigned char code[] = "";

void main()
{
	int len = strlen(shellcode);
	printf("Shellcode len : %d\n",len);
	strcpy(code,shellcode);
	(*(void(*)()) code)();
}
  • 다음과 같이 빌드 후 실행하면 "Hello, world!" 문자열이 출력됩니다.
Build & Run
lazenca0x0@ubuntu:~/ASM$ gcc -o shell -fno-stack-protector -z execstack --no-pie -m32 test.c
lazenca0x0@ubuntu:~/ASM$ ./shell 
Shellcode len : 43
Hello, world!
lazenca0x0@ubuntu:~/ASM$ 

Related site

  • No labels