System Hacking/Basics

[시스템 해킹] 셸코드(Shellcode) 2- orw 셸코드 컴파일, 실행, 디버깅

hanbunny 2025. 3. 10. 16:45

orw 셸코드 컴파일

어셈블리 코드를 컴파일하는 방법에는 여러가지 방법이 있다.

이번엔 스켈레톤 코드에 앞에서 작성한 셸코드를 채울거임.

 

*스켈레톤 코드: 핵심 내용이 비어있고 기본 구조만 갖춘 코드

 

↓스켈레톤 코드

// 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(); }

 

 

orw 셸코드 실행

#셸코드 파일 orw.c 생성
$ nano orw.c

먼저 실행 실습을 위해 위 스켈레톤 코드에 셸코드를 채운 코드가 담긴 파일을 생성해줬다.

 

 

# 'flag{this_is_open_read_write_shellcode!}' 문자열을 /tmp/flag 파일에 저장하는 명령어
echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag

 

위에서 만든 셸코드가 실행되는지 확인하기 위해 read로 읽어왔던 /tmp/flag 파일을 만들어줌.

안에는 "flag{this_is_open_read_write_shellcode!}"라는 내용이 담겨 있다.

셸코드가 제대로 작동된다면 안에 있는 내용이 출력됨.

 

# orw.c 컴파일
$ gcc -o orw orw.c -masm=intel

# orw.c 실행
$ ./orw

orw.c 파일을 컴파일해서 실행할 수 있는 프로그램이 되게 해준다.

이제 컴파일 후에 ./orw 명령어로 프로그램을 실행해주면 파일 안 내용이 출력됨.

 

 

이렇게 만약 공격 대상 시스템에서 이 셸코드를 실행할 수 있으면 , 상대 서버의 자료 유출이 가능하다.

 

 

orw 셸코드 디버깅

# orw를 gdb로 열기
$ gdb orw -q 
...
#run_sh() 에 브레이크 포인트 설정
pwndbg> b *run_sh
Breakpoint 1 at 0x1129
pwndbg>

먼저 gdb로 orw를 열고, run_sh()에 브레이크 포인트를 설정한다.

run_sh 브레이크 포인트 설정

 

pwndbg> r
Starting program: /home/dreamhack/orw
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, 0x0000555555555129 in run_sh ()
...
*RIP  0x555555555129 (run_sh) ◂— push 0x67
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
 ► 0x555555555129 <run_sh>       push   0x67
   0x55555555512b <run_sh+2>     movabs rax, 0x616c662f706d742f
   0x555555555135 <run_sh+12>    push   rax
   0x555555555136 <run_sh+13>    mov    rdi, rsp
   0x555555555139 <run_sh+16>    xor    rsi, rsi
   0x55555555513c <run_sh+19>    xor    rdx, rdx
   0x55555555513f <run_sh+22>    mov    rax, 2
   0x555555555146 <run_sh+29>    syscall
   0x555555555148 <run_sh+31>    mov    rdi, rax
   0x55555555514b <run_sh+34>    mov    rsi, rsp
   0x55555555514e <run_sh+37>    sub    rsi, 0x30
...
pwndbg>

 

그 다음, run 명령어로 run_sh()의 시작 부분까지 코드를 실행시킨다.

그럼 우리가 작성한 셸코드에 rip가 위치한 것을 확인할 수 있다.

 

 

이제 앞에서 구현한 각 시스템 콜들이 제대로 구현되었나 확인해볼거임.

 


1. int fd = open(“/tmp/flag”, O_RDONLY, NULL)

먼저 첫번째 syscall이 위치한 <run_sh+19> 브레티크 포인트를 설정한 후 실행해서, 해당 시점에 syscall에 들어가는 인자를 확인해봄.

 

실행 후 출력

pwndbg> b *run_sh+29
Breakpoint 2 at 0x555555555146
pwndbg> c
Continuing.

Breakpoint 2, 0x0000555555555146 in run_sh ()
...

─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX  0x2
 RBX  0x0
 RCX  0x555555557df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x5555555550e0 (__do_global_dtors_aux) ◂— endbr64
*RDX  0x0
*RDI  0x7fffffffe2f8 ◂— '/tmp/flag'
*RSI  0x0
...

──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x555555555135 <run_sh+12>    push   rax
   0x555555555136 <run_sh+13>    mov    rdi, rsp
   0x555555555139 <run_sh+16>    xor    rsi, rsi
   0x55555555513c <run_sh+19>    xor    rdx, rdx
   0x55555555513f <run_sh+22>    mov    rax, 2
 ► 0x555555555146 <run_sh+29>    syscall  <SYS_open>
        file: 0x7fffffffe2f8 ◂— '/tmp/flag'
        oflag: 0x0
        vararg: 0x0
   0x555555555148 <run_sh+31>    mov    rdi, rax
   0x55555555514b <run_sh+34>    mov    rsi, rsp
   0x55555555514e <run_sh+37>    sub    rsi, 0x30
   0x555555555152 <run_sh+41>    mov    rdx, 0x30
   0x555555555159 <run_sh+48>    mov    rax, 0
