1. Shellcoding란?
1-1. 개요
(Shellcode란?)
익스플로잇을 위해 제작된 어셈블리 코드 조각이다.
어셈블리어와 기계어 코드는 일대일 대응이기 때문에 공격자가 rip를 주입한 shellcode로 옮긴다면 cpu에게 원하는 명령을 하도록 유도할 수 있게 된다.
[shellcode작성법을 알아야하는 이유]
수행할 대상의 아키텍처와 운영체제 그리고 수행 목적에 따라 어셈블리어는 다르게 작성되어야 한다.
아키텍처별로 자주 사용하는 쉘코드를 공유하는 사이트가 있지만
해당 쉘코드들은 범용적으로 작성되었기 때문에 제대로 작동이 안 될 가능성이 있다.
따라서 타겟 시스템에 맞는 최적의 쉘코드를 상황에 따라서 언제든지 작성할 수 있어야한다.
=> 쉘코드 모음 사이트 : http://shell-storm.org/shellcode/index.html
(기계어 변환 과정)

- 현재 사용중인 프로그램은 고수준의 언어가 컴파일 과정을 거쳐서 Assembly, Machine code같은 저수준 언어로 변경된 것이다.
- 이렇게 변환된 Machine code는 실행시 메모리에 올라가서 코드를 실행하게 된다.
- 어셈블리어와 기계어코드는 일대일대응이다.
1-2. Syscall
[필요한 사전지식]
- 레지스터 bit별 표현단위
- 어셈블리어
- 함수호출규약
assembly code에서 시스템 함수를 호출하기 위해 "int 0x80"(32), "syscall"(32,64)명령어를 사용할 수 있다.
(system call 번호 확인)

32bit - /usr/include/x86_64-linux-gnu/asm/unistd_32.h
64bit - /usr/include/x86_64-linux-gnu/asm/unistd_64.h
system call 정보는 운영체제, 프로세서의 아키텍처마다 다르기 때문에 맞춰서 작성해야한다.
(system call 인자 전달)

