출처: 드림핵 강의
임의의 청크에 대해 free를 두 번이상 적용할 수 있다는 것은. 같은 청크를 free list에 여러번 추가할 수 있음을 의미한다. 해커들은 free list에 중복해서 존재하는(duplicated) 청크를 통해 임의 주소에 청크를 할당할 수 있음을 밝혀냈다.
하지만 Glibc 버전이 높아질 수록 새로운 보호 기법과 공격 기법의 복잡도는 더욱 높아졌다. 이번에는 Glibc 2.27버전이 내장된 우분투 18.04 64bit 환경을 기준으로 설명한다.
FROM ubuntu:18.04
ENV PATH="${PATH}:/usr/local/lib/python3.6/dist-packages/bin"
ENV LC_CTYPE=C.UTF-8
RUN apt update
RUN apt install -y \
gcc \
git \
python3 \
python3-pip \
ruby \
sudo \
tmux \
vim \
wget
# install pwndbg
WORKDIR /root
RUN git clone https://github.com/pwndbg/pwndbg
WORKDIR /root/pwndbg
RUN git checkout 2023.03.19
RUN ./setup.sh
# install pwntools
RUN pip3 install --upgrade pip
RUN pip3 install pwntools
# install one_gadget command
RUN gem install elftools -v 1.1.3
RUN gem install one_gadget -v 1.9.0
WORKDIR /root
DFB는 dangling pointer가 생성되는지, 그리고 이를 대상으로 free를 호출하는 것이 가능한지 살피면 DFB가 존재하는지 가늠할 수 있다. 이를 통해 duplicated free list를 만들 수 있다. 중복된 청크를 재할당에서 fd, bk를 조작, free list에 임의 주소를 포함시킬 수 있다.
tcache 도입 초기에는 double free가 잘 먹혔지만, 시간이 흐르면서 보호기법이 등장하고 이를 우회하지 않으면 청크를 두 번 해제하려는 즉시 프로세스가 종료된다.
아래 코드는 같은 청크를 두 번 해제하는 예제 코드다.
// Name: dfb.c
// Compile: gcc -o dfb dfb.c
#include <stdio.h>
#include <stdlib.h>
int main() {
char *chunk;
chunk = malloc(0x50);
printf("Address of chunk: %p\n", chunk);
free(chunk);
free(chunk); // Free again
}
이를 실행하면 비정상 종료가 이뤄진다.

자세한 내용은 여기
하단 코드를 보면 key 포인터가 tcache_entry에 추가되었다. tcache_entry는 해제된 tcache 청크들이 갖는 구조다. 일반 청크의 fd가 next로 대제되며, LIFO 구조를 가지므로 bk 대응 값은 없다.
typedef struct tcache_entry {
struct tcache_entry *next;
+ /* This field exists to detect double frees. */
+ struct tcache_perthread_struct *key;
} tcache_entry;
이 함수는 해제한 청크를 tcache에 추가하는 함수다. 하단 코드를 보면 key에 tcache라는 값을 대입한다. tcache는 tcache_perthread라는 구조체 변수를 가리킨다.
tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
assert(tc_idx < TCACHE_MAX_BINS);
+ /* Mark this chunk as "in the tcache" so the test in _int_free will detect a
+ double free. */
+ e->key = tcache;
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
tcache에 연결된 청크를 재사용할 때 사용하는 함수 e->key = NULL 을 실행하고 있다.
tcache_get (size_t tc_idx)
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
+ e->key = NULL;
return (void *) e;
}
_int_free 는 청크를 해제할 때 호출되는 함수로 하단 코드를 보면 재할당하려는 청크의 key값이 tcache면 Double Free가 발생한 것으로 판단하고 프로그램을 abort 한다. 이 부분 외에는 보호기법이 없으므로 이 부분만 우회하면 된다.
_int_free (mstate av, mchunkptr p, int have_lock)
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
-
- if (tcache
- && tc_idx < mp_.tcache_bins
- && tcache->counts[tc_idx] < mp_.tcache_count)
+ if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
- tcache_put (p, tc_idx);
- return;
+ /* Check to see if it's already in the tcache. */
+ tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+ /* This test succeeds on double free. However, we don't 100%
+ trust it (it also matches random payload data at a 1 in
+ 2^<size_t> chance), so verify it's not an unlikely
+ coincidence before aborting. */
+ if (__glibc_unlikely (e->key == tcache))
+ {
+ tcache_entry *tmp;
+ LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
+ for (tmp = tcache->entries[tc_idx];
+ tmp;
+ tmp = tmp->next)
+ if (tmp == e)
+ malloc_printerr ("free(): double free detected in tcache 2");
+ /* If we get here, it was a coincidence. We've wasted a
+ few cycles, but don't abort. */
+ }
+
+ if (tcache->counts[tc_idx] < mp_.tcache_count)
+ {
+ tcache_put (p, tc_idx);
+ return;
+ }
}
}
#endif
청크 할당 직후의 청크 정보를 heap으로 조회해보자.

0x555555602250가 malloc(0x50)으로 할당된 청크의 주소다. 이 메모리 값을 덤프해보면 아무런 값도 없는 것을 확인할 수 있다.

이 청크를 chunk 변수로 정의하고 넘어가자.

청크 해제까지 프로그램을 실행하고 청크 메모리를 출력해보자. 그리고 key에 담긴 주소를 조회하면 chunk의 주소가 entry에 포함되어 있음을 알 수 있다. 이는 tcache_perthread에 tcache들이 저장되기 때문이다.

if (__glibc_unlikely (e->key == tcache))만 우회한다면 tcache 청크를 double free 시킬 수 있다. 즉 해제된 청크의 key값을 1비트만이라도 바꾸면 된다.
// Name: tcache_dup.c
// Compile: gcc -o tcache_dup tcache_dup.c
#include <stdio.h>
#include <stdlib.h>
int main() {
void *chunk = malloc(0x20);
printf("Chunk to be double-freed: %p\n", chunk);
free(chunk);
*(char *)(chunk + 8) = 0xff; // manipulate chunk->key
free(chunk); // free chunk in twice
printf("First allocation: %p\n", malloc(0x20));
printf("Second allocation: %p\n", malloc(0x20));
return 0;
}

chunk가 tcache에 중복 연결되어 연속으로 재할당 된다.