Protostar Heap3 [ Double Free Bug ]

2017. 6. 14. 15:00SystemHacking/Protostar




참고한 사이트 : https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/


Protostar 3번 문제입니다. 더블프리버그(DFB)에 관한 문제입니다

[ 그림1 ]

해당 프로세스는 변수 a,b,c 가 32byte만큼 메모리가 동적할당 된 후 인자들을 입력받은 다음 반환되는 흐름입니다

우선 힙 구조를 살펴 보면 다음과 같습니다


[ 높은 주소 ]


Stack 

빈 공간 

c ( 32byte )

b ( 32byte )

a ( 32byte )


[ 낮은 주소 ]


낮은주소에서 높은주소의 방향으로 해당 그림과 같이 a,b,c가 위치하고 있습니다

free(c), free(b), free(a) 과정의 아래의 그림들을 살펴보면서 설명하겠습니다


$gdb file heap3

(gdb) breab *address

(gdb) run AAAAAAAA BBBBBBBB CCCCCCCC

[ 그림2 ]

현재는 free(c) 이전에 break를 걸어주고 각 변수들의 구조를 읽어보았습니다

각 변수들의 구조는 [ 이전 chunk의 크기(pre_size) ] [ chunk 크기 ] [ data ] 의 형태로 이루어져 있습니다

우리는 변수에게 malloc()함수를 통해 32byte를 주었지만 실제로 변수의 크기는 0x29 = 41byte입니다

why? 32byte + pre_size(8byte) + P플러그(1) 때문에 chunk의 사이즈는 0x29입니다


참고로 pre_size는 메모리 상에서 해당 chunk의 이전 chunk가 free되었을 때 그 chunk의 사이즈를 나타냅니다. 

현재 free된 chunk가 없기 때문에 모두 0으로 초기화 되어있는 상태입니다( 만약, a가 free되어 있다면 b의 pre_size=28 )

* pre_size = 이전의 chunk 크기 - P플래그


다음으로 free(c) 실행 후입니다

[ 그림3 ]

free(c) 가 실행되어서 c변수에 있었던 처음 " CCCC "데이터가 사라졌습니다 ( 뒤의 CCCC 는 무시해도 좋습니다 )

chunk가 free되면 data영역이 fd와 bk의 영역으로 사용되어집니다

fd : 이전의  chunk(free된)를 가리키는 주소 / bk : 다음 chunk(free된)를 가리키는 주소 ( unsorted chunk list 내에서 )


[pre_size][size][data]    =>    [pre_size][size][fd][bk]


[pre_size] : [ c-b-a ] 의 힙 구조에서 c이전의 chunk인 b가, b이전의 chunk인 a가 free상태가 아니므로 모두 0

* pre_size = 이전의 chunk 크기 - P플래그


다음으로 free(b)실행 후입니다

[ 그림4 ]

free(b)가 실행된 후의 모습입니다 b는 free되어서 unsorted chunk list 에 들어가게 되었다

그리고 unsorted chunk list내에서 b이전에 free되었던 c의 주소를 fd가 가리키고 있다


다음으로 free(a) 이후입니다

[ 그림5 ]

a가 unsorted chunk list내에 들어가서 a의 fd는 이전에 free되었던 b의 주소를 가리키고 있습니다


a

b 

[ unsorted chunk list ]


free함수의 순서가 c -> b - > a 순서로 실행되어 unlink매크로가 발생하지 않는다 => DFB 발생하지 않음 => 조작이 필요하다



[ DFB 원리 ]

상상을해보자. 위의 [ c-b-a ]의 힙구조에서 만약 a가 free되어 있는 상태라면? 

b의 size 는 a의 size-1(P플래그)이다 ( 41byte - 1(P플래그) => b의 size= 40byte )

b chunk의 P플래그값이 0 이라는 뜻은 b free될 때 a chunk와 합병이 일어난다는 뜻이다

두개의 chunk가 합병이 일어날 때 unlink매크로가 발생하게 될것이다


DFB는 아래와 같은 방법을 통해서 원하는 공간원하는 값을 입력시킬 수 있다 !


strcpy(a,argv[1]) 에서 오버플로우시켜 b의 P플래그 값과 pre_size값을 변화시킵니다


1> size값을 변질시키자 = P플래그값 변질 ( 이전의 chunk가 free되어 있다고 인식시킨다 )

size의 값을 -4로 변질시키면 마지막 비트값이 0 => P플래그 = 0 => 병합이발생하고 unlink매크로 발생

unlink매크로가 발생되면 우리는 원하는 주소에 원하는 값을 써넣을수 있습니다 How? 

unlink가 실행되면 fd가 가리키는 주소의 +12 지점에는 bk가 입력되고 bk가 가리키는 주소의 +8지점에는 fd가 입력된다


2> pre_size값을 변질시키자 ( 이전의 chunk의 위치를 조작한다 )

Heap에서 이전 chunk의 주소값은 이와같이 표현할 수 있다  &b - pre_size "

pre_size값을 0xfffffffc(-4) 로 변질시키면 어떻게 될까요? ( *참고 : DFB에서 -4 사용하는 것이 제일 편함 )

이전 chunk의 주소는 &b+4byte 가 됩니다.

chunk b는 해당 주소의 chunk를 자신의 이전 chunk로 간주합니다

즉 chunk a와 b사이에 가짜 chunk를 하나 만들어 준것입니다. 그리고 만든 가짜 chunk의 fd와 bk를 이용합니다

해당주소는 우리가 입력할 수 있는 공간이기 때문에 값을 써넣을 수 있습니다


3> 결론

그렇다면, 가짜 chunk의 fd위치에 puts의 GOT내에서의 주소-12 를 입력시키고 bk에는 &winner를 입력시키자