1-3. asm파일의 바이너리 변환 과정
[32bit - 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 ; 시스템 콜을 합니다.
=> 32, 64bit asm의 차이점은 인자전달에 사용되는 레지스터와 시스템 콜 번호이기 때문에
asm코드는 32bit만 작성해두겠다.
(Build)
(32bit)
$ nasm -f elf ASM32.asm
$ ld -m elf_i386 -o hello ASM32.o
$ ./hello
Hello, world!
$ file ./hello
`./hello: ELF` `32``-bit LSB executable, Intel` `80386``, version` `1` `(SYSV), statically linked, not stripped`
(64bit)
$ nasm -f elf64 ASM64.asm
$ ld -o hello64 ASM64.o
$ ./hello64
hello, world!
$ file ./hello64
./hello64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
- f인자 : nasm으로 어셈블하여 ELF바이너리로 링크할 수 있는 목적(object)파일로 만든 것
- ld명령어 : 어셈블된 목적 파일에서 실행가능한 바이너리로 변환
2. Shellcoding 방법
(목적파일(.o)이 쉘코드가 안되는 이유)
- 메모리 세그먼트를 사용
- 목적파일은 독립적으로 실행 가능한 프로그램이 아니라
- 링커에 의해 연결되어야 하는 프로그램의 일부이기 때문
2-1. 바이트 형태로 전달
(독자적으로 동작 가능하게끔 변경)
- 텍스트, 데이터 세그먼트를 사용하지 않음
- 함수의 호출을 call명령어를 사용
- 함수에서 사용될 문자를 해당 명령어 뒤에 작성후 스택포인터 조절로 사용
[32bit - 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)
(build)
$ nasm ASM32.s
$ ndisasm -b32 ASM32
(Test)
=> python3에 쉘코드 바이트값 전달하여 수행
# print bytecode
f = open('ASM32','r')
data = f.read()
print(data) # 바이트 값 출력
// 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)
$ gcc -o shellcode -fno-stack-protector -z execstack --no-pie -m32 shellcode.c
$ ./shellcode
Shellcode len : 2
Segmentation fault (core dumped)
=> 에러 발생????
-> 문제의 원인 및 해결 방식은 아래 3번에서 서술
(\x00이 제거된 asm코드)
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 ; 새 줄 바이트와 개행 문자 바이트
(Test)
#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\xff\
Hello, world!\n\r";
unsigned char code[] = "";
void main()
{
int len = strlen(shellcode);
printf("Shellcode len : %d\n",len);
strcpy(code,shellcode);
(*(void(*)()) code)();
$ gcc -o shellcode2 -fno-stack-protector -z execstack --no-pie -m32 shellcode2.c
$ ./shellcode2
Shellcode len : 43
Hello, world!
2-2. 인라인 어셈
[순서]
1. 실행할 어셈블리어
2. 테스트할 c코드 작성
(asm코드)
; write(1,"Hello world!",12)
mov rax, 0x646c72
push rax
mov rax, 0x6f57206f6c6c6548
push rax
xor rdi, rdi
inc 0x1
mov rsi, rsp
xor rdx rdx
add rdx 0xc
xor rax, rax
int 1
syscall
(c코드)
// write(1, "Hello world", 12);
// exit(0);
__asm__(
".global run_sh\n"
"run_sh:\n"
"mov rax, 0x646c72\n"
"push rax\n"
"mov rax, 0x6f57206f6c6c6548\n"
"push rax\n"
"xor rdi, rdi\n"
"inc rdi\n"
"mov rsi, rsp\n"
"xor rdx, rdx\n"
"add rdx, 0xc\n"
"xor rax, rax\n"
"inc rax\n"
"syscall\n"
"\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)");
void run_sh();
int main() { run_sh(); }
(컴파일)
$ gcc -o test test.c -masm=intel
$ ./test
Hello World
3. Shellcode 작성시 주의사항
(쉘코드에 0x00바이트가 들어가면 생기는 문제)
-> 에러가 발생한 이유 : 쉘코드에 포함되어있는 0x00값 때문
(문자열을 다루는 함수들은 문자열의 끝을 0x00으로 판단한다.)
-> 따라서 쉘코드상에서 0x00을 제거하는 작업을 해야한다.
[\x00 바이트 제거방법]
1. jmp "다른 함수"
2. mov 명령어
(\x00 바이트 제거방법)
(1) jmp "다른 함수"
[null byte가 들어간 이유]
call명령어의 피연산자에서 사용 가능한 값의 크기는 dword(32bits)이기 때문에
작은 값을 사용 될 경우 남는 부분에 null byte가 포함되게 된다.
(해결법)
- 하단의 새로운 함수(a)를 생성
- jmp명령어를 사용해 해당 함수로 점프
- 그리고 해당 함수에서는 원래 사용하려던 함수를 재호출한다.
(디스어셈블 확인)
$ nasm ASM32-2.s
$ 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
=> 컴파일된 어셈블리어를 보면 call명령어가 들어가 있음에도 null byte가 제거된 것을 볼 수 있다.
-> 제거된 이유는 음수이기 때문이다.
- jmp short 0x20로 주소 0x20으로 이동
- (jmp명령어의 피연산자에서 사용 가능한 값의 크기 dword(32bits))
=> 사용 가능한 값의 크기 -128 ~ 127 - call명령어 helloworld함수를 호출하기 위해 -0x20으로(0x2-0x24)로 이동
- -0x22를 표현하기 위해 2의 보수로 값을 표현하게 된다.
- 이 과정에서 null byte가 제거되게 된다.
(2) mov 명령어
(레지스터의 범위)
64bit - 8bit
(레지스터 값을 초기화하는 방법)
[레지스터 영역을 사용하기 전 초기화하는 이유]
=> 쉘코드가 실행되기 이전의 레지스터의 값을 그대로 사용하기 때문
- 그럼 하위비트의 값만 변경한다면?
-> 명령어마다 어떤 단위의 레지스터를 사용할지 다 알 수 없고.
그전에 존재하는 값들이 무엇인지 알 수도 없기때문에 0x00으로 초기화하는 것이 좋다.
(해결법)
1. sub명령어를 이용한 초기화
2. xor명령어를 이용한 초기화
(어셈)
section .text
global _start
_start:
; XOR을 사용하여 eax를 0으로 만들기
mov eax, 5 ; eax = 5
xor eax, eax ; eax = eax ^ eax = 0
; SUB을 사용하여 ebx를 0으로 만들기
mov ebx, 5 ; ebx = 5
sub ebx, ebx ; ebx = ebx - ebx = 0
; 프로그램 종료
mov eax, 1 ; 시스템 호출 번호 (exit)
xor ebx, ebx ; 반환 값 0
int 0x80 ; 시스템 호출
(결론)
=> sub명령어는 플래그 레지스터에 영향을 주어 이후 분기 명령어에 영향을 줄 수 있기 때문에 xor명령어로 레지스터 값을 초기화하는 것이 효율적이다.
참고링크
(1) https://www.lazenca.net/display/TEC/01.The+basics+technic+of+Shellcode
(2) System V ABI" Calling Convention
1. Shellcoding란?
1-1. 개요
(Shellcode란?)
익스플로잇을 위해 제작된 어셈블리 코드 조각이다.
어셈블리어와 기계어 코드는 일대일 대응이기 때문에 공격자가 rip를 주입한 shellcode로 옮긴다면 cpu에게 원하는 명령을 하도록 유도할 수 있게 된다.
[shellcode작성법을 알아야하는 이유]
수행할 대상의 아키텍처와 운영체제 그리고 수행 목적에 따라 어셈블리어는 다르게 작성되어야 한다.
아키텍처별로 자주 사용하는 쉘코드를 공유하는 사이트가 있지만
해당 쉘코드들은 범용적으로 작성되었기 때문에 제대로 작동이 안 될 가능성이 있다.
따라서 타겟 시스템에 맞는 최적의 쉘코드를 상황에 따라서 언제든지 작성할 수 있어야한다.
=> 쉘코드 모음 사이트 : http://shell-storm.org/shellcode/index.html
(기계어 변환 과정)

- 현재 사용중인 프로그램은 고수준의 언어가 컴파일 과정을 거쳐서 Assembly, Machine code같은 저수준 언어로 변경된 것이다.
- 이렇게 변환된 Machine code는 실행시 메모리에 올라가서 코드를 실행하게 된다.
- 어셈블리어와 기계어코드는 일대일대응이다.
1-2. Syscall
[필요한 사전지식]
- 레지스터 bit별 표현단위
- 어셈블리어
- 함수호출규약
assembly code에서 시스템 함수를 호출하기 위해 "int 0x80"(32), "syscall"(32,64)명령어를 사용할 수 있다.
(system call 번호 확인)

32bit - /usr/include/x86_64-linux-gnu/asm/unistd_32.h
64bit - /usr/include/x86_64-linux-gnu/asm/unistd_64.h
system call 정보는 운영체제, 프로세서의 아키텍처마다 다르기 때문에 맞춰서 작성해야한다.
(system call 인자 전달)

1-3. asm파일의 바이너리 변환 과정
[32bit - 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 ; 시스템 콜을 합니다.
=> 32, 64bit asm의 차이점은 인자전달에 사용되는 레지스터와 시스템 콜 번호이기 때문에
asm코드는 32bit만 작성해두겠다.
(Build)
(32bit)
$ nasm -f elf ASM32.asm
$ ld -m elf_i386 -o hello ASM32.o
$ ./hello
Hello, world!
$ file ./hello
`./hello: ELF` `32``-bit LSB executable, Intel` `80386``, version` `1` `(SYSV), statically linked, not stripped`
(64bit)
$ nasm -f elf64 ASM64.asm
$ ld -o hello64 ASM64.o
$ ./hello64
hello, world!
$ file ./hello64
./hello64: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
- f인자 : nasm으로 어셈블하여 ELF바이너리로 링크할 수 있는 목적(object)파일로 만든 것
- ld명령어 : 어셈블된 목적 파일에서 실행가능한 바이너리로 변환
2. Shellcoding 방법
(목적파일(.o)이 쉘코드가 안되는 이유)
- 메모리 세그먼트를 사용
- 목적파일은 독립적으로 실행 가능한 프로그램이 아니라
- 링커에 의해 연결되어야 하는 프로그램의 일부이기 때문
2-1. 바이트 형태로 전달
(독자적으로 동작 가능하게끔 변경)
- 텍스트, 데이터 세그먼트를 사용하지 않음
- 함수의 호출을 call명령어를 사용
- 함수에서 사용될 문자를 해당 명령어 뒤에 작성후 스택포인터 조절로 사용
[32bit - 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)
(build)
$ nasm ASM32.s
$ ndisasm -b32 ASM32
(Test)
=> python3에 쉘코드 바이트값 전달하여 수행
# print bytecode
f = open('ASM32','r')
data = f.read()
print(data) # 바이트 값 출력
// 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)
$ gcc -o shellcode -fno-stack-protector -z execstack --no-pie -m32 shellcode.c
$ ./shellcode
Shellcode len : 2
Segmentation fault (core dumped)
=> 에러 발생????
-> 문제의 원인 및 해결 방식은 아래 3번에서 서술
(\x00이 제거된 asm코드)
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 ; 새 줄 바이트와 개행 문자 바이트
(Test)
#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\xff\
Hello, world!\n\r";
unsigned char code[] = "";
void main()
{
int len = strlen(shellcode);
printf("Shellcode len : %d\n",len);
strcpy(code,shellcode);
(*(void(*)()) code)();
$ gcc -o shellcode2 -fno-stack-protector -z execstack --no-pie -m32 shellcode2.c
$ ./shellcode2
Shellcode len : 43
Hello, world!
2-2. 인라인 어셈
[순서]
1. 실행할 어셈블리어
2. 테스트할 c코드 작성
(asm코드)
; write(1,"Hello world!",12)
mov rax, 0x646c72
push rax
mov rax, 0x6f57206f6c6c6548
push rax
xor rdi, rdi
inc 0x1
mov rsi, rsp
xor rdx rdx
add rdx 0xc
xor rax, rax
int 1
syscall
(c코드)
// write(1, "Hello world", 12);
// exit(0);
__asm__(
".global run_sh\n"
"run_sh:\n"
"mov rax, 0x646c72\n"
"push rax\n"
"mov rax, 0x6f57206f6c6c6548\n"
"push rax\n"
"xor rdi, rdi\n"
"inc rdi\n"
"mov rsi, rsp\n"
"xor rdx, rdx\n"
"add rdx, 0xc\n"
"xor rax, rax\n"
"inc rax\n"
"syscall\n"
"\n"
"xor rdi, rdi # rdi = 0\n"
"mov rax, 0x3c # rax = sys_exit\n"
"syscall # exit(0)");
void run_sh();
int main() { run_sh(); }
(컴파일)
$ gcc -o test test.c -masm=intel
$ ./test
Hello World
3. Shellcode 작성시 주의사항
(쉘코드에 0x00바이트가 들어가면 생기는 문제)
-> 에러가 발생한 이유 : 쉘코드에 포함되어있는 0x00값 때문
(문자열을 다루는 함수들은 문자열의 끝을 0x00으로 판단한다.)
-> 따라서 쉘코드상에서 0x00을 제거하는 작업을 해야한다.
[\x00 바이트 제거방법]
1. jmp "다른 함수"
2. mov 명령어
(\x00 바이트 제거방법)
(1) jmp "다른 함수"
[null byte가 들어간 이유]
call명령어의 피연산자에서 사용 가능한 값의 크기는 dword(32bits)이기 때문에
작은 값을 사용 될 경우 남는 부분에 null byte가 포함되게 된다.
(해결법)
- 하단의 새로운 함수(a)를 생성
- jmp명령어를 사용해 해당 함수로 점프
- 그리고 해당 함수에서는 원래 사용하려던 함수를 재호출한다.
(디스어셈블 확인)
$ nasm ASM32-2.s
$ 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
=> 컴파일된 어셈블리어를 보면 call명령어가 들어가 있음에도 null byte가 제거된 것을 볼 수 있다.
-> 제거된 이유는 음수이기 때문이다.
- jmp short 0x20로 주소 0x20으로 이동
- (jmp명령어의 피연산자에서 사용 가능한 값의 크기 dword(32bits))
=> 사용 가능한 값의 크기 -128 ~ 127 - call명령어 helloworld함수를 호출하기 위해 -0x20으로(0x2-0x24)로 이동
- -0x22를 표현하기 위해 2의 보수로 값을 표현하게 된다.
- 이 과정에서 null byte가 제거되게 된다.
(2) mov 명령어
(레지스터의 범위)
64bit - 8bit
(레지스터 값을 초기화하는 방법)
[레지스터 영역을 사용하기 전 초기화하는 이유]
=> 쉘코드가 실행되기 이전의 레지스터의 값을 그대로 사용하기 때문
- 그럼 하위비트의 값만 변경한다면?
-> 명령어마다 어떤 단위의 레지스터를 사용할지 다 알 수 없고.
그전에 존재하는 값들이 무엇인지 알 수도 없기때문에 0x00으로 초기화하는 것이 좋다.
(해결법)
1. sub명령어를 이용한 초기화
2. xor명령어를 이용한 초기화
(어셈)
section .text
global _start
_start:
; XOR을 사용하여 eax를 0으로 만들기
mov eax, 5 ; eax = 5
xor eax, eax ; eax = eax ^ eax = 0
; SUB을 사용하여 ebx를 0으로 만들기
mov ebx, 5 ; ebx = 5
sub ebx, ebx ; ebx = ebx - ebx = 0
; 프로그램 종료
mov eax, 1 ; 시스템 호출 번호 (exit)
xor ebx, ebx ; 반환 값 0
int 0x80 ; 시스템 호출
(결론)
=> sub명령어는 플래그 레지스터에 영향을 주어 이후 분기 명령어에 영향을 줄 수 있기 때문에 xor명령어로 레지스터 값을 초기화하는 것이 효율적이다.
참고링크
(1) https://www.lazenca.net/display/TEC/01.The+basics+technic+of+Shellcode
(2) System V ABI" Calling Convention