[Pwn] Pwnable · 작성: 2026-07-19 19:31:03 · 수정: 2026-07-21 23:21:58 · 조회 128
힙 익스플로잇(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가 그대로 재현된다힙 익스플로잇을 "메모리를 잘못 관리하는 버그"가 아니라 "할당자가 성능을 위해 해제된 유저 데이터 영역을 관리 메타데이터로 재활용한다"는 설계와, 애플리케이션의 포인터 생명주기 관리 실수가 만나는 지점으로 이해하면, 왜 이 취약점 클래스가 스택 버퍼 오버플로우보다 훨씬 다루기 어렵지만 동시에 훨씬 강력한 임의 읽기/쓰기 원시 능력으로 이어지는지가 같은 논리로 설명된다.