그러면 fd+12 = bk 에 의해서 puts()함수를 실행하려 할 때 실제로는 &winner가 있으므로 winner함수가 실행될 것이다


[ 참고 ]

* printf()함수가 실행되는 지점을 gdb에서 살펴보면 puts@plt 호출되므로 puts의 GOT주소를 이용



[1] 1차시도

[ 그림6 ]

a변수의 공간을 "A"로 채운다음 b의 pre_size = -4, size=-4로 변환시켰습니다

size의 -4를 비트로 변환하면 마지막 부분이 0입니다 => b의 이전 chunk가 free되어 있다고 간주된다

그리고 우리가 만든 가짜 chunk의 시작주소는 &b + 4 이겟죠? 

그럼 두번째 인자에는 b의 첫 공간에 4byte(aaaa)를 채우고 가짜 chunk의 시작주소에 fd값과 bk값을 넣어줍시다

fd에는 puts()의 GOT내에서의 주소 - 12 이고 bk는 &winner를 입력합니다

따라서 만들어낸 가짜 chunk의 fd와 bk가 우리가 원하는 값으로 설정되었습니다


b가 free될 때 fd+12 지점(puts의 GOT내의 주소)에 bk값이 대입되어 프로그램 마지막에는 winner함수가 실행되리라 생각했지만, 오류가 발생

why? fd+12=bk 부분까지는 완벽하지만, bk+8=fd 에서 bk+8영역이 data영역이라 쓰기권한이 없어서 오류가 발생합니다


[2] 2차시도 ( winner함수를 호출하는 코드 작성 )

[ 그림7 ]

push 와 ret의 기계어 코드를 알아냈습니다

push : \x68

ret : \xc3


[ 그림8 ]

첫번째 인자가 들어가는 주소입니다 : \x0804c008 ( winner함수를 호출하는 코드가 입력되는 시작주소 )

bk공간이 입력가능 한 공간이 되어서 오류가 발생하지 않는다.

"\x68\x64\x88\x04\x08\xc3" : push + &winner + ret ( 기계어코드 )


 ./heap3 `python -c 'print "\x90"*22+"\x68\x64\x88\x04\x08\xc3"+"aaaa"+"\xfc\xff\xff\xff"*2'` `python -c 'print "aaaa"+"\x1c\xb1\x04\x08"+"\x08\xc0\x04\x08"'` C

[ 공격코드 ]


공격코드 실행결과입니다

[ 그림9 ]

첫번째 인자가 들어가는 32byte내에 winner()함수를 호출하는 쉘코드를 집어넣었습니다( push + &winner() + ret )

가짜 chunk의 시작주소에는 puts()함수의 GOT에서의 주소-12를 넣고( fd ), 그 다음 공간에는 &(winner호출) 를 입력했다(bk)

b가 free되어 fd+12=bk , bk+8=fd 가 수행되고, printf()가 실행될 때 실제로는 winner함수를 호출하는 기계어 코드가 실행되어 winner함수가 실행된 후 프로그램이 종료된다



# 참고 사항 #

# 추가적으로, DFB를 이용해 쉘을 떨어뜨릴 수 있다


<1>

DFB에서는 RET값을 찾는것 보다 .dtors+4 ( 소멸자 ) 의주소를 찾아서 사용하는것이 유용하다

#objdump -h file 명령어를 통해서 소멸자의 주소를 알아낼 수 있고,

#nm file 명령어를 통해서 .dtors 주소를 알아내어 +4 를해주면 소멸자의 주소가됩니다

소멸자는 main()함수가 종료될 때 실행되어집니다. 따라서 해당 주소에 쉘코드를 올리면 프로세스 종료시 쉘이 실행됩니다


<2>

위에서 구한 소멸자의 주소에서 -12한 값을 " fd "위치에 올려놓으면 소멸자주소에 bk값을 올릴 수 있습니다 !

bk에는 에그쉘을 이용한 쉘코드의 주소를 대입시킬 겁니다. 하지만 여기서 주의할 점이 있습니다

bk+8 = fd 에 의해서 쉘코드에 변화가 생겨 쉘이 떨어지지 않는다는 것입니다. => 해결법 : Jumpcode추가


에그쉘 파일에 Jumpcode를 추가해서 쉘코드를 생성해줍시다

Jumpcode는 " \xeb\x0c "입니다 ( 12byte만큼의 코드는 무시하고 그 이후부터 코드를 읽는다 )

왜 12byte인가 ? bk+8 지점부터 fd(4byte) 이므로 bk+8 ~ bk+11 지점은 fd값이 존재합니다

bk~bk+7 지점은 NOP 이므로 무시해도 되는부분입니다

따라서 bk+12지점의 NOP를 흘러내려가면서 쉘코드가 존재하는 부분에 도달해서 쉘이 실행됩니다

( 에그쉘의 원리를 이해하고 있어야 함 설명 링크 : http://itsaessak.tistory.com/113  )

( 에그쉘 : NOPNOP....Shellcode / 에그쉘로 얻은주소 : NOP중 어느 한 지점 )


<3>

위의 문제를 예로 들어서 공격코드를 만들어보면 다음과 같습니다

]$ ./heap3 `python -c 'print "A"*32+"\xfc\xff\xff\xff"*2'` `python -c 'print "aaaa"+"&소멸자-12"+"&EGG"'` C 


fd + 12 = fd(&소멸자-12) + 12 = &소멸자 = bk( &EGG )

bk + 8 = &EGG + 8 = fd("&소멸자-12")(4byte의 주소값)


main()함수 종료시 bk가 가리키는 주소로 이동

=> 점프코드를 통해서 변질된 값은 뛰어넘고, NOP를 타고 쉘코드를 읽어 쉘이 떨어진다