[Pwn] Unsafe unlink

코코·2023년 4월 7일
0

pwn

목록 보기
2/10
post-custom-banner

오늘은 Unsafe Unlink에 대해 이해해보려고 한다...!

Unsafe unlink 취약점은 Fake Chunk를 활용하여 다른 인접한 Chunk와 병합이 일어날 때 발생하는 취약점으로, 이를 통해 원하는 주소에 값을 쓰거나, 출력할 수 있다.


Unsafe unlink를 살펴보기 전 ! 간단하게 Unsorted bin의 특징에 대해 간단하게 짚어보자.
    1. Free 된 Chunk를 doubly-linked list로 관리
    2. Free 된 Chunk들이 인접할 경우, 병합(Consolidate)
    3. FIFO방식(First In First Out)


Unlink(AV, P, BK, FD)

glibc-2.26의 malloc.c

/* 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;		      
              }								      
          }								      
      }									      
}

위의 if문을 살펴보기에 앞서, chunksize(), prev_size(), next_chunk() 함수들을 살펴보자.


  1. chunksize()
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

해당 함수는 flag bits를 제외한 Chunk의 사이즈를 가져오는 함수로 정의되어있다.
참고로 chunksize_nomask 함수는 Chunk Header에서 크기 정보를 가져오는 함수이다.


  1. prev_size() & next_chunk()
/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))

/* Size of the chunk below P.  Only valid if prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

prev_size 함수는 mchunk_prev_size 함수와 같이 살펴봐야한다.

우선 next_chunk는 p(포인터) + chunksize(p)로 보아, 다음 Chunk를 가르키는 함수이다.

다음으로 prev_size 함수를 보면, p(chunk)->mchunk_prev_size, 이전 Chunk의 사이즈를 가르키는 함수이다.


이제 다시 malloc.c 코드를 살펴보자 〰!

/* 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))                 

if문 안의 코드를 보면,
chunksize(P), 현재 Chunk의 사이즈와 prev_size(next_chunk(P)) 다음 chunk(P 기준)의 prev_size가 같은지 확인한다..

즉, 현재 Chunk 사이즈와 다음 Chunk의 prev_size(이전 Chunk 사이즈, 결국 현재 Chunk의 Size)가 동일한지 확인하는 것! (prev_size는 이전 Chunk의 크기를 저장하고 있음.)

만약 두 값이 같다면, 해당 Chunk의 "fd"와 "bk" 값을 "FD", "BK"에 저장한다.
그 후 다음 if문에서 FD->bkBK->fd의 값이 해제할 chunk의 pointer와 같은지 확인한다!



Unsafe unlink는 이러한 과정을 악용하기 위해 아래과 같은 것들이 가능해야한다!

     1. Unsafe unlink를 구현하기 위해 2개의 Allocated Chunk가 필요.
     2. 첫 번째 메모리에 Fack Chunk를 생성할 수 있어야함.
     3. 두 번째 Chunk Header(prev_size, PREV_INUSE flag)를 조작할 수 있어야함.
        ➡ __builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0), Bypass
    4. fake_chunk->fd->bk & fake_chunk->bk->fd의 값이 첫 번째 Chunk의 mchunkptr이어야함.
         ➡ __builtin_expect (FD->bk != P || BK->fd != P, 0), Bypass

위의 조건이 만족되면, fake_chunk->fd, fake_chunk->bk가 가리키는 영역에 fake_chunk->fd 값을 저장할 수 있다!


실제 바이너리를 통해 살펴보자. 해당 바이너리 파일은 Lazenca 사이트에 있는 코드를 사용하였다!
Unsorted bin에 들어가도록 malloc시, Chunk 사이즈를 크게 만들어줬다.

  • 환경은 Ubuntu-18.04 버전에서 진행한다.
// gcc -o unlink lazenca_unlink.c -no-pie

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
  
unsigned long *buf1;
   
void main(){
    buf1 = malloc(0x420);
    unsigned long *buf2 = malloc(0x420);
 
    fprintf(stderr,"Global buf1's addr : %p\n",&buf1);
    fprintf(stderr,"buf1 : %p\n",buf1);
    fprintf(stderr,"buf2 : %p\n",buf2);
 
    buf1[2] = (unsigned long)&buf1 - (sizeof(unsigned long)*3);
    buf1[3] = (unsigned long)&buf1 - (sizeof(unsigned long)*2);
   
    *(buf2 - 2) = 0x420;
    *(buf2 - 1) &= ~1; // PREV_INUSE bit

    free(buf2);

    char str[16];
    buf1[3] = (unsigned long) str;
 
    read(STDIN_FILENO,buf1,0x80);
    fprintf(stderr, "Data from Str : %s\n",str);
}

코드는 Lazenca 사이트의 예제 코드를 사용하였다. 다만 malloc시 인자로 전달되는 사이즈를 tcache나 fastbins에 들어가지 않도록 0x80에서 0x420으로 수정하였다.

코드를 간단히 살펴보면 buf1은 전역 변수로 선언되어있고 main 함수에서 buf1, buf2에 각각 0x420 크기의 heap 영역을 할당한다.
buf1(전역 변수)의 fd에는 buf1 - 24의 값을, buf1의 bk에는 buf1 - 16의 값을 넣어준다.
buf2의 prev_size를 0x420으로 수정하고, size의 PREV_INUSE flag 비트를 빼준다.
그 후 buf2를 free해주고, str의 주소를 buf1[3]. 즉, buf1의 bk에 str의 주소를 넣어주면 해당 영역에 값을 쓸 수 있는 것이다!


pwngdb를 통해 동적 분석 시작.

pwndbg> disassemble main
Dump of assembler code for function main:
   0x0000000000400677 <+0>:     push   rbp
   0x0000000000400678 <+1>:     mov    rbp,rsp
   0x000000000040067b <+4>:     sub    rsp,0x30
   0x000000000040067f <+8>:     mov    rax,QWORD PTR fs:0x28
   0x0000000000400688 <+17>:    mov    QWORD PTR [rbp-0x8],rax
   0x000000000040068c <+21>:    xor    eax,eax
   0x000000000040068e <+23>:    mov    edi,0x420
   0x0000000000400693 <+28>:    call   0x400580 <malloc@plt>
   0x0000000000400698 <+33>:    mov    QWORD PTR [rip+0x2009d1],rax        # 0x601070 <buf1>
   0x000000000040069f <+40>:    mov    edi,0x420
   0x00000000004006a4 <+45>:    call   0x400580 <malloc@plt>
   0x00000000004006a9 <+50>:    mov    QWORD PTR [rbp-0x28],rax
   0x00000000004006ad <+54>:    mov    rax,QWORD PTR [rip+0x2009ac]        # 0x601060 <stderr@@GLIBC_2.2.5>
   0x00000000004006b4 <+61>:    lea    rdx,[rip+0x2009b5]        # 0x601070 <buf1>
   0x00000000004006bb <+68>:    lea    rsi,[rip+0x1a2]        # 0x400864
   0x00000000004006c2 <+75>:    mov    rdi,rax
   0x00000000004006c5 <+78>:    mov    eax,0x0
   0x00000000004006ca <+83>:    call   0x400570 <fprintf@plt>
   0x00000000004006cf <+88>:    mov    rdx,QWORD PTR [rip+0x20099a]        # 0x601070 <buf1>
   0x00000000004006d6 <+95>:    mov    rax,QWORD PTR [rip+0x200983]        # 0x601060 <stderr@@GLIBC_2.2.5>
   0x00000000004006dd <+102>:   lea    rsi,[rip+0x199]        # 0x40087d
   0x00000000004006e4 <+109>:   mov    rdi,rax
   0x00000000004006e7 <+112>:   mov    eax,0x0
   0x00000000004006ec <+117>:   call   0x400570 <fprintf@plt>
   0x00000000004006f1 <+122>:   mov    rax,QWORD PTR [rip+0x200968]        # 0x601060 <stderr@@GLIBC_2.2.5>
   0x00000000004006f8 <+129>:   mov    rdx,QWORD PTR [rbp-0x28]
   0x00000000004006fc <+133>:   lea    rsi,[rip+0x185]        # 0x400888
   0x0000000000400703 <+140>:   mov    rdi,rax
   0x0000000000400706 <+143>:   mov    eax,0x0
   0x000000000040070b <+148>:   call   0x400570 <fprintf@plt>
   0x0000000000400710 <+153>:   lea    rdx,[rip+0x200959]        # 0x601070 <buf1>
   0x0000000000400717 <+160>:   mov    rax,QWORD PTR [rip+0x200952]        # 0x601070 <buf1>
   0x000000000040071e <+167>:   add    rax,0x10
   0x0000000000400722 <+171>:   sub    rdx,0x18
   0x0000000000400726 <+175>:   mov    QWORD PTR [rax],rdx
   0x0000000000400729 <+178>:   lea    rdx,[rip+0x200940]        # 0x601070 <buf1>
   0x0000000000400730 <+185>:   mov    rax,QWORD PTR [rip+0x200939]        # 0x601070 <buf1>
   0x0000000000400737 <+192>:   add    rax,0x18
   0x000000000040073b <+196>:   sub    rdx,0x10
   0x000000000040073f <+200>:   mov    QWORD PTR [rax],rdx
   0x0000000000400742 <+203>:   mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400746 <+207>:   sub    rax,0x10
   0x000000000040074a <+211>:   mov    QWORD PTR [rax],0x420
   0x0000000000400751 <+218>:   mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400755 <+222>:   sub    rax,0x8
   0x0000000000400759 <+226>:   mov    rdx,QWORD PTR [rax]
   0x000000000040075c <+229>:   mov    rax,QWORD PTR [rbp-0x28]
   0x0000000000400760 <+233>:   sub    rax,0x8
   0x0000000000400764 <+237>:   and    rdx,0xfffffffffffffffe
   0x0000000000400768 <+241>:   mov    QWORD PTR [rax],rdx
   0x000000000040076b <+244>:   mov    rax,QWORD PTR [rbp-0x28]
   0x000000000040076f <+248>:   mov    rdi,rax
   0x0000000000400772 <+251>:   call   0x400540 <free@plt>
   0x0000000000400777 <+256>:   mov    rax,QWORD PTR [rip+0x2008f2]        # 0x601070 <buf1>
   0x000000000040077e <+263>:   lea    rdx,[rax+0x18]
   0x0000000000400782 <+267>:   lea    rax,[rbp-0x20]
   0x0000000000400786 <+271>:   mov    QWORD PTR [rdx],rax
   0x0000000000400789 <+274>:   mov    rax,QWORD PTR [rip+0x2008e0]        # 0x601070 <buf1>
   0x0000000000400790 <+281>:   mov    edx,0x80
   0x0000000000400795 <+286>:   mov    rsi,rax
   0x0000000000400798 <+289>:   mov    edi,0x0
   0x000000000040079d <+294>:   call   0x400560 <read@plt>
   0x00000000004007a2 <+299>:   mov    rax,QWORD PTR [rip+0x2008b7]        # 0x601060 <stderr@@GLIBC_2.2.5>
   0x00000000004007a9 <+306>:   lea    rdx,[rbp-0x20]
   0x00000000004007ad <+310>:   lea    rsi,[rip+0xdf]        # 0x400893
   0x00000000004007b4 <+317>:   mov    rdi,rax
   0x00000000004007b7 <+320>:   mov    eax,0x0
   0x00000000004007bc <+325>:   call   0x400570 <fprintf@plt>
   0x00000000004007c1 <+330>:   nop
   0x00000000004007c2 <+331>:   mov    rax,QWORD PTR [rbp-0x8]
   0x00000000004007c6 <+335>:   xor    rax,QWORD PTR fs:0x28
   0x00000000004007cf <+344>:   je     0x4007d6 <main+351>
   0x00000000004007d1 <+346>:   call   0x400550 <__stack_chk_fail@plt>
   0x00000000004007d6 <+351>:   leave
   0x00000000004007d7 <+352>:   ret

disassemble main의 결과이다. 주요 부분에 Breakpoint를 걸고, 하나씩 살펴보자.

breakpoint는 위와 같이 설정해두었다.

우선 실행하여 malloc으로 할당받은 영역의 주소 및 size 값을 확인한다.
첫 번째 할당받은 영역은 0x602250으로 보인다.

전역변수인 buf1의 주소는 0x601070으로, 해당 주소에 있는 값을 살펴보면 malloc으로 할당받은 0x602260의 주소값이 들어있는 것을 확인할 수 있다!

Breakpoint 이후 계속 실행하다보면, b *main+203으로 이동한다. 해당 부분에서 첫 번째로 할당받은 Chunk를 확인해보면 아래와 같이 구성되어있다.

Fake Chunk의 fd에는 0x601058, bk에는 0x601060이 들어가있는 것을 확인할 수 있다.


다음 b *main+241로 이동하면, 두 번째 할당받은 영역의 prev_size와 size 값이 조작된 것을 볼 수있다.
prev_size의 값을 0x420, size에서 PREV_INUSE flag 비트를 제외하여, 0x431 -> 0x430으로 바뀐 모습을 확인할 수 있다.
이로 인해 이전에 Free 된 Chunk가 있다고 속이는 것이다!


🤚 여기서 잠깐, 위의 Fake_Chunk->fd=0x601058, Fake_Chunk->bk=0x601060을 넣는 이유를 다시 한 번 짚어보자...

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

위의 코드때문에 buf1->fd = (buf1 - 24), buf1->bk = (buf1 - 16)을 넣는 것인데...

우선 FD->bk = (Fake_Chunk->fd)->bk이고, BK->fd = (Fake_Chunk->bk)->fd이다.
Fake_Chunk->fd는 0x601058이다. 그리고 해당 주소에서 bk는 0x601070이 된다!
두 번째로 BK->fd = (Fake_Chunk->bk)는 0x601060이다. 그리고 해당 주소(0x601060)에서 bk는 0x601070을 가르킨다!


간단하게, 도식화로 보면 아래와 같다.
Fake_Chunk->fd가 가르키는 주소는 0x0601058이고, 해당 주소의 bk는 0x601070이다.

Fake_Chunk->bk가 가르키는 주소는 0x601060이고, 해당 주소의 fd는 0x601070부분에 위치.


즉, FD->bk와 BK->fd 모두 전역 변수인 buf1의 주소, 0x601070을 가르킨다!!!
참고로, P는 buf1의 주소(0x601070)를 가르킨다~!


또한 위에서 첫 번째 조건도 어떻게 통과했느지 살펴보자.

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

chunksize(P, fake_chunk) = 0이고, prev_size(next_chunk(P))는 Fake_Chunk+size(0) = Fake_Chunk이므로, prev_size(P, fake_chunk)이다. 즉 0 == 0이므로 첫 번째 조건도 스무스하게 통과하게 되는 것이다.


이제 다음으로 이동해보자. 두 가지의 조건문(if)을 모두 지났으므로 아래의 2줄이 실행된다.

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

(Fake_Chunk->fd)->bk = Fake_chunk->bk, 0x601070(0x601058->bk) = 0x601060
(Fake_Chunk->bk)->fd = Fake_chunk->fd, 0x601070(0x601060->fd) = 0x601058


따라서, Free 이후에는 0x601070에는 0x601058을 가르키게 된다. (Free 전, b *main+251, Free 후 b *main+256)

Free 전 buf1에 저장된 주소 값, 0x602260

Free 이후 buf1에 저장된 주소 값, 0x601058!

이 상태에서 read(0, buf1, 0x80)을 호출하면, 0x601058의 주소에 값을 쓰게 되는 것이다.
그러나 아직 코드가 끝난 것이 아니므로 더 살펴보겠다..

char str[16];
buf1[3] = (unsigned long) str;

read(STDIN_FILENO,buf1,0x80);
fprintf(stderr, "Data from Str : %s\n",str);

남은 코드는 위 부분이다.
str 변수를 선언하고, buf1[3] 부분. 즉, 0x601058->bk부분인 0x601070에 str 변수의 주소를 덮어씌우는 것이다.

그리고 아래의 read(STDIN_FILENO,buf1,0x80);을 호출하면, str 변수에 값을 쓰는 것이다!!!

b *main+274로 이동하면, buf1의 주소 값이 str 변수의 주소(0x00007fffffffe450)로 바뀐 것을 확인할 수 있다.


이제 다음으로 read 함수부분으로 이동해보자.
b *main+294부분으로 이동해보면, 위와 같이 rsi에 buf1에 str의 주소(0x00007fffffffe450)가 담긴 것을 확인할 수 있다!


hihihihihihi을 입력하고 출력하는 부분으로 살펴보면, str 변수에도 마찬가지로 hihihihihihi 문자열이 들어있는 것을 확인할 수 있다!






unsafe unlink... 뭔가 어려웠다...........
-- 🤘


+++ 추가 +++
다음 Chunk를 찾을 때, Chunk 주소 + size를 통해 Chunk를 찾는다.
이전 Chunk는 Chunk주소 - prev_size를 통해 이전 Chunk를 찾는다.






※ 참고 블로그 및 링크
👉 http://lazenca.net/pages/viewpage.action?pageId=1148137
👉 https://jeongzero.oopy.io/c5b2e8fc-88b8-4d2a-a0fa-e38db89c7c6c
👉 https://github.com/shellphish/how2heap/blob/master/glibc_2.35/unsafe_unlink.c

profile
화이팅!
post-custom-banner

0개의 댓글