Unsafe Unlink

Unsafe Unlink는 doubly linked list에서 청크를 연결 해제하는 매크로인 unlink를 이용해 aaw 할 수 있게 해주는 공격 기법이다.

Conditions

  1. 힙 영역을 전역 변수같이 주소를 알고 있는 위치에 unlink 될 청크의 주소가 저장되어있어야 한다.
  2. 첫번째 청크를 이용해서 두번째 청크의 메타데이터를 조작할 수 있어야 한다.

Analysis

/*
    Consolidate other non-mmapped chunks as they arrive.
  */
  else if (!chunk_is_mmapped(p)) {
   ...
    if (!prev_inuse(p)) {
      prevsize = prev_size (p);
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      unlink_chunk (av, p, bck, fwd);
    }

...

_int_free의 일부이다.
small bin, large bin 같은 doubly linked list들이 여기까지 온다.
mmap으로 할당된 청크가 아니라면, prev_inuse bit를 체크한다.
prev_inuse bit가 세팅되어있다면, 현재 청크과 prev 청크는 병합 대상으로 간주된다.
그리고 병합이 되면서 unlink 매크로가 호출된다.
이때 p에서 prev_size를 뺀 청크, 즉 prev 청크가 p로 들어간다.


unlink 매크로는 doubly linked list에서 노드를 제거하는 것과 같은 동작을 수행한다.

/* 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 매크로의 소스 코드이다.

if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))  

첫번째 조건문은 P의 chunk size와 next_chunk의 prev_size를 비교한다.
즉, boundary tag가 제대로 들어가있는지 체크한다.
일반적으로 P의 size와 next chunk의 prev_size는 free된 상태라면, size == prev_size이다.

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))

두번째 조건문은 FD->bk(P->fd->bk)BK->fd(P->bk->fd)가 P인지 검증한다.

일반적으로 fd, bk는 인접한 청크를 가리키기 때문에,

FD->bkBK->fdP일 수 밖에 없다.

FD->bk = BK;                                                          
BK->fd = FD;

앞에 조건문들을 다 통과하면, 실질적인 unlink가 수행된다.

Exploitation

if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))  
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 


첫번째 if문과 두번째 if문을 우회하기 위해서 fake chunk를 만든다.


fake chunk의 prev size와 current size를 0으로 세팅하면 chunksize(P)는 0이 된다.

#define next_chunk(p) ((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))

next_chunk는 기본적으로 현재 p에서 size를 더해서 계산된다. 이때 하위 3비트 플래그 비트들은 무시된다.
이때 next_chunk(P)는 size가 0이기 때문에 현재 청크를 가리키게 되고, 그 청크의 prev_size는 0으로 세팅되어있으니 결과적으로 0이 된다.

아니면 굳이 prev size와 current size를 0으로 세팅하지 않고도 우회할 수 있다.
prev size와 current size를 잘 맞춰주고, 이후 overflow 등을 이용해서 next chunk의 prev size를 fake chunk의 size로 맞춰주면 우회가 가능하다.

청크를 전역 변수에서 관리한다고 가정하고, FD0x6020b0라면 FD->bk0x6020b0 + 24를 가리키게 된다.
그리고 BK0x6020b8로 세팅해주면, BK->fd0x6020b8 + 16을 가리킨다.
결국 똑같은 힙 청크를 가리키게 된다.

0x6020c8 - 24를 FD에 세팅하고, 0x6020c8 - 16을 BK에 세팅해주면 된다.

그러면 validation check를 우회할 수 있게 된다.


그 다음에는 overflow 등을 이용해서 next chunk 헤더를 조작해야된다.
size의 하위 3비트 플래그 비트와 prev size를 조작해서 fake chunk를 이용할 수 있도록 해주고, size의 PREV_INUSE bit0으로 세팅해서, free시에 병합 대상으로 간주될 수 있게 해준다.

그리고 마지막으로 prev size를 fake chunk의 size로 바꿔주면,

p = chunk_at_offset(p, -((long) prevsize));

chunk_at_offset 매크로에 의해서 fake chunk를 병합 대상 청크로 인식하게 할 수 있다.

FD->bk = BK;                                                          
BK->fd = FD;

이제 free해서 unlink 해주면, 위 로직이 실행된다.

FD->bk = BK;가 돌아가면 0x6020c8BK로 덮인다.

BK->fd = FD;가 돌아가면 0x6020c8이 최종적으로 FD, 즉 0x6020b0으로 덮인다.
그러면 0x6020c8을 덮을 수 있고, 임의 주소로 돌릴 수 있다.

profile
https://msh1307.kr

0개의 댓글