glibc 2.3.2 이하 버전의 double free bug

2015.05.15 23:58

이전 glibc 2.3.2 이하 버전의 double free bug를 공부하기 위해 아래 문서들을 참고했었는데, 가장 중요한 부분인 heap overflow로 뒷 청크를 덮어쓰고 다시 현재 청크를 free() 하여 공격하는 부분(아래 3개 문서 참고)이 이해되질 않아 gdb와 glibc 2.3.2 소스를 이용해 그 동작 원리를 자세하게 알아보았다.


http://www.hackerschool.org/HS_Boards/data/Lib_system/doublefree.txt

http://www.hackerschool.org/HS_Boards/data/Lib_system/dfb_leon.txt

http://agz.es/Reverse-Engineering/Buffer-Overflow/Double%20Free%20Bug%20%5Bseoty-_-v%5D.pdf


물론, glibc 2.3.3 이상 버전부터는 해당 취약점을 패치하여(이따가 확인) 위와 같은 방법으로 공격하는 것은 불가능하다. 그렇지만 앞으로도 공부하는 분들이 나와같이 햇갈릴 수 있기 때문에 조금이나마 보탬이 되려 작성하게 되었다.



실습은 glibc 2.3.2 이하 버전에서 진행해야하는 이유로 다음과 같은 환경에서 진행했다.



redhat linux 6.2 

glibc : 2.1.3

arch : x86




glibc 2.1.3 에서의 chunk


(glibc 2.1.3은 아주 오래전 버전이라 최근의 chunk 모양과 동작 과정이 많이 다르기 때문에 주의하기 바람!!

최근 버전에서는 Size of chunk 의 최하위 3비트를 Size 이외의 값으로 따로 사용하지만 , 2.1.3 버전에서는 최하위 2비트를 Size 이외의 값으로 사용함 )




이제 아래 소스를 컴파일하고 진행하도록 한다.



