what is house of force
https://dreamhack.io/wargame/challenges/53
해당 문제를 풀면서 얻은 지식을 다룬다.
정말 오랜만에 새로운 공격기법을 배운것 같다. 배운 것을 같이 여기에 풀어보겠다.
일단 새로운 공격기법은 house of force 공격기법이다. 예전 glibc malloc에 있는 취약점이다.
바닐라(glibc upstream) 기준: glibc ≤ 2.28 에서 House of Force가 성립. glibc 2.29부터 top 청크 사이즈 검증이 들어가 기본 기법은 막힘 ( 배포판은 다를 수 있다 )
일단 사용할려면 다음과 같은 조건이 필요하다.
- malloc(size) 에서 size를 내가 컨트롤 할 수 있을 것
- heap 영역에서 top chunk 영역의 size 부분을 내가 컨트롤 할 수 있을 것
이 두개가 가능하다면, house of force 공격이 가능하게 된다.
기본적인 원리는 다음과 같다.
glibc 2.27 버전 기준으로 정리된 해당 글을 보면 알겠지만, malloc은 tcache → fastbin → smallbin & largebin → unsorted bin 등을 순환하고 없으면 TOP chunk에서 때어오는 작업을 한다.
이때 발생하는 문제점이다. 이 문제가 glibc 2.23 버전이었기 때문에 해당 버전을 기준으로 작성하겠다.
if (bit > map || bit == 0) {
do {
if (++block >= BINMAPSIZE) /* out of bins */
goto use_top;
} while ((map = av->binmap[block]) == 0);
bin = bin_at (av, (block << BINMAPSHIFT));
bit = 1;
}여기서 unsorted bin을 순환하면서 정렬한뒤 각 bin을 돌면서 bin이 없는지 확인하는 작업을 한다.
즉, unsorted bin을 순환 이후에도 small, large , fast bin이 존재하지 않으면 ( 2.23은 tcache가 없음 )
goto use_top; 을 진행한다.
use_top
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).
We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
victim = av->top;
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (have_fastchunks (av))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}
/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}use top 코드를 살펴보도록 하자
victim = av->top;
size = chunksize (victim);arena의 top 즉, top chunk 주소를 victim 으로 일단 가져오고 top chunk의 사이즈를 size 로 가져온다.
이제 취약점이 있는 부분이다. ( 나머지 부분은 아까 블록링크에 자세히 분석해놓음 )
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}먼저, top chunk의 size 가 malloc(nb) 로 요청한 nb + MINSIZE ( 요청할 수 있는 최소 크기 ) 보다 커야한다. 즉, top chunk가 많이 남았다는 뜻
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;top chunk의remainder_size 를 기존 size - nb 로 구하고
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))remainder = victim ( top chunk 주소 ) + nb 로 설정한다.
여기서 취약점이 발생한다. 기억해두자
그리고 계산된 remainder 를
av->top = remainder; 으로 실제 top chunk의 주소로 설정이 된다.
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);이후 이걸로 각 청크에 메타데이터 ( 크기 , flag 비트 )를 설정해주고
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;검증 후에 반환을 해준다. 자 그럼 취약점 부분을 자세히 분석해보자
remainder = chunk_at_offset (victim, nb);
아까 말한 이 부분이다. 여기서 nb는 size_t 형이다. 즉, unsigned 이지만, unsigned 역시 터무니 없이 큰수 ( signed에서는 음수를 나타내는 수 ) 를 더하면 사실상 뺄셈을 할 수 있게 된다.
즉, remainder = victim + nb 에서 nb에 큰 수를 넣어서 overflow를 읽으켜 ( 사실상 뺄셈이랑 같음 ) 내가 원하는 주소를 remainder에 넣을 수 있게 된다.
자세한 공식을 구해보자면, remainder의 우리가 원하는 주소 ( ex. exit_got )를 넣어보자면,
remainder = victim + nb 인데 이 remainder는 청크의 시작 ( 즉, 메타데이터 ex. flag , size 등의 청크헤더를 포함한 ) 주소 이기 때문에 청크헤더를 계산해 줘야 한다.
32비트 운영체제 → 청크 헤더 8바이트
remainder - 0x8 = victim + nb
64비트 운영체제 → 청크 헤더 8바이트
remainder - 0x10 = victim + nb
가 된다. 근데 우리가 알아야 할 값은 우리가 넣어야 할 nb 가 얼마냐 이기에 이항을 해보면,
32비트 운영체제
nb = remainder - victim - 0x8
== nb = (원하는 주소) - topchunk_addr - 0x8
64비트 운영체제
nb = remainder - victim - 0x10
== nb = (원하는 주소) - topchunk_addr - 0x10
다음과 같은 공식이 된다. top chunk를 exit_got로 설정해서 다음번에 이 주소를 malloc 했을때 받고 싶으면,
nb = exit_got - topchunk_addr - 0x10. 을 하면 되는 것이다.
근데… 그렇다면, nb는 음수가 될 확률이 크다. exit_got 같이 우리가 원하는 주소는 메인 프로세스의 주소이고 topchunk_addr 의 주소는 heap의 주소이기 때문이다. 그러면..
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))맨 처음 조건인 여기서 문제가 될 수 있다. 그래서 heap 영역에서 top chunk 영역의 size 부분을 내가 컨트롤 할 수 있을 것 조건이 필요했던 것이다. top chunk의 size를 -1 ( 0xfffff…). 로 바꿀 수 있으면 해당 조건을 반드시 넘길 수 있게 되기 때문이다.
따라서,
- top chunk의 size를 -1 로 변조를 하고
- malloc(nb) 에서 nb를 공식을 통한 값을 넣으면
이제 top chunk가 내가 원하는 주소로 설정이 된 것이다. 이제 이후에
malloc(size) 함수를 호출하면 내가 원하는 주소가 반환이 된다.
주의 할점 ( 중요함!! )
이 house of force 공격 할때는 항상 malloc 내의 정렬 규칙을 따라야 한다.
x86_64 (64비트): SIZE_SZ=8 → MALLOC_ALIGNMENT = 16 ( 16바이트 정렬 )
i386 (32비트): glibc 2.23 당시엔 MALLOC_ALIGNMENT = 2*SIZE_SZ = 8 이 기본이었다. 이후 glibc 2.26부터 i386도 16바이트 정렬로 상향됨
즉, nb = (원하는 주소) - topchunk_addr - 0x10 여기서 (원하는 주소)가 결국 top chunk의 주소가 되므로 이 주소는 정렬이 되어 있어야 함.