FreeBSD ipfilter use-after-free 취약점
해당 글은 xorl %eax %eax의 취약점 분석 포스팅을 참고하여 작성하였습니다.
FreeBSD의 ipfilter에서 Use-After-Free 취약점(CVE-2017-1081)이 발생하였다.
sys/contrib/ipfilter/netinet/ip_frag.c
static ipfr_t *
ipfr_frag_new(softc, softf, fin, pass, table
#ifdef USE_MUTEXES
, lock
#endif
)
...
ipfr_t *fra, frag, *fran;
...
/*
* 가능한 경우 메모리를 할당하고, 실패 시 기록 후 NULL 반환
*/
KMALLOC(fran, ipfr_t *);
if (fran == NULL) {
FBUMPD(ifs_nomem);
return NULL;
}
...
/*
* 이미 존재하지 않는 지 확인
*/
for (fra = table[idx]; (fra != NULL); fra = fra->ipfr_hnext)
if (!bcmp((char *)&frag.ipfr_ifp, (char *)&fra->ipfr_ifp,
IPFR_CMPSZ)) {
RWLOCK_EXIT(lock);
FBUMPD(ifs_exists);
KFREE(fra);
return NULL;
}
fra = fran;
fran = NULL;
fr = fin->fin_fr;
fra->ipfr_rule = fr;
if (fr != NULL) {
MUTEX_ENTER(&fr->fr_lock);
fr->fr_ref++;
MUTEX_EXIT(&fr->fr_lock);
}
...
}
ipfr_frag_new() 함수는 캐시의 새로운 단편화 패킷을 처리하는 함수이다. for
문을 유의하여 코드를 살펴보자. 먼저 fra
에 저장된 패킷이 해시 테이블(frag
포인터)에 이미 저장되어 있는지 bcmp() 함수를 이용하여 확인한다. 만약 저장되어 있지 않다면 fra
를 할당한 메모리 fran
으로 업데이트하고 새로운 패킷을 저장한다. 만약 이미 테이블에 존재한다면 fra
를 lock, free 시키고 NULL을 반환한다. 알아차리기 어렵지만 여기에는 fran
(사용되지 않은 커널 버퍼)을 해제하지 않고 처리해야할 패킷을 가리키는 fra
해제하는 논리적 결함을 존재한다. 패치된 코드는 간단하다.
int bcmp(const void *s1, const void *s2, size_t n);
s1과 s2의 메모리 영역을 n 바이트 만큼 비교하는 함수로 동일한 경우 0을 리턴, s1[idx]이 s2[idx] 값보다 작다면 0보다 작은 값을 리턴, s1[idx]이 s2[idx]보다 크다면 0보다 큰 값을 리턴한다.
RWLOCK_EXIT(lock);
FBUMPD(ifs_exists);
- KFREE(fra);
+ KFREE(fran);
return NULL;
해제된 fra
에 접근할 수 있는 몇 가지 코드 경로가 존재한다. 그 중 하나는 단편화 캐시에서 필터 결과와 요청된 패킷의 항목을 검토하기 위해 사용되는 ipf_fag_lookup() 함수가 있다. 이로 인해 어떤 문제가 발생하는 지 살펴보자.
static ipfr_t *
ipf_frag_lookup(softc, softf, fin, table
#ifdef USE_MUTEXES
, lock
#endif
)
ipf_main_softc_t *softc;
ipf_frag_softc_t *softf;
fr_info_t *fin;
ipfr_t *table[];
#ifdef USE_MUTEXES
ipfrwlock_t *lock;
#endif
{
...
/*
* 테이블 데이터 양만 확인
*/
for (f = table[idx]; f; f = f->ipfr_hnext) {
...
return NULL;
}
두 번째 코드 경로로 sys/contrib/ipfilter/netinet/fil.c에 위치한 ipf_slowtimer() 함수는 Use-After-Free 취약점이 유발된다. 이 함수는 천천히 단편화 패킷을 만료하기 위해서 사용된다.
void
ipf_slowtimer(softc)
ipf_main_softc_t *softc;
{
ipf_token_expire(softc);
ipf_frag_expire(softc);
ipf_state_expire(softc);
ipf_nat_expire(softc);
ipf_auth_expire(softc);
ipf_lookup_expire(softc);
ipf_rule_expire(softc);
ipf_sync_expire(softc);
softc->ipf_ticks++;
# if defined(__OpenBSD__)
timeout_add(&ipf_slowtimer_ch, hz/2);
# endif
}
이 취약점이 흥미로운 점은 단편화 캐시 테이블에서 항목을 만료시키도록 설계된 ipf_frag_expire() 함수에 있다. ipf_frag_expire() 함수는 내부에서 ipf_frag_delete() 루틴을 사용한다.
static void
ipf_frag_delete(softc, fra, tail)
ipf_main_softc_t *softc;
ipfr_t *fra, ***tail;
{
ipf_frag_softc_t *softf = softc->ipf_frag_soft;
if (fra->ipfr_next)
fra->ipfr_next->ipfr_prev = fra->ipfr_prev;
*fra->ipfr_prev = fra->ipfr_next;
if (*tail == &fra->ipfr_next)
*tail = fra->ipfr_prev;
if (fra->ipfr_hnext)
fra->ipfr_hnext->ipfr_hprev = fra->ipfr_hprev;
*fra->ipfr_hprev = fra->ipfr_hnext;
if (fra->ipfr_rule != NULL) {
(void) ipf_derefrule(softc, &fra->ipfr_rule);
}
if (fra->ipfr_ref <= 0)
ipf_frag_free(softf, fra);
}
ipfr_frag_new() 함수에서 논리 결함으로 인해 이미 해제된 항목을 삭제하려고 시도할 수 있다.