[Pwn] Pwnable · 작성: 2026-07-19 19:31:03 · 수정: 2026-07-21 23:21:58 · 조회 23
힙 익스플로잇(UAF/Double Free)을 "해제된 메모리를 다시 쓰면 위험하다"는 규칙으로만 알면, 왜 그게 위험한지를 설명 못 한다. 진짜 원인은 glibc의 메모리 할당자(ptmalloc)가 해제된 청크의 데이터 영역 자체를 "다음 할당을 위한 관리 정보(free list)"로 재활용한다는, 성능을 위한 설계 결정에 있다.
free()된 메모리에 남는 것malloc()으로 받은 메모리 블록(청크, chunk)은 유저가 보는 데이터 영역 앞에 청크 헤더(크기, 이전 청크가 사용 중인지 여부 등의 메타데이터)가 붙어있다. free(ptr)를 호출하면, glibc는 그 메모리를 OS에 즉시 반납하지 않고 "이 크기의 청크가 나중에 다시 필요할 때 빠르게 재사용할 수 있도록" 같은 크기의 청크들을 모아두는 **연결 리스트(bin, 특히 작은 크기는 tcache/fastbin)**에 등록한다. 그런데 이 연결 리스트의 "다음 노드를 가리키는 포인터(fd, forward pointer)"를 어디에 저장할까? — 놀랍게도 이미 해제되어 "안 쓰는" 유저 데이터 영역의 맨 앞 8바이트를 그대로 재활용한다. 즉 free()된 직후의 청크는, 유저 입장에서는 "다 쓴 메모리"지만 할당자 입장에서는 그 안에 다음 가용 청크를 가리키는 살아있는 포인터가 들어있는 상태다.
struct user *u = malloc(sizeof(struct user));
u->is_admin = 0;
free(u);
// ... 코드 다른 어딘가에서 u를 또 쓸 수 있다면 (버그) ...
u->is_admin = 1; // 이미 free된 메모리에 씀 — UAF
free(u) 이후에도 u라는 포인터 변수 자체는 여전히 예전 주소값을 들고 있다(이게 댕글링 포인터다). 만약 이 시점 이후 프로그램 로직이 실수로 u를 다시 참조하면, 그 메모리가 그 사이에 다른 malloc() 호출로 재할당되어 완전히 다른 객체가 들어있을 수도 있다. 공격자가 이 타이밍을 노려 free된 직후, 같은 크기로 자신이 원하는 데이터를 채운 새 객체를 할당시키면(Heap Grooming), 원래 코드가 u->is_admin을 통해 접근하려던 위치가 실제로는 공격자가 채워 넣은 임의 데이터가 되어버린다 — 타입이 다른 두 객체가 같은 메모리 주소를 서로 다른 의미로 해석하게 만드는 것, 이게 UAF의 본질이다.
free(ptr);
free(ptr); // 같은 포인터를 두 번 해제
앞서 설명한 free list(tcache) 구조상, ptr을 한 번 더 free하면 같은 청크가 free list에 중복으로 등록된다. 이후 malloc()을 두 번 호출하면, 첫 번째 malloc()이 이 청크를 반환하면서 그 유저 데이터 영역(=free list의 fd 포인터가 저장된 자리)을 공격자가 자유롭게 덮어쓸 수 있게 된다 — 그 자리에 공격자가 원하는 임의 주소를 써넣으면, 그게 다음 malloc() 호출 시 free list의 "다음 가용 청크 주소"로 그대로 신뢰되어, 다음 malloc()이 공격자가 지정한 임의 주소를 그대로 반환한다. 이게 바로 Fake Chunk를 이용한 임의 주소 쓰기(Arbitrary Write) 원시 능력으로 확장되는 지점이다 — GOT 엔트리 주소를 이 방식으로 "할당받아서" 그 자리에 원하는 함수 포인터를 직접 써넣으면 실행 흐름을 통째로 하이재킹할 수 있다.
chunk_a, chunk_b)하고 둘 다 free() — tcache에 둘 다 등록됨chunk_b의 fd 포인터(원래는 chunk_a를 가리켜야 함)를 덮어써서 임의의 대상 주소(GOT 엔트리, 함수 포인터가 저장된 변수 등)를 가리키게 조작malloc()을 다시 호출 → tcache가 chunk_b를 반환malloc()을 한 번 더 호출 → tcache는 방금 조작해둔 fd가 가리키는 임의의 대상 주소를 그대로 "새 청크"로 반환*ptr = system_addr), 대상 주소(예: GOT 엔트리)에 임의 값이 그대로 써짐 — 이후 그 함수가 호출되는 순간 실행 흐름 하이재킹key 필드, fd 포인터를 그대로 저장하지 않고 난독화하는 safe-linking)를 추가해왔다 — 이는 정확히 위에서 설명한 "free list의 포인터를 유저가 접근 가능한 영역에 그대로 노출한다"는 근본 설계를 완화하려는 시도다. 그럼에도 오래된 バイナリ나 재구현된 커스텀 할당자에서는 여전히 고전적인 형태의 UAF/Double Free가 그대로 재현된다힙 익스플로잇을 "메모리를 잘못 관리하는 버그"가 아니라 "할당자가 성능을 위해 해제된 유저 데이터 영역을 관리 메타데이터로 재활용한다"는 설계와, 애플리케이션의 포인터 생명주기 관리 실수가 만나는 지점으로 이해하면, 왜 이 취약점 클래스가 스택 버퍼 오버플로우보다 훨씬 다루기 어렵지만 동시에 훨씬 강력한 임의 읽기/쓰기 원시 능력으로 이어지는지가 같은 논리로 설명된다.