이 글을 읽기 전에 시스템 콜(system call) 및 셸의 개념이 헷갈린다면 여기를 참고하자
익스플로잇과 셸 코드
먼저 익스플로잇이 무엇인지부터 알아보자!
해킹 분야에서의 익스플로잇은, '상대의 시스템을 공격하는 것'을 말한다.
그럼 오늘 배울 개념인 셸코드는 무엇일까?
셸코드는 익스플로잇을 위해 작성된 기계어 코드이다.
보통 셸의 획득을 목적으로 하는 경우가 많아서 셸 코드라고 불리게 되었다.
rip의 위치를 우리가 만든 셸 코드로 옮기면 상대 시스템은 우리의 셸코드를 실행하게 될 것이다!! 그러니까 rip 위치를 조정해주는게 우리의 목적이지 않을까?(걍 내 생각이다)
그런데 셸 코드는 결국 어셈블리어,기계어로 쓰여있기 때문에, 공격할 상대 시스템이 어떤 컴퓨터 구조를 가지고, 어떤 운영체제를 가지는지 등에 따라 달라질 것임!! 그래서 우리가 셸 코드를 직접 작성해보는 과정이 있어야 실력이 늘 듯하다.
긍까 운체 등에 따라 구조 같은게? 달라질 테니까 직접 셸 코드를 써보는 연습을 많이 해보장 ㅎㅎ
실습
orw 셸코드란 Open, Read, Write 를 해보는 셸코드이다. 오늘은 /tmp/flag라는 파일을 orw 해보는 간단한 셸코드를 작성해보겠다.
syscall은 맨위에 첨부한 링크에 자세히 설명해 놓았지만, 간단히 말해서 커널에게 내부 동작을 해달라고 부탁하는 명령어라고 생각하면 된다. 위 표는 open, read, write을 할 때 syscall의 종류에 따라 어떤 식으로 레지스터에 인자가 들어가는지 정리해 놓은 표이다. 이를 참고하여 코드를 짜볼 것이다.
먼저 C 형식의 의사코드를 작성하였다.
char buf[0x30]; //길이 0x30의 버퍼 생성. 여기에 문자열을 저장할 것임.
int fd=open("/tmp/flag",O_RONLY,NULL);
read(fd,buf,0x30); //0x30만큼 파일에서 '읽고' buf에 쓸 것
write(1,buf,0x30); //buf를 0x30만큼 1(stdout)에 쓸것
위 코드를 이제 어셈블리어로 바꿔보자. (char buf[30]은 따로 코드를 짜주진 않을 것이고 아래 orw 에 따라 안에서 내부적으로 추가해주겠다. 무슨 소린지 모르겠으면 그냥 따라오면 된다.)
int fd=open("/tmp/flag",O_RONLY,NULL);
"/tmp/flag"라는 파일을 O_RONLY(읽기만 가능)라는 권한으로 열어주는 코드이다.
C에서 open함수는 인자로 파일경로, 여는 용도, 권한을 인자로 받는다.
따라서 파일경로를 메모리에 push 한 후 rax=0x02, rdi=0 (O_RDONLY), rsi=NULL을 인자로 넣어준다.
이를 어셈블리 코드로 작성하면 다음과 같다.
push 0x616c662f706d742f67 ; 함수 경로 /tmp/flag를 push
rax=0x02 ;open해줘
mov rdi,rsp ;rsp가 0x616c662f706d742f67 가리키고 있으므로
xor rsi, rsi ; rsi를 0(O_RONLY)로 세팅
syscall
* open 함수 설명해준 좋은 글 -> IT 개발자 Note :: open(2) - 파일을 읽거나 쓰기 위해 열기 (it-note.kr)
이때 open 함수의 반환값은 rax에 전달된다.
read(fd,buf,0x30);
read 함수는 fd(파일 서술자), 파일에서 읽을 데이터를 저장할 위치, 길이를 인자로 받는다.
잠깐! 파일 서술자란?
파일 서술자(File Descriptor, fd)는 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자입니다. 프로세스마다 고유의 서술자 테이블을 갖고 있으며, 그 안에 여러 파일 서술자를 저장합니다. 서술자 각각은 번호로 구별되는데, 일반적으로 0번은 일반 입력(Standard Input, STDIN), 1번은 일반 출력(Standard Output, STDOUT), 2번은 일반 오류(Standard Error, STDERR)에 할당되어 있으며, 이들은 프로세스를 터미널과 연결해줍니다. 그래서 우리는 키보드 입력을 통해 프로세스에 입력을 전달하고, 출력을 터미널로 받아볼 수 있습니다.
프로세스가 생성된 이후, 위의 open같은 함수를 통해 어떤 파일과 프로세스를 연결하려고 하면, 기본으로 할당된 2번 이후의 번호를 새로운 fd에 차례로 할당해줍니다. 그러면 프로세스는 그 fd를 이용하여 파일에 접근할 수 있습니다. 출처: 드림핵
open함수의 반환값은 rax에 저장된다(syscall의 반환값 자체가 원래 rax에 들어간다!!) 따라서 fd는 rax에 저장되어있다.
그리고 buf는 0x30만큼 설정해줄 건데, 이는 스택에서 버퍼 크기를 30만큼 할당해주면 된다.
마지막 0x30은 얼마나 읽을지에 대한 것이다.
mov rdi, rax ;rax에 open의 return값 fd가 들어있으므로 걔를 rdi에 대입
mov rax, 0x00 ;0x00은 read
mov rsi, rsp
sub rsp, 0x30 ;rsp위로 0x30위로 rsi 설정해줘서 거기부터쓰게함
;그런데 rsp위에 중요한 정보 있었으면 어떡하지...? 궁금....
mov rdx, 0x30 ;0x30만큼 읽어줘
syscall
write(1,buf,0x30);
write함수는 인자로 어디다 쓸 건지, 쓸 내용이 들어있는 위치, 쓸 내용의 길이를 인자로 받는다. 그리고 쓴 바이트 수를 반환한다.
첫 번째 인자는 open을 통해 얻은 파일 서술자나, 0(standard input), 1(standart output), 2(standard error)를 넣을 수 있다.
우리는 그냥 stdout을 할 것이므로 1을 rdi에 대입해준다.
그리고 buf에는 read에서 사용한 동일한 버퍼를 사용할 것이다. 길이도 동일하다.
mov rax, 0x01; write 해줘
mov rdi, 1; fd=stdout
syscall
*write 함수에 대해 참고할만한 글 -> write (C System Call) - Code Wiki (wikidot.com)
최종 코드 orw.S
;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)
참고로 read, write둘다 null이 들어와도 그냥 계속 size만큼 받는다
pwntools의 shellcraft를 이용해서 쉘 코드를 작성하는 것은 아래에 잘 나와있으니 참고바란다.
https://ocw.cs.pub.ro/courses/cns/labs/lab-05
Reference
드림핵 system hacking Stage 4: Exploit Tech: Shellcode
'Linux Exploitation > Fundamentals' 카테고리의 다른 글
[Pwnable] PIE (0) | 2023.05.15 |
---|---|
[gdb] 로컬/서버의 라이브러리 다른 경우 (0) | 2023.04.28 |
[ROP 시리즈 (3)] 드림핵(Dreamhack) - Return to Library 개념 및 실습 (1) | 2023.04.18 |
[ROP 시리즈 (2)] PLT, GOT (0) | 2023.04.17 |
[BOF] Stack Buffer Overflow (스택 버퍼 오버플로우), BOF 기초 및 실습 (2) | 2023.03.29 |