셸코드(Shellcode)?
익스플로잇을 위해 제작된 어셈블리어로 작성된 작은 코드 조각임.
주로 셸을 획득하기 위해 셸코드를 사용한다.
*익스플로잇은 소프트웨어나 하드웨어의 취약점을 이용해 개발자가 의도하지 않은 동작을 유발하는 코드나 기법.
orw셸코드
파일을 열고(open), 읽은(read) 뒤 화면에 출력(write)해주는 셸코드이다.
구현하려는 셸코드의 동작을 C언어 형식의 의사코드로 표현하면 아래와 같다.
char buf[0x30]; // 0x30(48) 바이트 크기의 버퍼 선언 (데이터를 저장할 메모리 공간)
int fd = open("/tmp/flag", RD_ONLY, NULL); // "/tmp/flag" 파일을 읽기 전용(RD_ONLY)으로 열고 파일 디스크립터(fd) 반환
// NULL은 파일 생성 시 권한 설정으로, 읽기만 할 것이므로 필요 없음
read(fd, buf, 0x30); // 열린 파일(fd)에서 최대 0x30 바이트를
// 앞서 선언한 버퍼(buf)에 읽어들임
write(1, buf, 0x30); // 버퍼(buf)의 내용을 표준 출력(fd=1)에
// 0x30 바이트만큼 출력함
위 의사코드를 따라 orw Shellcode를 작성하려면 아래와 같은 syscall이 필요하다.
syscall
|
rax
|
arg0 (rdi)
|
arg1 (rsi)
|
arg2 (rdx)
|
read
|
0x00
|
unsigned int fd
|
char *buf
|
size_t count
|
write
|
0x01
|
unsigned int fd
|
const char *buf
|
size_t count
|
open
|
0x02
|
const char *filename
|
int flags
|
umode_t mode
|
표를 보면 rax레지스터에 어떤 시스템 콜 번호를 넣어야 하는지,
각 함수의 인자들은 어떤 레지스터(rdi, rsi, rdx)에 넣어야 하는지 보여준다.
이제 위에서 작성한 의사코드를 syscall을 사용해 한줄씩 구현해볼거임.
▶ int fd = open(“/tmp/flag”, O_RDONLY, NULL)
syscall
|
rax
|
arg0 (rdi)
|
arg1 (rsi)
|
arg2 (rdx)
|
open
|
0x02
|
const char *filename
|
int flags
|
umode_t mode
|
1. 문자열 /tmp/flag 저장하기
push 0x67
mov rax, 0x616c662f706d742f
push rax
가장 먼저 "/tmp/flag"라는 문자열을 메모리에 위치시켜야한다.
스택에 0x67616c662f706d742f를 push해 위치시켜 줄거임.
참고로 x86-64는 리틀 엔디안 방식을 사용하기 때문에, 문자열을 거꾸로 저장해야 한다.
2. 레지스터 설정
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
- mov rdi, rsp: 스택 포인터(rsp)를 첫 번째 인자 레지스터(rdi)에 복사. 이제 rdi는 스택에 저장된 /tmp/flag 문자열의 주소를 가리킨다.
- xor rsi, rsi: rsi를 0으로 설정한다. 이건 O_RDONLY(읽기 전용) 플래그를 의미한다. (xor reg, reg는 레지스터를 0으로 설정하는 방법)
- xor rdx, rdx: rdx를 0으로 설정한다. 이건 파일을 생성할 때만 사용되는 권한 모드인데, 파일을 읽기만 할 것이므로 0으로 설정한다.
- mov rax, 2: rax에 open 시스템 콜의 번호인 2를 저장합니다.
3. 시스템 콜 실행
syscall ; open("/tmp/flag", RD_ONLY, NULL)
커널은 rax 레지스터의 값(2)을 보고 open 시스템 콜을 실행한다.
open 시스템 콜이 성공하면, 파일 디스크립터(정수 값)가 rax에 반환된다. 이 값은 다음 read 시스템 콜에서 사용된다.
4. 구현
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)
▶ read(fd, buf, 0x30)
syscall
|
rax
|
arg0 (rdi)
|
arg1 (rsi)
|
arg2 (rdx)
|
read
|
0x00
|
unsigned int fd
|
char *buf
|
size_t count
|
1. 파일 디스크립터 설정
mov rdi, rax ; rdi = fd (파일 디스크립터)
- 이전 open 시스템 콜의 결과(파일 디스크립터)는 rax 레지스터에 저장된다.
- read 시스템 콜은 첫 번째 인자로 파일 디스크립터를 필요로 하며, 이 인자는 rdi 레지스터를 통해 전달됨.
- 따라서 rax의 값(파일 디스크립터)을 rdi로 옮겨서 read 함수가 올바른 파일을 읽도록 한다.
2. 버퍼 주소 설정
sub rsp, 0x30 ; 스택에 0x30 바이트 공간 확보
mov rsi, rsp ; rsi = 버퍼 주소
- 스택을 사용하여 임시 버퍼 공간을 만들려고 함.
- 파일에서 0x30(48) 바이트를 읽어올 예정이므로, 최소 그 크기의 버퍼가 필요하다.
- 스택은 위에서 아래로 성장하므로(높은 주소에서 낮은 주소로), 스택 포인터(rsp)에서 0x30만큼 빼면 새로운 공간이 확보됨.
- sub rsp, 0x30은 스택 포인터를 0x30만큼 감소시켜 새 공간을 확보.
- 그런 다음 mov rsi, rsp를 통해 이 새로 확보된 공간의 시작 주소를 rsi에 저장함.
- 결과적으로 rsi는 0x30 바이트 크기의 빈 버퍼를 가리키게 된다.
3. 읽을 바이트 수 설정
mov rdx, 0x30 ; rdx = 0x30 (읽을 바이트 수)
- rdx 레지스터는 read 시스템 콜의 세 번째 인자인 '읽을 바이트 수'를 지정.
- 0x30(48)은 C 의사코드에서 지정한 버퍼 크기와 일치.
4. 시스템 콜 번호 설정/ 실행
xor rax, rax ; rax = 0 (read 시스템 콜 번호)
syscall ; read(fd, buf, 0x30)
- read 시스템 콜의 번호는 0이다. (xor rax, rax는 rax를 0으로 설정하는 방법)
- 이제 모든 레지스터가 설정되었으므로 syscall 명령어로 read 시스템 콜을 실행한다.
- 실행 결과로 rax에는 실제로 읽은 바이트 수가 반환된다.
5. 구현
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)
▶ write(1, buf, 0x30)
syscall
|
rax
|
arg0 (rdi)
|
arg1 (rsi)
|
arg2 (rdx)
|
write
|
0x01
|
unsigned int fd
|
const char *buf
|
size_t count
|
1. 파일 디스크립터 설정
mov rdi, 0x1 ; rdi = 1 (stdout 파일 디스크립터)
- write 시스템 콜의 첫 번째 인자는 데이터를 쓸 파일 디스크립터.
- 리눅스 시스템에서 표준 출력(stdout)의 파일 디스크립터는 1이다.
- 따라서 rdi에 1을 저장하여 데이터를 화면에 출력하도록 지정.
2. 버퍼 주소 설정
; rsi는 read에서 사용한 값을 그대로 사용
- write 시스템 콜의 두 번째 인자는 출력할 데이터가 저장된 버퍼의 주소이다.
- 이전 read 시스템 콜에서 rsi는 이미 버퍼의 주소(스택에 확보한 공간)를 가리키고 있다.
- 같은 데이터를 출력하려는 것이므로, rsi 값을 변경할 필요가 없다.
3. 출력할 바이트 수 설정
; rdx는 read에서 사용한 값을 그대로 사용
- write 시스템 콜의 세 번째 인자는 출력할 바이트 수.
- 이전 read 시스템 콜에서 rdx에는 이미 0x30(48)이 저장되어 있다.
- 읽은 데이터를 모두 출력하려는 것이므로, rdx 값을 변경할 필요가 없다.
- (참고: 최적화를 위해서는 read의 반환값(실제로 읽은 바이트 수)을 사용할 수도 있다.)
4. 시스템 콜 번호 설정/ 실행
mov rax, 0x1 ; rax = 1 (write 시스템 콜 번호)
syscall ; write(1, buf, 0x30)
- write 시스템 콜의 번호는 1.
- 따라서 rax에 1을 저장한다.
- 이제 모든 레지스터가 설정되었으므로 syscall 명령어로 write 시스템 콜을 실행한다.
- 실행 결과로 rax에는 실제로 쓴 바이트 수가 반환된다.
5. 구현
mov rdi, 1 ; rdi = 1 ; fd = stdout
mov rax, 0x1 ; rax = 1 ; syscall_write
syscall ; write(fd, buf, 0x30)
최종 구현
;Name: orw.S
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)
'System Hacking > Basics' 카테고리의 다른 글
[시스템 해킹] 셸코드(Shellcode) 3- execve 셸코드/ execve shellcode와 orw shellcode 차이점 (0) | 2025.03.10 |
---|---|
[시스템 해킹] 셸코드(Shellcode) 2- orw 셸코드 컴파일, 실행, 디버깅 (0) | 2025.03.10 |
[시스템 해킹] pwntools 사용법 (0) | 2025.03.07 |
[시스템 해킹] pwntools 설치 방법 / Linux (0) | 2025.03.07 |
[시스템 해킹] gdb/pwndbg 사용법 (0) | 2025.03.07 |