(dumpcode.h : http://smleenull.tistory.com/438 )




위 덤프는 free(mol1)이 되기 이전의 mol1, mol2 두 개의 청크를 나타내고, 각 청크의 Chunk Size 부분에 빨간 네모칸을 쳐놓았다. 

또한 아래 덤프는 free(mol1) 이후의 heap 메모리를 덤프해놓은 것으로, 가운데 네모는 '첫 번째 청크'의 Size를 나타내고, 세번째 네모 부분은 '두 번째 청크'의 Size값에 PREV_INUSE 값을 제외한 ( 0x19 -> 0x18 ) 값을 가지고 있다. 

(  청크에 대한 자세한 설명은 다음 문서 참고

http://studyfoss.egloos.com/5206220

http://studyfoss.egloos.com/5206979

http://studyfoss.egloos.com/5209389  )




바로 공격 페이로드를 집어넣어 fake chunk의 fd, bk 값으로 0x0807c76c 를 지정해주도록 한다.




출력된 덤프에서 각각 mol1, mol2 청크를 사각형으로 구분해 놓았다. 


여기서 이해가 안되는것이, free(mol1)을 했을 때 mol1 청크의 마지막 부분에 -4를 넣어주고, mol2의 size 부분에 -1을 넣어 fake chunk를 만들어준다고만 되어있어 도저히 이해를 할 수가 없었다 ㅠㅠ




( 오른쪽 free(mol1) 이후 메모리의 맨위 chunk size는 0x00000019가 아니고 0x00000015임 )



위 페이로드에서 '두 번째 청크'의 chunk size를 이리저리 바꿔주다 보면 chunk_free() 함수에서 크래쉬가 나는 것을 볼 수 있는데, 해당 함수를 소스로 보면

다음과 같다. 



함수가 실행되면 여러 분기문이 있고, 각 분기문마다 몇가지 경우의 수가 있다. (예를 들면 다음 chunk가 top chunk 이면서 free chunk인지 아닌지 등등) 

그래서 위 공격 페이로드를 입력하여 gdb로 보면 결국 모든 분기문을 건너 뛰고 78라인의 분기로 들어가 89 번째 라인의 unlink()만 수행하여 fake chunk의 fd, bk 값의 +12, +8 부분에 각각 fd, bk를 써줌을 알 수 있다. 


이제 위 소스에서 78 라인의 분기문 부분을 도식화하면 정상적인 free() 함수에서는 다음과 같이 동작한다. 


1. free(p)의 '다음 청크'가 top chunk가 아니고 '다음 청크' 가 free chunk라면 동작

2. 위 '1'을 검사하기 위해 (mol1을 기준으로) '다음 청크의 포인터' + '다음 청크의 사이즈' + 4 에 위치한 값의 PREV_INUSE 비트 확인

3. 해당 PREV_INUSE 값이 0인 경우 합병 후 unlink() 실행





(위 그림에서 '다음 청크 포인터 + 다음 청크 사이즈' 를 변경 => '다음 청크 포인터 + 다음 청크 사이즈 + 4')



위 그림은 mol1, mol2, mol3 세 개의 16바이트 heap 청크가 있을 때의 메모리 구조이고, mol2 는 이미 free(mol2) 된 상태이다.

또한, malloc_chunk와 actual chunk의 차이 때문에 'mol2 chunk'와 '다음 청크의 포인터'가 가리키는 곳이 차이가 있다는 것을 알 수 있다. 

(malloc_chunk와 actual chunk의 차이 : http://pds16.egloos.com/pds/200912/25/35/c0098335_4b34adcad42cd.png )


이제 위 설명대로 free(mol1) 을 실행하면 chunk_free() 함수에서 free(p)의 '다음 청크'가 top chunk가 아니고 '다음 청크' 가 free chunk라면 동작 하는 것을 구분하기 위해  '다음 청크의 포인터' + '다음 청크의 사이즈' + 4 에 위치한 세 번째 mol3 청크의 chunk size 부분의 PREV_INUSE 비트가 0인지 검사하게 된다. 

( '다음 청크의 사이즈'는 0x00000019를 그냥 사용하는 것이 아니라 glibc 2.1.3 기준으로 하위 2비트를 제외한 크기가 진짜 청크의 크기이므로 0xfffffffc 와 and연산 하는 과정을 거친 다음 그 값을 '다음 청크의 사이즈'로 받아들여야 한다. 최근 버전에서는 '다음 청크 사이즈'를 구하기 위해 하위 3비트를 제외한 값이 진짜 '다음 청크 사이즈' 이다. )


위 그림에서는 해당 비트가 0으로 되어있으므로 mol2를 mol1과 합병하는 과정을 거치기 위해 unlink()를 실행하게 되고, 여기까지가 이미 mol2 청크가 free된 상황에서의 정상적인 free(mol1)의 동작 과정이다.




그래서 해커들은 이부분을 악용하여 맨 처음 확인햇던 공격 페이로드로 fake chunk를 만들어 내고 fd와 bk 값을 조작할 수 있게 하였다.


(위 그림에서 '다음 청크 포인터 + 다음 청크 사이즈' 를 변경 => '다음 청크 포인터 + 다음 청크 사이즈 + 4')




이제 위 정보를 바탕으로 공격 페이로드가 들어간 메모리 구조를 다시 보도록 하자.


free(mol1)이 동작하고 chunk_free() 에서는 '다음 청크(mol2)'가 top chunk가 아님을 확인했고, free chunk인지 확인하기 위해 다음과 같이 동작한다.


1. '다음 청크의 포인터' + '다음 청크의 사이즈(0xffffffff & 0xfffffffc = 0xfffffffc)' + 4 에 위치한 값의 PREV_INUSE 값 확인

2. 위 '1'번에서 다음 청크의 사이즈는 -4 이므로 다음과 같은 식이 나오게 된다.  ==>  [ '다음 청크의 포인터' - 4 + 4 ] = 제자리

3. 그럼 결국 제자리(mol1 청크의 prev_size 값)에 있는 값을 가지게 되고, 해당 위치를 '다음 청크' 라고 생각하고 해당 위치의 PREV_INUSE 값을 확인하게 된다.


4. 해당 값은 0xfffffffc 로 PREV_INUSE 값이 0 이므로 합병하기 위해 fake chunk의 fd, bk 값을 이용해 unlink() 를 수행하게 되어 우리가 원하는 곳에 값을 덮어쓸 수 있게 된다.


5. 합병 후 '다음 청크의 사이즈' + mol1 chunk size 를 계산해 합병 된 mol1 chunk의 chunk size 부분에 새로운 합병 후 크기를 입력해 준다. 

( 0x19 + (-0x4) = 0x15 )


6. '다음 청크의 포인터' 를 fake chunk로 인식했기 때문에 해당 부분의 -4 위치를 prev_size 값으로 인지하여 해당 부분에 PREV_INUSE 값을 제외한 청크의 크기를 써줌 (0x00000014)




위와 같이 동작하여 결국 아래와 같은 결과값을 나타내는 것을 알 수 있다.




결국 Double free bug 라는 공격 이름의 의미는 free()를 두 번 호출하기 때문에 붙여진 이름이 아니라 heap overflow로 뒷 부분의 청크를 free된 상태로 만들고 앞부분의 청크를 free() 하여 공격이 이뤄지므로 '한 번의 free() 호출 + 이미 free 되어있는 것으로 만들어진 fake chunk' 를 가리켜 double free bug 라 한다고 정리할 수 있을 것 같다. 



추가로 대희님 블로그에서 glibc 2.3.3 이후 버전에서는 위와 같은 공격을 방지하기 위해 패치 되었다고하여 직접 glibc 2.3.2 소스와 비교하며 glibc 2.3.3 의 malloc.c 의 free 부분을 찾아보았다. 다른 부분은 전부 같았는데, 2.3.3 소스에서는 아래와 같이 보안을 위해 청크의 사이즈를 검증하는 부분이 추가된 것을 알 수 있었다.

( reverseholic.com , 제목 : Heap Overflow Length Extension for GHOST vulnerability )





smleenull List/Linux

  1. Blog Icon

    비밀댓글입니다

  2. Blog Icon
    guest223

    와 .. 감사요 이해잘되네요

  3. Blog Icon
    iyw

    와 많은 도움이 ㄷ됐습니다 알기쉽네요ㅎㅎ

  4. 그런가여 ㅋㅋ

  5. Blog Icon
    gedfas

    관리자의 승인을 기다리고 있는 댓글입니다

  6. Blog Icon
    gedfas

    저 하나 여쭤봐도 될까요?

    제 개념이 제대로 잡혀 있나 해서 여쭙겠습니다.ㅠㅠㅠ
    double free bug는 해제 할 때 생기는 취약점이니까 free() 같은 함수가 지나고 나서 생기는 오류고 vtable overwrite는 해제 하기 전에 공격하는 거죠???
    그러면 vtable overwrite 와 double free bug 는 같이 공격할 수 없는 거죠?

티스토리 툴바