이전에 공부한 기법으로 로컬 환경(glibc-2.30
)에서 ctf 문제를 풀어보니 동작하지 않았다.
버전 차이가 얼마 안났기에 패치됐을 거라고 생각을 못했는데, 직접 디버깅 및 소스를 확인해보니 약간의 차이가 있었고 그것을 정리하는 글이다.
glibc-2.30
glibc-2.30
의 malloc.c
를 보면
#if USE_TCACHE /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. */ struct tcache_perthread_struct *key; } tcache_entry; /* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */ typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
tcache_entry
에 key
라는 요소가 생겼다.
tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); /* 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_put()
이 수행될 때, e->key
의 값을 tcache
로 설정한다.
#if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache != NULL && tc_idx < mp_.tcache_bins) { /* 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. */ }
그리고 tcache_put()
이 호출되기 전에 실행되는 코드를 보면, double free bug를 검사하는 코드가 추가되었음을 알 수 있다.
앞서 봤듯이 tcache_put()
에서 e->key
를 tcache
로 설정했다.
만약 이미 free된 청크라면 값이 일치하므로
if (__glibc_unlikely (e->key == tcache))
이 조건문이 실행될 것이다.
조건문 안에서는 tcache bin entries에 대해 루프를 돌며 현재 청크와 동일한 청크가 있는지 검사하게 된다.
만약 double free된 청크라면 청크가 이미 bin에 존재할 것이고
malloc_printerr()
가 호출되면서 에러가 발생할 것이다.
이전에 off-by-one을 이용하여 libc 주소를 leak할 때 다음과 같은 방법을 사용했었다.
1.
chunk_AAA
,chunk_BBB
,chunk_CCC
를 선언한다.
2.chunk_AAA
를 free시킨다.
3.chunk_BBB
를 오버플로우시켜chunk_CCC
의prev_inuse
를 0으로 설정하고,prev_size
를chunk_AAA
와chunk_BBB
의 크기를 합친 값으로 설정한다.
4.이제chunk_CCC
를 free시키면 A,B,C가 모두 병합된다.
5.chunk_AAA
를 다시 할당해서chunk_AAA
에 위치한fd
,bk
를chunk_BBB
로 옮긴다.
6.chunk_BBB
를 프린트하여 libc 값을 읽는다.
chunk_BBB
를 free된 것처럼 속이기 위해 chunk_C
의 prev_size
값만 수정하면 됬었다.
하지만, 다음과 같이 코드가 변경되었다.
기존의 코드를 보면
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
병합할 때 chunk_CCC
의 prev_inuse
만 검사하여, free된 청크일 경우 바로 병합시켰다.
그러나 glibc-2.30
의 코드를 보면
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
병합되는 이전 청크의 current_size를 검사한다.
즉 chunk_AAA
의 크기를 검사하는 것이다.
위 상황에서
chunk_AAA
는 0x120, chunk_BBB
는 0x120, chunk_CCC
는 오버플로우 되어 0x100이라고 할 때
chunk_CCC
의 prev_size
를 0x240으로 설정하는 것 뿐만 아니라
cuhnk_AAA
의 사이즈를 0x240으로 변경해줘야 병합된다.