...

 

출력된 걸 보면 <run_sh+29>  인자를 해석해서 보여준다.

/tmp/flag파일이 실행됨을 알 수 있다.

 

# ni명령어 실행 - 한 단계 (1 instruction)만 실행하고 다음 명령어에서 멈춤.
pwndbg> ni
0x0000555555555162 in run_sh ()
...

ni명령어를 사용해 syscall을 실행하고 나면, open 시스템 콜을 수행한 결과로 /tmp/flag의 fd(3)가 rax에 저장된다.

 

 

2. read(fd, buf, 0x30)

두 번째 syscall이 위치한 run_sh+55 에 브레이크 포인트를 설정하고 인자를 살펴봄.

 

실행 후 출력

pwndbg> b *run_sh+55
Breakpoint 3 at 0x555555555160
pwndbg> c
Continuing.

Breakpoint 3, 0x0000555555555160 in run_sh ()
...

─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX  0x0
 RBX  0x0
 RCX  0x555555555044 (_start+4) ◂— xor ebp, ebp
*RDX  0x30
*RDI  0x3
*RSI  0x7fffffffe2c8 ◂— 0x0
...

──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x555555555148     mov    rdi, rax
   0x55555555514b     mov    rsi, rsp
   0x55555555514e     sub    rsi, 0x30
   0x555555555152     mov    rdx, 0x30
   0x555555555159     mov    rax, 0
 ► 0x555555555160     syscall  
        fd: 0x3 (/tmp/flag)
        buf: 0x7fffffffe2c8 ◂— 0x0
        nbytes: 0x30
   0x555555555162     mov    rdi, 1
   0x555555555169     mov    rax, 1
   0x555555555170     syscall
   0x555555555172     xor    rdi, rdi
   0x555555555175     mov    rax, 0x3c
...

 

 

새로 할당한  /tmp/flag의 fd(3)에서 데이터를 0x30바이트만큼 읽어서 0x7fffffffde28에 저장.

 

# x/s 명령어 : 메모리의 문자열(String)을 출력
pwndbg> x/s 0x7fffffffde28
0x7fffffffe2c8: "flag{this_is_open_read_write_shellcode!}\n"

0x7fffffffde28/tmp/flag의 문자열이 성공적으로 저장된 것을 확인.

 

3. write(1, buf, 0x30)

마지막으로, 읽어낸 데이터를 출력하는 write 시스템 콜을 실행하기 직전의 모습이다.

pwndbg> c
Continuing.

Breakpoint 4, 0x0000555555555170 in run_sh ()
...

─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX  0x1
 RBX  0x0
 RCX  0x555555555044 (_start+4) ◂— xor ebp, ebp
 RDX  0x30
*RDI  0x1
 RSI  0x7fffffffe2c8 ◂— 'flag{this_is_open_read_write_shellcode!}\n'
...

──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
   0x555555555152 <run_sh+41>    mov    rdx, 0x30
   0x555555555159 <run_sh+48>    mov    rax, 0
   0x555555555160 <run_sh+55>    syscall
   0x555555555162 <run_sh+57>    mov    rdi, 1
   0x555555555169 <run_sh+64>    mov    rax, 1
 ► 0x555555555170 <run_sh+71>    syscall  <SYS_write>
        fd: 0x1 (/dev/pts/11)
        buf: 0x7fffffffe2c8 ◂— 'flag{this_is_open_read_write_shellcode!}\n'
        n: 0x30
   0x555555555172 <run_sh+73>    xor    rdi, rdi
   0x555555555175 <run_sh+76>    mov    rax, 0x3c
   0x55555555517c <run_sh+83>    syscall
   0x55555555517e <main>         endbr64
   0x555555555182 <main+4>       push   rbp
...

마찬가지로 ni명령어로 실행하면, 데이터를 저장한 0x7fffffffe2c8에서  48바이트를 출력한다.

 

  • RDI = 1 → stdout (표준 출력)
  • RSI = 0x7fffffffe2c8 → 출력할 데이터의 메모리 주소 (flag{this_is_open_read_write_shellcode!}\n)
  • RDX = 0x30 → 출력할 데이터 크기 (48바이트)

즉, write(1, 0x7fffffffe2c8, 0x30); 를 실행한 것이고,
0x30(16진수) = 48(10진수) 바이트가 출력됐음.